diff options
author | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-09-03 21:06:41 +0000 |
---|---|---|
committer | luke <luke@980ebf18-57e1-0310-9a29-db15c13687c0> | 2006-09-03 21:06:41 +0000 |
commit | 270f4448e26a45d213f8aa35c9ecf53843382358 (patch) | |
tree | f2db6782fe77275f30d0f4f40a08266cd97ec187 /lib | |
parent | b9b338432ee0dbad7798685736fcc80ff0164924 (diff) | |
download | puppet-270f4448e26a45d213f8aa35c9ecf53843382358.tar.gz puppet-270f4448e26a45d213f8aa35c9ecf53843382358.tar.xz puppet-270f4448e26a45d213f8aa35c9ecf53843382358.zip |
Beginning the process of moving parsedtypes to a provider. Each parsed
type will have a parsedfile provider, which will be responsible for all
of the file parsing and generation. This should allow us to create a
useful DSL for file handling, but it also *drastically* simplifies these
types.
These types have always been a bit fragile, and it was never quite clear
who was responsible for what. Now, with the type/provider split,
everything is much clearer.
I still need to convert host, sshkey, and mount, and something needs to
be done with cron.
git-svn-id: https://reductivelabs.com/svn/puppet/trunk@1547 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib')
-rw-r--r-- | lib/puppet/provider.rb | 7 | ||||
-rw-r--r-- | lib/puppet/provider/host/parsed.rb | 78 | ||||
-rwxr-xr-x | lib/puppet/provider/parsedfile.rb | 168 | ||||
-rw-r--r-- | lib/puppet/transaction.rb | 9 | ||||
-rw-r--r-- | lib/puppet/type.rb | 20 | ||||
-rwxr-xr-x | lib/puppet/type/host.rb | 90 | ||||
-rwxr-xr-x | lib/puppet/type/parsedtype.rb | 455 | ||||
-rwxr-xr-x | lib/puppet/type/parsedtype/host.rb | 14 | ||||
-rwxr-xr-x | lib/puppet/type/parsedtype/sshkey.rb | 10 |
9 files changed, 530 insertions, 321 deletions
diff --git a/lib/puppet/provider.rb b/lib/puppet/provider.rb index f51ea4450..04169e6e1 100644 --- a/lib/puppet/provider.rb +++ b/lib/puppet/provider.rb @@ -139,9 +139,7 @@ class Puppet::Provider return true end - # Remove the reference to the model, so GC can clean up. def clear - @model = nil end # Retrieve a named command. @@ -157,6 +155,11 @@ class Puppet::Provider @model.name end + # Remove the reference to the model, so GC can clean up. + def remove + @model = nil + end + def to_s "%s(provider=%s)" % [@model.to_s, self.class.name] end diff --git a/lib/puppet/provider/host/parsed.rb b/lib/puppet/provider/host/parsed.rb new file mode 100644 index 000000000..c606562a2 --- /dev/null +++ b/lib/puppet/provider/host/parsed.rb @@ -0,0 +1,78 @@ +require 'puppet/provider/parsedfile' + +Puppet::Type.type(:host).provide :parsed, :parent => Puppet::Provider::ParsedFile do + @path = "/etc/hosts" + @filetype = Puppet::FileType.filetype(:flat) + + confine :exists => @path + + # Parse a host file + # + # This method also stores existing comments, and it stores all host + # jobs in order, mostly so that comments are retained in the order + # they were written and in proximity to the same jobs. + def self.parse(text) + count = 0 + instances = [] + text.chomp.split("\n").each { |line| + hash = {} + case line + when /^#/, /^\s*$/: + # add comments and blank lines to the list as they are + instances << line + else + if line.sub!(/^(\S+)\s+(\S+)\s*/, '') + hash[:ip] = $1 + hash[:name] = $2 + + unless line == "" + line.sub!(/\s*/, '') + line.sub!(/^([^#]+)\s*/) do |value| + aliases = $1 + unless aliases =~ /^\s*$/ + hash[:alias] = aliases.split(/\s+/) + end + + "" + end + end + else + raise Puppet::Error, "Could not match '%s'" % line + end + + if hash[:alias] == "" + hash.delete(:alias) + end + + instances << hash + + count += 1 + end + } + + return instances + end + + # Convert the current object into a host-style string. + def self.to_record(hash) + [:ip, :name].each do |n| + unless hash.has_key? n + raise ArgumentError, "%s is a required attribute for hosts" % n + end + end + + str = "%s\t%s" % [hash[:ip], hash[:name]] + + if hash.include? :alias + if hash[:alias].is_a? Array + str += "\t%s" % hash[:alias].join("\t") + else + raise ArgumentError, "Aliases must be specified as an array" + end + end + + str + end +end + +# $Id$ diff --git a/lib/puppet/provider/parsedfile.rb b/lib/puppet/provider/parsedfile.rb new file mode 100755 index 000000000..2527edfeb --- /dev/null +++ b/lib/puppet/provider/parsedfile.rb @@ -0,0 +1,168 @@ +require 'puppet' + +class Puppet::Provider::ParsedFile < Puppet::Provider + class << self + attr_accessor :filetype, :fields + attr_reader :path + attr_writer :fileobj + end + + # Override 'newstate' so that all states default to having the + # correct parent type + def self.newstate(name, options = {}, &block) + options[:parent] ||= Puppet::State::ParsedParam + super(name, options, &block) + end + + # Add another type var. + def self.initvars + @instances = [] + super + end + + # Add a non-object comment or whatever to our list of instances + def self.comment(line) + @instances << line + end + + # Override the default Puppet::Type method, because instances + # also need to be deleted from the @instances hash + def self.delete(child) + if @instances.include?(child) + @instances.delete(child) + end + super + end + + # Initialize the object if necessary. + def self.fileobj + @fileobj ||= @filetype.new(@path) + + @fileobj + end + + # Return the header placed at the top of each generated file, warning + # users that modifying this file manually is probably a bad idea. + def self.header +%{# HEADER: This file was autogenerated at #{Time.now} +# HEADER: by puppet. While it can still be managed manually, it +# HEADER: is definitely not recommended.\n} + end + + # Parse a file + # + # Subclasses must override this method. + def self.parse(text) + raise Puppet::DevError, "Parse was not overridden in %s" % + self.name + end + + # If they change the path, we need to get rid of our cache object + def self.path=(path) + @fileobj = nil + @path = path + end + + # Retrieve the text for the file. Returns nil in the unlikely + # event that it doesn't exist. + def self.retrieve + text = fileobj.read + if text.nil? or text == "" + # there is no file + return [] + else + self.parse(text) + end + end + + # Write out the file. + def self.store(instances) + if instances.empty? + Puppet.notice "No %s instances for %s" % [self.name, @path] + else + fileobj.write(self.to_file(instances)) + end + end + + # Collect all Host instances convert them into literal text. + def self.to_file(instances) + str = self.header() + unless instances.empty? + # Reject empty hashes and those with :ensure == :absent + str += instances.reject { |obj| + obj.is_a? Hash and (obj.empty? or obj[:ensure] == :absent) + }.collect { |obj| + # If it's a hash, convert it, otherwise just write it out + if obj.is_a?(Hash) + to_record(obj) + else + obj.to_s + end + }.join("\n") + "\n" + + return str + else + Puppet.notice "No %s instances" % self.name + return "" + end + end + + def clear + super + @instances = nil + end + + # Return a hash that maps to our info, if possible. + def hash + @instances = self.class.retrieve + + if @instances and h = @instances.find do |o| + o.is_a? Hash and o[:name] == @model[:name] + end + @me = h + return h + else + @me = {} + if @instances.empty? + @instances = [@me] + else + @instances << @me + end + return @me + end + end + + def initialize(model) + super + + @instances = nil + end + + def store(hash = nil) + hash ||= self.model.to_hash + + unless @instances + self.hash + end + + if hash.empty? + @me.clear + else + hash.each do |name, value| + if @me[name] != hash[name] + @me[name] = hash[name] + end + end + + @me.each do |name, value| + unless hash.has_key? name + @me.delete(name) + end + end + end + + self.class.store(@instances) + end +end + +# $Id$ diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 5e66b63a1..f38d7cbbd 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -98,6 +98,15 @@ class Transaction events }.flatten.reject { |e| e.nil? } + # If our child responds to a 'flush' method, call it. + if child.respond_to? :flush + begin + child.flush + rescue => detail + raise Puppet::Error, "Could not flush: %s" % detail, detail.backtrace + end + end + unless changes.empty? # Record when we last synced child.cache(:synced, Time.now) diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 21e27ee6d..6964b399c 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -301,6 +301,7 @@ class Type < Puppet::Element def self.clear if defined? @objects @objects.each do |name, obj| + obj.clear obj.remove(true) end @objects.clear @@ -1259,7 +1260,7 @@ class Type < Puppet::Element # Remove the reference to the provider. if self.provider - @provider.clear + @provider.remove @provider = nil end end @@ -1708,6 +1709,10 @@ class Type < Puppet::Element @states.each do |name, state| state.is = nil end + + if provider and provider.respond_to? :clear + provider.clear + end end # Look up the schedule and set it appropriately. This is done after @@ -1958,6 +1963,19 @@ class Type < Puppet::Element is end + # Convert our object to a hash. This just includes states. + def to_hash + rethash = {} + + [@parameters, @metaparams, @states].each do |hash| + hash.each do |name, obj| + rethash[name] = obj.value + end + end + + rethash + end + # convert to a string def to_s self.title diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb new file mode 100755 index 000000000..d68fe25a2 --- /dev/null +++ b/lib/puppet/type/host.rb @@ -0,0 +1,90 @@ +require 'puppet/type/parsedtype' + +module Puppet + newtype(:host, Puppet::Type::ParsedType) do + newstate(:ip) do + desc "The host's IP address." + end + + newstate(:alias) do + desc "Any alias the host might have. Multiple values must be + specified as an array. Note that this state has the same name + as one of the metaparams; using this state to set aliases will + make those aliases available in your Puppet scripts and also on + disk." + + # Make sure our "is" value is always an array. + def is + current = super + unless current.is_a? Array + current = [current] + end + current + end + + def is_to_s + self.is.join(" ") + end + + # We have to override the feeding mechanism; it might be nil or + # white-space separated + def is=(value) + # If it's just whitespace, ignore it + case value + when /^\s+$/ + @is = nil + when String + @is = value.split(/\s+/) + else + @is = value + end + end + + # We actually want to return the whole array here, not just the first + # value. + def should + if defined? @should + if @should == [:absent] + return :absent + else + return @should + end + else + return nil + end + end + + def should_to_s + @should.join(" ") + end + + validate do |value| + if value =~ /\s/ + raise Puppet::Error, "Aliases cannot include whitespace" + end + end + + munge do |value| + if value == :absent or value == "absent" + :absent + else + # Add the :alias metaparam in addition to the state + @parent.newmetaparam(@parent.class.metaparamclass(:alias), value) + value + end + end + end + + newparam(:name) do + desc "The host name." + + isnamevar + end + + @doc = "Installs and manages host entries. For most systems, these + entries will just be in /etc/hosts, but some systems (notably OS X) + will have different solutions." + end +end + +# $Id$ diff --git a/lib/puppet/type/parsedtype.rb b/lib/puppet/type/parsedtype.rb index f6c227541..1b958db4d 100755 --- a/lib/puppet/type/parsedtype.rb +++ b/lib/puppet/type/parsedtype.rb @@ -4,350 +4,187 @@ require 'puppet/filetype' require 'puppet/type/state' module Puppet - class State - # The base parameter for all of these types. Its only job is to copy - # the 'should' value to the 'is' value and to do support the right logging - # and such. - class ParsedParam < Puppet::State - def self.isoptional - @isoptional = true - end - - def self.isoptional? - if defined? @isoptional - return @isoptional - else - return false - end - end + # The base parameter for all of these types. Its only job is to copy + # the 'should' value to the 'is' value and to do support the right logging + # and such. + class State::ParsedParam < Puppet::State + # This is the info retrieved from disk. + attr_accessor :found + + def self.isoptional + @isoptional = true + end - # By default, support ':absent' as a value for optional - # parameters. Any parameters that define their own validation - # need to do this manuallly. - validate do |value| - if self.class.isoptional? and ( - value == "absent" or value == :absent - ) - return :absent - else - return value - end + def self.isoptional? + if defined? @isoptional + return @isoptional + else + return false end + end - # Fix things so that the fields have to match exactly, instead - # of only kinda - def insync? - self.is == self.should + # By default, support ':absent' as a value for optional + # parameters. Any parameters that define their own validation + # need to do this manuallly. + validate do |value| + if self.class.isoptional? and ( + value == "absent" or value == :absent + ) + return :absent + else + return value end + end - # Normally this would retrieve the current value, but our state is not - # actually capable of doing so. - def retrieve - # If we've synced, then just copy the values over and return. - # This allows this state to behave like any other state. - if defined? @synced and @synced - # by default, we only copy over the first value. - @is = @synced - @synced = false - return - end - - unless defined? @is and ! @is.nil? - @is = :absent - end - end + def clear + super + @found = nil + end - # If the ensure state is out of sync, it will always be called - # first, so I don't need to worry about that. - def sync(value = nil, nostore = false) - ebase = @parent.class.name.to_s + # Fix things so that the fields have to match exactly, instead + # of only kinda + def insync? + self.is == self.should + end - tail = nil - if self.class.name == :ensure - # We're either creating or destroying the object - if @is == :absent - #@is = self.should - tail = "created" + # Normally this would retrieve the current value, but our state is not + # actually capable of doing so. So, we retrieve the whole object and + # just collect our current state. Note that this method is not called + # during a transaction, since transactions call the parent object method. + def retrieve + @parent.retrieve[self.name] + end - # If we're creating it, then sync all of the other states - # but tell them not to store (we'll store just once, - # at the end). - unless nostore - @parent.eachstate { |state| - next if state == self or state.name == :ensure - state.sync(true) - } - end - elsif self.should == :absent - @parent.remove(true) - tail = "deleted" - end - else - # We don't do the work here, it gets done in 'store' - tail = "changed" - end - @synced = self.should + # If the ensure state is out of sync, it will always be called + # first, so I don't need to worry about that. + def sync(value) + # Just copy the value to our 'is' state; it'll get flushed later + self.is = value - # This should really only be done once per run, rather than - # every time. I guess we need some kind of 'flush' mechanism. - if nostore - self.retrieve - else - @parent.store - end - - return (ebase + "_" + tail).intern - end + return nil end end - class Type - # The collection of classes that are just simple records aggregated - # into a file. See 'host.rb' for an example. - class ParsedType < Puppet::Type - @name = :parsedtype - class << self - attr_accessor :filetype, :hostfile, :fields - attr_reader :path - attr_writer :fileobj - end - - # Override 'newstate' so that all states default to having the - # correct parent type - def self.newstate(name, options = {}, &block) - options[:parent] ||= Puppet::State::ParsedParam - super(name, options, &block) - end - - # Add another type var. - def self.initvars - @instances = [] - super - end - - # In addition to removing the instances in @objects, we have to remove - # per-user host tab information. - def self.clear - @instances = [] - @fileobj = nil - super - end - - # Add a non-object comment or whatever to our list of instances - def self.comment(line) - @instances << line - end - - # Override the default Puppet::Type method, because instances - # also need to be deleted from the @instances hash - def self.delete(child) - if @instances.include?(child) - @instances.delete(child) - end - super - end - - # Initialize the object if necessary. - def self.fileobj - @fileobj ||= @filetype.new(@path) - - @fileobj - end - - # Return the header placed at the top of each generated file, warning - # users that modifying this file manually is probably a bad idea. - def self.header -%{# HEADER: This file was autogenerated at #{Time.now} -# HEADER: by puppet. While it can still be managed manually, it -# HEADER: is definitely not recommended.\n} - end - - # Convert the hash to an object. - def self.hash2obj(hash) - obj = nil - - namevar = self.namevar - unless hash.include?(namevar) and hash[namevar] - raise Puppet::DevError, "Hash was not passed with namevar" - end - - # if the obj already exists with that name... - if obj = self[hash[namevar]] - # We're assuming here that objects with the same name - # are the same object, which *should* be the case, assuming - # we've set up our naming stuff correctly everywhere. - - # Mark found objects as present - obj.is = [:ensure, :present] - hash.each { |param, value| - if state = obj.state(param) - state.is = value - elsif val = obj[param] - obj[param] = val - else - # There is a value on disk, but it should go away - obj.is = [param, value] - obj[param] = :absent - end - } - else - # create a new obj, since no existing one seems to - # match - obj = self.create(namevar => hash[namevar]) - - # We can't just pass the hash in at object creation time, - # because it sets the should value, not the is value. - hash.delete(namevar) - hash.each { |param, value| + # The collection of classes that are just simple records aggregated + # into a file. See 'host.rb' for an example. + class Type::ParsedType < Puppet::Type + @name = :parsedtype + + # Convert the hash to an object. + def self.hash2obj(hash) + obj = nil + + namevar = self.namevar + unless hash.include?(namevar) and hash[namevar] + raise Puppet::DevError, "Hash was not passed with namevar" + end + + # if the obj already exists with that name... + if obj = self[hash[namevar]] + # We're assuming here that objects with the same name + # are the same object, which *should* be the case, assuming + # we've set up our naming stuff correctly everywhere. + + # Mark found objects as present + obj.is = [:ensure, :present] + obj.state(:ensure).found = :present + hash.each { |param, value| + if state = obj.state(param) + state.is = value + elsif val = obj[param] + obj[param] = val + else + # There is a value on disk, but it should go away obj.is = [param, value] - } - end - - # And then add it to our list of instances. This maintains the order - # in the file. - @instances << obj - end - - def self.list - retrieve - - self.collect do |obj| - obj - end - end - - # Return the last time the file was loaded. Could - # be used for reducing writes, but currently is not. - def self.loaded?(user) - fileobj().loaded + obj[param] = :absent + end + } + else + # create a new obj, since no existing one seems to + # match + obj = self.create(namevar => hash[namevar]) + + # We can't just pass the hash in at object creation time, + # because it sets the should value, not the is value. + hash.delete(namevar) + hash.each { |param, value| + obj.is = [param, value] + } end + end - # Parse a file - # - # Subclasses must override this method. - def self.parse(text) - raise Puppet::DevError, "Parse was not overridden in %s" % - self.name - end + # Override 'newstate' so that all states default to having the + # correct parent type + def self.newstate(name, options = {}, &block) + options[:parent] ||= Puppet::State::ParsedParam + super(name, options, &block) + end - # If they change the path, we need to get rid of our cache object - def self.path=(path) - @fileobj = nil - @path = path - end + def self.list + retrieve.collect { |i| i.is_a? Hash }.collect { |i| hash2obj(i) } + end - # Retrieve the text for the file. Returns nil in the unlikely - # event that it doesn't exist. - def self.retrieve - text = fileobj.read - if text.nil? or text == "" - # there is no file - return nil - else - # First we mark all of our objects absent; any objects - # subsequently found will be marked present - self.each { |obj| - obj.is = [:ensure, :absent] - } - - # We clear this, so that non-objects don't get duplicated - @instances.clear - self.parse(text) - end - end + def self.listbyname + retrieve.collect { |i| i.is_a? Hash }.collect { |i| i[:name] } + end - # Write out the file. - def self.store - # Make sure all of our instances are in the to-be-written array - self.each do |inst| - @instances << inst unless @instances.include? inst - end + # Make sure they've got an explicit :ensure class. + def self.postinit + unless validstate? :ensure + newstate(:ensure) do + newvalue(:present, :event => :host_created) do + @parent.create + end - if @instances.empty? - Puppet.notice "No %s instances for %s" % [self.name, @path] - else - fileobj.write(self.to_file()) - end - end + newvalue(:absent, :event => :host_deleted) do + @parent.destroy + end - # Collect all Host instances convert them into literal text. - def self.to_file - str = self.header() - unless @instances.empty? - str += @instances.reject { |obj| - # Don't write out objects that should be absent - if obj.is_a?(self) - if obj.should(:ensure) == :absent - true - end - end - }.collect { |obj| - if obj.is_a?(self) - obj.to_record + defaultto do + if @parent.managed? + :present else - obj.to_s + :absent end - }.join("\n") + "\n" - - return str - else - Puppet.notice "No %s instances" % self.name - return "" + end end end + end - # The 'store' method knows how to handle absence vs. presence - def create - self.store - end - - # The 'store' method knows how to handle absence vs. presence - def destroy - self.store - end - - # hash2obj marks the 'ensure' state as present - def exists? - @states.include?(:ensure) and @states[:ensure].is == :present - end - - # Override the default Puppet::Type method because we need to call - # the +@filetype+ retrieve method. - def retrieve - self.class.retrieve() + def exists? + h = provider.hash - self.eachstate { |st| - st.retrieve - } + if h[:ensure] == :absent + return false + else + return true end + end - # Write the entire file out. - def store - self.class.store() - end + # Flush our content to disk. + def flush + provider.store(self.to_hash) + end - def value(name) - unless name.is_a? Symbol - name = name.intern - end - if @states.include? name - val = @states[name].value - if val == :absent - return nil - else - return val - end - elsif @parameters.include? name - return @parameters[name].value - else - return nil + # Retrieve our current state from our provider + def retrieve + h = nil + if h = provider.hash and ! h.empty? + h[:ensure] ||= :present + return h + else + h = {} + @states.each do |name, state| + h[name] = :absent end end + + return h end end end -require 'puppet/type/parsedtype/host' +#require 'puppet/type/parsedtype/host' require 'puppet/type/parsedtype/port' require 'puppet/type/parsedtype/mount' require 'puppet/type/parsedtype/sshkey' diff --git a/lib/puppet/type/parsedtype/host.rb b/lib/puppet/type/parsedtype/host.rb index 00cfe7857..df8ed3df7 100755 --- a/lib/puppet/type/parsedtype/host.rb +++ b/lib/puppet/type/parsedtype/host.rb @@ -5,7 +5,6 @@ require 'puppet/type/state' module Puppet newtype(:host, Puppet::Type::ParsedType) do - newstate(:ip) do desc "The host's IP address." end @@ -17,6 +16,19 @@ module Puppet make those aliases available in your Puppet scripts and also on disk." + # Make sure our "is" value is always an array. + def is + current = super + unless current.is_a? Array + current = [current] + end + current + end + + def is_to_s + self.is.join(" ") + end + # We have to override the feeding mechanism; it might be nil or # white-space separated def is=(value) diff --git a/lib/puppet/type/parsedtype/sshkey.rb b/lib/puppet/type/parsedtype/sshkey.rb index d12d4c697..26c9b4f30 100755 --- a/lib/puppet/type/parsedtype/sshkey.rb +++ b/lib/puppet/type/parsedtype/sshkey.rb @@ -65,14 +65,8 @@ module Puppet @fields = [:name, :type, :key] @filetype = Puppet::FileType.filetype(:flat) -# case Facter["operatingsystem"].value -# when "Solaris": -# @filetype = Puppet::FileType::SunOS -# else -# @filetype = Puppet::CronType::Default -# end - - # Parse a host file + + # Parse an sshknownhosts file # # This method also stores existing comments, and it stores all host # jobs in order, mostly so that comments are retained in the order |