diff options
| author | Luke Kanies <luke@madstop.com> | 2008-12-09 15:33:28 -0600 |
|---|---|---|
| committer | Luke Kanies <luke@madstop.com> | 2008-12-18 11:10:21 -0600 |
| commit | d48fff6658535ca1781443aba9ab21893c13e55c (patch) | |
| tree | 4fcfd8b502a0f23f057a6e0695f466195978f366 /lib/puppet/resource | |
| parent | c927ce05bbd96fa9aacc8e52f8eb797403a1e433 (diff) | |
| download | puppet-d48fff6658535ca1781443aba9ab21893c13e55c.tar.gz puppet-d48fff6658535ca1781443aba9ab21893c13e55c.tar.xz puppet-d48fff6658535ca1781443aba9ab21893c13e55c.zip | |
Renaming Puppet::Node::Catalog to Puppet::Resource::Catalog
Signed-off-by: Luke Kanies <luke@madstop.com>
Diffstat (limited to 'lib/puppet/resource')
| -rw-r--r-- | lib/puppet/resource/catalog.rb | 508 |
1 files changed, 508 insertions, 0 deletions
diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb new file mode 100644 index 000000000..5b1022799 --- /dev/null +++ b/lib/puppet/resource/catalog.rb @@ -0,0 +1,508 @@ +require 'puppet/indirector' +require 'puppet/simple_graph' +require 'puppet/transaction' + +require 'puppet/util/cacher' + +require 'puppet/util/tagging' + +# 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::Resource::Catalog < Puppet::SimpleGraph + class DuplicateResourceError < Puppet::Error; end + + extend Puppet::Indirector + indirects :catalog, :terminus_class => :compiler + + include Puppet::Util::Tagging + include Puppet::Util::Cacher::Expirer + + # 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 + + # 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 + + # 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. + # This is actually a relatively complicated method, because it handles multiple + # aspects of Catalog behaviour: + # * Add the resource to the resource table + # * Add the resource to the resource graph + # * Add the resource to the relationship graph + # * Add any aliases that make sense for the resource (e.g., name != title) + def add_resource(*resources) + resources.each do |resource| + unless resource.respond_to?(:ref) + raise ArgumentError, "Can only add objects that respond to :ref, not instances of %s" % resource.class + end + end.find_all { |resource| fail_or_skip_unless_unique(resource) }.each do |resource| + ref = resource.ref + + @transient_resources << resource if applying? + @resource_table[ref] = resource + + # If the name and title differ, set up an alias + #self.alias(resource, resource.name) if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.name != resource.title + if resource.respond_to?(:name) and resource.respond_to?(:title) and resource.name != resource.title + self.alias(resource, resource.name) if resource.isomorphic? + end + + resource.catalog = self if resource.respond_to?(:catalog=) + + add_vertex(resource) + + if @relationship_graph + @relationship_graph.add_vertex(resource) + end + + yield(resource) if block_given? + end + end + + # Create an alias for a resource. + def alias(resource, name) + #set $1 + resource.ref =~ /^(.+)\[/ + + newref = "%s[%s]" % [$1 || resource.class.name, name] + + # 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. + return if newref == resource.ref + if existing = @resource_table[newref] + return if existing == resource + raise(ArgumentError, "Cannot alias %s to %s; resource %s already exists" % [resource.ref, name, newref]) + end + @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 + + # Expire all of the resource data -- this ensures that all + # data we're operating against is entirely current. + expire() + + 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 + @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 %s" % type + end + return unless resource = klass.create(options) + + add_resource(resource) + resource + end + + def expired?(ts) + if applying? + return super + else + return true + end + 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 + walk(main, :out) do |source, target| + # The sources are always non-builtins. + 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 + + # 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 + make_default_resources + + @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 + @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 + + # 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 + + # Create a graph of all of the relationships in our catalog. + def 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::SimpleGraph.new + + # 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(self).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) if host_config? + + # Then splice in the container information + @relationship_graph.splice!(self, Puppet::Type::Component) + + @relationship_graph.write_graph(:expanded_relationships) if host_config? + 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::Resource::Reference.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::Resource::Reference.new(nil, type).to_s + end + @resource_table[ref] + end + + # Return an array of all resources. + def resources + @resource_table.keys + 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? + + super + end + + private + + def cleanup + unless @transient_resources.empty? + remove_resource(*@transient_resources) + @transient_resources.clear + @relationship_graph = nil + end + + # Expire any cached data the resources are keeping. + expire() + end + + # Verify that the given resource isn't defined elsewhere. + def fail_or_skip_unless_unique(resource) + # Short-curcuit the common case, + return resource unless existing_resource = @resource_table[resource.ref] + + if resource.implicit? + resource.debug "Generated resource conflicts with explicit resource; ignoring generated resource" + return nil + elsif old = resource(resource.ref) and old.implicit? + # The existing resource is implicit; remove it and replace it with + # the new one. + old.debug "Replacing with new resource" + remove_resource(old) + return resource + end + + # If we've gotten this far, it's a real conflict + + # Either it's a defined type, which are never + # isomorphic, or it's a non-isomorphic type, so + # we should throw an exception. + msg = "Duplicate definition: %s is already defined" % resource.ref + + if existing_resource.file and existing_resource.line + msg << " in file %s at line %s" % + [existing_resource.file, existing_resource.line] + end + + if resource.line or resource.file + msg << "; cannot redefine" + end + + raise DuplicateResourceError.new(msg) + 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? + + #This is hackity hack for 1094 + #Aliases aren't working in the ral catalog because the current instance of the resource + #has a reference to the catalog being converted. . . So, give it a reference to the new one + #problem solved. . . + if resource.is_a?(Puppet::TransObject) + resource = resource.dup + resource.catalog = result + elsif resource.is_a?(Puppet::Parser::Resource) + resource = resource.to_transobject + resource.catalog = result + end + + 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 |
