diff --git a/lib/puppet/log.rb b/lib/puppet/log.rb index 7a08ba57d..ee825e406 100644 --- a/lib/puppet/log.rb +++ b/lib/puppet/log.rb @@ -1,517 +1,518 @@ require 'syslog' module Puppet # 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 Log + include Puppet::Util PINK="" GREEN="" YELLOW="" SLATE="" ORANGE="" BLUE="" RESET="" @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @desttypes = {} # A type of log destination. class Destination class << self attr_accessor :name end def self.initvars @matches = [] end # Mark the things we're supposed to match. def self.match(obj) @matches ||= [] @matches << obj end # See whether we match a given thing. def self.match?(obj) # Convert single-word strings into symbols like :console and :syslog if obj.is_a? String and obj =~ /^\w+$/ obj = obj.downcase.intern end @matches.each do |thing| # Search for direct matches or class matches return true if thing === obj or thing == obj.class.to_s end return false end def name if defined? @name return @name else return self.class.name end end # Set how to handle a message. def self.sethandler(&block) define_method(:handle, &block) end # Mark how to initialize our object. def self.setinit(&block) define_method(:initialize, &block) end end # Create a new destination type. def self.newdesttype(name, options = {}, &block) dest = genclass(name, :parent => Destination, :prefix => "Dest", :block => block, :hash => @desttypes, :attributes => options ) dest.match(dest.name) return dest end @destinations = {} class << self include Puppet::Util include Puppet::Util::ClassGen end # Reset all logs to basics. Basically just closes all files and undefs # all of the other objects. def Log.close(dest = nil) if dest if @destinations.include?(dest) if @destinations.respond_to?(:close) @destinations[dest].close end @destinations.delete(dest) end else @destinations.each { |name, dest| if dest.respond_to?(:flush) dest.flush end if dest.respond_to?(:close) dest.close end } @destinations = {} end end # Flush any log destinations that support such operations. def Log.flush @destinations.each { |type, dest| if dest.respond_to?(:flush) dest.flush end } 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) unless hash.include?(:level) raise Puppet::DevError, "Logs require a level" end unless @levels.index(hash[:level]) raise Puppet::DevError, "Invalid log level %s" % hash[:level] end if @levels.index(hash[:level]) >= @loglevel return Puppet::Log.new(hash) else return nil end end def Log.destinations return @destinations.keys end # Yield each valid level in turn def Log.eachlevel @levels.each { |level| yield level } end # Return the current log level. def Log.level return @levels[@loglevel] end # Set the current log level. def Log.level=(level) unless level.is_a?(Symbol) level = level.intern end unless @levels.include?(level) raise Puppet::DevError, "Invalid loglevel %s" % level end @loglevel = @levels.index(level) end def Log.levels @levels.dup end newdesttype :syslog do def close Syslog.close end def initialize if Syslog.opened? Syslog.close end name = Puppet.name name = "puppet-#{name}" unless name =~ /puppet/ options = Syslog::LOG_PID | Syslog::LOG_NDELAY # XXX This should really be configurable. facility = Syslog::LOG_DAEMON @syslog = Syslog.open(name, options, facility) end def handle(msg) # XXX Syslog currently has a bug that makes it so you # cannot log a message with a '%' in it. So, we get rid # of them. if msg.source == "Puppet" @syslog.send(msg.level, msg.to_s.gsub("%", '%%')) else @syslog.send(msg.level, "(%s) %s" % [msg.source.to_s.gsub("%", ""), msg.to_s.gsub("%", '%%') ] ) end end end newdesttype :file do match(/^\//) def close if defined? @file @file.close @file = nil end end def flush if defined? @file @file.flush end end def initialize(path) @name = path # first make sure the directory exists # We can't just use 'Config.use' here, because they've # specified a "special" destination. unless FileTest.exist?(File.dirname(path)) Puppet.recmkdir(File.dirname(path)) Puppet.info "Creating log directory %s" % File.dirname(path) end # create the log file, if it doesn't already exist file = File.open(path, File::WRONLY|File::CREAT|File::APPEND) @file = file end def handle(msg) @file.puts("%s %s (%s): %s" % [msg.time, msg.source, msg.level, msg.to_s]) end end newdesttype :console do @@colors = { :debug => SLATE, :info => GREEN, :notice => PINK, :warning => ORANGE, :err => YELLOW, :alert => BLUE, :emerg => RESET, :crit => RESET } def initialize # Flush output immediately. $stdout.sync = true end def handle(msg) color = "" reset = "" if Puppet[:color] color = @@colors[msg.level] reset = RESET end if msg.source == "Puppet" puts color + "%s: %s" % [ msg.level, msg.to_s ] + reset else puts color + "%s: %s: %s" % [ msg.level, msg.source, msg.to_s ] + reset end end end newdesttype :host do def initialize(host) Puppet.info "Treating %s as a hostname" % host args = {} if host =~ /:(\d+)/ args[:Port] = $1 args[:Server] = host.sub(/:\d+/, '') else args[:Server] = host end @name = host @driver = Puppet::Client::LogClient.new(args) end def handle(msg) unless msg.is_a?(String) or msg.remote unless defined? @hostname @hostname = Facter["hostname"].value end unless defined? @domain @domain = Facter["domain"].value if @domain @hostname += "." + @domain end end if msg.source =~ /^\// msg.source = @hostname + ":" + msg.source elsif msg.source == "Puppet" msg.source = @hostname + " " + msg.source else msg.source = @hostname + " " + msg.source end begin #puts "would have sent %s" % msg #puts "would have sent %s" % # CGI.escape(YAML.dump(msg)) begin tmp = CGI.escape(YAML.dump(msg)) rescue => detail puts "Could not dump: %s" % detail.to_s return end # Add the hostname to the source @driver.addlog(tmp) rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err detail Puppet::Log.close(self) end end end end # Log to a transaction report. newdesttype :report do match "Puppet::Transaction::Report" def initialize(report) @report = report end def handle(msg) # Only add messages from objects, since anything else is # probably unrelated to this run. if msg.objectsource? @report.newlog(msg) end end 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 unless type raise Puppet::DevError, "Unknown destination type %s" % dest end begin if type.instance_method(:initialize).arity == 1 @destinations[dest] = type.new(dest) else @destinations[dest] = type.new() end rescue => detail if Puppet[:debug] puts detail.backtrace end # If this was our only destination, then add the console back in. if @destinations.empty? and (dest != :console and dest != "console") newdestination(:console) end end end # Route the actual message. FIXME There are lots of things this method # should do, like caching, storing messages when there are not yet # destinations, 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) if @levels.index(msg.level) < @loglevel return end @destinations.each do |name, dest| threadlock(dest) do dest.handle(msg) end end end def Log.sendlevel?(level) @levels.index(level) >= @loglevel end # Reopen all of our logs. def Log.reopen types = @destinations.keys @destinations.each { |type, dest| if dest.respond_to?(:close) dest.close end } @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 :level, :message, :time, :tags, :remote attr_reader :source def initialize(args) unless args.include?(:level) && args.include?(:message) raise Puppet::DevError, "Puppet::Log called incorrectly" end if args[:level].class == String @level = args[:level].intern elsif args[:level].class == Symbol @level = args[:level] else raise Puppet::DevError, "Level is not a string or symbol: #{args[:level].class}" end # Just return unless we're actually at a level we should send #return unless self.class.sendlevel?(@level) @message = args[:message].to_s @time = Time.now # this should include the host name, and probly lots of other # stuff, at some point unless self.class.validlevel?(level) raise Puppet::DevError, "Invalid message level #{level}" end if args.include?(:tags) @tags = args[:tags] end if args.include?(:source) self.source = args[:source] else @source = "Puppet" end Log.newmessage(self) end # Was the source of this log an object? def objectsource? if defined? @objectsource and @objectsource @objectsource else false end 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) # We can't store the actual source, we just store the path. This # is a bit of a stupid hack, specifically testing for elements, but # eh. if source.is_a?(Puppet::Element) and source.respond_to?(:path) @objectsource = true @source = source.path else @objectsource = false @source = source.to_s end unless defined? @tags and @tags if source.respond_to?(:tags) @tags = source.tags end end end def tagged?(tag) - @tags.include?(tag) + @tags.detect { |t| t.to_s == tag.to_s } end def to_report "%s %s (%s): %s" % [self.time, self.source, self.level, self.to_s] end def to_s return @message end end end # $Id$ diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb index ba929b7fc..477246e0b 100644 --- a/lib/puppet/reports/tagmail.rb +++ b/lib/puppet/reports/tagmail.rb @@ -1,102 +1,107 @@ require 'puppet' Puppet.config.setdefaults(:reporting, :tagmap => ["$confdir/tagmail.conf", "The mapping between reporting tags and email addresses."], :sendmail => [%x{which sendmail 2>/dev/null}.chomp, "Where to find the sendmail binary with which to send email."], :reportfrom => ["report@" + [Facter["hostname"].value, Facter["domain"].value].join("."), "The 'from' email address for the reports."], :smtpserver => ["none", "The server through which to send email reports."] ) require 'net/smtp' Puppet::Server::Report.newreport(:tagmail) do |report| unless FileTest.exists?(Puppet[:tagmap]) Puppet.notice "Cannot send tagmail report; no tagmap file %s" % Puppet[:tagmap] return end + p report + # Load the config file tags = {} File.readlines(Puppet[:tagmap]).each do |line| taglist = emails = nil case line.chomp when /^\s*#/: next when /^\s*$/: next when /^\s*(.+)\s*:\s*(.+)\s*$/: taglist = $1 emails = $2 else raise ArgumentError, "Invalid tagmail config file" end taglist.split(/\s*,\s*/).each do |tag| tags[tag] = emails.split(/\s*,\s*/) end end # Now find any appropriately tagged messages. reports = {} tags.each do |tag, emails| messages = nil if tag == "all" messages = report.logs else messages = report.logs.find_all do |log| log.tagged?(tag) end end if messages and ! messages.empty? reports[emails] = messages.collect { |m| m.to_report }.join("\n") else Puppet.info "No messages to report" end end # Let's fork for the sending of the email, since you never know what might # happen. fork do if Puppet[:smtpserver] != "none" begin Net::SMTP.start(Puppet[:smtpserver]) do |smtp| reports.each do |emails, messages| Puppet.info "Sending report to %s" % emails.join(", ") smtp.send_message(messages, Puppet[:reportfrom], *emails) end end rescue => detail if Puppet[:trace] puts detail.backtrace end raise Puppet::Error, "Could not send report emails through smtp: %s" % detail end elsif Puppet[:sendmail] != "" begin reports.each do |emails, messages| Puppet.info "Sending report to %s" % emails.join(", ") # We need to open a separate process for every set of email addresses IO.popen(Puppet[:sendmail] + " " + emails.join(" "), "w") do |p| p.puts "From: #{Puppet[:reportfrom]}" + p.puts "To: %s" % emails.join(', ') p.puts "Subject: Puppet Report for %s" % report.host p.puts messages end end rescue => detail if Puppet[:debug] puts detail.backtrace end raise Puppet::Error, "Could not send report emails via sendmail: %s" % detail end else raise Puppet::Error, "SMTP server is unset and could not find sendmail" end end end + +# $Id$ diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 1f8af8e82..0e0628202 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,2661 +1,2663 @@ require 'puppet' require 'puppet/log' require 'puppet/element' require 'puppet/event' require 'puppet/metric' require 'puppet/type/state' require 'puppet/parameter' require 'puppet/util' require 'puppet/autoload' # see the bottom of the file for the rest of the inclusions module Puppet # The type is unknown class UnknownTypeError < Puppet::Error; end class UnknownProviderError < Puppet::Error; end class Type < Puppet::Element # Types (which map to elements in the languages) are entirely composed of # attribute value pairs. Generally, Puppet calls any of these things an # 'attribute', but these attributes always take one of three specific # forms: parameters, metaparams, or states. # In naming methods, I have tried to consistently name the method so # that it is clear whether it operates on all attributes (thus has 'attr' in # the method name, or whether it operates on a specific type of attributes. attr_accessor :children attr_reader :provider attr_accessor :file, :line attr_reader :tags, :parent attr_writer :implicit, :title def implicit? if defined? @implicit and @implicit return true else return false end end include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name, :states attr_accessor :providerloader attr_writer :defaultprovider include Enumerable, Puppet::Util::ClassGen end # iterate across all of the subclasses of Type def self.eachtype @types.each do |name, type| # Only consider types that have names #if ! type.parameters.empty? or ! type.validstates.empty? yield type #end end end # Create the 'ensure' class. This is a separate method so other types # can easily call it and create their own 'ensure' values. def self.ensurable(&block) if block_given? self.newstate(:ensure, :parent => Puppet::State::Ensure, &block) else self.newstate(:ensure, :parent => Puppet::State::Ensure) do self.defaultvalues end end end # Should we add the 'ensure' state to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. #ens = [:create, :destroy].inject { |set, method| ens = [:exists?, :create, :destroy].inject { |set, method| set &&= self.public_method_defined?(method) } #puts "%s ensurability: %s" % [self.name, ens] return ens end # all of the variables that must be initialized for each subclass def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @providers = Hash.new @defaults = {} unless defined? @parameters @parameters = [] end @validstates = {} @states = [] @parameters = [] @paramhash = {} @paramdoc = Hash.new { |hash,key| if key.is_a?(String) key = key.intern end if hash.include?(key) hash[key] else "Param Documentation for %s not found" % key end } unless defined? @doc @doc = "" end unless defined? @states @states = [] end end # Do an on-demand plugin load def self.loadplugin(name) unless Puppet[:pluginpath].split(":").include?(Puppet[:plugindest]) Puppet.notice "Adding plugin destination %s to plugin search path" % Puppet[:plugindest] Puppet[:pluginpath] += ":" + Puppet[:plugindest] end Puppet[:pluginpath].split(":").each do |dir| file = ::File.join(dir, name.to_s + ".rb") if FileTest.exists?(file) begin load file Puppet.info "loaded %s" % file return true rescue LoadError => detail Puppet.info "Could not load plugin %s: %s" % [file, detail] return false end end end end # Define a new type. def self.newtype(name, parent = nil, &block) # First make sure we don't have a method sitting around name = symbolize(name) newmethod = "new#{name.to_s}" # Used for method manipulation. selfobj = metaclass() @types ||= {} if @types.include?(name) if self.respond_to?(newmethod) # Remove the old newmethod selfobj.send(:remove_method,newmethod) end end # Then create the class. klass = genclass(name, :parent => (parent || Puppet::Type), :overwrite => true, :hash => @types, &block ) # Now define a "new" method for convenience. if self.respond_to? newmethod # Refuse to overwrite existing methods like 'newparam' or 'newtype'. Puppet.warning "'new#{name.to_s}' method already exists; skipping" else selfobj.send(:define_method, newmethod) do |*args| klass.create(*args) end end # If they've got all the necessary methods defined and they haven't # already added the state, then do so now. if klass.ensurable? and ! klass.validstate?(:ensure) klass.ensurable end # Now set up autoload any providers that might exist for this type. klass.providerloader = Puppet::Autoload.new(klass, "puppet/provider/#{klass.name.to_s}" ) # We have to load everything so that we can figure out the default type. klass.providerloader.loadall() klass end # Return a Type instance by name. def self.type(name) @types ||= {} if name.is_a?(String) name = name.intern end unless @types.include? name begin require "puppet/type/#{name}" unless @types.include? name Puppet.warning "Loaded puppet/type/#{name} but no class was created" end rescue LoadError => detail # If we can't load it from there, try loading it as a plugin. loadplugin(name) end end @types[name] end # class methods dealing with type instance management public # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( "Cannot create alias %s: object already exists" % [name] ) end end if @aliases.include?(name) unless @aliases[name] == obj raise Puppet::Error.new( "Object %s already has alias %s" % [@aliases[name].name, name] ) end end @aliases[name] = obj end # retrieve a named instance of the current type def self.[](name) if @objects.has_key?(name) return @objects[name] elsif @aliases.has_key?(name) return @aliases[name] else return nil end end # add an instance by name to the class list of instances def self.[]=(name,object) newobj = nil if object.is_a?(Puppet::Type) newobj = object else raise Puppet::DevError, "must pass a Puppet::Type object" end if exobj = @objects.has_key?(name) and self.isomorphic? msg = "Object '%s[%s]' already exists" % [name, newobj.class.name] if exobj.file and exobj.line msg += ("in file %s at line %s" % [object.file, object.line]) end if object.file and object.line msg += ("and cannot be redefined in file %s at line %s" % [object.file, object.line]) end error = Puppet::Error.new(msg) else #Puppet.info("adding %s of type %s to class list" % # [name,object.class]) @objects[name] = newobj end end # remove all type instances; this is mostly only useful for testing def self.allclear Puppet::Event::Subscription.clear @types.each { |name, type| type.clear } end # remove all of the instances of a single type def self.clear if defined? @objects @objects.each do |name, obj| obj.remove(true) end @objects.clear end if defined? @aliases @aliases.clear end end # remove a specified object def self.delete(object) return unless defined? @objects if @objects.include?(object.title) @objects.delete(object.title) end if @aliases.include?(object.title) @aliases.delete(object.title) end end # iterate across each of the type's instances def self.each return unless defined? @objects @objects.each { |name,instance| yield instance } end # does the type have an object with the given name? def self.has_key?(name) return @objects.has_key?(name) end # Allow an outside party to specify the 'is' value for a state. The # arguments are an array because you can't use parens with 'is=' calls. # Most classes won't use this. def is=(ary) param, value = ary if param.is_a?(String) param = param.intern end if self.class.validstate?(param) unless @states.include?(param) self.newstate(param) end @states[param].is = value else self[param] = value end end # class and instance methods dealing with parameters and states public # Find the namevar def self.namevar unless defined? @namevar params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } if params.length > 1 raise Puppet::DevError, "Found multiple namevars for %s" % self.name elsif params.length == 1 @namevar = params[0].name else raise Puppet::DevError, "No namevar for %s" % self.name end end @namevar end # Copy an existing class parameter. This allows other types to avoid # duplicating a parameter definition, and is mostly used by subclasses # of the File class. def self.copyparam(klass, name) param = klass.attrclass(name) unless param raise Puppet::DevError, "Class %s has no param %s" % [klass, name] end @parameters << param @parameters.each { |p| @paramhash[name] = p } if param.isnamevar? @namevar = param.name end end # Create a new metaparam. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newmetaparam(name, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) param = genclass(name, :parent => Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, &block ) param.ismetaparameter return param end def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Find the default provider. def self.defaultprovider unless defined? @defaultprovider and @defaultprovider suitable = suitableprovider() # Find which providers are a default for this system. defaults = suitable.find_all { |provider| provider.default? } # If we don't have any default we use suitable providers defaults = suitable if defaults.empty? max = defaults.collect { |provider| provider.defaultnum }.max defaults = defaults.find_all { |provider| provider.defaultnum == max } retval = nil if defaults.length > 1 Puppet.warning( "Found multiple default providers for %s: %s; using %s" % [self.name, defaults.collect { |i| i.name.to_s }.join(", "), defaults[0].name] ) retval = defaults.shift elsif defaults.length == 1 retval = defaults.shift else raise Puppet::DevError, "Could not find a default provider for %s" % self.name end @defaultprovider = retval end return @defaultprovider end # Retrieve a provider by name. def self.provider(name) name = Puppet::Util.symbolize(name) # If we don't have it yet, try loading it. unless @providers.has_key?(name) @providerloader.load(name) end return @providers[name] end # Just list all of the providers. def self.providers @providers.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) return (@providers.has_key?(name) && @providers[name].suitable?) end # Create a new provider of a type. This method must be called # directly on the type that it's implementing. def self.provide(name, options = {}, &block) name = Puppet::Util.symbolize(name) model = self parent = if pname = options[:parent] if pname.is_a? Class pname else if provider = self.provider(pname) provider else raise Puppet::DevError, "Could not find parent provider %s of %s" % [pname, name] end end else Puppet::Type::Provider end self.providify provider = genclass(name, :parent => parent, :hash => @providers, :prefix => "Provider", :block => block, :attributes => { :model => model } ) return provider end # Make sure we have a :provider parameter defined. Only gets called if there # are providers. def self.providify return if @paramhash.has_key? :provider model = self newparam(:provider) do desc "The specific backend for #{self.name.to_s} to use. You will seldom need to specify this -- Puppet will usually discover the appropriate provider for your platform." # This is so we can refer back to the type to get a list of # providers for documentation. class << self attr_accessor :parenttype end # We need to add documentation for each provider. def self.doc @doc + " Available providers are:\n\n" + parenttype().providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| "* **%s**: %s" % [i, parenttype().provider(i).doc] }.join("\n") end defaultto { @parent.class.defaultprovider.name } validate do |value| value = value[0] if value.is_a? Array if provider = @parent.class.provider(value) unless provider.suitable? raise ArgumentError, "Provider '%s' is not functional on this platform" % [value] end else raise ArgumentError, "Invalid %s provider '%s'" % [@parent.class.name, value] end end munge do |provider| provider = provider[0] if provider.is_a? Array if provider.is_a? String provider = provider.intern end @parent.provider = provider provider end end.parenttype = self end def self.unprovide(name) if @providers.has_key? name if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end @providers.delete(name) end end # Return an array of all of the suitable providers. def self.suitableprovider @providers.find_all { |name, provider| provider.suitable? }.collect { |name, provider| provider } end def provider=(name) if klass = self.class.provider(name) @provider = klass.new(self) else raise UnknownProviderError, "Could not find %s provider of %s" % [name, self.class.name] end end # Create a new parameter. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newparam(name, options = {}, &block) param = genclass(name, :parent => options[:parent] || Puppet::Parameter, :attributes => { :element => self }, :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) # These might be enabled later. # define_method(name) do # @parameters[name].value # end # # define_method(name.to_s + "=") do |value| # newparam(param, value) # end if param.isnamevar? @namevar = param.name end return param end # Create a new state. The first parameter must be the name of the state; # this is how users will refer to the state when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the state. Defaults to Puppet::State. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newstate(name, options = {}, &block) name = symbolize(name) # This is here for types that might still have the old method of defining # a parent class. unless options.is_a? Hash raise Puppet::DevError, "Options must be a hash, not %s" % options.inspect end if @validstates.include?(name) raise Puppet::DevError, "Class %s already has a state named %s" % [self.name, name] end # We have to create our own, new block here because we want to define # an initial :retrieve method, if told to, and then eval the passed # block if available. s = genclass(name, :parent => options[:parent] || Puppet::State, :hash => @validstates ) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do instance_variable_set( "@is", provider.send(options[:retrieve]) ) end end if block class_eval(&block) end end # If it's the 'ensure' state, always put it first. if name == :ensure @states.unshift s else @states << s end # define_method(name) do # @states[name].should # end # # define_method(name.to_s + "=") do |value| # newstate(name, :should => value) # end return s end # Specify a block for generating a list of objects to autorequire. This # makes it so that you don't have to manually specify things that you clearly # require. def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Yield each of those autorequires in turn, yo. def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Return the parameter names def self.parameters return [] unless defined? @parameters @parameters.collect { |klass| klass.name } end # Find the metaparameter class associated with a given metaparameter name. def self.metaparamclass(name) @@metaparamhash[symbolize(name)] end # Find the parameter class associated with a given parameter name. def self.paramclass(name) @paramhash[name] end # Find the class associated with any given attribute. def self.attrclass(name) @attrclasses ||= {} # We cache the value, since this method gets called such a huge number # of times (as in, hundreds of thousands in a given run). unless @attrclasses.include?(name) @attrclasses[name] = case self.attrtype(name) when :state: @validstates[name] when :meta: @@metaparamhash[name] when :param: @paramhash[name] end end @attrclasses[name] end def self.to_s if defined? @name "Puppet::Type::" + @name.to_s.capitalize else super end end # Create a block to validate that our object is set up entirely. This will # be run before the object is operated on. def self.validate(&block) define_method(:validate, &block) #@validate = block end # does the name reflect a valid state? def self.validstate?(name) name = name.intern if name.is_a? String if @validstates.include?(name) return @validstates[name] else return false end end # Return the list of validstates def self.validstates return {} unless defined? @states return @validstates.keys end # Return the state class associated with a name def self.statebyname(name) @validstates[name] end # does the name reflect a valid parameter? def self.validparameter?(name) unless defined? @parameters raise Puppet::DevError, "Class %s has not defined parameters" % self end if @paramhash.include?(name) or @@metaparamhash.include?(name) return true else return false end end # What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. def self.attrtype(attr) @attrtypes ||= {} unless @attrtypes.include?(attr) @attrtypes[attr] = case when @validstates.include?(attr): :state when @@metaparamhash.include?(attr): :meta when @paramhash.include?(attr): :param else raise Puppet::DevError, "Invalid attribute '%s' for class '%s'" % [attr, self.name] end end @attrtypes[attr] end # All parameters, in the appropriate order. The namevar comes first, # then the states, then the params and metaparams in the order they # were specified in the files. def self.allattrs # now get all of the arguments, in a specific order # Cache this, since it gets called so many times namevar = self.namevar order = [namevar] order << [self.states.collect { |state| state.name }, self.parameters, self.metaparams].flatten.reject { |param| # we don't want our namevar in there multiple times param == namevar } order.flatten! return order end # A similar function but one that yields the name, type, and class. # This is mainly so that setdefaults doesn't call quite so many functions. def self.eachattr(*ary) # now get all of the arguments, in a specific order # Cache this, since it gets called so many times if ary.empty? ary = nil end self.states.each { |state| yield(state, :state) if ary.nil? or ary.include?(state.name) } @parameters.each { |param| yield(param, :param) if ary.nil? or ary.include?(param.name) } @@metaparams.each { |param| yield(param, :meta) if ary.nil? or ary.include?(param.name) } end def self.validattr?(name) if name.is_a?(String) name = name.intern end if self.validstate?(name) or self.validparameter?(name) or self.metaparam?(name) return true else return false end end # abstract accessing parameters and states, and normalize # access to always be symbols, not strings # This returns a value, not an object. It returns the 'is' # value, but you can also specifically return 'is' and 'should' # values using 'object.is(:state)' or 'object.should(:state)'. def [](name) if name.is_a?(String) name = name.intern end if name == :name name = self.class.namevar end case self.class.attrtype(name) when :state if @states.include?(name) return @states[name].is else return nil end when :meta if @metaparams.include?(name) return @metaparams[name].value else if default = self.class.metaparamclass(name).default return default else return nil end end when :param if @parameters.include?(name) return @parameters[name].value else if default = self.class.paramclass(name).default return default else return nil end end else raise TypeError.new("Invalid parameter %s(%s)" % [name, name.inspect]) end end # Abstract setting parameters and states, and normalize # access to always be symbols, not strings. This sets the 'should' # value on states, and otherwise just sets the appropriate parameter. def []=(name,value) if name.is_a?(String) name = name.intern end if name == :name name = self.class.namevar end if value.nil? raise Puppet::Error.new("Got nil value for %s" % name) end case self.class.attrtype(name) when :state if value.is_a?(Puppet::State) self.debug "'%s' got handed a state for '%s'" % [self,name] @states[name] = value else if @states.include?(name) @states[name].should = value else # newstate returns true if it successfully created the state, # false otherwise; I just don't know what to do with that # fact. unless newstate(name, :should => value) #self.info "%s failed" % name end end end when :meta self.newmetaparam(self.class.metaparamclass(name), value) when :param klass = self.class.attrclass(name) # if they've got a method to handle the parameter, then do it that way self.newparam(klass, value) else raise Puppet::Error, "Invalid parameter %s" % [name] end end # remove a state from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) case attr when Puppet::Type if @children.include?(attr) @children.delete(attr) end else if @states.has_key?(attr) @states.delete(attr) elsif @parameters.has_key?(attr) @parameters.delete(attr) elsif @metaparams.has_key?(attr) @metaparams.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end end # iterate across all children, and then iterate across states # we do children first so we're sure that all dependent objects # are checked first # we ignore parameters here, because they only modify how work gets # done, they don't ever actually result in work specifically def each # we want to return the states in the order that each type # specifies it, because it may (as in the case of File#create) # be important if self.class.depthfirst? @children.each { |child| yield child } end self.eachstate { |state| yield state } unless self.class.depthfirst? @children.each { |child| yield child } end end # Recurse deeply through the tree, but only yield types, not states. def delve(&block) self.each do |obj| if obj.is_a? Puppet::Type obj.delve(&block) end end block.call(self) end # iterate across the existing states def eachstate # states() is a private method states().each { |state| yield state } end def devfail(msg) self.fail(Puppet::DevError, msg) end # Throw an error, defaulting to a Puppet::Error def fail(*args) type = nil if args[0].is_a?(Class) type = args.shift else type = Puppet::Error end error = type.new(args.join(" ")) if defined? @line and @line error.line = @line end if defined? @file and @file error.file = @file end raise error end # retrieve the 'is' value for a specified state def is(state) if @states.include?(state) return @states[state].is else return nil end end # retrieve the 'should' value for a specified state def should(state) if @states.include?(state) return @states[state].should else return nil end end # create a log at specified level def log(msg) Puppet::Log.create( :level => @metaparams[:loglevel].value, :message => msg, :source => self ) end # is the instance a managed instance? A 'yes' here means that # the instance was created from the language, vs. being created # in order resolve other questions, such as finding a package # in a list def managed? # Once an object is managed, it always stays managed; but an object # that is listed as unmanaged might become managed later in the process, # so we have to check that every time if defined? @managed and @managed return @managed else @managed = false states.each { |state| if state.should and ! state.class.unmanaged @managed = true break end } return @managed end end # Create a new parameter. def newparam(klass, value = nil) newattr(:param, klass, value) end # Create a new parameter or metaparameter. We'll leave the calling # method to store it appropriately. def newmetaparam(klass, value = nil) newattr(:meta, klass, value) end # The base function that the others wrap. def newattr(type, klass, value = nil) # This should probably be a bit, um, different, but... if type == :state return newstate(klass) end param = klass.new param.parent = self unless value.nil? param.value = value end case type when :meta @metaparams[klass.name] = param when :param @parameters[klass.name] = param else self.devfail("Invalid param type %s" % type) end return param end # create a new state def newstate(name, hash = {}) stateklass = nil if name.is_a?(Class) stateklass = name name = stateklass.name else stateklass = self.class.validstate?(name) unless stateklass self.fail("Invalid state %s" % name) end end if @states.include?(name) hash.each { |var,value| @states[name].send(var.to_s + "=", value) } else #Puppet.warning "Creating state %s for %s" % # [stateklass.name,self.name] begin hash[:parent] = self # make sure the state doesn't have any errors newstate = stateklass.new(hash) @states[name] = newstate return newstate rescue Puppet::Error => detail # the state failed, so just ignore it self.warning "State %s failed: %s" % [name, detail] return false rescue Puppet::DevError => detail # the state failed, so just ignore it self.err "State %s failed: %s" % [name, detail] return false rescue => detail # the state failed, so just ignore it self.err "State %s failed: %s (%s)" % [name, detail, detail.class] return false end end end # return the value of a parameter def parameter(name) unless name.is_a? Symbol name = name.intern end return @parameters[name].value end def parent=(parent) if self.parentof?(parent) devfail "%s[%s] is already the parent of %s[%s]" % [self.class.name, self.title, parent.class.name, parent.title] end @parent = parent end # Add a hook for testing for recursion. def parentof?(child) if (self == child) debug "parent is equal to child" return true elsif defined? @parent and @parent.parentof?(child) debug "My parent is parent of child" return true elsif @children.include?(child) debug "child is already in children array" return true else return false end end def push(*childs) unless defined? @children @children = [] end childs.each { |child| # Make sure we don't have any loops here. if parentof?(child) devfail "Already the parent of %s[%s]" % [child.class.name, child.title] end unless child.is_a?(Puppet::Element) self.debug "Got object of type %s" % child.class self.devfail( "Containers can only contain Puppet::Elements, not %s" % child.class ) end @children.push(child) child.parent = self } end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # Our children remove themselves from our @children array (else the object # we called this on at the top would not be removed), so we duplicate the # array and iterate over that. If we don't do this, only half of the # objects get removed. @children.dup.each { |child| child.remove(rmdeps) } @children.clear # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. [@states, @parameters, @metaparams].each do |hash| hash.each do |name, obj| obj.remove end hash.clear end if rmdeps Puppet::Event::Subscription.dependencies(self).each { |dep| #info "Deleting dependency %s" % dep #begin # self.unsubscribe(dep) #rescue # # ignore failed unsubscribes #end dep.delete } Puppet::Event::Subscription.subscribers(self).each { |dep| #info "Unsubscribing from %s" % dep begin dep.unsubscribe(self) rescue # ignore failed unsubscribes end } end self.class.delete(self) if defined? @parent and @parent @parent.delete(self) @parent = nil end # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end # Is the named state defined? def statedefined?(name) unless name.is_a? Symbol name = name.intern end return @states.include?(name) end # return an actual type by name; to return the value, use 'inst[name]' # FIXME this method should go away def state(name) unless name.is_a? Symbol name = name.intern end return @states[name] end private def states #debug "%s has %s states" % [self,@states.length] tmpstates = [] self.class.states.each { |state| if @states.include?(state.name) tmpstates.push(@states[state.name]) end } unless tmpstates.length == @states.length self.devfail( "Something went very wrong with tmpstates creation" ) end return tmpstates end # instance methods related to instance intrinsics # e.g., initialize() and name() public # Force users to call this, so that we can merge objects if # necessary. FIXME This method should be responsible for most of the # error handling. def self.create(args) # Don't modify the original hash; instead, create a duplicate and modify it. # We have to dup and use the ! so that it stays a TransObject if it is # one. hash = args.dup symbolizehash!(hash) # If we're the base class, then pass the info on appropriately if self == Puppet::Type type = nil if hash.is_a? TransObject type = hash.type else # If we're using the type to determine object type, then delete it if type = hash[:type] hash.delete(:type) end end if type if typeklass = self.type(type) return typeklass.create(hash) else raise Puppet::Error, "Unknown type %s" % type end else raise Puppet::Error, "No type found for %s" % hash.inspect end end # Handle this new object being implicit implicit = hash[:implicit] || false if hash.include?(:implicit) hash.delete(:implicit) end name = nil unless hash.is_a? TransObject hash = self.hash2trans(hash) end # XXX This will have to change when transobjects change to using titles title = hash.name #Puppet.debug "Creating %s[%s]" % [self.name, title] # if the object already exists if self.isomorphic? and retobj = self[title] # if only one of our objects is implicit, then it's easy to see # who wins -- the non-implicit one. if retobj.implicit? and ! implicit Puppet.notice "Removing implicit %s" % retobj.title # Remove all of the objects, but do not remove their subscriptions. retobj.remove(false) # now pass through and create the new object elsif implicit Puppet.notice "Ignoring implicit %s" % title return retobj else # If only one of the objects is being managed, then merge them if retobj.managed? raise Puppet::Error, "%s '%s' is already being managed" % [self.name, title] else retobj.merge(hash) return retobj end # We will probably want to support merging of some kind in # the future, but for now, just throw an error. #retobj.merge(hash) #return retobj end end # create it anew # if there's a failure, destroy the object if it got that far, but raise # the error. begin obj = new(hash) rescue => detail Puppet.err "Could not create %s: %s" % [title, detail.to_s] if obj obj.remove(true) elsif obj = self[title] obj.remove(true) end raise end if implicit obj.implicit = true end # Store the object by title self[obj.title] = obj return obj end # Convert a hash to a TransObject. def self.hash2trans(hash) title = nil if hash.include? :title title = hash[:title] hash.delete(:title) elsif hash.include? self.namevar title = hash[self.namevar] hash.delete(self.namevar) if hash.include? :name raise ArgumentError, "Cannot provide both name and %s to %s" % [self.namevar, self.name] end elsif hash[:name] title = hash[:name] hash.delete :name end unless title raise Puppet::Error, "You must specify a title for objects of type %s" % self.to_s end if hash.include? :type unless self.validattr? :type hash.delete :type end end # okay, now make a transobject out of hash begin trans = TransObject.new(title, self.name.to_s) hash.each { |param, value| trans[param] = value } rescue => detail raise Puppet::Error, "Could not create %s: %s" % [name, detail] end return trans end def self.implicitcreate(hash) unless hash.include?(:implicit) hash[:implicit] = true end if obj = self.create(hash) obj.implicit = true return obj else return nil end end # Is this type's name isomorphic with the object? That is, if the # name conflicts, does it necessarily mean that the objects conflict? # Defaults to true. def self.isomorphic? if defined? @isomorphic return @isomorphic else return true end end # and then make 'new' private class << self private :new end def initvars @children = [] @evalcount = 0 @tags = [] # callbacks are per object and event @callbacks = Hash.new { |chash, key| chash[key] = {} } # states and parameters are treated equivalently from the outside: # as name-value pairs (using [] and []=) # internally, however, parameters are merely a hash, while states # point to State objects # further, the lists of valid states and parameters are defined # at the class level unless defined? @states @states = Hash.new(false) end unless defined? @parameters @parameters = Hash.new(false) end unless defined? @metaparams @metaparams = Hash.new(false) end # set defalts @noop = false # keeping stats for the total number of changes, and how many were # completely sync'ed # this isn't really sufficient either, because it adds lots of special # cases such as failed changes # it also doesn't distinguish between changes from the current transaction # vs. changes over the process lifetime @totalchanges = 0 @syncedchanges = 0 @failedchanges = 0 @inited = true end # initialize the type instance def initialize(hash) unless defined? @inited self.initvars end namevar = self.class.namevar orighash = hash # If we got passed a transportable object, we just pull a bunch of info # directly from it. This is the main object instantiation mechanism. if hash.is_a?(Puppet::TransObject) #self[:name] = hash[:name] [:file, :line, :tags].each { |getter| if hash.respond_to?(getter) setter = getter.to_s + "=" if val = hash.send(getter) self.send(setter, val) end end } # XXX This will need to change when transobjects change to titles. @title = hash.name hash = hash.to_hash elsif hash[:title] # XXX This should never happen @title = hash[:title] hash.delete(:title) end # Before anything else, set our parent if it was included if hash.include?(:parent) @parent = hash[:parent] hash.delete(:parent) end # Munge up the namevar stuff so we only have one value. hash = self.argclean(hash) # If we've got both a title via some other mechanism, set it as an alias. # if defined? @title and @title and ! hash[:name] # if aliases = hash[:alias] # aliases = [aliases] unless aliases.is_a? Array # aliases << @title # hash[:alias] = aliases # else # hash[:alias] = @title # end # end # Let's do the name first, because some things need to happen once # we have the name but before anything else attrs = self.class.allattrs if hash.include?(namevar) #self.send(namevar.to_s + "=", hash[namevar]) self[namevar] = hash[namevar] hash.delete(namevar) if attrs.include?(namevar) attrs.delete(namevar) else self.devfail "My namevar isn\'t a valid attribute...?" end else self.devfail "I was not passed a namevar" end # If the name and title differ, set up an alias if self.name != self.title if obj = self.class[self.name] if self.class.isomorphic? raise Puppet::Error, "%s already exists with name %s" % [obj.title, self.name] end else self.class.alias(self.name, self) end end # The information to cache to disk. We have to do this after # the name is set because it uses the name and/or path, but before # everything else is set because the states need to be able to # retrieve their stored info. #@cache = Puppet::Storage.cache(self) # This is all of our attributes except the namevar. attrs.each { |attr| if hash.include?(attr) begin self[attr] = hash[attr] rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail self.devfail( "Could not set %s on %s: %s" % [attr, self.class.name, detail] ) end hash.delete attr end } # While this could theoretically be set after all of the objects are # created, it seems to make more sense to set them immediately. self.setdefaults if hash.length > 0 self.debug hash.inspect self.fail("Class %s does not accept argument(s) %s" % [self.class.name, hash.keys.join(" ")]) end if self.respond_to?(:validate) self.validate end end # Figure out of there are any objects we can automatically add as # dependencies. def autorequire self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. next unless typeobj = Puppet.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) unless list.is_a?(Array) list = [list] end # Collect the current prereqs list.each { |dep| obj = nil # Support them passing objects directly, to save some effort. if dep.is_a? Puppet::Type type = dep.class.name obj = dep # Now change our dependency to just the string, instead of # the object itself. dep = dep.title else # Skip autorequires that we aren't managing unless obj = typeobj[dep] next end end # Skip autorequires that we already require next if self.requires?(obj) debug "Autorequiring %s %s" % [obj.class.name, obj.title] self[:require] = [type, dep] } #self.info reqs.inspect #self[:require] = reqs } end # Set up all of our autorequires. def finish self.autorequire # Scheduling has to be done when the whole config is instantiated, so # that file order doesn't matter in finding them. self.schedule end # Return a cached value def cached(name) Puppet::Storage.cache(self)[name] #@cache[name] ||= nil end # Cache a value def cache(name, value) Puppet::Storage.cache(self)[name] = value #@cache[name] = value end # Look up the schedule and set it appropriately. This is done after # the instantiation phase, so that the schedule can be anywhere in the # file. def schedule # If we've already set the schedule, then just move on return if self[:schedule].is_a?(Puppet.type(:schedule)) return unless self[:schedule] # Schedules don't need to be scheduled #return if self.is_a?(Puppet.type(:schedule)) # Nor do components #return if self.is_a?(Puppet.type(:component)) if sched = Puppet.type(:schedule)[self[:schedule]] self[:schedule] = sched else self.fail "Could not find schedule %s" % self[:schedule] end end # Check whether we are scheduled to run right now or not. def scheduled? return true if Puppet[:ignoreschedules] return true unless schedule = self[:schedule] # We use 'checked' here instead of 'synced' because otherwise we'll # end up checking most elements most times, because they will generally # have been synced a long time ago (e.g., a file only gets updated # once a month on the server and its schedule is daily; the last sync time # will have been a month ago, so we'd end up checking every run). return schedule.match?(self.cached(:checked).to_i) end # Add a new tag. def tag(tag) tag = tag.intern if tag.is_a? String unless @tags.include? tag @tags << tag end end # Define the initial list of tags. def tags=(list) list = [list] unless list.is_a? Array @tags = list.collect do |t| case t when String: t.intern when Symbol: t else self.warning "Ignoring tag %s of type %s" % [tag.inspect, tag.class] end end + + @tags << self.class.name unless @tags.include?(self.class.name) end # Figure out of any of the specified tags apply to this object. This is an # OR operation. def tagged?(tags) tags = [tags] unless tags.is_a? Array tags = tags.collect { |t| t.intern } return tags.find { |tag| @tags.include? tag } end # Is the specified parameter set? def attrset?(type, attr) case type when :state: return @states.include?(attr) when :param: return @parameters.include?(attr) when :meta: return @metaparams.include?(attr) else self.devfail "Invalid set type %s" % [type] end end # def set(name, value) # send(name.to_s + "=", value) # end # # def get(name) # send(name) # end # For any parameters or states that have defaults and have not yet been # set, set them now. def setdefaults(*ary) self.class.eachattr(*ary) { |klass, type| # not many attributes will have defaults defined, so we short-circuit # those away next unless klass.method_defined?(:default) next if self.attrset?(type, klass.name) obj = self.newattr(type, klass) value = obj.default unless value.nil? #self.debug "defaulting %s to %s" % [obj.name, obj.default] obj.value = value else #self.debug "No default for %s" % obj.name # "obj" is a Parameter. self.delete(obj.name) end } end # Merge new information with an existing object, checking for conflicts # and such. This allows for two specifications of the same object and # the same values, but it's pretty limited right now. The result of merging # states is very different from the result of merging parameters or metaparams. # This is currently unused. def merge(hash) hash.each { |param, value| if param.is_a?(String) param = param.intern end # Of course names are the same, duh. next if param == :name or param == self.class.namevar unless value.is_a?(Array) value = [value] end if @states.include?(param) and oldvals = @states[param].shouldorig unless oldvals.is_a?(Array) oldvals = [oldvals] end # If the values are exactly the same, order and everything, # then it's okay. if oldvals == value return true end # take the intersection newvals = oldvals & value if newvals.empty? self.fail "No common values for %s on %s(%s)" % [param, self.class.name, self.title] elsif newvals.length > 1 self.fail "Too many values for %s on %s(%s)" % [param, self.class.name, self.title] else self.debug "Reduced old values %s and new values %s to %s" % [oldvals.inspect, value.inspect, newvals.inspect] @states[param].should = newvals #self.should = newvals return true end else self[param] = value end } # Set the defaults again, just in case. self.setdefaults end # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name return self[:name] # unless defined? @name and @name # namevar = self.class.namevar # if self.class.validparameter?(namevar) # @name = self[:name] # elsif self.class.validstate?(namevar) # @name = self.should(namevar) # else # self.devfail "Could not find namevar %s for %s" % # [namevar, self.class.name] # end # end # # unless @name # self.devfail "Could not find namevar '%s' for %s" % # [self.class.namevar, self.class.name] # end # # return @name end # Retrieve the title of an object. If no title was set separately, # then use the object's name. def title unless defined? @title and @title namevar = self.class.namevar if self.class.validparameter?(namevar) @title = self[:name] elsif self.class.validstate?(namevar) @title = self.should(namevar) else self.devfail "Could not find namevar %s for %s" % [namevar, self.class.name] end end return @title end # fix any namevar => param translations def argclean(oldhash) # This duplication is here because it might be a transobject. hash = oldhash.dup.to_hash if hash.include?(:parent) hash.delete(:parent) end namevar = self.class.namevar # Do a simple translation for those cases where they've passed :name # but that's not our namevar if hash.include? :name and namevar != :name if hash.include? namevar raise ArgumentError, "Cannot provide both name and %s" % namevar end hash[namevar] = hash[:name] hash.delete(:name) end # Make sure we have a name, one way or another unless hash.include? namevar if defined? @title and @title hash[namevar] = @title else raise Puppet::Error, "Was not passed a namevar or title" end end return hash end # retrieve the current value of all contained states def retrieve # it's important to use the method here, as it follows the order # in which they're defined in the object states().each { |state| state.retrieve } end # convert to a string def to_s self.title end # Convert to a transportable object def to_trans # Collect all of the "is" values retrieve() trans = TransObject.new(self.title, self.class.name) states().each do |state| trans[state.name] = state.is end @parameters.each do |name, param| # Avoid adding each instance name as both the name and the namevar next if param.class.isnamevar? and param.value == self.title trans[name] = param.value end @metaparams.each do |name, param| trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' return trans end # instance methods dealing with actually doing work public # this is a retarded hack method to get around the difference between # component children and file children def self.depthfirst? if defined? @depthfirst return @depthfirst else return false end end # Retrieve the changes associated with all of the states. def statechanges # If we are changing the existence of the object, then none of # the other states matter. changes = [] if @states.include?(:ensure) and ! @states[:ensure].insync? #self.info "ensuring %s from %s" % # [@states[:ensure].should, @states[:ensure].is] changes = [Puppet::StateChange.new(@states[:ensure])] # Else, if the 'ensure' state is correctly absent, then do # nothing elsif @states.include?(:ensure) and @states[:ensure].is == :absent #self.info "Object is correctly absent" return [] else #if @states.include?(:ensure) # self.info "ensure: Is: %s, Should: %s" % # [@states[:ensure].is, @states[:ensure].should] #else # self.info "no ensure state" #end changes = states().find_all { |state| ! state.insync? }.collect { |state| Puppet::StateChange.new(state) } end if Puppet[:debug] and changes.length > 0 self.debug("Changing " + changes.collect { |ch| ch.state.name }.join(",") ) end changes end # this method is responsible for collecting state changes # we always descend into the children before we evaluate our current # states # this returns any changes resulting from testing, thus 'collect' # rather than 'each' def evaluate now = Time.now #Puppet.err "Evaluating %s" % self.path.join(":") unless defined? @evalcount self.err "No evalcount defined on '%s' of type '%s'" % [self.title,self.class] @evalcount = 0 end @evalcount += 1 changes = [] # this only operates on states, not states + children # it's important that we call retrieve() on the type instance, # not directly on the state, because it allows the type to override # the method, like pfile does self.retrieve # states() is a private method, returning an ordered list unless self.class.depthfirst? changes += statechanges() end changes << @children.collect { |child| ch = child.evaluate child.cache(:checked, now) ch } if self.class.depthfirst? changes += statechanges() end changes.flatten! # now record how many changes we've resulted in if changes.length > 0 self.debug "%s change(s)" % [changes.length] end self.cache(:checked, now) return changes.flatten end # if all contained objects are in sync, then we're in sync # FIXME I don't think this is used on the type instances any more, # it's really only used for testing def insync? insync = true if state = @states[:ensure] if state.insync? and state.should == :absent return true end end states.each { |state| unless state.insync? state.debug("Not in sync: %s vs %s" % [state.is.inspect, state.should.inspect]) insync = false #else # state.debug("In sync") end } #self.debug("%s sync status is %s" % [self,insync]) return insync end # Meta-parameter methods: These methods deal with the results # of specifying metaparameters def self.metaparams @@metaparams.collect { |param| param.name } end # Is the parameter in question a meta-parameter? def self.metaparam?(param) @@metaparamhash.include?(param) end # Subscription and relationship methods #def addcallback(object, event, method) # @callbacks[object][event] = method #end # Build the dependencies associated with an individual object. def builddepends # Handle the requires if self[:require] self.handledepends(self[:require], :NONE, nil, true) end # And the subscriptions if self[:subscribe] self.handledepends(self[:subscribe], :ALL_EVENTS, :refresh, true) end if self[:notify] self.handledepends(self[:notify], :ALL_EVENTS, :refresh, false) end if self[:before] self.handledepends(self[:before], :NONE, nil, false) end end # return all objects that we depend on def eachdependency Puppet::Event::Subscription.dependencies(self).each { |dep| yield dep.source } end # return all objects subscribed to the current object def eachsubscriber Puppet::Event::Subscription.subscribers(self).each { |sub| yield sub.target } end def handledepends(requires, event, method, up) # Requires are specified in the form of [type, name], so they're always # an array. But we want them to be an array of arrays. unless requires[0].is_a?(Array) requires = [requires] end requires.each { |rname| # we just have a name and a type, and we need to convert it # to an object... type = nil object = nil tname = rname[0] unless type = Puppet::Type.type(tname) self.fail "Could not find type %s" % tname.inspect end name = rname[1] unless object = type[name] self.fail "Could not retrieve object '%s' of type '%s'" % [name,type] end self.debug("subscribes to %s" % [object]) # Are we requiring them, or vice versa? source = target = nil if up source = object target = self else source = self target = object end # ok, both sides of the connection store some information # we store the method to call when a given subscription is # triggered, but the source object decides whether subargs = { :event => event, :source => source, :target => target } if method and target.respond_to?(method) subargs[:callback] = method end Puppet::Event::Subscription.new(subargs) } end def requires?(object) req = false self.eachdependency { |dep| if dep == object req = true break end } return req end def subscribe(hash) hash[:source] = self Puppet::Event::Subscription.new(hash) # add to the correct area #@subscriptions.push sub end def subscribesto?(object) sub = false self.eachsubscriber { |o| if o == object sub = true break end } return sub end # Unsubscribe from a given object, possibly with a specific event. def unsubscribe(object, event = nil) Puppet::Event::Subscription.dependencies(self).find_all { |sub| if event sub.match?(event) else sub.source == object end }.each { |sub| sub.delete } end # we've received an event # we only support local events right now, so we can pass actual # objects around, including the transaction object # the assumption here is that container objects will pass received # methods on to contained objects # i.e., we don't trigger our children, our refresh() method calls # refresh() on our children def trigger(event, source) trans = event.transaction if @callbacks.include?(source) [:ALL_EVENTS, event.event].each { |eventname| if method = @callbacks[source][eventname] if trans.triggered?(self, method) > 0 next end if self.respond_to?(method) self.send(method) end trans.triggered(self, method) end } end end # Documentation methods def self.paramdoc(param) @paramhash[param].doc end def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Add all of the meta parameters. #newmetaparam(:onerror) do # desc "How to handle errors -- roll back innermost # transaction, roll back entire transaction, ignore, etc. Currently # non-functional." #end newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done. *true*/**false**" munge do |noop| if noop == "true" or noop == true return true elsif noop == "false" or noop == false return false else self.fail("Invalid noop value '%s'" % noop) end end end newmetaparam(:schedule) do desc "On what schedule the object should be managed. You must create a schedule object, and then reference the name of that object to use that for your schedule: schedule { daily: period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => daily } The creation of the schedule object does not need to appear in the configuration before objects that use it." munge do |name| if schedule = Puppet.type(:schedule)[name] return schedule else return name end end end newmetaparam(:check) do desc "States which should have their values retrieved but which should not actually be modified. This is currently used internally, but will eventually be used for querying, so that you could specify that you wanted to check the install state of all packages, and then query the Puppet client daemon to get reports on all packages." munge do |args| # If they've specified all, collect all known states if args == :all args = @parent.class.states.collect do |state| state.name end end unless args.is_a?(Array) args = [args] end unless defined? @parent self.devfail "No parent for %s, %s?" % [self.class, self.name] end args.each { |state| unless state.is_a?(Symbol) state = state.intern end next if @parent.statedefined?(state) stateklass = @parent.class.validstate?(state) unless stateklass raise Puppet::Error, "%s is not a valid attribute for %s" % [state, self.class.name] end next unless stateklass.checkable? @parent.newstate(state) } end end # For each object we require, subscribe to all events that it generates. We # might reduce the level of subscription eventually, but for now... newmetaparam(:require) do desc "One or more objects that this object depends on. This is used purely for guaranteeing that changes to required objects happen before the dependent object. For instance: # Create the destination directory before you copy things down file { \"/usr/local/scripts\": ensure => directory } file { \"/usr/local/scripts/myscript\": source => \"puppet://server/module/myscript\", mode => 755, require => file[\"/usr/local/scripts\"] } Note that Puppet will autorequire everything that it can, and there are hooks in place so that it's easy for elements to add new ways to autorequire objects, so if you think Puppet could be smarter here, let us know. In fact, the above code was redundant -- Puppet will autorequire any parent directories that are being managed; it will automatically realize that the parent directory should be created before the script is pulled down. Currently, exec elements will autorequire their CWD (if it is specified) plus any fully qualified paths that appear in the command. For instance, if you had an ``exec`` command that ran the ``myscript`` mentioned above, the above code that pulls the file down would be automatically listed as a requirement to the ``exec`` code, so that you would always be running againts the most recent version. " # Take whatever dependencies currently exist and add these. # Note that this probably doesn't behave correctly with unsubscribe. munge do |requires| # We need to be two arrays deep... unless requires.is_a?(Array) requires = [requires] end unless requires[0].is_a?(Array) requires = [requires] end if values = @parent[:require] requires = values + requires end requires end end # For each object we require, subscribe to all events that it generates. # We might reduce the level of subscription eventually, but for now... newmetaparam(:subscribe) do desc "One or more objects that this object depends on. Changes in the subscribed to objects result in the dependent objects being refreshed (e.g., a service will get restarted). For instance: class nagios { file { \"/etc/nagios/nagios.conf\": source => \"puppet://server/module/nagios.conf\", alias => nagconf # just to make things easier for me } service { nagios: running => true, subscribe => file[nagconf] } } " munge do |requires| if values = @parent[:subscribe] requires = values + requires end requires # @parent.handledepends(requires, :ALL_EVENTS, :refresh) end end newmetaparam(:loglevel) do desc "Sets the level that information will be logged. The log levels have the biggest impact when logs are sent to syslog (which is currently the default)." defaultto :notice newvalues(*Puppet::Log.levels) newvalues(:verbose) munge do |loglevel| val = super(loglevel) if val == :verbose val = :info end val end end newmetaparam(:alias) do desc "Creates an alias for the object. Puppet uses this internally when you provide a symbolic name: file { sshdconfig: path => $operatingsystem ? { solaris => \"/usr/local/etc/ssh/sshd_config\", default => \"/etc/ssh/sshd_config\" }, source => \"...\" } service { sshd: subscribe => file[sshdconfig] } When you use this feature, the parser sets ``sshdconfig`` as the name, and the library sets that as an alias for the file so the dependency lookup for ``sshd`` works. You can use this parameter yourself, but note that only the library can use these aliases; for instance, the following code will not work: file { \"/etc/ssh/sshd_config\": owner => root, group => root, alias => sshdconfig } file { sshdconfig: mode => 644 } There's no way here for the Puppet parser to know that these two stanzas should be affecting the same file. See the [language tutorial][] for more information. [language tutorial]: languagetutorial.html " munge do |aliases| unless aliases.is_a?(Array) aliases = [aliases] end @parent.info "Adding aliases %s" % aliases.collect { |a| a.inspect }.join(", ") aliases.each do |other| if obj = @parent.class[other] unless obj == @parent self.fail( "%s can not create alias %s: object already exists" % [@parent.title, other] ) end next end @parent.class.alias(other, @parent) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated element. While all elements are automatically tagged with as much information as possible (e.g., each class and component containing the element), it can be useful to add your own tags to a given element. Tags are currently useful for things like applying a subset of a host's configuration: puppetd --test --tag mytag This way, when you're testing a configuration you can run just the portion you're testing." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @parent.tag(tag) end end end newmetaparam(:notify) do desc %{This parameter is the opposite of **subscribe** -- it sends events to the specified object: file { "/etc/sshd_config": source => "....", notify => service[sshd] } service { sshd: ensure => running } This will restart the sshd service if the sshd config file changes.} # Take whatever dependencies currently exist and add these. munge do |notifies| # We need to be two arrays deep... unless notifies.is_a?(Array) notifies = [notifies] end unless notifies[0].is_a?(Array) notifies = [notifies] end if values = @parent[:notify] notifies = values + notifies end notifies end end newmetaparam(:before) do desc %{This parameter is the opposite of **require** -- it guarantees that the specified object is applied later than the specifying object: file { "/var/nagios/configuration": source => "...", recurse => true, before => exec["nagios-rebuid"] } exec { "nagios-rebuild": command => "/usr/bin/make", cwd => "/var/nagios/configuration" } This will make sure all of the files are up to date before the make command is run.} # Take whatever dependencies currently exist and add these. munge do |notifies| # We need to be two arrays deep... unless notifies.is_a?(Array) notifies = [notifies] end unless notifies[0].is_a?(Array) notifies = [notifies] end if values = @parent[:notify] notifies = values + notifies end notifies end end end # Puppet::Type end require 'puppet/statechange' require 'puppet/provider' require 'puppet/type/component' require 'puppet/type/cron' require 'puppet/type/exec' require 'puppet/type/group' require 'puppet/type/package' require 'puppet/type/pfile' require 'puppet/type/pfilebucket' require 'puppet/type/schedule' require 'puppet/type/service' require 'puppet/type/symlink' require 'puppet/type/user' require 'puppet/type/tidy' require 'puppet/type/parsedtype' # $Id$ diff --git a/lib/puppet/type/state.rb b/lib/puppet/type/state.rb index e372db6db..e5aa3b1b6 100644 --- a/lib/puppet/type/state.rb +++ b/lib/puppet/type/state.rb @@ -1,439 +1,452 @@ # The virtual base class for states, which are the self-contained building # blocks for actually doing work on the system. require 'puppet' require 'puppet/element' require 'puppet/statechange' require 'puppet/parameter' module Puppet class State < Puppet::Parameter attr_accessor :is # Because 'should' uses an array, we have a special method for handling # it. We also want to keep copies of the original values, so that # they can be retrieved and compared later when merging. attr_reader :shouldorig class << self attr_accessor :unmanaged attr_reader :name def checkable @checkable = true end def uncheckable @checkable = false end def checkable? if defined? @checkable return @checkable else return true end end end # Only retrieve the event, don't autogenerate one. def self.event(value) if hash = @parameteroptions[value] hash[:event] else nil end end # Create the value management variables. def self.initvars @parametervalues = {} @aliasvalues = {} @parameterregexes = {} @parameteroptions = {} end # Define a new valid value for a state. You must provide the value itself, # usually as a symbol, or a regex to match the value. # # The first argument to the method is either the value itself or a regex. # The second argument is an option hash; valid options are: # * :event: The event that should be returned when this value is set. def self.newvalue(name, options = {}, &block) name = name.intern if name.is_a? String @parameteroptions[name] = {} paramopts = @parameteroptions[name] # Symbolize everything options.each do |opt, val| paramopts[symbolize(opt)] = symbolize(val) end case name when Symbol if @parametervalues.include?(name) Puppet.warning "%s reassigning value %s" % [self.name, name] end @parametervalues[name] = block method = "set_" + name.to_s settor = paramopts[:settor] || (self.name.to_s + "=") define_method(method, &block) when Regexp # The regexes are handled in parameter.rb @parameterregexes[name] = block else raise ArgumentError, "Invalid value %s of type %s" % [name, name.class] end end # How should a state change be printed as a string? def change_to_s begin if @is == :absent return "defined '%s' as '%s'" % [self.name, self.should_to_s] elsif self.should == :absent or self.should == [:absent] return "undefined %s from '%s'" % [self.name, self.is_to_s] else return "%s changed '%s' to '%s'" % [self.name, self.is_to_s, self.should_to_s] end rescue Puppet::Error, Puppet::DevError raise rescue => detail raise Puppet::DevError, "Could not convert change %s to string: %s" % [self.name, detail] end end # initialize our state def initialize(hash) super() @is = nil unless hash.include?(:parent) self.devfail "State %s was not passed a parent" % self end @parent = hash[:parent] if hash.include?(:should) self.should = hash[:should] end if hash.include?(:is) self.is = hash[:is] end end def inspect str = "State('%s', " % self.name if self.is str += "@is = '%s', " % [self.is] else str += "@is = nil, " end if defined? @should and @should str += "@should = '%s')" % @should.join(", ") else str += "@should = nil)" end end # Determine whether the state is in-sync or not. If @should is # not defined or is set to a non-true value, then we do not have # a valid value for it and thus consider the state to be in-sync # since we cannot fix it. Otherwise, we expect our should value # to be an array, and if @is matches any of those values, then # we consider it to be in-sync. def insync? #debug "%s value is '%s', should be '%s'" % # [self,self.is.inspect,self.should.inspect] unless defined? @should and @should return true end unless @should.is_a?(Array) self.devfail "%s's should is not array" % self.class.name end # an empty array is analogous to no should values if @should.empty? return true end # Look for a matching value @should.each { |val| if @is == val return true end } # otherwise, return false return false end # because the @should and @is vars might be in weird formats, # we need to set up a mechanism for pretty printing of the values # default to just the values, but this way individual states can # override these methods def is_to_s @is end # Send a log message. def log(msg) unless @parent[:loglevel] self.devfail "Parent %s has no loglevel" % @parent.name end Puppet::Log.create( :level => @parent[:loglevel], :message => msg, :source => self ) end # each state class must define the name() method, and state instances # do not change that name # this implicitly means that a given object can only have one state # instance of a given state class def name return self.class.name end # for testing whether we should actually do anything def noop unless defined? @noop @noop = false end tmp = @noop || self.parent.noop || Puppet[:noop] || false #debug "noop is %s" % tmp return tmp end # return the full path to us, for logging and rollback; not currently # used def path if defined? @parent and @parent return [@parent.path, self.name].join("/") else return self.name end end # Retrieve the parent's provider. Some types don't have providers, in which # case we return the parent object itself. def provider @parent.provider || @parent end # By default, call the method associated with the state name on our # provider. In other words, if the state name is 'gid', we'll call # 'provider.gid' to retrieve the current value. def retrieve @is = provider.send(self.class.name) end # Call the method associated with a given value. def set if self.insync? self.log "already in sync" return nil end value = self.should method = "set_" + value.to_s event = nil if self.respond_to?(method) self.debug "setting %s (currently %s)" % [value, self.is] begin event = self.send(method) rescue Puppet::Error raise rescue => detail if Puppet[:trace] puts detail.backtrace end self.fail "Could not set %s on %s: %s" % [value, self.class.name, detail] end elsif ary = self.class.match?(value) # FIXME It'd be better here to define a method, so that # the blocks could return values. event = self.instance_eval(&ary[1]) else begin provider.send(self.class.name.to_s + "=", self.should) rescue NoMethodError self.fail "The %s provider can not handle attribute %s" % [provider.class.name, self.class.name] end end if setevent = self.class.event(value) return setevent else if event and event.is_a?(Symbol) if event == :nochange return nil else return event end else # Return the appropriate event. event = case self.should when :present: (@parent.class.name.to_s + "_created").intern when :absent: (@parent.class.name.to_s + "_removed").intern else (@parent.class.name.to_s + "_changed").intern end #self.log "made event %s because 'should' is %s, 'is' is %s" % # [event, self.should.inspect, self.is.inspect] return event end end end # Only return the first value def should if defined? @should unless @should.is_a?(Array) self.devfail "should for %s on %s is not an array" % [self.class.name, @parent.name] end return @should[0] else return nil end end # Set the should value. def should=(values) unless values.is_a?(Array) values = [values] end @shouldorig = values if self.respond_to?(:validate) values.each { |val| validate(val) } end if self.respond_to?(:munge) @should = values.collect { |val| self.munge(val) } else @should = values end end def should_to_s if defined? @should @should.join(" ") else return nil end end # The default 'sync' method only selects among a list of registered # values. def sync if self.insync? self.info "already in sync" return nil #else #self.info "%s vs %s" % [self.is.inspect, self.should.inspect] end unless self.class.values self.devfail "No values defined for %s" % self.class.name end # Set ourselves to whatever our should value is. self.set end + # The states need to return tags so that logs correctly collect them. + def tags + unless defined? @tags + @tags = [] + # This might not be true in testing + if @parent.respond_to? :tags + @tags = @parent.tags + end + @tags << self.name + end + @tags + end + def to_s return "%s(%s)" % [@parent.name,self.name] end # This state will get automatically added to any type that responds # to the methods 'exists?', 'create', and 'destroy'. class Ensure < Puppet::State @name = :ensure def self.defaultvalues newvalue(:present) do @parent.create end newvalue(:absent) do @parent.destroy end # This doc will probably get overridden @doc ||= "The basic state that the object should be in." end def self.inherited(sub) # Add in the two states that everyone will have. sub.class_eval do end end def change_to_s begin if @is == :absent return "created" elsif self.should == :absent return "removed" else return "%s changed '%s' to '%s'" % [self.name, self.is_to_s, self.should_to_s] end rescue Puppet::Error, Puppet::DevError raise rescue => detail raise Puppet::DevError, "Could not convert change %s to string: %s" % [self.name, detail] end end def retrieve # XXX This is a problem -- whether the object exists or not often # depends on the results of other states, yet we're the first state # to get checked, which means that those other states do not have # @is values set. This seems to be the source of quite a few bugs, # although they're mostly logging bugs, not functional ones. if @parent.exists? @is = :present else @is = :absent end end # If they're talking about the thing at all, they generally want to # say it should exist. #defaultto :present defaultto do if @parent.managed? :present else nil end end end end end # $Id$ diff --git a/test/other/log.rb b/test/other/log.rb index 750014e05..cb049b30d 100644 --- a/test/other/log.rb +++ b/test/other/log.rb @@ -1,222 +1,235 @@ require 'puppet' require 'puppet/log' require 'puppettest' # $Id$ class TestLog < Test::Unit::TestCase include PuppetTest def setup super @oldloglevel = Puppet::Log.level Puppet::Log.close end def teardown super Puppet::Log.close Puppet::Log.level = @oldloglevel Puppet::Log.newdestination(:console) end def getlevels levels = nil assert_nothing_raised() { levels = [] Puppet::Log.eachlevel { |level| levels << level } } # Don't test the top levels; too annoying return levels.reject { |level| level == :emerg or level == :crit } end def mkmsgs(levels) levels.collect { |level| next if level == :alert assert_nothing_raised() { Puppet::Log.new( :level => level, :source => "Test", :message => "Unit test for %s" % level ) } } end def test_logfile fact = nil levels = nil Puppet::Log.level = :debug levels = getlevels logfile = tempfile() fact = nil assert_nothing_raised() { Puppet::Log.newdestination(logfile) } msgs = mkmsgs(levels) assert(msgs.length == levels.length) Puppet::Log.close count = 0 assert(FileTest.exists?(logfile), "Did not create logfile") assert_nothing_raised() { File.open(logfile) { |of| count = of.readlines.length } } assert(count == levels.length - 1) # skip alert end def test_syslog levels = nil assert_nothing_raised() { levels = getlevels.reject { |level| level == :emerg || level == :crit } } assert_nothing_raised() { Puppet::Log.newdestination("syslog") } # there's really no way to verify that we got syslog messages... msgs = mkmsgs(levels) assert(msgs.length == levels.length) end def test_consolelog fact = nil levels = getlevels assert_nothing_raised() { Puppet::Log.newdestination(:console) } msgs = mkmsgs(levels) assert(msgs.length == levels.length) Puppet::Log.close end def test_levelmethods assert_nothing_raised() { Puppet::Log.newdestination("/dev/null") } getlevels.each { |level| assert_nothing_raised() { Puppet.send(level,"Testing for %s" % level) } } end def test_output Puppet::Log.level = :notice assert(Puppet.err("This is an error").is_a?(Puppet::Log)) assert(Puppet.debug("This is debugging").nil?) Puppet::Log.level = :debug assert(Puppet.err("This is an error").is_a?(Puppet::Log)) assert(Puppet.debug("This is debugging").is_a?(Puppet::Log)) end def test_creatingdirs dir = tempfile() file = File.join(dir, "logfile") Puppet::Log.newdestination file Puppet.info "testing logs" assert(FileTest.directory?(dir)) assert(FileTest.file?(file)) end def test_logtags path = tempfile File.open(path, "w") { |f| f.puts "yayness" } file = Puppet.type(:file).create( :path => path, - :check => [:owner, :group, :mode, :checksum] + :check => [:owner, :group, :mode, :checksum], + :ensure => :file ) file.tags = %w{this is a test} + state = file.state(:ensure) + assert(state, "Did not get state") log = nil assert_nothing_raised { log = Puppet::Log.new( :level => :info, - :source => file, + :source => state, :message => "A test message" ) } - assert(log.tags, "Got no tags") + # Now yaml and de-yaml it, and test again + yamllog = YAML.load(YAML.dump(log)) + + {:log => log, :yaml => yamllog}.each do |type, msg| + assert(msg.tags, "Got no tags") + + msg.tags.each do |tag| + assert(msg.tagged?(tag), "Was not tagged with %s" % tag) + end + + assert_equal(msg.tags, state.tags, "Tags were not equal") + assert_equal(msg.source, state.path, "Source was not set correctly") + end - assert_equal(log.tags, file.tags, "Tags were not equal") - assert_equal(log.source, file.path, "Source was not set correctly") end # Verify that we can pass strings that match printf args def test_percentlogs Puppet::Log.newdestination :syslog assert_nothing_raised { Puppet::Log.new( :level => :info, :message => "A message with %s in it" ) } end # Verify that the error and source are always strings def test_argsAreStrings msg = nil file = Puppet.type(:file).create( :path => tempfile(), :check => %w{owner group} ) assert_nothing_raised { msg = Puppet::Log.new(:level => :info, :message => "This is a message") } assert_nothing_raised { msg.source = file } assert_instance_of(String, msg.to_s) assert_instance_of(String, msg.source) end # Verify that loglevel behaves as one expects def test_loglevel path = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file" ) assert_nothing_raised { assert_equal(:notice, file[:loglevel]) } assert_nothing_raised { file[:loglevel] = "warning" } assert_nothing_raised { assert_equal(:warning, file[:loglevel]) } end def test_destination_matching dest = nil assert_nothing_raised { dest = Puppet::Log.newdesttype("Destine") do def handle(msg) puts msg end end } [:destine, "Destine", "destine"].each do |name| assert(dest.match?(name), "Did not match %s" % name.inspect) end assert_nothing_raised { dest.match(:yayness) } assert(dest.match("Yayness"), "Did not match yayness") Puppet::Log.close(dest) end end diff --git a/test/types/state.rb b/test/types/state.rb index c3ea0965f..8aad4c101 100644 --- a/test/types/state.rb +++ b/test/types/state.rb @@ -1,124 +1,145 @@ require 'puppet/type' require 'puppettest' class TestState < Test::Unit::TestCase include PuppetTest def newinst(state) inst = nil assert_nothing_raised { return state.new(:parent => nil) } end def newstate(name = :fakestate) assert_nothing_raised { state = Class.new(Puppet::State) do @name = :fakeparam end state.initvars return state } end def test_newvalue state = newstate() # These are bogus because they don't define events. :/ assert_nothing_raised { state.newvalue(:one) do @is = 1 end } assert_nothing_raised { state.newvalue("two") do @is = 2 end } inst = newinst(state) assert_nothing_raised { inst.should = "one" } assert_equal(:one, inst.should) ret = nil assert_nothing_raised { inst.set_one } assert_equal(1, inst.is) assert_nothing_raised { inst.should = :two } assert_equal(:two, inst.should) assert_nothing_raised { inst.set_two } assert_equal(2, inst.is) end def test_newstatevaluewithregexes state = newstate() assert_nothing_raised { state.newvalue(/^\w+$/) do @is = self.should.upcase return :regex_matched end } inst = newinst(state) assert_nothing_raised { inst.should = "yayness" } assert_equal("yayness", inst.should) assert_nothing_raised { inst.sync } assert_equal("yayness".upcase, inst.is) end def test_newvalue_event_option state = newstate() assert_nothing_raised do state.newvalue(:myvalue, :event => :fake_valued) do @is = :valued end state.newvalue(:other, :event => "fake_other") do @is = :valued end end inst = newinst(state) assert_nothing_raised { inst.should = :myvalue } ret = nil assert_nothing_raised { ret = inst.sync } assert_equal(:fake_valued, ret, "Event did not get returned correctly") assert_nothing_raised { inst.should = :other } assert_nothing_raised { ret = inst.sync } assert_equal(:fake_other, ret, "Event did not get returned correctly") end + + def test_tags + obj = "yay" + metaobj = class << obj; self; end + + metaobj.send(:attr_accessor, :tags) + + tags = [:some, :tags, :for, :testing] + obj.tags = tags + + stateklass = newstate + + inst = nil + assert_nothing_raised do + inst = stateklass.new(:parent => obj) + end + + assert_nothing_raised do + assert_equal(tags + [inst.name], inst.tags) + end + end end # $Id$ diff --git a/test/types/type.rb b/test/types/type.rb index f8785fd3b..929fc616e 100644 --- a/test/types/type.rb +++ b/test/types/type.rb @@ -1,750 +1,760 @@ require 'puppet/type' require 'puppettest' class TestType < Test::Unit::TestCase include PuppetTest def test_typemethods Puppet::Type.eachtype { |type| name = nil assert_nothing_raised("Searching for name for %s caused failure" % type.to_s) { name = type.name } assert(name, "Could not find name for %s" % type.to_s) assert_equal( type, Puppet::Type.type(name), "Failed to retrieve %s by name" % name ) # Skip types with no parameters or valid states #unless ! type.parameters.empty? or ! type.validstates.empty? # next #end assert_nothing_raised { assert( type.namevar, "Failed to retrieve namevar for %s" % name ) assert_not_nil( type.states, "States for %s are nil" % name ) assert_not_nil( type.validstates, "Valid states for %s are nil" % name ) } } end def test_stringvssymbols file = nil path = tempfile() assert_nothing_raised() { system("rm -f %s" % path) file = Puppet.type(:file).create( :path => path, :ensure => "file", :recurse => true, :checksum => "md5" ) } assert_nothing_raised() { file.retrieve } assert_nothing_raised() { file.evaluate } Puppet.type(:file).clear assert_nothing_raised() { system("rm -f %s" % path) file = Puppet.type(:file).create( "path" => path, "ensure" => "file", "recurse" => true, "checksum" => "md5" ) } assert_nothing_raised() { file.retrieve } assert_nothing_raised() { file[:path] } assert_nothing_raised() { file["path"] } assert_nothing_raised() { file[:recurse] } assert_nothing_raised() { file["recurse"] } assert_nothing_raised() { file.evaluate } end # This was supposed to test objects whose name was a state, but that # fundamentally doesn't make much sense, and we now don't have any such # types. def disabled_test_nameasstate # currently groups are the only objects with the namevar as a state group = nil assert_nothing_raised { group = Puppet.type(:group).create( :name => "testing" ) } assert_equal("testing", group.name, "Could not retrieve name") end # Verify that values get merged correctly def test_mergestatevalues file = tempfile() # Create the first version assert_nothing_raised { Puppet.type(:file).create( :path => file, :owner => ["root", "bin"] ) } # Make sure no other statements are allowed assert_raise(Puppet::Error) { Puppet.type(:file).create( :path => file, :group => "root" ) } end # Verify that aliasing works def test_aliasing file = tempfile() baseobj = nil assert_nothing_raised { baseobj = Puppet.type(:file).create( :name => file, :ensure => "file", :alias => ["funtest"] ) } # Verify our adding ourselves as an alias isn't an error. assert_nothing_raised { baseobj[:alias] = file } assert_instance_of(Puppet.type(:file), Puppet.type(:file)["funtest"], "Could not retrieve alias") end # Verify that requirements don't depend on file order def test_prereqorder one = tempfile() two = tempfile() twoobj = nil oneobj = nil assert_nothing_raised("Could not create prereq that doesn't exist yet") { twoobj = Puppet.type(:file).create( :name => two, :require => [:file, one] ) } assert_nothing_raised { oneobj = Puppet.type(:file).create( :name => one ) } comp = newcomp(twoobj, oneobj) assert_nothing_raised { comp.finalize } assert(twoobj.requires?(oneobj), "Requirement was not created") end # Verify that names are aliases, not equivalents def test_nameasalias file = nil # Create the parent dir, so we make sure autorequiring the parent dir works parentdir = tempfile() dir = Puppet.type(:file).create( :name => parentdir, :ensure => "directory" ) assert_apply(dir) path = File.join(parentdir, "subdir") name = "a test file" transport = Puppet::TransObject.new(name, "file") transport[:path] = path transport[:ensure] = "file" assert_nothing_raised { file = transport.to_type } assert_equal(path, file[:path]) assert_equal(name, file.title) assert_nothing_raised { file.retrieve } assert_apply(file) assert(Puppet.type(:file)[name], "Could not look up object by name") end def test_ensuredefault user = nil assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestAA", :check => [:uid] ) } # make sure we don't get :ensure for unmanaged files assert(! user.state(:ensure), "User got an ensure state") assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestAA", :comment => "Testingness" ) } # but make sure it gets added once we manage them assert(user.state(:ensure), "User did not add ensure state") assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestBB", :comment => "A fake user" ) } # and make sure managed objects start with them assert(user.state(:ensure), "User did not get an ensure state") end # Make sure removal works def test_remove objects = {} top = Puppet.type(:component).create(:name => "top") objects[top.class] = top base = tempfile() # now make a two-tier, 5 piece tree %w{a b}.each do |letter| name = "comp%s" % letter comp = Puppet.type(:component).create(:name => name) top.push comp objects[comp.class] = comp 5.times do |i| file = base + letter + i.to_s obj = Puppet.type(:file).create(:name => file, :ensure => "file") comp.push obj objects[obj.class] = obj end end assert_nothing_raised do top.remove end objects.each do |klass, obj| assert_nil(klass[obj.name], "object %s was not removed" % obj.name) end end # Verify that objects can't be their own children. def test_object_recursion comp = Puppet.type(:component).create(:name => "top") file = Puppet.type(:file).create(:path => tempfile, :ensure => :file) assert_raise(Puppet::DevError) do comp.push(comp) end assert_raise(Puppet::DevError) do file.push(file) end assert_raise(Puppet::DevError) do comp.parent = comp end assert_raise(Puppet::DevError) do file.parent = file end assert_nothing_raised { comp.push(file) } assert_raise(Puppet::DevError) do file.push(comp) end assert_raise(Puppet::DevError) do comp.parent = file end end def test_loadplugins names = %w{loadedplugin1 loadplugin2 loadplugin3} dirs = [] 3.times { dirs << tempfile() } # Set plugindest to something random Puppet[:plugindest] = tempfile() Puppet[:pluginpath] = dirs.join(":") names.each do |name| dir = dirs.shift Dir.mkdir(dir) # Create an extra file for later [name, name + "2ness"].each do |n| file = File.join(dir, n + ".rb") File.open(file, "w") do |f| f.puts %{Puppet::Type.newtype('#{n}') do newparam(:argument) do isnamevar end end } end end assert(Puppet::Type.type(name), "Did not get loaded plugin") assert_nothing_raised { Puppet::Type.type(name).create( :name => "myname" ) } end # Now make sure the plugindest got added to our pluginpath assert(Puppet[:pluginpath].split(":").include?(Puppet[:plugindest]), "Plugin dest did not get added to plugin path") # Now make sure it works with just a single path, using the extra files # created above. Puppet[:pluginpath] = Puppet[:pluginpath].split(":")[0] assert(Puppet::Type.type("loadedplugin12ness"), "Did not get loaded plugin") end def test_newtype_methods assert_nothing_raised { Puppet::Type.newtype(:mytype) do newparam(:wow) do isnamevar end end } assert(Puppet::Type.respond_to?(:newmytype), "new method did not get created") obj = nil assert_nothing_raised { obj = Puppet::Type.newmytype(:wow => "yay") } assert(obj.is_a?(Puppet::Type.type(:mytype)), "Obj is not the correct type") # Now make the type again, just to make sure it works on refreshing. assert_nothing_raised { Puppet::Type.newtype(:mytype) do newparam(:yay) do isnamevar end end } obj = nil # Make sure the old class was thrown away and only the new one is sitting # around. assert_raise(Puppet::Error) { obj = Puppet::Type.newmytype(:wow => "yay") } assert_nothing_raised { obj = Puppet::Type.newmytype(:yay => "yay") } # Now make sure that we don't replace existing, non-type methods parammethod = Puppet::Type.method(:newparam) assert_nothing_raised { Puppet::Type.newtype(:param) do newparam(:rah) do isnamevar end end } assert_equal(parammethod, Puppet::Type.method(:newparam), "newparam method got replaced by newtype") end def test_notify_metaparam file = Puppet::Type.newfile( :path => tempfile(), :notify => ["exec", "notifytest"], :ensure => :file ) path = tempfile() exec = Puppet::Type.newexec( :title => "notifytest", :path => "/usr/bin:/bin", :command => "touch #{path}", :refreshonly => true ) assert_apply(file, exec) assert(exec.requires?(file), "Notify did not correctly set up the requirement chain.") assert(FileTest.exists?(path), "Exec path did not get created.") end def test_before_metaparam file = Puppet::Type.newfile( :path => tempfile(), :before => ["exec", "beforetest"], :content => "yaytest" ) path = tempfile() exec = Puppet::Type.newexec( :title => "beforetest", :command => "/bin/cp #{file[:path]} #{path}" ) assert_apply(file, exec) assert(exec.requires?(file), "Before did not correctly set up the requirement chain.") assert(FileTest.exists?(path), "Exec path did not get created.") assert_equal("yaytest", File.read(path), "Exec did not correctly copy file.") end def test_newstate_options # Create a type with a fake provider providerclass = Class.new do def method_missing(method, *args) return method end end self.class.const_set("ProviderClass", providerclass) type = Puppet::Type.newtype(:mytype) do newparam(:name) do isnamevar end def provider @provider ||= ProviderClass.new @provider end end # Now make a state with no options. state = nil assert_nothing_raised do state = type.newstate(:noopts) do end end # Now create an instance obj = type.create(:name => :myobj) inst = state.new(:parent => obj) # And make sure it's correctly setting @is ret = nil assert_nothing_raised { ret = inst.retrieve } assert_equal(:noopts, inst.is) # Now create a state with a different way of doing it state = nil assert_nothing_raised do state = type.newstate(:setretrieve, :retrieve => :yayness) end inst = state.new(:parent => obj) # And make sure it's correctly setting @is ret = nil assert_nothing_raised { ret = inst.retrieve } assert_equal(:yayness, inst.is) end def test_name_vs_title path = tempfile() trans = nil assert_nothing_raised { trans = Puppet::TransObject.new(path, :file) } file = nil assert_nothing_raised { file = Puppet::Type.newfile(trans) } assert(file.respond_to?(:title), "No 'title' method") assert(file.respond_to?(:name), "No 'name' method") assert_equal(file.title, file.name, "Name and title were not marked equal") assert_nothing_raised { file.title = "My file" } assert_equal("My file", file.title) assert_equal(path, file.name) end # Make sure the title is sufficiently differentiated from the namevar. def test_title_at_creation_with_hash file = nil fileclass = Puppet::Type.type(:file) path = tempfile() assert_nothing_raised do file = fileclass.create( :title => "Myfile", :path => path ) end assert_equal("Myfile", file.title, "Did not get correct title") assert_equal(path, file[:name], "Did not get correct name") file = nil Puppet::Type.type(:file).clear # Now make sure we can specify both and still get the right answers assert_nothing_raised do file = fileclass.create( :title => "Myfile", :name => path ) end assert_instance_of(fileclass, file) assert_equal("Myfile", file.title, "Did not get correct title") assert_equal(path, file[:name], "Did not get correct name") end # Make sure the "create" class method behaves appropriately. def test_class_create title = "Myfile" validate = proc do |element| assert(element, "Did not create file") assert_instance_of(Puppet::Type.type(:file), element) assert_equal(title, element.title, "Title is not correct") end type = :file args = {:path => tempfile(), :owner => "root"} trans = Puppet::TransObject.new(title, type) args.each do |name, val| trans[name] = val end # First call it on the appropriate typeclass obj = nil assert_nothing_raised do obj = Puppet::Type.type(:file).create(trans) end validate.call(obj) # Now try it using the class method on Type oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear assert_nothing_raised { obj = Puppet::Type.create(trans) } validate.call(obj) assert(oldid != obj.object_id, "Got same object back") # Now try the same things with hashes instead of a transobject oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear hash = { :type => :file, :title => "Myfile", :path => tempfile(), :owner => "root" } # First call it on the appropriate typeclass obj = nil assert_nothing_raised do obj = Puppet::Type.type(:file).create(hash) end validate.call(obj) assert_equal(:file, obj.should(:type), "Type param did not pass through") assert(oldid != obj.object_id, "Got same object back") # Now try it using the class method on Type oldid = obj.object_id obj = nil Puppet::Type.type(:file).clear assert_nothing_raised { obj = Puppet::Type.create(hash) } validate.call(obj) assert(oldid != obj.object_id, "Got same object back") assert_nil(obj.should(:type), "Type param passed through") end def test_multiplenames obj = nil path = tempfile() assert_raise ArgumentError do obj = Puppet::Type.type(:file).create( :name => path, :path => path ) end end def test_title_and_name obj = nil path = tempfile() fileobj = Puppet::Type.type(:file) assert_nothing_raised do obj = fileobj.create( :title => "myfile", :path => path ) end assert_equal(obj, fileobj["myfile"], "Could not retrieve obj by title") assert_equal(obj, fileobj[path], "Could not retrieve obj by name") end # Make sure default providers behave correctly def test_defaultproviders # Make a fake type type = Puppet::Type.newtype(:defaultprovidertest) do newparam(:name) do end end basic = type.provide(:basic) do defaultfor :operatingsystem => :somethingelse, :operatingsystemrelease => :yayness end assert_equal(basic, type.defaultprovider) type.defaultprovider = nil greater = type.provide(:greater) do defaultfor :operatingsystem => Facter.value("operatingsystem") end assert_equal(greater, type.defaultprovider) end # Make sure that we can have multiple isomorphic objects with the same name, # but not with non-isomorphic objects. def test_isomorphic_names # First do execs, since they're not isomorphic. echo = Puppet::Util.binary "echo" exec1 = exec2 = nil assert_nothing_raised do exec1 = Puppet::Type.type(:exec).create( :title => "exec1", :command => "#{echo} funtest" ) end assert_nothing_raised do exec2 = Puppet::Type.type(:exec).create( :title => "exec2", :command => "#{echo} funtest" ) end assert_apply(exec1, exec2) # Now do files, since they are. This should fail. file1 = file2 = nil path = tempfile() assert_nothing_raised do file1 = Puppet::Type.type(:file).create( :title => "file1", :path => path, :content => "yayness" ) end # This will fail, but earlier systems will catch it. assert_raise(Puppet::Error) do file2 = Puppet::Type.type(:file).create( :title => "file2", :path => path, :content => "rahness" ) end assert(file1, "Did not create first file") assert_nil(file2, "Incorrectly created second file") end + + def test_tags + obj = Puppet::Type.type(:file).create(:path => tempfile()) + + tags = [:some, :test, :tags] + + obj.tags = tags + + assert_equal(tags + [:file], obj.tags) + end end # $Id$