summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2006-09-03 21:06:41 +0000
committerluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2006-09-03 21:06:41 +0000
commit270f4448e26a45d213f8aa35c9ecf53843382358 (patch)
treef2db6782fe77275f30d0f4f40a08266cd97ec187
parentb9b338432ee0dbad7798685736fcc80ff0164924 (diff)
downloadpuppet-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
-rw-r--r--lib/puppet/provider.rb7
-rw-r--r--lib/puppet/provider/host/parsed.rb78
-rwxr-xr-xlib/puppet/provider/parsedfile.rb168
-rw-r--r--lib/puppet/transaction.rb9
-rw-r--r--lib/puppet/type.rb20
-rwxr-xr-xlib/puppet/type/host.rb90
-rwxr-xr-xlib/puppet/type/parsedtype.rb455
-rwxr-xr-xlib/puppet/type/parsedtype/host.rb14
-rwxr-xr-xlib/puppet/type/parsedtype/sshkey.rb10
-rw-r--r--test/providers/parsedhost.rb264
-rw-r--r--test/puppettest.rb4
-rwxr-xr-xtest/types/cron.rb2
-rwxr-xr-xtest/types/host.rb103
-rwxr-xr-xtest/types/sshkey.rb2
14 files changed, 836 insertions, 390 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
diff --git a/test/providers/parsedhost.rb b/test/providers/parsedhost.rb
new file mode 100644
index 000000000..da286d779
--- /dev/null
+++ b/test/providers/parsedhost.rb
@@ -0,0 +1,264 @@
+if __FILE__ == $0
+ $:.unshift '..'
+ $:.unshift '../../lib'
+ $puppetbase = "../.."
+end
+
+require 'etc'
+require 'puppet/type'
+require 'puppettest'
+require 'test/unit'
+
+class TestParsedHostProvider < Test::Unit::TestCase
+ include TestPuppet
+
+ def setup
+ super
+ @provider = Puppet.type(:host).provider(:parsed)
+
+ @oldfiletype = @provider.filetype
+ end
+
+ def teardown
+ Puppet::FileType.filetype(:ram).clear
+ @provider.filetype = @oldfiletype
+ super
+ end
+
+ def test_provider_existence
+ assert(@provider, "Could not retrieve provider")
+ end
+
+ # Here we just create a fake host type that answers to all of the methods
+ # but does not modify our actual system.
+ def mkfaketype
+ @provider.filetype = Puppet::FileType.filetype(:ram)
+ end
+
+ def mkhosthash
+ if defined? @hcount
+ @hcount += 1
+ else
+ @hcount = 1
+ end
+
+ return {
+ :name => "fakehost%s" % @hcount,
+ :ip => "192.168.27.%s" % @hcount,
+ :alias => ["alias%s" % @hcount]
+ }
+ end
+
+ def mkhost
+ hash = mkhosthash()
+
+ fakemodel = fakemodel(:host, hash[:name])
+
+ host = @provider.new(fakemodel)
+
+ hash.each do |name, val|
+ fakemodel[name] = val
+ end
+ assert(host, "Could not create provider host")
+
+ return host
+ end
+
+ # Make sure we convert both directlys correctly using a simple host.
+ def test_basic_isomorphism
+ hash = {:name => "myhost", :ip => "192.168.43.56", :alias => %w{another host}}
+
+ str = nil
+ assert_nothing_raised do
+ str = @provider.to_record(hash)
+ end
+
+ assert_equal("192.168.43.56\tmyhost\tanother\thost", str)
+
+ newhash = nil
+ assert_nothing_raised do
+ newhash = @provider.parse(str).shift
+ end
+
+ assert_equal(hash, newhash)
+ end
+
+ # Make sure parsing gets comments, blanks, and hosts
+ def test_blanks_and_comments
+ mkfaketype()
+ text = %{# comment one
+
+192.168.43.56\tmyhost\tanother\thost
+
+# another comment
+192.168.43.57\tanotherhost
+}
+
+ instances = nil
+ assert_nothing_raised do
+ instances = @provider.parse(text)
+ end
+
+ assert_equal([
+ "# comment one",
+ "",
+ {:name => "myhost", :ip => "192.168.43.56", :alias => %w{another host}},
+ " ",
+ "# another comment",
+ {:name => "anotherhost", :ip => "192.168.43.57"}
+ ], instances)
+
+ assert_nothing_raised do
+ @provider.store(instances)
+ end
+ newtext = nil
+ assert_nothing_raised do
+ newtext = @provider.fileobj.read
+ end
+
+ assert_equal(text, newtext)
+ end
+
+ def test_empty_and_absent_hashes_are_not_written
+ mkfaketype()
+
+ instances = [
+ {:name => "myhost", :ip => "192.168.43.56", :alias => %w{another host}},
+ {},
+ {:ensure => :absent, :name => "anotherhost", :ip => "192.168.43.57"}
+ ]
+
+ assert_nothing_raised do
+ newtext = @provider.store(instances)
+ end
+ newtext = nil
+ assert_nothing_raised do
+ newtext = @provider.fileobj.read
+ end
+ text = "192.168.43.56\tmyhost\tanother\thost\n"
+
+ assert_equal(text, newtext)
+ end
+
+ def test_simplehost
+ mkfaketype
+ # Start out with no content.
+ assert_nothing_raised {
+ assert_equal([], @provider.retrieve)
+ }
+
+ # Now create a provider
+ host = nil
+ assert_nothing_raised {
+ host = mkhost
+ }
+
+ # Make sure we're still empty
+ assert_nothing_raised {
+ assert_equal([], @provider.retrieve)
+ }
+
+ hash = host.model.to_hash
+
+ # Try storing it
+ assert_nothing_raised do
+ host.store(hash)
+ end
+
+ # Make sure we get the host back
+ assert_nothing_raised {
+ assert_equal([hash], @provider.retrieve)
+ }
+
+ # Remove a single field and make sure it gets tossed
+ hash.delete(:alias)
+
+ assert_nothing_raised {
+ host.store(hash)
+ assert_equal([hash], @provider.retrieve)
+ }
+
+ # Make sure it throws up if we remove a required field
+ hash.delete(:ip)
+
+ assert_raise(ArgumentError) {
+ host.store(hash)
+ }
+
+ # Now remove the whole object
+ assert_nothing_raised {
+ host.store({})
+ assert_equal([], @provider.retrieve)
+ }
+ end
+
+ # Parse our sample data and make sure we regenerate it correctly.
+ def test_hostsparse
+ fakedata("data/types/hosts").each { |file|
+ @provider.path = file
+ Puppet.info "Parsing %s" % file
+ instances = nil
+ assert_nothing_raised {
+ instances = @provider.retrieve
+ }
+
+ text = @provider.fileobj.read
+
+ dest = tempfile()
+ @provider.path = dest
+
+ # Now write it back out
+ assert_nothing_raised {
+ @provider.store(instances)
+ }
+
+ newtext = @provider.fileobj.read
+
+ # Don't worry about difference in whitespace
+ assert_equal(text.gsub(/\s+/, ' '), newtext.gsub(/\s+/, ' '))
+ }
+ end
+
+ # Make sure we can modify the file elsewhere and those modifications will
+ # get taken into account.
+ def test_modifyingfile
+ hostfile = tempfile()
+ @provider.path = hostfile
+
+ hosts = []
+ 3.times {
+ h = mkhost()
+ hosts << h
+ }
+
+ hosts.each do |host|
+ host.store
+ end
+
+ newhost = mkhost()
+ hosts << newhost
+
+ # Now store our new host
+ newhost.store()
+
+ # Verify we can retrieve that info
+ assert_nothing_raised("Could not retrieve after second write") {
+ newhost.hash
+ }
+
+ text = @provider.fileobj.read
+
+ instances = @provider.retrieve
+
+ # And verify that we have data for everything
+ hosts.each { |host|
+ name = host.model[:name]
+ assert(text.include?(name), "Host %s is not in file" % name)
+ hash = host.hash
+ assert(! hash.empty?, "Could not find host %s" % name)
+ assert(hash[:ip], "Could not find ip for host %s" % name)
+ }
+ end
+end
+
+# $Id$
diff --git a/test/puppettest.rb b/test/puppettest.rb
index f094045cd..21b539288 100644
--- a/test/puppettest.rb
+++ b/test/puppettest.rb
@@ -35,6 +35,10 @@ module TestPuppet
def name
self[:name]
end
+
+ def to_hash
+ self
+ end
end
class FakeProvider
diff --git a/test/types/cron.rb b/test/types/cron.rb
index caada9f91..94e490e40 100755
--- a/test/types/cron.rb
+++ b/test/types/cron.rb
@@ -97,7 +97,7 @@ class TestCron < Test::Unit::TestCase
cron[:ensure] = :absent
- assert_events([:cron_removed], comp)
+ assert_events([:cron_deleted], comp)
cron.retrieve
diff --git a/test/types/host.rb b/test/types/host.rb
index 8af9e3e4a..54596aa7f 100755
--- a/test/types/host.rb
+++ b/test/types/host.rb
@@ -13,24 +13,25 @@ require 'facter'
class TestHost < Test::Unit::TestCase
include TestPuppet
+
def setup
super
- # god i'm lazy
@hosttype = Puppet.type(:host)
- @oldhosttype = @hosttype.filetype
- end
- def teardown
- Puppet::FileType.filetype(:ram).clear
- @hosttype.filetype = @oldhosttype
- Puppet.type(:file).clear
- super
- end
+ @provider = @hosttype.defaultprovider
- # Here we just create a fake host type that answers to all of the methods
- # but does not modify our actual system.
- def mkfaketype
- @hosttype.filetype = Puppet::FileType.filetype(:ram)
+ # Make sure they aren't using something funky like netinfo
+ unless @provider.name == :parsed
+ @hosttype.defaultprovider = @hosttype.provider(:parsed)
+ end
+
+ cleanup do @hosttype.defaultprovider = nil end
+
+ oldpath = @provider.path
+ cleanup do
+ @provider.path = oldpath
+ end
+ @provider.path = tempfile()
end
def mkhost
@@ -52,10 +53,16 @@ class TestHost < Test::Unit::TestCase
end
def test_simplehost
- mkfaketype
host = nil
assert_nothing_raised {
- assert_nil(Puppet.type(:host).retrieve)
+ Puppet.type(:host).defaultprovider.retrieve
+
+ count = 0
+ @hosttype.each do |h|
+ count += 1
+ end
+
+ assert_equal(0, count, "Found hosts in empty file somehow")
}
assert_nothing_raised {
@@ -65,49 +72,14 @@ class TestHost < Test::Unit::TestCase
)
}
- assert_nothing_raised {
- Puppet.type(:host).store
- }
-
- assert_nothing_raised {
- assert(
- Puppet.type(:host).to_file.include?(
- Puppet.type(:host).fileobj.read
- ),
- "File does not include all of our objects"
- )
- }
- end
-
- def test_hostsparse
- fakedata("data/types/hosts").each { |file|
- @hosttype.path = file
- Puppet.info "Parsing %s" % file
- assert_nothing_raised {
- Puppet.type(:host).retrieve
- }
-
- text = Puppet.type(:host).fileobj.read
+ assert_apply(host)
- dest = tempfile()
- @hosttype.path = dest
+ assert_nothing_raised { host.retrieve }
- # Now write it back out
- assert_nothing_raised {
- Puppet.type(:host).store
- }
-
- newtext = Puppet.type(:host).fileobj.read
-
- # Don't worry about difference in whitespace
- assert_equal(text.gsub(/\s+/, ' '), newtext.gsub(/\s+/, ' '))
-
- @hosttype.clear
- }
+ assert_equal(:present, host.is(:ensure))
end
def test_moddinghost
- mkfaketype
host = mkhost()
assert_events([:host_created], host)
@@ -145,34 +117,33 @@ class TestHost < Test::Unit::TestCase
end
def test_removal
- mkfaketype
host = mkhost()
assert_nothing_raised {
host[:ensure] = :present
}
assert_events([:host_created], host)
- host.retrieve
- assert(host.insync?)
+ assert(host.exists?, "Host is not considered in sync")
+
+ assert_equal(:present, host.is(:ensure))
+
assert_nothing_raised {
host[:ensure] = :absent
}
+ assert_events([:host_deleted], host)
+
+ text = host.provider.class.fileobj.read
- assert_events([:host_removed], host)
+ assert(! text.include?(host[:name]), "Host is still in text")
host.retrieve
assert_events([], host)
end
def test_modifyingfile
- hostfile = tempfile()
- Puppet.type(:host).path = hostfile
-
hosts = []
names = []
3.times {
h = mkhost()
- #h[:ensure] = :present
- #h.retrieve
hosts << h
names << h.name
}
@@ -180,20 +151,18 @@ class TestHost < Test::Unit::TestCase
hosts.clear
Puppet.type(:host).clear
newhost = mkhost()
- #newhost[:ensure] = :present
names << newhost.name
assert_apply(newhost)
- Puppet.type(:host).clear
# Verify we can retrieve that info
assert_nothing_raised("Could not retrieve after second write") {
newhost.retrieve
}
+ text = newhost.provider.class.fileobj.read
+
# And verify that we have data for everything
names.each { |name|
- host = Puppet.type(:host)[name]
- assert(host)
- assert(host[:ip])
+ assert(text.include?(name), "Host is not in file")
}
end
end
diff --git a/test/types/sshkey.rb b/test/types/sshkey.rb
index ff9adc336..1fc4b1f82 100755
--- a/test/types/sshkey.rb
+++ b/test/types/sshkey.rb
@@ -139,7 +139,7 @@ class TestSSHKey < Test::Unit::TestCase
sshkey[:ensure] = :absent
}
- assert_events([:sshkey_removed], sshkey)
+ assert_events([:sshkey_deleted], sshkey)
sshkey.retrieve
assert_events([], sshkey)
end