diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb index f40c603b4..96f1faac8 100644 --- a/lib/puppet/file_serving/metadata.rb +++ b/lib/puppet/file_serving/metadata.rb @@ -1,147 +1,142 @@ require 'puppet' require 'puppet/indirector' require 'puppet/file_serving' require 'puppet/file_serving/base' require 'puppet/util/checksums' # A class that handles retrieving file metadata. class Puppet::FileServing::Metadata < Puppet::FileServing::Base include Puppet::Util::Checksums extend Puppet::Indirector indirects :file_metadata, :terminus_class => :selector attr_reader :path, :owner, :group, :mode, :checksum_type, :checksum, :ftype, :destination PARAM_ORDER = [:mode, :ftype, :owner, :group] def checksum_type=(type) raise(ArgumentError, "Unsupported checksum type #{type}") unless Puppet::Util::Checksums.respond_to?("#{type}_file") @checksum_type = type end class MetaStat extend Forwardable def initialize(stat, source_permissions = nil) @stat = stat @source_permissions_ignore = (!source_permissions || source_permissions == :ignore) end def owner @source_permissions_ignore ? Process.euid : @stat.uid end def group @source_permissions_ignore ? Process.egid : @stat.gid end def mode @source_permissions_ignore ? 0644 : @stat.mode end def_delegators :@stat, :ftype end class WindowsStat < MetaStat if Puppet.features.microsoft_windows? require 'puppet/util/windows/security' end def initialize(stat, path, source_permissions = nil) super(stat, source_permissions) @path = path raise(ArgumentError, "Unsupported Windows source permissions option #{source_permissions}") unless @source_permissions_ignore end { :owner => 'S-1-5-32-544', :group => 'S-1-0-0', :mode => 0644 }.each do |method, default_value| define_method method do return default_value end end end def collect_stat(path, source_permissions) stat = stat() if Puppet.features.microsoft_windows? WindowsStat.new(stat, path, source_permissions) else MetaStat.new(stat, source_permissions) end end # Retrieve the attributes for this file, relative to a base directory. # Note that Puppet::FileSystem.stat(path) raises Errno::ENOENT # if the file is absent and this method does not catch that exception. def collect(source_permissions = nil) real_path = full_path stat = collect_stat(real_path, source_permissions) @owner = stat.owner @group = stat.group @ftype = stat.ftype # We have to mask the mode, yay. @mode = stat.mode & 007777 case stat.ftype when "file" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s when "directory" # Always just timestamp the directory. @checksum_type = "ctime" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", path).to_s when "link" @destination = Puppet::FileSystem.readlink(real_path) @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s rescue nil else raise ArgumentError, "Cannot manage files of type #{stat.ftype}" end end def initialize(path,data={}) @owner = data.delete('owner') @group = data.delete('group') @mode = data.delete('mode') if checksum = data.delete('checksum') @checksum_type = checksum['type'] @checksum = checksum['value'] end @checksum_type ||= Puppet[:digest_algorithm] @ftype = data.delete('type') @destination = data.delete('destination') super(path,data) end def to_data_hash super.update( { 'owner' => owner, 'group' => group, 'mode' => mode, 'checksum' => { 'type' => checksum_type, 'value' => checksum }, 'type' => ftype, 'destination' => destination, } ) end def self.from_data_hash(data) new(data.delete('path'), data) end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - end diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb index 8fc43dc0b..d196e2ab9 100644 --- a/lib/puppet/node.rb +++ b/lib/puppet/node.rb @@ -1,171 +1,166 @@ require 'puppet/indirector' # A class for managing nodes, including their facts and environment. class Puppet::Node require 'puppet/node/facts' require 'puppet/node/environment' # Set up indirection, so that nodes can be looked for in # the node sources. extend Puppet::Indirector # Use the node source as the indirection terminus. indirects :node, :terminus_setting => :node_terminus, :doc => "Where to find node information. A node is composed of its name, its facts, and its environment." attr_accessor :name, :classes, :source, :ipaddress, :parameters, :trusted_data, :environment_name attr_reader :time, :facts def self.from_data_hash(data) raise ArgumentError, "No name provided in serialized data" unless name = data['name'] node = new(name) node.classes = data['classes'] node.parameters = data['parameters'] node.environment_name = data['environment'] node end - def self.from_pson(pson) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(pson) - end - def to_data_hash result = { 'name' => name, 'environment' => environment.name, } result['classes'] = classes unless classes.empty? result['parameters'] = parameters unless parameters.empty? result end def environment if @environment @environment else if env = parameters["environment"] self.environment = env elsif environment_name self.environment = environment_name else # This should not be :current_environment, this is the default # for a node when it has not specified its environment # Tt will be used to establish what the current environment is. # self.environment = Puppet.lookup(:environments).get!(Puppet[:environment]) end @environment end end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = Puppet.lookup(:environments).get!(env) else @environment = env end end def has_environment_instance? !@environment.nil? end def initialize(name, options = {}) raise ArgumentError, "Node names cannot be nil" unless name @name = name if classes = options[:classes] if classes.is_a?(String) @classes = [classes] else @classes = classes end else @classes = [] end @parameters = options[:parameters] || {} @facts = options[:facts] if env = options[:environment] self.environment = env end @time = Time.now end # Merge the node facts with parameters from the node source. def fact_merge if @facts = Puppet::Node::Facts.indirection.find(name, :environment => environment) @facts.sanitize merge(@facts.values) end rescue => detail error = Puppet::Error.new("Could not retrieve facts for #{name}: #{detail}") error.set_backtrace(detail.backtrace) raise error end # Merge any random parameters into our parameter list. def merge(params) params.each do |name, value| @parameters[name] = value unless @parameters.include?(name) end @parameters["environment"] ||= self.environment.name.to_s end # Calculate the list of names we might use for looking # up our node. This is only used for AST nodes. def names return [name] if Puppet.settings[:strict_hostname_checking] names = [] names += split_name(name) if name.include?(".") # First, get the fqdn unless fqdn = parameters["fqdn"] if parameters["hostname"] and parameters["domain"] fqdn = parameters["hostname"] + "." + parameters["domain"] else Puppet.warning "Host is missing hostname and/or domain: #{name}" end end # Now that we (might) have the fqdn, add each piece to the name # list to search, in order of longest to shortest. names += split_name(fqdn) if fqdn # And make sure the node name is first, since that's the most # likely usage. # The name is usually the Certificate CN, but it can be # set to the 'facter' hostname instead. if Puppet[:node_name] == 'cert' names.unshift name else names.unshift parameters["hostname"] end names.uniq end def split_name(name) list = name.split(".") tmp = [] list.each_with_index do |short, i| tmp << list[0..i].join(".") end tmp.reverse end # Ensures the data is frozen # def trusted_data=(data) Puppet.warning("Trusted node data modified for node #{name}") unless @trusted_data.nil? @trusted_data = data.freeze end end diff --git a/lib/puppet/node/facts.rb b/lib/puppet/node/facts.rb index 20288cb00..415fc5d08 100644 --- a/lib/puppet/node/facts.rb +++ b/lib/puppet/node/facts.rb @@ -1,135 +1,130 @@ require 'time' require 'puppet/node' require 'puppet/indirector' # Manage a given node's facts. This either accepts facts and stores them, or # returns facts for a given node. class Puppet::Node::Facts # Set up indirection, so that nodes can be looked for in # the node sources. extend Puppet::Indirector # We want to expire any cached nodes if the facts are saved. module NodeExpirer def save(instance, key = nil, options={}) Puppet::Node.indirection.expire(instance.name, options) super end end indirects :facts, :terminus_setting => :facts_terminus, :extend => NodeExpirer attr_accessor :name, :values, :timestamp def add_local_facts values["clientcert"] = Puppet.settings[:certname] values["clientversion"] = Puppet.version.to_s values["clientnoop"] = Puppet.settings[:noop] end def initialize(name, values = {}) @name = name @values = values add_timestamp end def initialize_from_hash(data) @name = data['name'] @values = data['values'] # Timestamp will be here in YAML, e.g. when reading old reports timestamp = @values.delete('_timestamp') # Timestamp will be here in JSON timestamp ||= data['timestamp'] if timestamp.is_a? String @timestamp = Time.parse(timestamp) else @timestamp = timestamp end self.expiration = data['expiration'] if expiration.is_a? String self.expiration = Time.parse(expiration) end end # Sanitize fact values by converting everything not a string, boolean # numeric, array or hash into strings. def sanitize values.each do |fact, value| values[fact] = sanitize_fact value end end def ==(other) return false unless self.name == other.name values == other.values end def self.from_data_hash(data) new_facts = allocate new_facts.initialize_from_hash(data) new_facts end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - def to_data_hash result = { 'name' => name, 'values' => values } if @timestamp if @timestamp.is_a? Time result['timestamp'] = @timestamp.iso8601(9) else result['timestamp'] = @timestamp end end if expiration if expiration.is_a? Time result['expiration'] = expiration.iso8601(9) else result['expiration'] = expiration end end result end def add_timestamp @timestamp = Time.now end # @deprecated Use {#values} instead of this method. def strip_internal values end private def sanitize_fact(fact) if fact.is_a? Hash then ret = {} fact.each_pair { |k,v| ret[sanitize_fact k]=sanitize_fact v } ret elsif fact.is_a? Array then fact.collect { |i| sanitize_fact i } elsif fact.is_a? Numeric \ or fact.is_a? TrueClass \ or fact.is_a? FalseClass \ or fact.is_a? String fact else fact.to_s end end end diff --git a/lib/puppet/relationship.rb b/lib/puppet/relationship.rb index 3fa10092e..2e7b7b95f 100644 --- a/lib/puppet/relationship.rb +++ b/lib/puppet/relationship.rb @@ -1,95 +1,90 @@ # subscriptions are permanent associations determining how different # objects react to an event # This is Puppet's class for modeling edges in its configuration graph. # It used to be a subclass of GRATR::Edge, but that class has weird hash # overrides that dramatically slow down the graphing. class Puppet::Relationship # FormatSupport for serialization methods include Puppet::Network::FormatSupport attr_accessor :source, :target, :callback attr_reader :event def self.from_data_hash(data) source = data["source"] target = data["target"] args = {} if event = data["event"] args[:event] = event end if callback = data["callback"] args[:callback] = callback end new(source, target, args) end - def self.from_pson(pson) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(pson) - end - def event=(event) raise ArgumentError, "You must pass a callback for non-NONE events" if event != :NONE and ! callback @event = event end def initialize(source, target, options = {}) @source, @target = source, target options = (options || {}).inject({}) { |h,a| h[a[0].to_sym] = a[1]; h } [:callback, :event].each do |option| if value = options[option] send(option.to_s + "=", value) end end end # Does the passed event match our event? This is where the meaning # of :NONE comes from. def match?(event) if self.event.nil? or event == :NONE or self.event == :NONE return false elsif self.event == :ALL_EVENTS or event == self.event return true else return false end end def label result = {} result[:callback] = callback if callback result[:event] = event if event result end def ref "#{source} => #{target}" end def inspect "{ #{source} => #{target} }" end def to_data_hash data = { 'source' => source.to_s, 'target' => target.to_s } ["event", "callback"].each do |attr| next unless value = send(attr) data[attr] = value end data end def to_s ref end end diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb index 52a913bfb..6733a5fff 100644 --- a/lib/puppet/resource.rb +++ b/lib/puppet/resource.rb @@ -1,608 +1,603 @@ require 'puppet' require 'puppet/util/tagging' require 'puppet/parameter' # The simplest resource class. Eventually it will function as the # base class for all resource-like behaviour. # # @api public class Puppet::Resource include Puppet::Util::Tagging include Enumerable attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict attr_reader :type, :title require 'puppet/indirector' extend Puppet::Indirector indirects :resource, :terminus_class => :ral ATTRIBUTES = [:file, :line, :exported] def self.from_data_hash(data) raise ArgumentError, "No resource type provided in serialized data" unless type = data['type'] raise ArgumentError, "No resource title provided in serialized data" unless title = data['title'] resource = new(type, title) if params = data['parameters'] params.each { |param, value| resource[param] = value } end if tags = data['tags'] tags.each { |tag| resource.tag(tag) } end ATTRIBUTES.each do |a| if value = data[a.to_s] resource.send(a.to_s + "=", value) end end resource end - def self.from_pson(pson) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(pson) - end - def inspect "#{@type}[#{@title}]#{to_hash.inspect}" end def to_data_hash data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param| next hash unless value = self.send(param) hash[param.to_s] = value hash end data["exported"] ||= false params = self.to_hash.inject({}) do |hash, ary| param, value = ary # Don't duplicate the title as the namevar next hash if param == namevar and value == title hash[param] = Puppet::Resource.value_to_pson_data(value) hash end data["parameters"] = params unless params.empty? data end def self.value_to_pson_data(value) if value.is_a? Array value.map{|v| value_to_pson_data(v) } elsif value.is_a? Puppet::Resource value.to_s else value end end def yaml_property_munge(x) case x when Hash x.inject({}) { |h,kv| k,v = kv h[k] = self.class.value_to_pson_data(v) h } else self.class.value_to_pson_data(x) end end YAML_ATTRIBUTES = [:@file, :@line, :@exported, :@type, :@title, :@tags, :@parameters] # Explicitly list the instance variables that should be serialized when # converting to YAML. # # @api private # @return [Array] The intersection of our explicit variable list and # all of the instance variables defined on this class. def to_yaml_properties YAML_ATTRIBUTES & super end # Proxy these methods to the parameters hash. It's likely they'll # be overridden at some point, but this works for now. %w{has_key? keys length delete empty? <<}.each do |method| define_method(method) do |*args| parameters.send(method, *args) end end # Set a given parameter. Converts all passed names # to lower-case symbols. def []=(param, value) validate_parameter(param) if validate_parameters parameters[parameter_name(param)] = value end # Return a given parameter's value. Converts all passed names # to lower-case symbols. def [](param) parameters[parameter_name(param)] end def ==(other) return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title return false unless to_hash == other.to_hash true end # Compatibility method. def builtin? builtin_type? end # Is this a builtin resource type? def builtin_type? resource_type.is_a?(Class) end # Iterate over each param/value pair, as required for Enumerable. def each parameters.each { |p,v| yield p, v } end def include?(parameter) super || parameters.keys.include?( parameter_name(parameter) ) end %w{exported virtual strict}.each do |m| define_method(m+"?") do self.send(m) end end def class? @is_class ||= @type == "Class" end def stage? @is_stage ||= @type.to_s.downcase == "stage" end # Construct a resource from data. # # Constructs a resource instance with the given `type` and `title`. Multiple # type signatures are possible for these arguments and most will result in an # expensive call to {Puppet::Node::Environment#known_resource_types} in order # to resolve `String` and `Symbol` Types to actual Ruby classes. # # @param type [Symbol, String] The name of the Puppet Type, as a string or # symbol. The actual Type will be looked up using # {Puppet::Node::Environment#known_resource_types}. This lookup is expensive. # @param type [String] The full resource name in the form of # `"Type[Title]"`. This method of calling should only be used when # `title` is `nil`. # @param type [nil] If a `nil` is passed, the title argument must be a string # of the form `"Type[Title]"`. # @param type [Class] A class that inherits from `Puppet::Type`. This method # of construction is much more efficient as it skips calls to # {Puppet::Node::Environment#known_resource_types}. # # @param title [String, :main, nil] The title of the resource. If type is `nil`, may also # be the full resource name in the form of `"Type[Title]"`. # # @api public def initialize(type, title = nil, attributes = {}) @parameters = {} if type.is_a?(Class) && type < Puppet::Type # Set the resource type to avoid an expensive `known_resource_types` # lookup. self.resource_type = type # From this point on, the constructor behaves the same as if `type` had # been passed as a symbol. type = type.name end # Set things like strictness first. attributes.each do |attr, value| next if attr == :parameters send(attr.to_s + "=", value) end @type, @title = extract_type_and_title(type, title) @type = munge_type_name(@type) if self.class? @title = :main if @title == "" @title = munge_type_name(@title) end if params = attributes[:parameters] extract_parameters(params) end if resource_type && resource_type.respond_to?(:deprecate_params) resource_type.deprecate_params(title, attributes[:parameters]) end tag(self.type) tag(self.title) if valid_tag?(self.title) if strict? and ! resource_type if self.class? raise ArgumentError, "Could not find declared class #{title}" else raise ArgumentError, "Invalid resource type #{type}" end end end def ref to_s end # Find our resource. def resolve catalog ? catalog.resource(to_s) : nil end # The resource's type implementation # @return [Puppet::Type, Puppet::Resource::Type] # @api private def resource_type @rstype ||= case type when "Class"; environment.known_resource_types.hostclass(title == :main ? "" : title) when "Node"; environment.known_resource_types.node(title) else Puppet::Type.type(type) || environment.known_resource_types.definition(type) end end # Set the resource's type implementation # @param type [Puppet::Type, Puppet::Resource::Type] # @api private def resource_type=(type) @rstype = type end def environment @environment ||= if catalog catalog.environment_instance else Puppet.lookup(:current_environment) { Puppet::Node::Environment::NONE } end end def environment=(environment) @environment = environment end # Produce a simple hash of our parameters. def to_hash parse_title.merge parameters end def to_s "#{type}[#{title}]" end def uniqueness_key # Temporary kludge to deal with inconsistant use patters h = self.to_hash h[namevar] ||= h[:name] h[:name] ||= h[namevar] h.values_at(*key_attributes.sort_by { |k| k.to_s }) end def key_attributes resource_type.respond_to?(:key_attributes) ? resource_type.key_attributes : [:name] end # Convert our resource to yaml for Hiera purposes. def to_hierayaml # Collect list of attributes to align => and move ensure first attr = parameters.keys attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max } attr.sort! if attr.first != :ensure && attr.include?(:ensure) attr.delete(:ensure) attr.unshift(:ensure) end attributes = attr.collect { |k| v = parameters[k] " %-#{attr_max}s: %s\n" % [k, Puppet::Parameter.format_value_for_display(v)] }.join " %s:\n%s" % [self.title, attributes] end # Convert our resource to Puppet code. def to_manifest # Collect list of attributes to align => and move ensure first attr = parameters.keys attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max } attr.sort! if attr.first != :ensure && attr.include?(:ensure) attr.delete(:ensure) attr.unshift(:ensure) end attributes = attr.collect { |k| v = parameters[k] " %-#{attr_max}s => %s,\n" % [k, Puppet::Parameter.format_value_for_display(v)] }.join "%s { '%s':\n%s}" % [self.type.to_s.downcase, self.title, attributes] end def to_ref ref end # Convert our resource to a RAL resource instance. Creates component # instances for resource types that don't exist. def to_ral typeklass = Puppet::Type.type(self.type) || Puppet::Type.type(:component) typeklass.new(self) end def name # this is potential namespace conflict # between the notion of an "indirector name" # and a "resource name" [ type, title ].join('/') end def missing_arguments resource_type.arguments.select do |param, default| the_param = parameters[param.to_sym] the_param.nil? || the_param.value.nil? || the_param.value == :undef end end private :missing_arguments # Consult external data bindings for class parameter values which must be # namespaced in the backend. # # Example: # # class foo($port=0){ ... } # # We make a request to the backend for the key 'foo::port' not 'foo' # def lookup_external_default_for(param, scope) # Only lookup parameters for host classes return nil unless resource_type.type == :hostclass name = "#{resource_type.name}::#{param}" lookup_with_databinding(name, scope) end private :lookup_external_default_for def lookup_with_databinding(name, scope) begin Puppet::DataBinding.indirection.find( name, :environment => scope.environment.to_s, :variables => scope) rescue Puppet::DataBinding::LookupError => e raise Puppet::Error.new("Error from DataBinding '#{Puppet[:data_binding_terminus]}' while looking up '#{name}': #{e.message}", e) end end private :lookup_with_databinding def set_default_parameters(scope) return [] unless resource_type and resource_type.respond_to?(:arguments) unless is_a?(Puppet::Parser::Resource) fail Puppet::DevError, "Cannot evaluate default parameters for #{self} - not a parser resource" end missing_arguments.collect do |param, default| external_value = lookup_external_default_for(param, scope) if external_value.nil? && default.nil? next elsif external_value.nil? value = default.safeevaluate(scope) else value = external_value end self[param.to_sym] = value param end.compact end def copy_as_resource result = Puppet::Resource.new(type, title) result.file = self.file result.line = self.line result.exported = self.exported result.virtual = self.virtual result.tag(*self.tags) result.environment = environment result.instance_variable_set(:@rstype, resource_type) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = Puppet::Resource.new(v.type, v.title) elsif v.is_a?(Array) # flatten resource references arrays v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) } v = v.collect do |av| av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource) av end end if Puppet[:parser] == 'current' # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. # # This behavior is not done in the future parser, but we can't issue a # deprecation warning either since there isn't anything that a user can # do about it. result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end else result[p] = v end end result end def valid_parameter?(name) resource_type.valid_parameter?(name) end # Verify that all required arguments are either present or # have been provided with defaults. # Must be called after 'set_default_parameters'. We can't join the methods # because Type#set_parameters needs specifically ordered behavior. def validate_complete return unless resource_type and resource_type.respond_to?(:arguments) resource_type.arguments.each do |param, default| param = param.to_sym fail Puppet::ParseError, "Must pass #{param} to #{self}" unless parameters.include?(param) end # Perform optional type checking if Puppet[:parser] == 'future' # Perform type checking arg_types = resource_type.argument_types # Parameters is a map from name, to parameter, and the parameter again has name and value parameters.each do |name, value| next unless t = arg_types[name.to_s] # untyped, and parameters are symbols here (aargh, strings in the type) unless Puppet::Pops::Types::TypeCalculator.instance?(t, value.value) inferred_type = Puppet::Pops::Types::TypeCalculator.infer(value.value) actual = Puppet::Pops::Types::TypeCalculator.generalize!(inferred_type) fail Puppet::ParseError, "Expected parameter '#{name}' of '#{self}' to have type #{t.to_s}, got #{actual.to_s}" end end end end def validate_parameter(name) raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name) end def prune_parameters(options = {}) properties = resource_type.properties.map(&:name) dup.collect do |attribute, value| if value.to_s.empty? or Array(value).empty? delete(attribute) elsif value.to_s == "absent" and attribute.to_s != "ensure" delete(attribute) end parameters_to_include = options[:parameters_to_include] || [] delete(attribute) unless properties.include?(attribute) || parameters_to_include.include?(attribute) end self end private # Produce a canonical method name. def parameter_name(param) param = param.to_s.downcase.to_sym if param == :name and namevar param = namevar end param end # The namevar for our resource type. If the type doesn't exist, # always use :name. def namevar if builtin_type? and t = resource_type and t.key_attributes.length == 1 t.key_attributes.first else :name end end def extract_parameters(params) params.each do |param, value| validate_parameter(param) if strict? self[param] = value end end def extract_type_and_title(argtype, argtitle) if (argtype.nil? || argtype == :component || argtype == :whit) && argtitle =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle.nil? && argtype =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle then [ argtype, argtitle ] elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ] elsif argtype.is_a?(Hash) then raise ArgumentError, "Puppet::Resource.new does not take a hash as the first argument. "+ "Did you mean (#{(argtype[:type] || argtype["type"]).inspect}, #{(argtype[:title] || argtype["title"]).inspect }) ?" else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference" end end def munge_type_name(value) return :main if value == :main return "Class" if value == "" or value.nil? or value.to_s.downcase == "component" value.to_s.split("::").collect { |s| s.capitalize }.join("::") end def parse_title h = {} type = resource_type if type.respond_to? :title_patterns type.title_patterns.each { |regexp, symbols_and_lambdas| if captures = regexp.match(title.to_s) symbols_and_lambdas.zip(captures[1..-1]).each do |symbol_and_lambda,capture| symbol, proc = symbol_and_lambda # Many types pass "identity" as the proc; we might as well give # them a shortcut to delivering that without the extra cost. # # Especially because the global type defines title_patterns and # uses the identity patterns. # # This was worth about 8MB of memory allocation saved in my # testing, so is worth the complexity for the API. if proc then h[symbol] = proc.call(capture) else h[symbol] = capture end end return h end } # If we've gotten this far, then none of the provided title patterns # matched. Since there's no way to determine the title then the # resource should fail here. raise Puppet::Error, "No set of title patterns matched the title \"#{title}\"." else return { :name => title.to_s } end end def parameters # @parameters could have been loaded from YAML, causing it to be nil (by # bypassing initialize). @parameters ||= {} end end diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb index d70e04650..2a984f987 100644 --- a/lib/puppet/resource/catalog.rb +++ b/lib/puppet/resource/catalog.rb @@ -1,537 +1,532 @@ require 'puppet/node' require 'puppet/indirector' require 'puppet/transaction' require 'puppet/util/tagging' require 'puppet/graph' # This class models a node catalog. It is the thing meant to be passed # from server to client, and it contains all of the information in the # catalog, including the resources and the relationships between them. # # @api public class Puppet::Resource::Catalog < Puppet::Graph::SimpleGraph class DuplicateResourceError < Puppet::Error include Puppet::ExternalFileError end extend Puppet::Indirector indirects :catalog, :terminus_setting => :catalog_terminus include Puppet::Util::Tagging # The host name this is a catalog for. attr_accessor :name # The catalog version. Used for testing whether a catalog # is up to date. attr_accessor :version # How long this catalog took to retrieve. Used for reporting stats. attr_accessor :retrieval_duration # Whether this is a host catalog, which behaves very differently. # In particular, reports are sent, graphs are made, and state is # stored in the state database. If this is set incorrectly, then you often # end up in infinite loops, because catalogs are used to make things # that the host catalog needs. attr_accessor :host_config # Whether this catalog was retrieved from the cache, which affects # whether it is written back out again. attr_accessor :from_cache # Some metadata to help us compile and generally respond to the current state. attr_accessor :client_version, :server_version # A String representing the environment for this catalog attr_accessor :environment # The actual environment instance that was used during compilation attr_accessor :environment_instance # Add classes to our class list. def add_class(*classes) classes.each do |klass| @classes << klass end # Add the class names as tags, too. tag(*classes) end def title_key_for_ref( ref ) ref =~ /^([-\w:]+)\[(.*)\]$/m [$1, $2] end def add_resource(*resources) resources.each do |resource| add_one_resource(resource) end end # @param resource [A Resource] a resource in the catalog # @return [A Resource, nil] the resource that contains the given resource # @api public def container_of(resource) adjacent(resource, :direction => :in)[0] end def add_one_resource(resource) title_key = title_key_for_ref(resource.ref) if @resource_table[title_key] fail_on_duplicate_type_and_title(resource, title_key) end add_resource_to_table(resource, title_key) create_resource_aliases(resource) resource.catalog = self if resource.respond_to?(:catalog=) add_resource_to_graph(resource) end private :add_one_resource def add_resource_to_table(resource, title_key) @resource_table[title_key] = resource @resources << title_key end private :add_resource_to_table def add_resource_to_graph(resource) add_vertex(resource) @relationship_graph.add_vertex(resource) if @relationship_graph end private :add_resource_to_graph def create_resource_aliases(resource) if resource.respond_to?(:isomorphic?) and resource.isomorphic? and resource.name != resource.title self.alias(resource, resource.uniqueness_key) end end private :create_resource_aliases # Create an alias for a resource. def alias(resource, key) resource.ref =~ /^(.+)\[/ class_name = $1 || resource.class.name newref = [class_name, key].flatten if key.is_a? String ref_string = "#{class_name}[#{key}]" return if ref_string == resource.ref end # LAK:NOTE It's important that we directly compare the references, # because sometimes an alias is created before the resource is # added to the catalog, so comparing inside the below if block # isn't sufficient. if existing = @resource_table[newref] return if existing == resource resource_declaration = " at #{resource.file}:#{resource.line}" if resource.file and resource.line existing_declaration = " at #{existing.file}:#{existing.line}" if existing.file and existing.line msg = "Cannot alias #{resource.ref} to #{key.inspect}#{resource_declaration}; resource #{newref.inspect} already declared#{existing_declaration}" raise ArgumentError, msg end @resource_table[newref] = resource @aliases[resource.ref] ||= [] @aliases[resource.ref] << newref end # Apply our catalog to the local host. # @param options [Hash{Symbol => Object}] a hash of options # @option options [Puppet::Transaction::Report] :report # The report object to log this transaction to. This is optional, # and the resulting transaction will create a report if not # supplied. # @option options [Array[String]] :tags # Tags used to filter the transaction. If supplied then only # resources tagged with any of these tags will be evaluated. # @option options [Boolean] :ignoreschedules # Ignore schedules when evaluating resources # @option options [Boolean] :for_network_device # Whether this catalog is for a network device # # @return [Puppet::Transaction] the transaction created for this # application # # @api public def apply(options = {}) Puppet::Util::Storage.load if host_config? transaction = create_transaction(options) begin transaction.report.as_logging_destination do transaction.evaluate end rescue Puppet::Error => detail Puppet.log_exception(detail, "Could not apply complete catalog: #{detail}") rescue => detail Puppet.log_exception(detail, "Got an uncaught exception of type #{detail.class}: #{detail}") ensure # Don't try to store state unless we're a host config # too recursive. Puppet::Util::Storage.store if host_config? end yield transaction if block_given? transaction end # The relationship_graph form of the catalog. This contains all of the # dependency edges that are used for determining order. # # @param given_prioritizer [Puppet::Graph::Prioritizer] The prioritization # strategy to use when constructing the relationship graph. Defaults the # being determined by the `ordering` setting. # @return [Puppet::Graph::RelationshipGraph] # @api public def relationship_graph(given_prioritizer = nil) if @relationship_graph.nil? @relationship_graph = Puppet::Graph::RelationshipGraph.new(given_prioritizer || prioritizer) @relationship_graph.populate_from(self) end @relationship_graph end def clear(remove_resources = true) super() # We have to do this so that the resources clean themselves up. @resource_table.values.each { |resource| resource.remove } if remove_resources @resource_table.clear @resources = [] if @relationship_graph @relationship_graph.clear @relationship_graph = nil end end def classes @classes.dup end # Create a new resource and register it in the catalog. def create_resource(type, options) unless klass = Puppet::Type.type(type) raise ArgumentError, "Unknown resource type #{type}" end return unless resource = klass.new(options) add_resource(resource) resource end # Make sure all of our resources are "finished". def finalize make_default_resources @resource_table.values.each { |resource| resource.finish } write_graph(:resources) end def host_config? host_config end def initialize(name = nil, environment = Puppet::Node::Environment::NONE) super() @name = name @classes = [] @resource_table = {} @resources = [] @relationship_graph = nil @host_config = true @environment_instance = environment @environment = environment.to_s @aliases = {} if block_given? yield(self) finalize end end # Make the default objects necessary for function. def make_default_resources # We have to add the resources to the catalog, or else they won't get cleaned up after # the transaction. # First create the default scheduling objects Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) } # And filebuckets if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket add_resource(bucket) unless resource(bucket.ref) end end # Remove the resource from our catalog. Notice that we also call # 'remove' on the resource, at least until resource classes no longer maintain # references to the resource instances. def remove_resource(*resources) resources.each do |resource| title_key = title_key_for_ref(resource.ref) @resource_table.delete(title_key) if aliases = @aliases[resource.ref] aliases.each { |res_alias| @resource_table.delete(res_alias) } @aliases.delete(resource.ref) end remove_vertex!(resource) if vertex?(resource) @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) @resources.delete(title_key) resource.remove end end # Look a resource up by its reference (e.g., File[/etc/passwd]). def resource(type, title = nil) # Always create a resource reference, so that it always # canonicalizes how we are referring to them. if title res = Puppet::Resource.new(type, title) else # If they didn't provide a title, then we expect the first # argument to be of the form 'Class[name]', which our # Reference class canonicalizes for us. res = Puppet::Resource.new(nil, type) end res.catalog = self title_key = [res.type, res.title.to_s] uniqueness_key = [res.type, res.uniqueness_key].flatten @resource_table[title_key] || @resource_table[uniqueness_key] end def resource_refs resource_keys.collect{ |type, name| name.is_a?( String ) ? "#{type}[#{name}]" : nil}.compact end def resource_keys @resource_table.keys end def resources @resources.collect do |key| @resource_table[key] end end def self.from_data_hash(data) result = new(data['name'], Puppet::Node::Environment::NONE) if tags = data['tags'] result.tag(*tags) end if version = data['version'] result.version = version end if environment = data['environment'] result.environment = environment result.environment_instance = Puppet::Node::Environment.remote(environment.to_sym) end if resources = data['resources'] result.add_resource(*resources.collect do |res| Puppet::Resource.from_data_hash(res) end) end if edges = data['edges'] edges.each do |edge_hash| edge = Puppet::Relationship.from_data_hash(edge_hash) unless source = result.resource(edge.source) raise ArgumentError, "Could not intern from data: Could not find relationship source #{edge.source.inspect} for #{edge.target.to_s}" end edge.source = source unless target = result.resource(edge.target) raise ArgumentError, "Could not intern from data: Could not find relationship target #{edge.target.inspect} for #{edge.source.to_s}" end edge.target = target result.add_edge(edge) end end if classes = data['classes'] result.add_class(*classes) end result end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - def to_data_hash { 'tags' => tags, 'name' => name, 'version' => version, 'environment' => environment.to_s, 'resources' => @resources.collect { |v| @resource_table[v].to_data_hash }, 'edges' => edges. collect { |e| e.to_data_hash }, 'classes' => classes } end # Convert our catalog into a RAL catalog. def to_ral to_catalog :to_ral end # Convert our catalog into a catalog of Puppet::Resource instances. def to_resource to_catalog :to_resource end # filter out the catalog, applying +block+ to each resource. # If the block result is false, the resource will # be kept otherwise it will be skipped def filter(&block) to_catalog :to_resource, &block end # Store the classes in the classfile. def write_class_file ::File.open(Puppet[:classfile], "w") do |f| f.puts classes.join("\n") end rescue => detail Puppet.err "Could not create class file #{Puppet[:classfile]}: #{detail}" end # Store the list of resources we manage def write_resource_file ::File.open(Puppet[:resourcefile], "w") do |f| to_print = resources.map do |resource| next unless resource.managed? if resource.name_var "#{resource.type}[#{resource[resource.name_var]}]" else "#{resource.ref.downcase}" end end.compact f.puts to_print.join("\n") end rescue => detail Puppet.err "Could not create resource file #{Puppet[:resourcefile]}: #{detail}" end # Produce the graph files if requested. def write_graph(name) # We only want to graph the main host catalog. return unless host_config? super end private def prioritizer @prioritizer ||= case Puppet[:ordering] when "title-hash" Puppet::Graph::TitleHashPrioritizer.new when "manifest" Puppet::Graph::SequentialPrioritizer.new when "random" Puppet::Graph::RandomPrioritizer.new else raise Puppet::DevError, "Unknown ordering type #{Puppet[:ordering]}" end end def create_transaction(options) transaction = Puppet::Transaction.new(self, options[:report], prioritizer) transaction.tags = options[:tags] if options[:tags] transaction.ignoreschedules = true if options[:ignoreschedules] transaction.for_network_device = options[:network_device] transaction end # Verify that the given resource isn't declared elsewhere. def fail_on_duplicate_type_and_title(resource, title_key) # Short-circuit the common case, return unless existing_resource = @resource_table[title_key] # If we've gotten this far, it's a real conflict msg = "Duplicate declaration: #{resource.ref} is already declared" msg << " in file #{existing_resource.file}:#{existing_resource.line}" if existing_resource.file and existing_resource.line msg << "; cannot redeclare" raise DuplicateResourceError.new(msg, resource.file, resource.line) end # An abstracted method for converting one catalog into another type of catalog. # This pretty much just converts all of the resources from one class to another, using # a conversion method. def to_catalog(convert) result = self.class.new(self.name, self.environment_instance) result.version = self.version map = {} resources.each do |resource| next if virtual_not_exported?(resource) next if block_given? and yield resource newres = resource.copy_as_resource newres.catalog = result if convert != :to_resource newres = newres.to_ral end # We can't guarantee that resources don't munge their names # (like files do with trailing slashes), so we have to keep track # of what a resource got converted to. map[resource.ref] = newres result.add_resource newres end message = convert.to_s.gsub "_", " " edges.each do |edge| # Skip edges between virtual resources. next if virtual_not_exported?(edge.source) next if block_given? and yield edge.source next if virtual_not_exported?(edge.target) next if block_given? and yield edge.target unless source = map[edge.source.ref] raise Puppet::DevError, "Could not find resource #{edge.source.ref} when converting #{message} resources" end unless target = map[edge.target.ref] raise Puppet::DevError, "Could not find resource #{edge.target.ref} when converting #{message} resources" end result.add_edge(source, target, edge.label) end map.clear result.add_class(*self.classes) result.tag(*self.tags) result end def virtual_not_exported?(resource) resource.virtual && !resource.exported end end diff --git a/lib/puppet/resource/status.rb b/lib/puppet/resource/status.rb index e08902f07..0b3a7c378 100644 --- a/lib/puppet/resource/status.rb +++ b/lib/puppet/resource/status.rb @@ -1,211 +1,206 @@ require 'time' require 'puppet/network/format_support' module Puppet class Resource # This class represents the result of evaluating a given resource. It # contains file and line information about the source, events generated # while evaluating the resource, timing information, and the status of the # resource evaluation. # # @api private class Status include Puppet::Util::Tagging include Puppet::Network::FormatSupport # @!attribute [rw] file # @return [String] The file where `@real_resource` was defined. attr_accessor :file # @!attribute [rw] line # @return [Integer] The line number in the file where `@real_resource` was defined. attr_accessor :line # @!attribute [rw] evaluation_time # @return [Float] The time elapsed in sections while evaluating `@real_resource`. # measured in seconds. attr_accessor :evaluation_time # Boolean status types set while evaluating `@real_resource`. STATES = [:skipped, :failed, :failed_to_restart, :restarted, :changed, :out_of_sync, :scheduled] attr_accessor *STATES # @!attribute [r] source_description # @return [String] The textual description of the path to `@real_resource` # based on the containing structures. This is in contrast to # `@containment_path` which is a list of containment path components. # @example # status.source_description #=> "/Stage[main]/Myclass/Exec[date]" attr_reader :source_description # @!attribute [r] containment_path # @return [Array] A list of resource references that contain # `@real_resource`. # @example A normal contained type # status.containment_path #=> ["Stage[main]", "Myclass", "Exec[date]"] # @example A whit associated with a class # status.containment_path #=> ["Whit[Admissible_class[Main]]"] attr_reader :containment_path # @!attribute [r] time # @return [Time] The time that this status object was created attr_reader :time # @!attribute [r] resource # @return [String] The resource reference for `@real_resource` attr_reader :resource # @!attribute [r] change_count # @return [Integer] A count of the successful changes made while # evaluating `@real_resource`. attr_reader :change_count # @!attribute [r] out_of_sync_count # @return [Integer] A count of the audited changes made while # evaluating `@real_resource`. attr_reader :out_of_sync_count # @!attribute [r] resource_type # @example # status.resource_type #=> 'Notify' # @return [String] The class name of `@real_resource` attr_reader :resource_type # @!attribute [r] title # @return [String] The title of `@real_resource` attr_reader :title # @!attribute [r] events # @return [Array] A list of events generated # while evaluating `@real_resource`. attr_reader :events # A list of instance variables that should be serialized with this object # when converted to YAML. YAML_ATTRIBUTES = %w{@resource @file @line @evaluation_time @change_count @out_of_sync_count @tags @time @events @out_of_sync @changed @resource_type @title @skipped @failed @containment_path}. map(&:to_sym) def self.from_data_hash(data) obj = self.allocate obj.initialize_from_hash(data) obj end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - # Provide a boolean method for each of the states. STATES.each do |attr| define_method("#{attr}?") do !! send(attr) end end def <<(event) add_event(event) self end def add_event(event) @events << event if event.status == 'failure' self.failed = true elsif event.status == 'success' @change_count += 1 @changed = true end if event.status != 'audit' @out_of_sync_count += 1 @out_of_sync = true end end def failed_because(detail) @real_resource.log_exception(detail, "Could not evaluate: #{detail}") failed = true # There's a contract (implicit unfortunately) that a status of failed # will always be accompanied by an event with some explanatory power. This # is useful for reporting/diagnostics/etc. So synthesize an event here # with the exception detail as the message. add_event(@real_resource.event(:name => :resource_error, :status => "failure", :message => detail.to_s)) end def initialize(resource) @real_resource = resource @source_description = resource.path @containment_path = resource.pathbuilder @resource = resource.to_s @change_count = 0 @out_of_sync_count = 0 @changed = false @out_of_sync = false @skipped = false @failed = false @file = resource.file @line = resource.line tag(*resource.tags) @time = Time.now @events = [] @resource_type = resource.type.to_s.capitalize @title = resource.title end def initialize_from_hash(data) @resource_type = data['resource_type'] @title = data['title'] @resource = data['resource'] @containment_path = data['containment_path'] @file = data['file'] @line = data['line'] @evaluation_time = data['evaluation_time'] @change_count = data['change_count'] @out_of_sync_count = data['out_of_sync_count'] @tags = Puppet::Util::TagSet.new(data['tags']) @time = data['time'] @time = Time.parse(@time) if @time.is_a? String @out_of_sync = data['out_of_sync'] @changed = data['changed'] @skipped = data['skipped'] @failed = data['failed'] @events = data['events'].map do |event| Puppet::Transaction::Event.from_data_hash(event) end end def to_data_hash { 'title' => @title, 'file' => @file, 'line' => @line, 'resource' => @resource, 'resource_type' => @resource_type, 'containment_path' => @containment_path, 'evaluation_time' => @evaluation_time, 'tags' => @tags, 'time' => @time.iso8601(9), 'failed' => @failed, 'changed' => @changed, 'out_of_sync' => @out_of_sync, 'skipped' => @skipped, 'change_count' => @change_count, 'out_of_sync_count' => @out_of_sync_count, 'events' => @events, } end def to_yaml_properties YAML_ATTRIBUTES & super end end end end diff --git a/lib/puppet/resource/type.rb b/lib/puppet/resource/type.rb index 826ef6110..44ad47e60 100644 --- a/lib/puppet/resource/type.rb +++ b/lib/puppet/resource/type.rb @@ -1,406 +1,401 @@ require 'puppet/parser' require 'puppet/util/warnings' require 'puppet/util/errors' require 'puppet/util/inline_docs' require 'puppet/parser/ast/leaf' require 'puppet/parser/ast/block_expression' # Puppet::Resource::Type represents nodes, classes and defined types. # # It has a standard format for external consumption, usable from the # resource_type indirection via rest and the resource_type face. See the # {file:api_docs/http_resource_type.md#Schema resource type schema # description}. # # @api public class Puppet::Resource::Type Puppet::ResourceType = self include Puppet::Util::InlineDocs include Puppet::Util::Warnings include Puppet::Util::Errors RESOURCE_KINDS = [:hostclass, :node, :definition] # Map the names used in our documentation to the names used internally RESOURCE_KINDS_TO_EXTERNAL_NAMES = { :hostclass => "class", :node => "node", :definition => "defined_type", } RESOURCE_EXTERNAL_NAMES_TO_KINDS = RESOURCE_KINDS_TO_EXTERNAL_NAMES.invert attr_accessor :file, :line, :doc, :code, :parent, :resource_type_collection attr_reader :namespace, :arguments, :behaves_like, :module_name # Map from argument (aka parameter) names to Puppet Type # @return [Hash :parser def self.from_data_hash(data) name = data.delete('name') or raise ArgumentError, "Resource Type names must be specified" kind = data.delete('kind') || "definition" unless type = RESOURCE_EXTERNAL_NAMES_TO_KINDS[kind] raise ArgumentError, "Unsupported resource kind '#{kind}'" end data = data.inject({}) { |result, ary| result[ary[0].intern] = ary[1]; result } # External documentation uses "parameters" but the internal name # is "arguments" data[:arguments] = data.delete(:parameters) new(type, name, data) end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - def to_data_hash data = [:doc, :line, :file, :parent].inject({}) do |hash, param| next hash unless (value = self.send(param)) and (value != "") hash[param.to_s] = value hash end # External documentation uses "parameters" but the internal name # is "arguments" data['parameters'] = arguments.dup unless arguments.empty? data['name'] = name unless RESOURCE_KINDS_TO_EXTERNAL_NAMES.has_key?(type) raise ArgumentError, "Unsupported resource kind '#{type}'" end data['kind'] = RESOURCE_KINDS_TO_EXTERNAL_NAMES[type] data end # Are we a child of the passed class? Do a recursive search up our # parentage tree to figure it out. def child_of?(klass) return false unless parent return(klass == parent_type ? true : parent_type.child_of?(klass)) end # Now evaluate the code associated with this class or definition. def evaluate_code(resource) static_parent = evaluate_parent_type(resource) scope = static_parent || resource.scope scope = scope.newscope(:namespace => namespace, :source => self, :resource => resource) unless resource.title == :main scope.compiler.add_class(name) unless definition? set_resource_parameters(resource, scope) resource.add_edge_to_stage if code if @match # Only bother setting up the ephemeral scope if there are match variables to add into it begin elevel = scope.ephemeral_level scope.ephemeral_from(@match, file, line) code.safeevaluate(scope) ensure scope.unset_ephemeral_var(elevel) end else code.safeevaluate(scope) end end end def initialize(type, name, options = {}) @type = type.to_s.downcase.to_sym raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_KINDS.include?(@type) name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName) set_name_and_namespace(name) [:code, :doc, :line, :file, :parent].each do |param| next unless value = options[param] send(param.to_s + "=", value) end set_arguments(options[:arguments]) set_argument_types(options[:argument_types]) @match = nil @module_name = options[:module_name] end # This is only used for node names, and really only when the node name # is a regexp. def match(string) return string.to_s.downcase == name unless name_is_regex? @match = @name.match(string) end # Add code from a new instance to our code. def merge(other) fail "#{name} is not a class; cannot add code to it" unless type == :hostclass fail "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass fail "Cannot have code outside of a class/node/define because 'freeze_main' is enabled" if name == "" and Puppet.settings[:freeze_main] if parent and other.parent and parent != other.parent fail "Cannot merge classes with different parent classes (#{name} => #{parent} vs. #{other.name} => #{other.parent})" end # We know they're either equal or only one is set, so keep whichever parent is specified. self.parent ||= other.parent if other.doc self.doc ||= "" self.doc += other.doc end # This might just be an empty, stub class. return unless other.code unless self.code self.code = other.code return end self.code = Puppet::Parser::ParserFactory.code_merger.concatenate([self, other]) # self.code = self.code.sequence_with(other.code) end # Make an instance of the resource type, and place it in the catalog # if it isn't in the catalog already. This is only possible for # classes and nodes. No parameters are be supplied--if this is a # parameterized class, then all parameters take on their default # values. def ensure_in_catalog(scope, parameters=nil) type == :definition and raise ArgumentError, "Cannot create resources for defined resource types" resource_type = type == :hostclass ? :class : :node # Do nothing if the resource already exists; this makes sure we don't # get multiple copies of the class resource, which helps provide the # singleton nature of classes. # we should not do this for classes with parameters # if parameters are passed, we should still try to create the resource # even if it exists so that we can fail # this prevents us from being able to combine param classes with include if resource = scope.catalog.resource(resource_type, name) and !parameters return resource end resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self) assign_parameter_values(parameters, resource) instantiate_resource(scope, resource) scope.compiler.add_resource(scope, resource) resource end def instantiate_resource(scope, resource) # Make sure our parent class has been evaluated, if we have one. if parent && !scope.catalog.resource(resource.type, parent) parent_type(scope).ensure_in_catalog(scope) end if ['Class', 'Node'].include? resource.type scope.catalog.tag(*resource.tags) end end def name return @name unless @name.is_a?(Regexp) @name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'') end def name_is_regex? @name.is_a?(Regexp) end def assign_parameter_values(parameters, resource) return unless parameters # It'd be nice to assign default parameter values here, # but we can't because they often rely on local variables # created during set_resource_parameters. parameters.each do |name, value| resource.set_parameter name, value end end # MQR TODO: # # The change(s) introduced by the fix for #4270 are mostly silly & should be # removed, though we didn't realize it at the time. If it can be established/ # ensured that nodes never call parent_type and that resource_types are always # (as they should be) members of exactly one resource_type_collection the # following method could / should be replaced with: # # def parent_type # @parent_type ||= parent && ( # resource_type_collection.find_or_load([name],parent,type.to_sym) || # fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{resource_type_collection.environment}" # ) # end # # ...and then the rest of the changes around passing in scope reverted. # def parent_type(scope = nil) return nil unless parent unless @parent_type raise "Must pass scope to parent_type when called first time" unless scope unless @parent_type = scope.environment.known_resource_types.send("find_#{type}", [name], parent) fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{scope.environment}" end end @parent_type end # Set any arguments passed by the resource as variables in the scope. def set_resource_parameters(resource, scope) set = {} resource.to_hash.each do |param, value| param = param.to_sym fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless valid_parameter?(param) exceptwrap { scope[param.to_s] = value } set[param] = true end if @type == :hostclass scope["title"] = resource.title.to_s.downcase unless set.include? :title scope["name"] = resource.name.to_s.downcase unless set.include? :name else scope["title"] = resource.title unless set.include? :title scope["name"] = resource.name unless set.include? :name end scope["module_name"] = module_name if module_name and ! set.include? :module_name if caller_name = scope.parent_module_name and ! set.include?(:caller_module_name) scope["caller_module_name"] = caller_name end scope.class_set(self.name,scope) if hostclass? or node? # Evaluate the default parameters, now that all other variables are set default_params = resource.set_default_parameters(scope) default_params.each { |param| scope[param] = resource[param] } # This has to come after the above parameters so that default values # can use their values resource.validate_complete end # Check whether a given argument is valid. def valid_parameter?(param) param = param.to_s return true if param == "name" return true if Puppet::Type.metaparam?(param) return false unless defined?(@arguments) return(arguments.include?(param) ? true : false) end def set_arguments(arguments) @arguments = {} return if arguments.nil? arguments.each do |arg, default| arg = arg.to_s warn_if_metaparam(arg, default) @arguments[arg] = default end end # Sets the argument name to Puppet Type hash used for type checking. # Names must correspond to available arguments (they must be defined first). # Arguments not mentioned will not be type-checked. Only supported when parser == "future" # def set_argument_types(name_to_type_hash) @argument_types = {} # Stop here if not running under future parser, the rest requires pops to be initialized # and that the type system is available return unless Puppet[:parser] == 'future' && name_to_type_hash name_to_type_hash.each do |name, t| # catch internal errors unless @arguments.include?(name) raise Puppet::DevError, "Parameter '#{name}' is given a type, but is not a valid parameter." end unless t.is_a? Puppet::Pops::Types::PAnyType raise Puppet::DevError, "Parameter '#{name}' is given a type that is not a Puppet Type, got #{t.class}" end @argument_types[name] = t end end private def convert_from_ast(name) value = name.value if value.is_a?(Puppet::Parser::AST::Regex) name = value.value else name = value end end def evaluate_parent_type(resource) return unless klass = parent_type(resource.scope) and parent_resource = resource.scope.compiler.catalog.resource(:class, klass.name) || resource.scope.compiler.catalog.resource(:node, klass.name) parent_resource.evaluate unless parent_resource.evaluated? parent_scope(resource.scope, klass) end # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end def parent_scope(scope, klass) scope.class_scope(klass) || raise(Puppet::DevError, "Could not find scope for #{klass.name}") end def set_name_and_namespace(name) if name.is_a?(Regexp) @name = name @namespace = "" else @name = name.to_s.downcase # Note we're doing something somewhat weird here -- we're setting # the class's namespace to its fully qualified name. This means # anything inside that class starts looking in that namespace first. @namespace, ignored_shortname = @type == :hostclass ? [@name, ''] : namesplit(@name) end end def warn_if_metaparam(param, default) return unless Puppet::Type.metaparamclass(param) if default warnonce "#{param} is a metaparam; this value will inherit to all contained resources in the #{self.name} definition" else raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition" end end end diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb index 442c648bf..c680546a1 100644 --- a/lib/puppet/ssl/host.rb +++ b/lib/puppet/ssl/host.rb @@ -1,372 +1,367 @@ require 'puppet/indirector' require 'puppet/ssl' require 'puppet/ssl/key' require 'puppet/ssl/certificate' require 'puppet/ssl/certificate_request' require 'puppet/ssl/certificate_revocation_list' require 'puppet/ssl/certificate_request_attributes' # The class that manages all aspects of our SSL certificates -- # private keys, public keys, requests, etc. class Puppet::SSL::Host # Yay, ruby's strange constant lookups. Key = Puppet::SSL::Key CA_NAME = Puppet::SSL::CA_NAME Certificate = Puppet::SSL::Certificate CertificateRequest = Puppet::SSL::CertificateRequest CertificateRevocationList = Puppet::SSL::CertificateRevocationList extend Puppet::Indirector indirects :certificate_status, :terminus_class => :file, :doc => < :file, :disabled_ca => nil, :file => nil, :rest => :rest} if term = host_map[terminus] self.indirection.terminus_class = term else self.indirection.reset_terminus_class end if cache # This is weird; we don't actually cache our keys, we # use what would otherwise be the cache as our normal # terminus. Key.indirection.terminus_class = cache else Key.indirection.terminus_class = terminus end if cache Certificate.indirection.cache_class = cache CertificateRequest.indirection.cache_class = cache CertificateRevocationList.indirection.cache_class = cache else # Make sure we have no cache configured. puppet master # switches the configurations around a bit, so it's important # that we specify the configs for absolutely everything, every # time. Certificate.indirection.cache_class = nil CertificateRequest.indirection.cache_class = nil CertificateRevocationList.indirection.cache_class = nil end end CA_MODES = { # Our ca is local, so we use it as the ultimate source of information # And we cache files locally. :local => [:ca, :file], # We're a remote CA client. :remote => [:rest, :file], # We are the CA, so we don't have read/write access to the normal certificates. :only => [:ca], # We have no CA, so we just look in the local file store. :none => [:disabled_ca] } # Specify how we expect to interact with our certificate authority. def self.ca_location=(mode) modes = CA_MODES.collect { |m, vals| m.to_s }.join(", ") raise ArgumentError, "CA Mode can only be one of: #{modes}" unless CA_MODES.include?(mode) @ca_location = mode configure_indirection(*CA_MODES[@ca_location]) end # Puppet::SSL::Host is actually indirected now so the original implementation # has been moved into the certificate_status indirector. This method is in-use # in `puppet cert -c `. def self.destroy(name) indirection.destroy(name) end def self.from_data_hash(data) instance = new(data["name"]) if data["desired_state"] instance.desired_state = data["desired_state"] end instance end - def self.from_pson(pson) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(pson) - end - # Puppet::SSL::Host is actually indirected now so the original implementation # has been moved into the certificate_status indirector. This method does not # appear to be in use in `puppet cert -l`. def self.search(options = {}) indirection.search("*", options) end # Is this a ca host, meaning that all of its files go in the CA location? def ca? ca end def key @key ||= Key.indirection.find(name) end # This is the private key; we can create it from scratch # with no inputs. def generate_key @key = Key.new(name) @key.generate begin Key.indirection.save(@key) rescue @key = nil raise end true end def certificate_request @certificate_request ||= CertificateRequest.indirection.find(name) end # Our certificate request requires the key but that's all. def generate_certificate_request(options = {}) generate_key unless key # If this CSR is for the current machine... if name == Puppet[:certname].downcase # ...add our configured dns_alt_names if Puppet[:dns_alt_names] and Puppet[:dns_alt_names] != '' options[:dns_alt_names] ||= Puppet[:dns_alt_names] elsif Puppet::SSL::CertificateAuthority.ca? and fqdn = Facter.value(:fqdn) and domain = Facter.value(:domain) options[:dns_alt_names] = "puppet, #{fqdn}, puppet.#{domain}" end end csr_attributes = Puppet::SSL::CertificateRequestAttributes.new(Puppet[:csr_attributes]) if csr_attributes.load options[:csr_attributes] = csr_attributes.custom_attributes options[:extension_requests] = csr_attributes.extension_requests end @certificate_request = CertificateRequest.new(name) @certificate_request.generate(key.content, options) begin CertificateRequest.indirection.save(@certificate_request) rescue @certificate_request = nil raise end true end def certificate unless @certificate generate_key unless key # get the CA cert first, since it's required for the normal cert # to be of any use. return nil unless Certificate.indirection.find("ca", :fail_on_404 => true) unless ca? return nil unless @certificate = Certificate.indirection.find(name) validate_certificate_with_key end @certificate end def validate_certificate_with_key raise Puppet::Error, "No certificate to validate." unless certificate raise Puppet::Error, "No private key with which to validate certificate with fingerprint: #{certificate.fingerprint}" unless key unless certificate.content.check_private_key(key.content) raise Puppet::Error, < name } my_state = state result[:state] = my_state result[:desired_state] = desired_state if desired_state thing_to_use = (my_state == 'requested') ? certificate_request : my_cert # this is for backwards-compatibility # we should deprecate it and transition people to using # pson[:fingerprints][:default] # It appears that we have no internal consumers of this api # --jeffweiss 30 aug 2012 result[:fingerprint] = thing_to_use.fingerprint # The above fingerprint doesn't tell us what message digest algorithm was used # No problem, except that the default is changing between 2.7 and 3.0. Also, as # we move to FIPS 140-2 compliance, MD5 is no longer allowed (and, gasp, will # segfault in rubies older than 1.9.3) # So, when we add the newer fingerprints, we're explicit about the hashing # algorithm used. # --jeffweiss 31 july 2012 result[:fingerprints] = {} result[:fingerprints][:default] = thing_to_use.fingerprint suitable_message_digest_algorithms.each do |md| result[:fingerprints][md] = thing_to_use.fingerprint md end result[:dns_alt_names] = thing_to_use.subject_alt_names result end # eventually we'll probably want to move this somewhere else or make it # configurable # --jeffweiss 29 aug 2012 def suitable_message_digest_algorithms [:SHA1, :SHA256, :SHA512] end # Attempt to retrieve a cert, if we don't already have one. def wait_for_cert(time) begin return if certificate generate return if certificate rescue SystemExit,NoMemoryError raise rescue Exception => detail Puppet.log_exception(detail, "Could not request certificate: #{detail.message}") if time < 1 puts "Exiting; failed to retrieve certificate and waitforcert is disabled" exit(1) else sleep(time) end retry end if time < 1 puts "Exiting; no certificate found and waitforcert is disabled" exit(1) end while true sleep time begin break if certificate Puppet.notice "Did not receive certificate" rescue StandardError => detail Puppet.log_exception(detail, "Could not request certificate: #{detail.message}") end end end def state if certificate_request return 'requested' end begin Puppet::SSL::CertificateAuthority.new.verify(name) return 'signed' rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError return 'revoked' end end end require 'puppet/ssl/certificate_authority' diff --git a/lib/puppet/status.rb b/lib/puppet/status.rb index eebb9583a..35aaf5396 100644 --- a/lib/puppet/status.rb +++ b/lib/puppet/status.rb @@ -1,45 +1,40 @@ require 'puppet/indirector' class Puppet::Status extend Puppet::Indirector indirects :status, :terminus_class => :local attr_accessor :status def initialize( status = nil ) @status = status || {"is_alive" => true} end def to_data_hash @status end def self.from_data_hash(data) if data.include?('status') self.new(data['status']) else self.new(data) end end - def self.from_pson(pson) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(pson) - end - def name "status" end def name=(name) # NOOP end def version @status['version'] end def version=(version) @status['version'] = version end end diff --git a/lib/puppet/transaction/event.rb b/lib/puppet/transaction/event.rb index 0a594331b..7ed444c00 100644 --- a/lib/puppet/transaction/event.rb +++ b/lib/puppet/transaction/event.rb @@ -1,111 +1,106 @@ require 'puppet/transaction' require 'puppet/util/tagging' require 'puppet/util/logging' require 'puppet/util/methodhelper' require 'puppet/network/format_support' # A simple struct for storing what happens on the system. class Puppet::Transaction::Event include Puppet::Util::MethodHelper include Puppet::Util::Tagging include Puppet::Util::Logging include Puppet::Network::FormatSupport ATTRIBUTES = [:name, :resource, :property, :previous_value, :desired_value, :historical_value, :status, :message, :file, :line, :source_description, :audited, :invalidate_refreshes] YAML_ATTRIBUTES = %w{@audited @property @previous_value @desired_value @historical_value @message @name @status @time}.map(&:to_sym) attr_accessor *ATTRIBUTES attr_accessor :time attr_reader :default_log_level EVENT_STATUSES = %w{noop success failure audit} def self.from_data_hash(data) obj = self.allocate obj.initialize_from_hash(data) obj end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - def initialize(options = {}) @audited = false set_options(options) @time = Time.now end def initialize_from_hash(data) @audited = data['audited'] @property = data['property'] @previous_value = data['previous_value'] @desired_value = data['desired_value'] @historical_value = data['historical_value'] @message = data['message'] @name = data['name'].intern if data['name'] @status = data['status'] @time = data['time'] @time = Time.parse(@time) if @time.is_a? String end def to_data_hash { 'audited' => @audited, 'property' => @property, 'previous_value' => @previous_value, 'desired_value' => @desired_value, 'historical_value' => @historical_value, 'message' => @message, 'name' => @name, 'status' => @status, 'time' => @time.iso8601(9), } end def property=(prop) @property = prop.to_s end def resource=(res) if res.respond_to?(:[]) and level = res[:loglevel] @default_log_level = level end @resource = res.to_s end def send_log super(log_level, message) end def status=(value) raise ArgumentError, "Event status can only be #{EVENT_STATUSES.join(', ')}" unless EVENT_STATUSES.include?(value) @status = value end def to_s message end def inspect %Q(#<#{self.class.name} @name="#{@name.inspect}" @message="#{@message.inspect}">) end def to_yaml_properties YAML_ATTRIBUTES & super end private # If it's a failure, use 'err', else use either the resource's log level (if available) # or 'notice'. def log_level status == "failure" ? :err : (@default_log_level || :notice) end # Used by the Logging module def log_source source_description || property || resource end end diff --git a/lib/puppet/transaction/report.rb b/lib/puppet/transaction/report.rb index 05306547e..b8db8c1eb 100644 --- a/lib/puppet/transaction/report.rb +++ b/lib/puppet/transaction/report.rb @@ -1,383 +1,378 @@ require 'puppet' require 'puppet/indirector' # This class is used to report what happens on a client. # There are two types of data in a report; _Logs_ and _Metrics_. # # * **Logs** - are the output that each change produces. # * **Metrics** - are all of the numerical data involved in the transaction. # # Use {Puppet::Reports} class to create a new custom report type. This class is indirectly used # as a source of data to report in such a registered report. # # ##Metrics # There are three types of metrics in each report, and each type of metric has one or more values. # # * Time: Keeps track of how long things took. # * Total: Total time for the configuration run # * File: # * Exec: # * User: # * Group: # * Config Retrieval: How long the configuration took to retrieve # * Service: # * Package: # * Resources: Keeps track of the following stats: # * Total: The total number of resources being managed # * Skipped: How many resources were skipped, because of either tagging or scheduling restrictions # * Scheduled: How many resources met any scheduling restrictions # * Out of Sync: How many resources were out of sync # * Applied: How many resources were attempted to be fixed # * Failed: How many resources were not successfully fixed # * Restarted: How many resources were restarted because their dependencies changed # * Failed Restarts: How many resources could not be restarted # * Changes: The total number of changes in the transaction. # # @api public class Puppet::Transaction::Report extend Puppet::Indirector indirects :report, :terminus_class => :processor # The version of the configuration # @todo Uncertain what this is? # @return [???] the configuration version attr_accessor :configuration_version # An agent generated transaction uuid, useful for connecting catalog and report # @return [String] uuid attr_accessor :transaction_uuid # The host name for which the report is generated # @return [String] the host name attr_accessor :host # The name of the environment the host is in # @return [String] the environment name attr_accessor :environment # A hash with a map from resource to status # @return [Hash{String => Puppet::Resource::Status}] Resource name to status. attr_reader :resource_statuses # A list of log messages. # @return [Array] logged messages attr_reader :logs # A hash of metric name to metric value. # @return [Hash<{String => Object}>] A map of metric name to value. # @todo Uncertain if all values are numbers - now marked as Object. # attr_reader :metrics # The time when the report data was generated. # @return [Time] A time object indicating when the report data was generated # attr_reader :time # The 'kind' of report is the name of operation that triggered the report to be produced. # Typically "apply". # @return [String] the kind of operation that triggered the generation of the report. # attr_reader :kind # The status of the client run is an enumeration: 'failed', 'changed' or 'unchanged' # @return [String] the status of the run - one of the values 'failed', 'changed', or 'unchanged' # attr_reader :status # @return [String] The Puppet version in String form. # @see Puppet::version() # attr_reader :puppet_version # @return [Integer] report format version number. This value is constant for # a given version of Puppet; it is incremented when a new release of Puppet # changes the API for the various objects that make up a report. # attr_reader :report_format def self.from_data_hash(data) obj = self.allocate obj.initialize_from_hash(data) obj end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - def as_logging_destination(&block) Puppet::Util::Log.with_destination(self, &block) end # @api private def <<(msg) @logs << msg self end # @api private def add_times(name, value) @external_times[name] = value end # @api private def add_metric(name, hash) metric = Puppet::Util::Metric.new(name) hash.each do |name, value| metric.newvalue(name, value) end @metrics[metric.name] = metric metric end # @api private def add_resource_status(status) @resource_statuses[status.resource] = status end # @api private def compute_status(resource_metrics, change_metric) if (resource_metrics["failed"] || 0) > 0 'failed' elsif change_metric > 0 'changed' else 'unchanged' end end # @api private def prune_internal_data resource_statuses.delete_if {|name,res| res.resource_type == 'Whit'} end # @api private def finalize_report prune_internal_data resource_metrics = add_metric(:resources, calculate_resource_metrics) add_metric(:time, calculate_time_metrics) change_metric = calculate_change_metric add_metric(:changes, {"total" => change_metric}) add_metric(:events, calculate_event_metrics) @status = compute_status(resource_metrics, change_metric) end # @api private def initialize(kind, configuration_version=nil, environment=nil, transaction_uuid=nil) @metrics = {} @logs = [] @resource_statuses = {} @external_times ||= {} @host = Puppet[:node_name_value] @time = Time.now @kind = kind @report_format = 4 @puppet_version = Puppet.version @configuration_version = configuration_version @transaction_uuid = transaction_uuid @environment = environment @status = 'failed' # assume failed until the report is finalized end # @api private def initialize_from_hash(data) @puppet_version = data['puppet_version'] @report_format = data['report_format'] @configuration_version = data['configuration_version'] @transaction_uuid = data['transaction_uuid'] @environment = data['environment'] @status = data['status'] @host = data['host'] @time = data['time'] if @time.is_a? String @time = Time.parse(@time) end @kind = data['kind'] @metrics = {} data['metrics'].each do |name, hash| @metrics[name] = Puppet::Util::Metric.from_data_hash(hash) end @logs = data['logs'].map do |record| Puppet::Util::Log.from_data_hash(record) end @resource_statuses = {} data['resource_statuses'].map do |record| if record[1] == {} status = nil else status = Puppet::Resource::Status.from_data_hash(record[1]) end @resource_statuses[record[0]] = status end end def to_data_hash { 'host' => @host, 'time' => @time.iso8601(9), 'configuration_version' => @configuration_version, 'transaction_uuid' => @transaction_uuid, 'report_format' => @report_format, 'puppet_version' => @puppet_version, 'kind' => @kind, 'status' => @status, 'environment' => @environment, 'logs' => @logs, 'metrics' => @metrics, 'resource_statuses' => @resource_statuses, } end # @return [String] the host name # @api public # def name host end # Provide a human readable textual summary of this report. # @note This is intended for debugging purposes # @return [String] A string with a textual summary of this report. # @api public # def summary report = raw_summary ret = "" report.keys.sort { |a,b| a.to_s <=> b.to_s }.each do |key| ret += "#{Puppet::Util::Metric.labelize(key)}:\n" report[key].keys.sort { |a,b| # sort by label if a == :total 1 elsif b == :total -1 else report[key][a].to_s <=> report[key][b].to_s end }.each do |label| value = report[key][label] next if value == 0 value = "%0.2f" % value if value.is_a?(Float) ret += " %15s %s\n" % [Puppet::Util::Metric.labelize(label) + ":", value] end end ret end # Provides a raw hash summary of this report. # @return [Hash<{String => Object}>] A hash with metrics key to value map # @api public # def raw_summary report = { "version" => { "config" => configuration_version, "puppet" => Puppet.version } } @metrics.each do |name, metric| key = metric.name.to_s report[key] = {} metric.values.each do |name, label, value| report[key][name.to_s] = value end report[key]["total"] = 0 unless key == "time" or report[key].include?("total") end (report["time"] ||= {})["last_run"] = Time.now.tv_sec report end # Computes a single number that represents the report's status. # The computation is based on the contents of this report's metrics. # The resulting number is a bitmask where # individual bits represent the presence of different metrics. # # * 0x2 set if there are changes # * 0x4 set if there are resource failures or resources that failed to restart # @return [Integer] A bitmask where 0x2 is set if there are changes, and 0x4 is set of there are failures. # @api public # def exit_status status = 0 status |= 2 if @metrics["changes"]["total"] > 0 status |= 4 if @metrics["resources"]["failed"] > 0 status |= 4 if @metrics["resources"]["failed_to_restart"] > 0 status end # @api private # def to_yaml_properties super - [:@external_times] end def self.supported_formats [:pson, :yaml] end def self.default_format Puppet[:report_serialization_format].intern end private def calculate_change_metric resource_statuses.map { |name, status| status.change_count || 0 }.inject(0) { |a,b| a+b } end def calculate_event_metrics metrics = Hash.new(0) %w{total failure success}.each { |m| metrics[m] = 0 } resource_statuses.each do |name, status| metrics["total"] += status.events.length status.events.each do |event| metrics[event.status] += 1 end end metrics end def calculate_resource_metrics metrics = {} metrics["total"] = resource_statuses.length # force every resource key in the report to be present # even if no resources is in this given state Puppet::Resource::Status::STATES.each do |state| metrics[state.to_s] = 0 end resource_statuses.each do |name, status| Puppet::Resource::Status::STATES.each do |state| metrics[state.to_s] += 1 if status.send(state) end end metrics end def calculate_time_metrics metrics = Hash.new(0) resource_statuses.each do |name, status| type = Puppet::Resource.new(name).type metrics[type.to_s.downcase] += status.evaluation_time if status.evaluation_time end @external_times.each do |name, value| metrics[name.to_s.downcase] = value end metrics["total"] = metrics.values.inject(0) { |a,b| a+b } metrics end end diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb index 05f0b3fad..04382cbd0 100644 --- a/lib/puppet/util/log.rb +++ b/lib/puppet/util/log.rb @@ -1,343 +1,338 @@ require 'puppet/util/tagging' require 'puppet/util/classgen' require 'puppet/network/format_support' # Pass feedback to the user. Log levels are modeled after syslog's, and it is # expected that that will be the most common log destination. Supports # multiple destinations, one of which is a remote server. class Puppet::Util::Log include Puppet::Util extend Puppet::Util::ClassGen include Puppet::Util::Tagging include Puppet::Network::FormatSupport @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @desttypes = {} # Create a new destination type. def self.newdesttype(name, options = {}, &block) dest = genclass( name, :parent => Puppet::Util::Log::Destination, :prefix => "Dest", :block => block, :hash => @desttypes, :attributes => options ) dest.match(dest.name) dest end require 'puppet/util/log/destination' require 'puppet/util/log/destinations' @destinations = {} @queued = [] class << self include Puppet::Util include Puppet::Util::ClassGen attr_reader :desttypes end # Reset log to basics. Basically just flushes and closes files and # undefs other objects. def Log.close(destination) if @destinations.include?(destination) @destinations[destination].flush if @destinations[destination].respond_to?(:flush) @destinations[destination].close if @destinations[destination].respond_to?(:close) @destinations.delete(destination) end end def self.close_all destinations.keys.each { |dest| close(dest) } raise Puppet::DevError.new("Log.close_all failed to close #{@destinations.keys.inspect}") if !@destinations.empty? end # Flush any log destinations that support such operations. def Log.flush @destinations.each { |type, dest| dest.flush if dest.respond_to?(:flush) } end def Log.autoflush=(v) @destinations.each do |type, dest| dest.autoflush = v if dest.respond_to?(:autoflush=) 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) raise Puppet::DevError, "Logs require a level" unless hash.include?(:level) raise Puppet::DevError, "Invalid log level #{hash[:level]}" unless @levels.index(hash[:level]) @levels.index(hash[:level]) >= @loglevel ? Puppet::Util::Log.new(hash) : nil end def Log.destinations @destinations end # Yield each valid level in turn def Log.eachlevel @levels.each { |level| yield level } end # Return the current log level. def Log.level @levels[@loglevel] end # Set the current log level. def Log.level=(level) level = level.intern unless level.is_a?(Symbol) raise Puppet::DevError, "Invalid loglevel #{level}" unless @levels.include?(level) @loglevel = @levels.index(level) end def Log.levels @levels.dup end # Create a new log destination. def Log.newdestination(dest) # Each destination can only occur once. if @destinations.find { |name, obj| obj.name == dest } return end name, type = @desttypes.find do |name, klass| klass.match?(dest) end if type.respond_to?(:suitable?) and not type.suitable?(dest) return end raise Puppet::DevError, "Unknown destination type #{dest}" unless type begin if type.instance_method(:initialize).arity == 1 @destinations[dest] = type.new(dest) else @destinations[dest] = type.new end flushqueue @destinations[dest] rescue => detail Puppet.log_exception(detail) # If this was our only destination, then add the console back in. newdestination(:console) if @destinations.empty? and (dest != :console and dest != "console") end end def Log.with_destination(destination, &block) if @destinations.include?(destination) yield else newdestination(destination) begin yield ensure close(destination) end end end # Route the actual message. FIXME There are lots of things this method # should do, like caching and a bit more. It's worth noting that there's # a potential for a loop here, if the machine somehow gets the destination set as # itself. def Log.newmessage(msg) return if @levels.index(msg.level) < @loglevel queuemessage(msg) if @destinations.length == 0 @destinations.each do |name, dest| dest.handle(msg) end end def Log.queuemessage(msg) @queued.push(msg) end def Log.flushqueue return unless @destinations.size >= 1 @queued.each do |msg| Log.newmessage(msg) end @queued.clear end # Flush the logging queue. If there are no destinations available, # adds in a console logger before flushing the queue. # This is mainly intended to be used as a last-resort attempt # to ensure that logging messages are not thrown away before # the program is about to exit--most likely in a horrific # error scenario. # @return nil def Log.force_flushqueue() if (@destinations.empty? and !(@queued.empty?)) newdestination(:console) end flushqueue end def Log.sendlevel?(level) @levels.index(level) >= @loglevel end # Reopen all of our logs. def Log.reopen Puppet.notice "Reopening log files" types = @destinations.keys @destinations.each { |type, dest| dest.close if dest.respond_to?(:close) } @destinations.clear # We need to make sure we always end up with some kind of destination begin types.each { |type| Log.newdestination(type) } rescue => detail if @destinations.empty? Log.setup_default Puppet.err detail.to_s end end end def self.setup_default Log.newdestination( (Puppet.features.syslog? ? :syslog : (Puppet.features.eventlog? ? :eventlog : Puppet[:puppetdlog]))) end # Is the passed level a valid log level? def self.validlevel?(level) @levels.include?(level) end def self.from_data_hash(data) obj = allocate obj.initialize_from_hash(data) obj end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - attr_accessor :time, :remote, :file, :line, :source attr_reader :level, :message def initialize(args) self.level = args[:level] self.message = args[:message] self.source = args[:source] || "Puppet" @time = Time.now if tags = args[:tags] tags.each { |t| self.tag(t) } end [:file, :line].each do |attr| next unless value = args[attr] send(attr.to_s + "=", value) end Log.newmessage(self) end def initialize_from_hash(data) @level = data['level'].intern @message = data['message'] @source = data['source'] @tags = Puppet::Util::TagSet.new(data['tags']) @time = data['time'] if @time.is_a? String @time = Time.parse(@time) end @file = data['file'] if data['file'] @line = data['line'] if data['line'] end def to_hash self.to_data_hash end def to_data_hash { 'level' => @level, 'message' => @message, 'source' => @source, 'tags' => @tags, 'time' => @time.iso8601(9), 'file' => @file, 'line' => @line, } end def to_pson(*args) to_data_hash.to_pson(*args) end def message=(msg) raise ArgumentError, "Puppet::Util::Log requires a message" unless msg @message = msg.to_s end def level=(level) raise ArgumentError, "Puppet::Util::Log requires a log level" unless level raise ArgumentError, "Puppet::Util::Log requires a symbol or string" unless level.respond_to? "to_sym" @level = level.to_sym raise ArgumentError, "Invalid log level #{@level}" unless self.class.validlevel?(@level) # Tag myself with my log level tag(level) end # If they pass a source in to us, we make sure it is a string, and # we retrieve any tags we can. def source=(source) if source.respond_to?(:path) @source = source.path source.tags.each { |t| tag(t) } self.file = source.file self.line = source.line else @source = source.to_s end end def to_report "#{time} #{source} (#{level}): #{to_s}" end def to_s message end end # This is for backward compatibility from when we changed the constant to Puppet::Util::Log # because the reports include the constant name. Apparently the alias was created in # March 2007, should could probably be removed soon. Puppet::Log = Puppet::Util::Log diff --git a/lib/puppet/util/metric.rb b/lib/puppet/util/metric.rb index c12d92202..559418f2c 100644 --- a/lib/puppet/util/metric.rb +++ b/lib/puppet/util/metric.rb @@ -1,66 +1,61 @@ # included so we can test object types require 'puppet' require 'puppet/network/format_support' # A class for handling metrics. This is currently ridiculously hackish. class Puppet::Util::Metric include Puppet::Network::FormatSupport attr_accessor :type, :name, :value, :label attr_writer :values def self.from_data_hash(data) metric = new(data['name'], data['label']) metric.values = data['values'] metric end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - def to_data_hash { 'name' => @name, 'label' => @label, 'values' => @values } end def to_pson(*args) to_data_hash.to_pson(*args) end # Return a specific value def [](name) if value = @values.find { |v| v[0] == name } return value[2] else return 0 end end def initialize(name,label = nil) @name = name.to_s @label = label || self.class.labelize(name) @values = [] end def newvalue(name,value,label = nil) raise ArgumentError.new("metric name #{name.inspect} is not a string") unless name.is_a? String label ||= self.class.labelize(name) @values.push [name,label,value] end def values @values.sort { |a, b| a[1] <=> b[1] } end # Convert a name into a label. def self.labelize(name) name.to_s.capitalize.gsub("_", " ") end end diff --git a/lib/puppet/util/tag_set.rb b/lib/puppet/util/tag_set.rb index 6f83ff870..e74f228f7 100644 --- a/lib/puppet/util/tag_set.rb +++ b/lib/puppet/util/tag_set.rb @@ -1,41 +1,36 @@ require 'set' require 'puppet/network/format_support' class Puppet::Util::TagSet < Set include Puppet::Network::FormatSupport def self.from_yaml(yaml) self.new(YAML.load(yaml)) end def to_yaml @hash.keys.to_yaml end def self.from_data_hash(data) self.new(data) end - def self.from_pson(data) - Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") - self.from_data_hash(data) - end - def to_data_hash to_a end def to_pson(*args) to_data_hash.to_pson end # this makes puppet serialize it as an array for backwards # compatibility def to_zaml(z) to_data_hash.to_zaml(z) end def join(*args) to_a.join(*args) end end