diff --git a/lib/puppet/parser/ast.rb b/lib/puppet/parser/ast.rb index 54e034acb..a5aaeddc4 100644 --- a/lib/puppet/parser/ast.rb +++ b/lib/puppet/parser/ast.rb @@ -1,134 +1,138 @@ # the parent class for all of our syntactical objects require 'puppet' require 'puppet/util/autoload' require 'puppet/file_collection/lookup' # The base class for all of the objects that make up the parse trees. # Handles things like file name, line #, and also does the initialization # for all of the parameters of all of the child objects. class Puppet::Parser::AST # Do this so I don't have to type the full path in all of the subclasses AST = Puppet::Parser::AST include Puppet::FileCollection::Lookup include Puppet::Util::Errors include Puppet::Util::MethodHelper include Puppet::Util::Docs attr_accessor :parent, :scope + def inspect + "( #{self.class} #{self.to_s} #{@children.inspect} )" + end + # don't fetch lexer comment by default def use_docs self.class.use_docs end # allow our subclass to specify they want documentation class << self attr_accessor :use_docs def associates_doc self.use_docs = true end end # Does this ast object set something? If so, it gets evaluated first. def self.settor? if defined?(@settor) @settor else false end end # Evaluate the current object. Just a stub method, since the subclass # should override this method. # of the contained children and evaluates them in turn, returning a # list of all of the collected values, rejecting nil values def evaluate(*options) raise Puppet::DevError, "Did not override #evaluate in #{self.class}" end # Throw a parse error. def parsefail(message) self.fail(Puppet::ParseError, message) end # Wrap a statemp in a reusable way so we always throw a parse error. def parsewrap exceptwrap :type => Puppet::ParseError do yield end end # The version of the evaluate method that should be called, because it # correctly handles errors. It is critical to use this method because # it can enable you to catch the error where it happens, rather than # much higher up the stack. def safeevaluate(*options) # We duplicate code here, rather than using exceptwrap, because this # is called so many times during parsing. begin return self.evaluate(*options) rescue Puppet::Error => detail raise adderrorcontext(detail) rescue => detail error = Puppet::Error.new(detail.to_s) # We can't use self.fail here because it always expects strings, # not exceptions. raise adderrorcontext(error, detail) end end # Initialize the object. Requires a hash as the argument, and # takes each of the parameters of the hash and calls the settor # method for them. This is probably pretty inefficient and should # likely be changed at some point. def initialize(args) set_options(args) end # evaluate ourselves, and match def evaluate_match(value, scope) obj = self.safeevaluate(scope) obj = obj.downcase if obj.respond_to?(:downcase) value = value.downcase if value.respond_to?(:downcase) obj = Puppet::Parser::Scope.number?(obj) || obj value = Puppet::Parser::Scope.number?(value) || value # "" == undef for case/selector/if obj == value or (obj == "" and value == :undef) end end # And include all of the AST subclasses. require 'puppet/parser/ast/arithmetic_operator' require 'puppet/parser/ast/astarray' require 'puppet/parser/ast/asthash' require 'puppet/parser/ast/branch' require 'puppet/parser/ast/boolean_operator' require 'puppet/parser/ast/caseopt' require 'puppet/parser/ast/casestatement' require 'puppet/parser/ast/collection' require 'puppet/parser/ast/collexpr' require 'puppet/parser/ast/comparison_operator' require 'puppet/parser/ast/else' require 'puppet/parser/ast/function' require 'puppet/parser/ast/ifstatement' require 'puppet/parser/ast/in_operator' require 'puppet/parser/ast/leaf' require 'puppet/parser/ast/match_operator' require 'puppet/parser/ast/minus' require 'puppet/parser/ast/nop' require 'puppet/parser/ast/not' require 'puppet/parser/ast/resource' require 'puppet/parser/ast/resource_defaults' require 'puppet/parser/ast/resource_override' require 'puppet/parser/ast/resource_reference' require 'puppet/parser/ast/resourceparam' require 'puppet/parser/ast/selector' require 'puppet/parser/ast/tag' require 'puppet/parser/ast/vardef' require 'puppet/parser/ast/relationship' diff --git a/lib/puppet/relationship.rb b/lib/puppet/relationship.rb index 7079fb44b..08d7d042b 100644 --- a/lib/puppet/relationship.rb +++ b/lib/puppet/relationship.rb @@ -1,94 +1,98 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2006-11-24. # Copyright (c) 2006. All rights reserved. # subscriptions are permanent associations determining how different # objects react to an event require 'puppet/util/pson' # 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 extend Puppet::Util::Pson attr_accessor :source, :target, :callback attr_reader :event def self.from_pson(pson) source = pson["source"] target = pson["target"] args = {} if event = pson["event"] args[:event] = event end if callback = pson["callback"] args[:callback] = callback end new(source, target, args) 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_pson_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_pson(*args) to_pson_data_hash.to_pson(*args) end def to_s ref end end diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb index 4f0d50750..b0a3ecee6 100644 --- a/lib/puppet/resource.rb +++ b/lib/puppet/resource.rb @@ -1,416 +1,420 @@ require 'puppet' require 'puppet/util/tagging' require 'puppet/util/pson' # The simplest resource class. Eventually it will function as the # base class for all resource-like behaviour. class Puppet::Resource include Puppet::Util::Tagging require 'puppet/resource/type_collection_helper' include Puppet::Resource::TypeCollectionHelper extend Puppet::Util::Pson 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_pson(pson) raise ArgumentError, "No resource type provided in pson data" unless type = pson['type'] raise ArgumentError, "No resource title provided in pson data" unless title = pson['title'] resource = new(type, title) if params = pson['parameters'] params.each { |param, value| resource[param] = value } end if tags = pson['tags'] tags.each { |tag| resource.tag(tag) } end ATTRIBUTES.each do |a| if value = pson[a.to_s] resource.send(a.to_s + "=", value) end end resource.exported ||= false resource end + def inspect + "#{@type}[#{@title}]#{to_hash.inspect}" + end + def to_pson_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 def to_pson(*args) to_pson_data_hash.to_pson(*args) 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 # These two methods are extracted into a Helper # module, but file load order prevents me # from including them in the class, and I had weird # behaviour (i.e., sometimes it didn't work) when # I directly extended each resource with the helper. def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end %w{exported virtual strict}.each do |m| define_method(m+"?") do self.send(m) end end # This stub class is only needed for serialization compatibility with 0.25.x class Reference attr_accessor :type,:title def initialize(type,title) @type,@title = type,title end end # Create our resource. def initialize(type, title = nil, attributes = {}) @parameters = {} # 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 @type == "Class" @title = :main if @title == "" @title = munge_type_name(@title) end if params = attributes[:parameters] extract_parameters(params) end tag(self.type) tag(self.title) if valid_tag?(self.title) @reference = Reference.new(@type,@title) # for serialization compatibility with 0.25.x raise ArgumentError, "Invalid resource type #{type}" if strict? and ! resource_type end def ref to_s end # Find our resource. def resolve return(catalog ? catalog.resource(to_s) : nil) end def resource_type case type when "Class"; known_resource_types.hostclass(title == :main ? "" : title) when "Node"; known_resource_types.node(title) else Puppet::Type.type(type.to_s.downcase.to_sym) || known_resource_types.definition(type) end 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 return(resource_type.respond_to? :key_attributes) ? resource_type.key_attributes : [:name] end # Convert our resource to Puppet code. def to_manifest "%s { '%s':\n%s\n}" % [self.type.to_s.downcase, self.title, @parameters.collect { |p, v| if v.is_a? Array " #{p} => [\'#{v.join("','")}\']" else " #{p} => \'#{v}\'" end }.join(",\n") ] 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 if typeklass = Puppet::Type.type(self.type) return typeklass.new(self) else return Puppet::Type::Component.new(self) end end # Translate our object to a backward-compatible transportable object. def to_trans if builtin_type? and type.downcase.to_s != "stage" result = to_transobject else result = to_transbucket end result.file = self.file result.line = self.line result end def to_trans_ref [type.to_s, title.to_s] end # Create an old-style TransObject instance, for builtin resource types. def to_transobject # Now convert to a transobject result = Puppet::TransObject.new(title, type) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = v.to_trans_ref elsif v.is_a?(Array) v = v.collect { |av| av = av.to_trans_ref if av.is_a?(Puppet::Resource) av } end # 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. result[p.to_s] = if v.is_a?(Array) and v.length == 1 v[0] else v end end result.tags = self.tags result end def name # this is potential namespace conflict # between the notion of an "indirector name" # and a "resource name" [ type, title ].join('/') end def to_resource self end def valid_parameter?(name) resource_type.valid_parameter?(name) end def validate_parameter(name) raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name) end private # Produce a canonical method name. def parameter_name(param) param = param.to_s.downcase.to_sym if param == :name and n = 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 # Create an old-style TransBucket instance, for non-builtin resource types. def to_transbucket bucket = Puppet::TransBucket.new([]) bucket.type = self.type bucket.name = self.title # TransBuckets don't support parameters, which is why they're being deprecated. bucket 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 (argtitle || 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 { |symbol_and_lambda,capture| sym, lam = symbol_and_lambda #self[sym] = lam.call(capture) h[sym] = lam.call(capture) } return h end } else return { :name => title.to_s } end end end diff --git a/lib/puppet/resource/type_collection.rb b/lib/puppet/resource/type_collection.rb index 6a03458b3..277d37b18 100644 --- a/lib/puppet/resource/type_collection.rb +++ b/lib/puppet/resource/type_collection.rb @@ -1,213 +1,217 @@ class Puppet::Resource::TypeCollection attr_reader :environment def clear @hostclasses.clear @definitions.clear @nodes.clear end def initialize(env) @environment = env.is_a?(String) ? Puppet::Node::Environment.new(env) : env @hostclasses = {} @definitions = {} @nodes = {} # So we can keep a list and match the first-defined regex @node_list = [] @watched_files = {} end + def inspect + "TypeCollection" + { :hostclasses => @hostclasses.keys, :definitions => @definitions.keys, :nodes => @nodes.keys }.inspect + end + def <<(thing) add(thing) self end def add(instance) if instance.type == :hostclass and other = @hostclasses[instance.name] and other.type == :hostclass other.merge(instance) return other end method = "add_#{instance.type}" send(method, instance) instance.resource_type_collection = self instance end def add_hostclass(instance) dupe_check(instance, @hostclasses) { |dupe| "Class '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined as a class" } @hostclasses[instance.name] = instance instance end def hostclass(name) @hostclasses[munge_name(name)] end def add_node(instance) dupe_check(instance, @nodes) { |dupe| "Node '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } @node_list << instance @nodes[instance.name] = instance instance end def loader require 'puppet/parser/type_loader' @loader ||= Puppet::Parser::TypeLoader.new(environment) end def node(name) name = munge_name(name) if node = @nodes[name] return node end @node_list.each do |node| next unless node.name_is_regex? return node if node.match(name) end nil end def node_exists?(name) @nodes[munge_name(name)] end def nodes? @nodes.length > 0 end def add_definition(instance) dupe_check(instance, @hostclasses) { |dupe| "'#{instance.name}' is already defined#{dupe.error_context} as a class; cannot redefine as a definition" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined" } @definitions[instance.name] = instance end def definition(name) @definitions[munge_name(name)] end def find(namespaces, name, type) #Array("") == [] for some reason namespaces = [namespaces] unless namespaces.is_a?(Array) if name =~ /^::/ return send(type, name.sub(/^::/, '')) end namespaces.each do |namespace| ary = namespace.split("::") while ary.length > 0 tmp_namespace = ary.join("::") if r = find_partially_qualified(tmp_namespace, name, type) return r end # Delete the second to last object, which reduces our namespace by one. ary.pop end if result = send(type, name) return result end end nil end def find_or_load(namespaces, name, type) name = name.downcase namespaces = [namespaces] unless namespaces.is_a?(Array) namespaces = namespaces.collect { |ns| ns.downcase } # This could be done in the load_until, but the knowledge seems to # belong here. if r = find(namespaces, name, type) return r end loader.load_until(namespaces, name) { find(namespaces, name, type) } end def find_node(namespaces, name) find("", name, :node) end def find_hostclass(namespaces, name) find_or_load(namespaces, name, :hostclass) end def find_definition(namespaces, name) find_or_load(namespaces, name, :definition) end [:hostclasses, :nodes, :definitions].each do |m| define_method(m) do instance_variable_get("@#{m}").dup end end def perform_initial_import parser = Puppet::Parser::Parser.new(environment) if code = Puppet.settings.uninterpolated_value(:code, environment.to_s) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, environment.to_s) return unless File.exist?(file) parser.file = file end parser.parse rescue => detail msg = "Could not parse for environment #{environment}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end def stale? @watched_files.values.detect { |file| file.changed? } end def version return @version if defined?(@version) if environment[:config_version] == "" @version = Time.now.to_i return @version end @version = Puppet::Util.execute([environment[:config_version]]).strip rescue Puppet::ExecutionFailure => e raise Puppet::ParseError, "Unable to set config_version: #{e.message}" end def watch_file(file) @watched_files[file] = Puppet::Util::LoadedFile.new(file) end def watching_file?(file) @watched_files.include?(file) end private def find_partially_qualified(namespace, name, type) send(type, [namespace, name].join("::")) end def munge_name(name) name.to_s.downcase end def dupe_check(instance, hash) return unless dupe = hash[instance.name] message = yield dupe instance.fail Puppet::ParseError, message end end diff --git a/lib/puppet/simple_graph.rb b/lib/puppet/simple_graph.rb index 55b39fadf..c5dac0f6c 100644 --- a/lib/puppet/simple_graph.rb +++ b/lib/puppet/simple_graph.rb @@ -1,448 +1,452 @@ # Created by Luke A. Kanies on 2007-11-07. # Copyright (c) 2007. All rights reserved. require 'puppet/external/dot' require 'puppet/relationship' require 'set' # A hopefully-faster graph class to replace the use of GRATR. class Puppet::SimpleGraph # An internal class for handling a vertex's edges. class VertexWrapper attr_accessor :in, :out, :vertex # Remove all references to everything. def clear @adjacencies[:in].clear @adjacencies[:out].clear @vertex = nil end def initialize(vertex) @vertex = vertex @adjacencies = {:in => {}, :out => {}} end # Find adjacent vertices or edges. def adjacent(options) direction = options[:direction] || :out options[:type] ||= :vertices return send(direction.to_s + "_edges") if options[:type] == :edges @adjacencies[direction].keys.reject { |vertex| @adjacencies[direction][vertex].empty? } end # Add an edge to our list. def add_edge(direction, edge) opposite_adjacencies(direction, edge) << edge end # Return all known edges. def edges in_edges + out_edges end # Test whether we share an edge with a given vertex. def has_edge?(direction, vertex) return(vertex_adjacencies(direction, vertex).length > 0 ? true : false) end # Create methods for returning the degree and edges. [:in, :out].each do |direction| # LAK:NOTE If you decide to create methods for directly # testing the degree, you'll have to get the values and flatten # the results -- you might have duplicate edges, which can give # a false impression of what the degree is. That's just # as expensive as just getting the edge list, so I've decided # to only add this method. define_method("#{direction}_edges") do @adjacencies[direction].values.inject([]) { |total, adjacent| total += adjacent.to_a; total } end end # The other vertex in the edge. def other_vertex(direction, edge) case direction when :in; edge.source else edge.target end end # Remove an edge from our list. Assumes that we've already checked # that the edge is valid. def remove_edge(direction, edge) opposite_adjacencies(direction, edge).delete(edge) end def to_s vertex.to_s end + def inspect + { :@adjacencies => @adjacencies, :@vertex => @vertex.to_s }.inspect + end + private # These methods exist so we don't need a Hash with a default proc. # Look up the adjacencies for a vertex at the other end of an # edge. def opposite_adjacencies(direction, edge) opposite_vertex = other_vertex(direction, edge) vertex_adjacencies(direction, opposite_vertex) end # Look up the adjacencies for a given vertex. def vertex_adjacencies(direction, vertex) @adjacencies[direction][vertex] ||= Set.new @adjacencies[direction][vertex] end end def initialize @vertices = {} @edges = [] end # Clear our graph. def clear @vertices.each { |vertex, wrapper| wrapper.clear } @vertices.clear @edges.clear end # Which resources a given resource depends upon. def dependents(resource) tree_from_vertex(resource).keys end # Which resources depend upon the given resource. def dependencies(resource) # Cache the reversal graph, because it's somewhat expensive # to create. @reversal ||= reversal # Strangely, it's significantly faster to search a reversed # tree in the :out direction than to search a normal tree # in the :in direction. @reversal.tree_from_vertex(resource, :out).keys end # Whether our graph is directed. Always true. Used to produce dot files. def directed? true end # Determine all of the leaf nodes below a given vertex. def leaves(vertex, direction = :out) tree = tree_from_vertex(vertex, direction) l = tree.keys.find_all { |c| adjacent(c, :direction => direction).empty? } end # Collect all of the edges that the passed events match. Returns # an array of edges. def matching_edges(event, base = nil) source = base || event.resource unless vertex?(source) Puppet.warning "Got an event from invalid vertex #{source.ref}" return [] end # Get all of the edges that this vertex should forward events # to, which is the same thing as saying all edges directly below # This vertex in the graph. adjacent(source, :direction => :out, :type => :edges).find_all do |edge| edge.match?(event.name) end end # Return a reversed version of this graph. def reversal result = self.class.new vertices.each { |vertex| result.add_vertex(vertex) } edges.each do |edge| newedge = edge.class.new(edge.target, edge.source, edge.label) result.add_edge(newedge) end result end # Return the size of the graph. def size @vertices.length end # Return the graph as an array. def to_a @vertices.keys end # Provide a topological sort. def topsort degree = {} zeros = [] result = [] # Collect each of our vertices, with the number of in-edges each has. @vertices.each do |name, wrapper| edges = wrapper.in_edges zeros << wrapper if edges.length == 0 degree[wrapper.vertex] = edges end # Iterate over each 0-degree vertex, decrementing the degree of # each of its out-edges. while wrapper = zeros.pop result << wrapper.vertex wrapper.out_edges.each do |edge| degree[edge.target].delete(edge) zeros << @vertices[edge.target] if degree[edge.target].length == 0 end end # If we have any vertices left with non-zero in-degrees, then we've found a cycle. if cycles = degree.find_all { |vertex, edges| edges.length > 0 } and cycles.length > 0 message = cycles.collect { |vertex, edges| edges.collect { |e| e.to_s }.join(", ") }.join(", ") raise Puppet::Error, "Found dependency cycles in the following relationships: #{message}; try using the '--graph' option and open the '.dot' files in OmniGraffle or GraphViz" end result end # Add a new vertex to the graph. def add_vertex(vertex) @reversal = nil return false if vertex?(vertex) setup_vertex(vertex) true # don't return the VertexWrapper instance. end # Remove a vertex from the graph. def remove_vertex!(vertex) return nil unless vertex?(vertex) @vertices[vertex].edges.each { |edge| remove_edge!(edge) } @edges -= @vertices[vertex].edges @vertices[vertex].clear @vertices.delete(vertex) end # Test whether a given vertex is in the graph. def vertex?(vertex) @vertices.include?(vertex) end # Return a list of all vertices. def vertices @vertices.keys end # Add a new edge. The graph user has to create the edge instance, # since they have to specify what kind of edge it is. def add_edge(source, target = nil, label = nil) @reversal = nil if target edge = Puppet::Relationship.new(source, target, label) else edge = source end [edge.source, edge.target].each { |vertex| setup_vertex(vertex) unless vertex?(vertex) } @vertices[edge.source].add_edge :out, edge @vertices[edge.target].add_edge :in, edge @edges << edge true end # Find a matching edge. Note that this only finds the first edge, # not all of them or whatever. def edge(source, target) @edges.each_with_index { |test_edge, index| return test_edge if test_edge.source == source and test_edge.target == target } end def edge_label(source, target) return nil unless edge = edge(source, target) edge.label end # Is there an edge between the two vertices? def edge?(source, target) return false unless vertex?(source) and vertex?(target) @vertices[source].has_edge?(:out, target) end def edges @edges.dup end # Remove an edge from our graph. def remove_edge!(edge) @vertices[edge.source].remove_edge(:out, edge) @vertices[edge.target].remove_edge(:in, edge) @edges.delete(edge) nil end # Find adjacent edges. def adjacent(vertex, options = {}) return [] unless wrapper = @vertices[vertex] wrapper.adjacent(options) end private # An internal method that skips the validation, so we don't have # duplicate validation calls. def setup_vertex(vertex) @vertices[vertex] = VertexWrapper.new(vertex) end public # # For some reason, unconnected vertices do not show up in # # this graph. # def to_jpg(path, name) # gv = vertices # Dir.chdir(path) do # induced_subgraph(gv).write_to_graphic_file('jpg', name) # end # end # Take container information from another graph and use it # to replace any container vertices with their respective leaves. # This creates direct relationships where there were previously # indirect relationships through the containers. def splice!(other, type) # We have to get the container list via a topological sort on the # configuration graph, because otherwise containers that contain # other containers will add those containers back into the # graph. We could get a similar affect by only setting relationships # to container leaves, but that would result in many more # relationships. stage_class = Puppet::Type.type(:stage) whit_class = Puppet::Type.type(:whit) containers = other.topsort.find_all { |v| (v.is_a?(type) or v.is_a?(stage_class)) and vertex?(v) } containers.each do |container| # Get the list of children from the other graph. children = other.adjacent(container, :direction => :out) # MQR TODO: Luke suggests that it should be possible to refactor the system so that # container nodes are retained, thus obviating the need for the whit. children = [whit_class.new(:name => container.name, :catalog => other)] if children.empty? # First create new edges for each of the :in edges [:in, :out].each do |dir| edges = adjacent(container, :direction => dir, :type => :edges) edges.each do |edge| children.each do |child| if dir == :in s = edge.source t = child else s = child t = edge.target end add_edge(s, t, edge.label) end # Now get rid of the edge, so remove_vertex! works correctly. remove_edge!(edge) end end remove_vertex!(container) end end # Just walk the tree and pass each edge. def walk(source, direction) # Use an iterative, breadth-first traversal of the graph. One could do # this recursively, but Ruby's slow function calls and even slower # recursion make the shorter, recursive algorithm cost-prohibitive. stack = [source] seen = Set.new until stack.empty? node = stack.shift next if seen.member? node connected = adjacent(node, :direction => direction) connected.each do |target| yield node, target end stack.concat(connected) seen << node end end # A different way of walking a tree, and a much faster way than the # one that comes with GRATR. def tree_from_vertex(start, direction = :out) predecessor={} walk(start, direction) do |parent, child| predecessor[child] = parent end predecessor end # LAK:FIXME This is just a paste of the GRATR code with slight modifications. # Return a DOT::DOTDigraph for directed graphs or a DOT::DOTSubgraph for an # undirected Graph. _params_ can contain any graph property specified in # rdot.rb. If an edge or vertex label is a kind of Hash then the keys # which match +dot+ properties will be used as well. def to_dot_graph (params = {}) params['name'] ||= self.class.name.gsub(/:/,'_') fontsize = params['fontsize'] ? params['fontsize'] : '8' graph = (directed? ? DOT::DOTDigraph : DOT::DOTSubgraph).new(params) edge_klass = directed? ? DOT::DOTDirectedEdge : DOT::DOTEdge vertices.each do |v| name = v.to_s params = {'name' => '"'+name+'"', 'fontsize' => fontsize, 'label' => name} v_label = v.to_s params.merge!(v_label) if v_label and v_label.kind_of? Hash graph << DOT::DOTNode.new(params) end edges.each do |e| params = {'from' => '"'+ e.source.to_s + '"', 'to' => '"'+ e.target.to_s + '"', 'fontsize' => fontsize } e_label = e.to_s params.merge!(e_label) if e_label and e_label.kind_of? Hash graph << edge_klass.new(params) end graph end # Output the dot format as a string def to_dot (params={}) to_dot_graph(params).to_s; end # Call +dotty+ for the graph which is written to the file 'graph.dot' # in the # current directory. def dotty (params = {}, dotfile = 'graph.dot') File.open(dotfile, 'w') {|f| f << to_dot(params) } system('dotty', dotfile) end # Use +dot+ to create a graphical representation of the graph. Returns the # filename of the graphics file. def write_to_graphic_file (fmt='png', dotfile='graph') src = dotfile + '.dot' dot = dotfile + '.' + fmt File.open(src, 'w') {|f| f << self.to_dot << "\n"} system( "dot -T#{fmt} #{src} -o #{dot}" ) dot end # Produce the graph files if requested. def write_graph(name) return unless Puppet[:graph] Puppet.settings.use(:graphing) file = File.join(Puppet[:graphdir], "#{name}.dot") File.open(file, "w") { |f| f.puts to_dot("name" => name.to_s.capitalize) } end end