diff options
| author | Luke Kanies <luke@madstop.com> | 2007-12-11 15:35:36 -0600 |
|---|---|---|
| committer | Luke Kanies <luke@madstop.com> | 2007-12-11 15:35:36 -0600 |
| commit | cb0c4eebb0a7b2fadc5e0487e1c692007cb8b2e5 (patch) | |
| tree | b4284addea69adf9d19028ff1bab94035b1c1827 /lib/puppet/node/catalog.rb | |
| parent | 7ac3bd79621f6c66cd3b5b7041aeba83c27c3602 (diff) | |
| download | puppet-cb0c4eebb0a7b2fadc5e0487e1c692007cb8b2e5.tar.gz puppet-cb0c4eebb0a7b2fadc5e0487e1c692007cb8b2e5.tar.xz puppet-cb0c4eebb0a7b2fadc5e0487e1c692007cb8b2e5.zip | |
Renaming 'configuration' to 'catalog', fixing #954.
Diffstat (limited to 'lib/puppet/node/catalog.rb')
| -rw-r--r-- | lib/puppet/node/catalog.rb | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/lib/puppet/node/catalog.rb b/lib/puppet/node/catalog.rb new file mode 100644 index 000000000..a02d59ae9 --- /dev/null +++ b/lib/puppet/node/catalog.rb @@ -0,0 +1,480 @@ +require 'puppet/indirector' +require 'puppet/external/gratr/digraph' + +# 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. +class Puppet::Node::Catalog < Puppet::PGraph + extend Puppet::Indirector + indirects :catalog, :terminus_class => :compiler + + # 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 + + # How we should extract the catalog for sending to the client. + attr_reader :extraction_format + + # We need the ability to set this externally, so we can yaml-dump the + # catalog. + attr_accessor :edgelist_class + + # 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 graph is another catalog's relationship graph. + # We don't want to accidentally create a relationship graph for another + # relationship graph. + attr_accessor :is_relationship_graph + + # Whether this catalog was retrieved from the cache, which affects + # whether it is written back out again. + attr_accessor :from_cache + + # 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 + + # Add one or more resources to our graph and to our resource table. + def add_resource(*resources) + resources.each do |resource| + unless resource.respond_to?(:ref) + raise ArgumentError, "Can only add objects that respond to :ref" + end + + ref = resource.ref + if @resource_table.include?(ref) + raise ArgumentError, "Resource %s is already defined" % ref + else + @resource_table[ref] = resource + end + resource.catalog = self unless is_relationship_graph + add_vertex!(resource) + end + end + + # Create an alias for a resource. + def alias(resource, name) + resource.ref =~ /^(.+)\[/ + + newref = "%s[%s]" % [$1 || resource.class.name, name] + raise(ArgumentError, "Cannot alias %s to %s; resource %s already exists" % [resource.ref, name, newref]) if @resource_table[newref] + @resource_table[newref] = resource + @aliases[resource.ref] << newref + end + + # Apply our catalog to the local host. Valid options + # are: + # :tags - set the tags that restrict what resources run + # during the transaction + # :ignoreschedules - tell the transaction to ignore schedules + # when determining the resources to run + def apply(options = {}) + @applying = true + + Puppet::Util::Storage.load if host_config? + transaction = Puppet::Transaction.new(self) + + transaction.tags = options[:tags] if options[:tags] + transaction.ignoreschedules = true if options[:ignoreschedules] + + transaction.addtimes :config_retrieval => @retrieval_duration + + + begin + transaction.evaluate + rescue Puppet::Error => detail + Puppet.err "Could not apply complete catalog: %s" % detail + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Got an uncaught exception of type %s: %s" % [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.send_report if host_config and (Puppet[:report] or Puppet[:summarize]) + + return transaction + ensure + @applying = false + cleanup() + transaction.cleanup if defined? transaction and transaction + end + + # Are we in the middle of applying the catalog? + def applying? + @applying + 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 + + if defined?(@relationship_graph) and @relationship_graph + @relationship_graph.clear(false) + @relationship_graph = nil + end + end + + def classes + @classes.dup + end + + # Create an implicit resource, meaning that it will lose out + # to any explicitly defined resources. This method often returns + # nil. + # The quirk of this method is that it's not possible to create + # an implicit resource before an explicit resource of the same name, + # because all explicit resources are created before any generate() + # methods are called on the individual resources. Thus, this + # method can safely just check if an explicit resource already exists + # and toss this implicit resource if so. + def create_implicit_resource(type, options) + unless options.include?(:implicit) + options[:implicit] = true + end + + # This will return nil if an equivalent explicit resource already exists. + # When resource classes no longer retain references to resource instances, + # this will need to be modified to catch that conflict and discard + # implicit resources. + if resource = create_resource(type, options) + resource.implicit = true + + return resource + else + return nil + end + 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 %s" % type + end + return unless resource = klass.create(options) + + @transient_resources << resource if applying? + add_resource(resource) + if @relationship_graph + @relationship_graph.add_resource(resource) unless @relationship_graph.resource(resource.ref) + end + resource + end + + # Make sure we support the requested extraction format. + def extraction_format=(value) + unless respond_to?("extract_to_%s" % value) + raise ArgumentError, "Invalid extraction format %s" % value + end + @extraction_format = value + end + + # Turn our catalog graph into whatever the client is expecting. + def extract + send("extract_to_%s" % extraction_format) + end + + # Create the traditional TransBuckets and TransObjects from our catalog + # graph. This will hopefully be deprecated soon. + def extract_to_transportable + top = nil + current = nil + buckets = {} + + unless main = vertices.find { |res| res.type == "Class" and res.title == :main } + raise Puppet::DevError, "Could not find 'main' class; cannot generate catalog" + end + + # Create a proc for examining edges, which we'll use to build our tree + # of TransBuckets and TransObjects. + bucket = nil + edges = proc do |edge| + # The sources are always non-builtins. + source, target = edge.source, edge.target + unless tmp = buckets[source.to_s] + if tmp = buckets[source.to_s] = source.to_trans + bucket = tmp + else + # This is because virtual resources return nil. If a virtual + # container resource contains realized resources, we still need to get + # to them. So, we keep a reference to the last valid bucket + # we returned and use that if the container resource is virtual. + end + end + bucket = tmp || bucket + if child = target.to_trans + unless bucket + raise "No bucket created for %s" % source + end + bucket.push child + + # It's important that we keep a reference to any TransBuckets we've created, so + # we don't create multiple buckets for children. + unless target.builtin? + buckets[target.to_s] = child + end + end + end + dfs(:start => main, :examine_edge => edges) + + unless main + raise Puppet::DevError, "Could not find 'main' class; cannot generate catalog" + end + + # Retrieve the bucket for the top-level scope and set the appropriate metadata. + unless result = buckets[main.to_s] + # This only happens when the catalog is entirely empty. + result = buckets[main.to_s] = main.to_trans + end + + result.classes = classes + + # Clear the cache to encourage the GC + buckets.clear + return result + end + + # Make sure all of our resources are "finished". + def finalize + @resource_table.values.each { |resource| resource.finish } + + write_graph(:resources) + end + + def host_config? + host_config || false + end + + def initialize(name = nil) + super() + @name = name if name + @extraction_format ||= :transportable + @tags = [] + @classes = [] + @resource_table = {} + @transient_resources = [] + @applying = false + @relationship_graph = nil + + @aliases = Hash.new { |hash, key| hash[key] = [] } + + if block_given? + yield(self) + finalize() + end + end + + # Create a graph of all of the relationships in our catalog. + def relationship_graph + raise(Puppet::DevError, "Tried get a relationship graph for a relationship graph") if self.is_relationship_graph + + unless defined? @relationship_graph and @relationship_graph + # It's important that we assign the graph immediately, because + # the debug messages below use the relationships in the + # relationship graph to determine the path to the resources + # spitting out the messages. If this is not set, + # then we get into an infinite loop. + @relationship_graph = Puppet::Node::Catalog.new + @relationship_graph.host_config = host_config? + @relationship_graph.is_relationship_graph = true + + # First create the dependency graph + self.vertices.each do |vertex| + @relationship_graph.add_vertex! vertex + vertex.builddepends.each do |edge| + @relationship_graph.add_edge!(edge) + end + end + + # Lastly, add in any autorequires + @relationship_graph.vertices.each do |vertex| + vertex.autorequire.each do |edge| + unless @relationship_graph.edge?(edge.source, edge.target) # don't let automatic relationships conflict with manual ones. + unless @relationship_graph.edge?(edge.target, edge.source) + vertex.debug "Autorequiring %s" % [edge.source] + @relationship_graph.add_edge!(edge) + else + vertex.debug "Skipping automatic relationship with %s" % (edge.source == vertex ? edge.target : edge.source) + end + end + end + end + + @relationship_graph.write_graph(:relationships) + + # Then splice in the container information + @relationship_graph.splice!(self, Puppet::Type::Component) + + @relationship_graph.write_graph(:expanded_relationships) + end + @relationship_graph + 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| + @resource_table.delete(resource.ref) + @aliases[resource.ref].each { |res_alias| @resource_table.delete(res_alias) } + @aliases[resource.ref].clear + remove_vertex!(resource) if vertex?(resource) + @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) + 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 canonizes how we + # are referring to them. + if title + ref = Puppet::ResourceReference.new(type, title).to_s + 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 canonizes for us. + ref = Puppet::ResourceReference.new(nil, type).to_s + end + if resource = @resource_table[ref] + return resource + elsif defined?(@relationship_graph) and @relationship_graph + @relationship_graph.resource(ref) + end + end + + # Add a tag. + def tag(*names) + names.each do |name| + name = name.to_s + @tags << name unless @tags.include?(name) + if name.include?("::") + name.split("::").each do |sub| + @tags << sub unless @tags.include?(sub) + end + end + end + nil + end + + # Return the list of tags. + def tags + @tags.dup + end + + # Convert our catalog into a RAL catalog. + def to_ral + to_catalog :to_type + end + + # Turn our parser catalog into a transportable catalog. + def to_transportable + to_catalog :to_transobject + end + + # Produce the graph files if requested. + def write_graph(name) + # We only want to graph the main host catalog. + return unless host_config? + + return unless Puppet[:graph] + + Puppet.settings.use(:graphing) + + file = File.join(Puppet[:graphdir], "%s.dot" % name.to_s) + File.open(file, "w") { |f| + f.puts to_dot("name" => name.to_s.capitalize) + } + end + + # LAK:NOTE We cannot yaml-dump the class in the edgelist_class, because classes cannot be + # dumped by default, nor does yaml-dumping # the edge-labels work at this point (I don't + # know why). + # Neither of these matters right now, but I suppose it could at some point. + # We also have to have the vertex_dict dumped after the resource table, because yaml can't + # seem to handle the output of yaml-dumping the vertex_dict. + def to_yaml_properties + props = instance_variables.reject { |v| %w{@edgelist_class @edge_labels @vertex_dict}.include?(v) } + props << "@vertex_dict" + props + end + + private + + def cleanup + unless @transient_resources.empty? + remove_resource(*@transient_resources) + @transient_resources.clear + @relationship_graph = nil + end + 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) + + map = {} + vertices.each do |resource| + next if resource.respond_to?(:virtual?) and resource.virtual? + + newres = resource.send(convert) + + # 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 edge.source.respond_to?(:virtual?) and edge.source.virtual? + next if edge.target.respond_to?(:virtual?) and edge.target.virtual? + + unless source = map[edge.source.ref] + raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.source.ref, message] + end + + unless target = map[edge.target.ref] + raise Puppet::DevError, "Could not find resource %s when converting %s resources" % [edge.target.ref, message] + end + + result.add_edge!(source, target, edge.label) + end + + map.clear + + result.add_class *self.classes + result.tag(*self.tags) + + return result + end +end |
