summaryrefslogtreecommitdiffstats
path: root/lib/puppet
diff options
context:
space:
mode:
authorLuke Kanies <luke@madstop.com>2005-06-27 21:44:46 +0000
committerLuke Kanies <luke@madstop.com>2005-06-27 21:44:46 +0000
commit8f95084cd854aef4e3493854e58cefd352cdc68d (patch)
treef31288d1cbbd60c0fdc7c04bbd6960516a6893be /lib/puppet
parent6f074138779e558fd7017880f606dcf3527233f9 (diff)
downloadpuppet-8f95084cd854aef4e3493854e58cefd352cdc68d.tar.gz
puppet-8f95084cd854aef4e3493854e58cefd352cdc68d.tar.xz
puppet-8f95084cd854aef4e3493854e58cefd352cdc68d.zip
renaming blink to puppet
git-svn-id: https://reductivelabs.com/svn/puppet/library/trunk@302 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib/puppet')
-rw-r--r--lib/puppet/client.rb120
-rw-r--r--lib/puppet/element.rb41
-rw-r--r--lib/puppet/event.rb201
-rw-r--r--lib/puppet/fact.rb65
-rw-r--r--lib/puppet/function.rb72
-rw-r--r--lib/puppet/message.rb64
-rw-r--r--lib/puppet/selector.rb82
-rw-r--r--lib/puppet/statechange.rb111
-rw-r--r--lib/puppet/storage.rb48
-rw-r--r--lib/puppet/transaction.rb162
-rw-r--r--lib/puppet/transportable.rb200
-rw-r--r--lib/puppet/type.rb851
-rw-r--r--lib/puppet/type/component.rb74
-rw-r--r--lib/puppet/type/file.rb450
-rw-r--r--lib/puppet/type/package.rb366
-rw-r--r--lib/puppet/type/process.rb83
-rw-r--r--lib/puppet/type/service.rb186
-rw-r--r--lib/puppet/type/state.rb135
-rw-r--r--lib/puppet/type/symlink.rb109
-rw-r--r--lib/puppet/type/typegen.rb146
-rw-r--r--lib/puppet/type/typegen/filerecord.rb243
-rw-r--r--lib/puppet/type/typegen/filetype.rb316
22 files changed, 4125 insertions, 0 deletions
diff --git a/lib/puppet/client.rb b/lib/puppet/client.rb
new file mode 100644
index 000000000..f266d2dfb
--- /dev/null
+++ b/lib/puppet/client.rb
@@ -0,0 +1,120 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# the available clients
+
+require 'blink'
+require 'blink/function'
+require 'blink/type'
+require 'blink/fact'
+require 'blink/transaction'
+require 'blink/transportable'
+require 'http-access2'
+require 'soap/rpc/driver'
+require 'soap/rpc/httpserver'
+#require 'webrick/https'
+require 'logger'
+
+module Blink
+ class ClientError < RuntimeError; end
+ #---------------------------------------------------------------
+ class Client < SOAP::RPC::HTTPServer
+ def initialize(hash)
+ # to whom do we connect?
+ @server = nil
+ @nil = nil
+ @url = hash[:Server]
+ if hash.include?(:Listen) and hash[:Listen] == false
+ Blink.notice "We're a local client"
+ @localonly = true
+ @driver = @url
+ else
+ Blink.notice "We're a networked client"
+ @localonly = false
+ @driver = SOAP::RPC::Driver.new(@url, 'urn:blink-server')
+ @driver.add_method("getconfig", "name")
+ end
+ unless @localonly
+ hash.delete(:Server)
+
+ Blink.notice "Server is %s" % @url
+
+ hash[:BindAddress] ||= "0.0.0.0"
+ hash[:Port] ||= 17444
+ hash[:Debug] ||= true
+ hash[:AccessLog] ||= []
+
+ super(hash)
+ end
+ end
+
+ def getconfig
+ Blink.debug "server is %s" % @url
+ #client.loadproperty('files/sslclient.properties')
+ Blink.notice("getting config")
+ objects = nil
+ if @localonly
+ objects = @driver.getconfig(self)
+ else
+ objects = @driver.getconfig(Blink::Fact["hostname"])
+ end
+ self.config(objects)
+ end
+
+ # this method is how the client receives the tree of Transportable
+ # objects
+ # for now, just descend into the tree and perform and necessary
+ # manipulations
+ def config(tree)
+ Blink.notice("Calling config")
+ container = Marshal::load(tree).to_type
+
+ # this is a gross hack... but i don't see a good way around it
+ # set all of the variables to empty
+ Blink::Transaction.init
+ # for now we just evaluate the top-level container, but eventually
+ # there will be schedules and such associated with each object,
+ # and probably with the container itself
+ transaction = container.evaluate
+ #transaction = Blink::Transaction.new(objects)
+ transaction.toplevel = true
+ transaction.evaluate
+ self.shutdown
+ end
+
+ def callfunc(name,args)
+ Blink.notice("Calling callfunc on %s" % name)
+ if function = Blink::Function[name]
+ #Blink.debug("calling function %s" % function)
+ value = function.call(args)
+ #Blink.debug("from %s got %s" % [name,value])
+ return value
+ else
+ raise "Function '%s' not found" % name
+ end
+ end
+
+ private
+
+ def on_init
+ @default_namespace = 'urn:blink-client'
+ add_method(self, 'config', 'config')
+ add_method(self, 'callfunc', 'name', 'arguments')
+ end
+
+ def cert(filename)
+ OpenSSL::X509::Certificate.new(File.open(File.join(@dir, filename)) { |f|
+ f.read
+ })
+ end
+
+ def key(filename)
+ OpenSSL::PKey::RSA.new(File.open(File.join(@dir, filename)) { |f|
+ f.read
+ })
+ end
+
+ end
+ #---------------------------------------------------------------
+end
diff --git a/lib/puppet/element.rb b/lib/puppet/element.rb
new file mode 100644
index 000000000..dd187977a
--- /dev/null
+++ b/lib/puppet/element.rb
@@ -0,0 +1,41 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# included so we can test object types
+require 'blink'
+
+
+#---------------------------------------------------------------
+# the base class for both types and states
+# very little functionality; basically just defines the interface
+# and provides a few simple across-the-board functions like 'noop'
+class Blink::Element
+ attr_writer :noop
+
+ #---------------------------------------------------------------
+ # all of our subclasses must respond to each of these methods...
+ @@interface_methods = [
+ :retrieve, :insync?, :sync, :fqpath, :evaluate
+ ]
+
+ # so raise an error if a method that isn't overridden gets called
+ @@interface_methods.each { |method|
+ self.send(:define_method,method) {
+ raise "%s has not overridden %s" % [self.class,method]
+ }
+ }
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # for testing whether we should actually do anything
+ def noop
+ unless defined? @noop
+ @noop = false
+ end
+ return @noop || Blink[:noop] || false
+ end
+ #---------------------------------------------------------------
+
+end
+#---------------------------------------------------------------
diff --git a/lib/puppet/event.rb b/lib/puppet/event.rb
new file mode 100644
index 000000000..2cdda9976
--- /dev/null
+++ b/lib/puppet/event.rb
@@ -0,0 +1,201 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# included so we can test object types
+require 'blink'
+require 'blink/type'
+
+module Blink
+ # events are transient packets of information; they result in one or more (or none)
+ # subscriptions getting triggered, and then they get cleared
+ # eventually, these will be passed on to some central event system
+ class Event
+ # subscriptions are permanent associations determining how different
+ # objects react to an event
+ class Subscription
+ attr_accessor :source, :event, :target, :method
+
+ def initialize(hash)
+ @triggered = false
+
+ hash.each { |method,value|
+ # assign each value appropriately
+ # this is probably wicked-slow
+ self.send(method.to_s + "=",value)
+ }
+ Blink.warning "New Subscription: '%s' => '%s'" %
+ [@source,@event]
+ end
+
+ # the transaction is passed in so that we can notify it if
+ # something fails
+ def trigger(transaction)
+ # this is potentially incomplete, because refreshing an object
+ # could theoretically kick off an event, which would not get run
+ # or, because we're executing the first subscription rather than
+ # the last, a later-refreshed object could somehow be connected
+ # to the "old" object rather than "new"
+ # but we're pretty far from that being a problem
+ if transaction.triggercount(self) > 0
+ Blink.verbose "%s has already run" % self
+ else
+ Blink.verbose "'%s' matched '%s'; triggering '%s' on '%s'" %
+ [@source,@event,@method,@target]
+ begin
+ if @target.respond_to?(@method)
+ @target.send(@method)
+ else
+ Blink.verbose "'%s' of type '%s' does not respond to '%s'" %
+ [@target,@target.class,@method.inspect]
+ end
+ rescue => detail
+ # um, what the heck do i do when an object fails to refresh?
+ # shouldn't that result in the transaction rolling back?
+ # XXX yeah, it should
+ Blink.error "'%s' failed to %s: '%s'" %
+ [@target,@method,detail]
+ raise
+ #raise "We need to roll '%s' transaction back" %
+ #transaction
+ end
+ transaction.triggered(self)
+ end
+ end
+ end
+
+ attr_accessor :event, :object, :transaction
+
+ @@events = []
+
+ @@subscriptions = []
+
+ def Event.process
+ Blink.warning "Processing events"
+ @@events.each { |event|
+ @@subscriptions.find_all { |sub|
+ #Blink.warning "Sub source: '%s'; event object: '%s'" %
+ # [sub.source.inspect,event.object.inspect]
+ sub.source == event.object and
+ (sub.event == event.event or
+ sub.event == :ALL_EVENTS)
+ }.each { |sub|
+ Blink.notice "Found sub"
+ sub.trigger(event.transaction)
+ }
+ }
+
+ @@events.clear
+ end
+
+ def Event.subscribe(hash)
+ if hash[:event] == '*'
+ hash[:event] = :ALL_EVENTS
+ end
+ sub = Subscription.new(hash)
+
+ # add to the correct area
+ @@subscriptions.push sub
+ end
+
+ def initialize(args)
+ unless args.include?(:event) and args.include?(:object)
+ raise "Event.new called incorrectly"
+ end
+
+ @event = args[:event]
+ @object = args[:object]
+ @transaction = args[:transaction]
+
+ Blink.warning "New Event: '%s' => '%s'" %
+ [@object,@event]
+
+ # initially, just stuff all instances into a central bucket
+ # to be handled as a batch
+ @@events.push self
+ end
+ end
+end
+
+
+#---------------------------------------------------------------
+# here i'm separating out the methods dealing with handling events
+# currently not in use, so...
+
+class Blink::NotUsed
+ #---------------------------------------------------------------
+ # return action array
+ # these are actions to use for responding to events
+ # no, this probably isn't the best way, because we're providing
+ # access to the actual hash, which is silly
+ def action
+ if not defined? @actions
+ puts "defining action hash"
+ @actions = Hash.new
+ end
+ @actions
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # call an event
+ # this is called on subscribers by the trigger method from the obj
+ # which sent the event
+ # event handling should probably be taking place in a central process,
+ # but....
+ def event(event,obj)
+ Blink.debug "#{self} got event #{event} from #{obj}"
+ if @actions.key?(event)
+ Blink.debug "calling it"
+ @actions[event].call(self,obj,event)
+ else
+ p @actions
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # subscribe to an event or all events
+ # this entire event system is a hack job and needs to
+ # be replaced with a central event handler
+ def subscribe(args,&block)
+ obj = args[:object]
+ event = args[:event] || '*'.intern
+ if obj.nil? or event.nil?
+ raise "subscribe was called wrongly; #{obj} #{event}"
+ end
+ obj.action[event] = block
+ #events.each { |event|
+ unless @notify.key?(event)
+ @notify[event] = Array.new
+ end
+ unless @notify[event].include?(obj)
+ Blink.debug "pushing event '%s' for object '%s'" % [event,obj]
+ @notify[event].push(obj)
+ end
+ # }
+ #else
+ # @notify['*'.intern].push(obj)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # initiate a response to an event
+ def trigger(event)
+ subscribers = Array.new
+ if @notify.include?('*') and @notify['*'].length > 0
+ @notify['*'].each { |obj| subscribers.push(obj) }
+ end
+ if (@notify.include?(event) and (! @notify[event].empty?) )
+ @notify[event].each { |obj| subscribers.push(obj) }
+ end
+ Blink.debug "triggering #{event}"
+ subscribers.each { |obj|
+ Blink.debug "calling #{event} on #{obj}"
+ obj.event(event,self)
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+end # Blink::Type
diff --git a/lib/puppet/fact.rb b/lib/puppet/fact.rb
new file mode 100644
index 000000000..2b46aabd8
--- /dev/null
+++ b/lib/puppet/fact.rb
@@ -0,0 +1,65 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# an interface for registering and retrieving facts
+# this is an abstract interface, and should just be used to interact
+# with another library
+
+# currently a very thin veneer on 'facter'
+
+require 'facter'
+require 'blink'
+require 'blink/type'
+
+module Blink
+ class Fact
+ def Fact.[](name)
+ fact = Facter[name]
+ if fact.value.nil?
+ raise "Could not retrieve fact %s" % name
+ end
+ Blink.debug("fact: got %s from %s for %s" % [fact.value,fact,name])
+ return fact.value
+ end
+
+ # just pass the block to 'add'
+ # the block has to do things like set the interpreter,
+ # the code (which can be a ruby block), and maybe the
+ # os and osrelease
+ def Fact.add(name,&block)
+ Facter[name].add(&block)
+ end
+
+ def Fact.name
+ return :fact
+ end
+
+ def Fact.namevar
+ return :name
+ end
+
+ #Blink::Type.newtype(self)
+
+ # we're adding a new resolution mechanism here; this is just how
+ # types work
+ # we don't have any real interest in the returned object
+ def initialize(hash)
+ name = hash[:name]
+ hash.delete(:name)
+ Fact.add(name) { |fact|
+ method = nil
+ hash.each { |key,value|
+ if key.is_a?(String)
+ method = key + "="
+ elsif key.is_a?(Symbol)
+ method = key.id2name + "="
+ else
+ raise "Key must be either string or symbol"
+ end
+ fact.send(method,value)
+ }
+ }
+ end
+ end
+end
diff --git a/lib/puppet/function.rb b/lib/puppet/function.rb
new file mode 100644
index 000000000..d5cc11107
--- /dev/null
+++ b/lib/puppet/function.rb
@@ -0,0 +1,72 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'blink'
+require 'blink/fact'
+
+module Blink
+ class Function
+ @@functions = Hash.new(nil)
+
+ #---------------------------------------------------------------
+ def Function.[](name)
+ return @@functions[name]
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def call(args)
+ @code.call(args)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # we want a 'proc' item instead of a block, so that we can return
+ # from it
+ def initialize(name,code)
+ @name = name
+ @code = code
+
+ @@functions[name] = self
+ end
+ #---------------------------------------------------------------
+ end
+
+ Function.new("fact", proc { |fact|
+ require 'blink/fact'
+
+ value = Fact[fact]
+ Blink.debug("retrieved %s as %s" % [fact,value])
+ value
+ })
+
+ Function.new("addfact", proc { |args|
+ require 'blink/fact'
+ #Blink.debug("running addfact")
+
+ hash = nil
+ if args.is_a?(Array)
+ hash = Hash[*args]
+ end
+ name = nil
+ if hash.has_key?("name")
+ name = hash["name"]
+ hash.delete("name")
+ elsif hash.has_key?(:name)
+ name = hash[:name]
+ hash.delete(:name)
+ else
+ raise "Functions must have names"
+ end
+ #Blink.debug("adding fact %s" % name)
+ newfact = Fact.add(name) { |fact|
+ hash.each { |key,value|
+ method = key + "="
+ fact.send(method,value)
+ }
+ }
+
+ #Blink.debug("got fact %s" % newfact)
+ })
+end
diff --git a/lib/puppet/message.rb b/lib/puppet/message.rb
new file mode 100644
index 000000000..31d5fa503
--- /dev/null
+++ b/lib/puppet/message.rb
@@ -0,0 +1,64 @@
+# $Id$
+
+module Blink
+ #------------------------------------------------------------
+ # provide feedback of various types to the user
+ # modeled after syslog messages
+ # each level of message prints in a different color
+ class Message
+ @@messages = Array.new
+ @@levels = [ :debug, :verbose, :notice, :warning, :error ]
+ @@colors = {
+ :debug => SLATE,
+ :verbose => ORANGE,
+ :notice => PINK,
+ :warning => GREEN,
+ :error => YELLOW
+ }
+
+ attr_accessor :level, :message, :source
+
+ def Message.loglevels
+ return @@levels
+ end
+
+ def initialize(args)
+ unless args.include?(:level) && args.include?(:message) &&
+ args.include?(:source)
+ raise "Blink::Message called incorrectly"
+ end
+
+ if args[:level].class == String
+ @level = args[:level].intern
+ elsif args[:level].class == Symbol
+ @level = args[:level]
+ else
+ raise "Level is not a string or symbol: #{args[:level].class}"
+ end
+ @message = args[:message]
+ @source = args[:source]
+ @time = Time.now
+ # this should include the host name, and probly lots of other
+ # stuff, at some point
+ unless @@levels.include?(level)
+ raise "Invalid message level #{level}"
+ end
+
+ @@messages.push(self)
+ Blink.newmessage(self)
+ end
+
+ def to_s
+ # this probably won't stay, but until this leaves the console,
+ # i'm going to use coloring...
+ #return "#{@time} #{@source} (#{@level}): #{@message}"
+ #return @@colors[@level] + "%s %s (%s): %s" % [
+ # @time, @source, @level, @message
+ #] + RESET
+ return @@colors[@level] + "%s (%s): %s" % [
+ @source, @level, @message
+ ] + RESET
+ end
+ end
+ #------------------------------------------------------------
+end
diff --git a/lib/puppet/selector.rb b/lib/puppet/selector.rb
new file mode 100644
index 000000000..51e82b09d
--- /dev/null
+++ b/lib/puppet/selector.rb
@@ -0,0 +1,82 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'blink'
+require 'blink/fact'
+
+module Blink
+ #---------------------------------------------------------------
+ # this class will provide something like a 'select' statement, but it will
+ # return a value
+ # it will be used something like this:
+ # value = Selector.new(
+ # proc { test() } => value,
+ # proc { test2() } => value2,
+ # )
+
+ # each test gets evaluated in turn; the first one to return true has its
+ # value returned as the value of the statement
+ # this will be used to provide abstraction in objects, but it's currently
+ # unused
+
+ class Selector < Array
+ attr_accessor :default
+
+ def add(value,&block)
+ option = Option.new(value,&block)
+ @ohash[value] = option
+ @oarray.push(option)
+ end
+
+ def evaluate
+ @oarray.each { |option|
+ if option.true?
+ return option.value
+ end
+ }
+ return nil
+ end
+
+ # we have to support providing different values based on
+ # different criteria, e.g., default is X, SunOS gets Y, and
+ # host Yayness gets Z.
+ # thus, no invariant
+ def initialize
+ @oarray = []
+ @ohash = {}
+
+ if block_given?
+ yield self
+ end
+ end
+
+ def to_s
+ return self.evaluate()
+ end
+
+ class Option
+ attr_accessor :value, :test, :invariant
+
+ def initialize(value,&block)
+ @value = value
+ @test = block
+ end
+
+ def to_s
+ if self.evaluate
+ return value
+ end
+ end
+
+ def true?
+ unless @test.is_a?(Proc)
+ raise "Cannot yet evaluate non-code tests"
+ end
+
+ return @test.call()
+ end
+ end
+ #---------------------------------------------------------------
+ end
+end
diff --git a/lib/puppet/statechange.rb b/lib/puppet/statechange.rb
new file mode 100644
index 000000000..f88108d8a
--- /dev/null
+++ b/lib/puppet/statechange.rb
@@ -0,0 +1,111 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# the class responsible for actually doing any work
+
+# enables no-op and logging/rollback
+
+module Blink
+ class StateChange
+ attr_accessor :is, :should, :type, :path, :state, :transaction, :run
+
+ #---------------------------------------------------------------
+ def initialize(state)
+ @state = state
+ #@state.parent.newchange
+ @path = state.fqpath
+ @is = state.is
+ @should = state.should
+
+ @run = false
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def go
+ if @state.noop
+ #Blink.notice "%s is noop" % @state
+ return nil
+ end
+
+ begin
+ event = @state.sync
+ @run = true
+
+ # default to a simple event type
+ if event.nil?
+ event = @state.parent.class.name.id2name + "_changed"
+ elsif ! event.is_a?(Symbol)
+ Blink.notice "State '%s' returned invalid event '%s'; resetting to default" %
+ [@state.class,event]
+
+ event = @state.parent.class.name.id2name + "_changed"
+ end
+
+ # i should maybe include object type, but the event type
+ # should basically point to that, right?
+ return Blink::Event.new(
+ :event => event,
+ :object => @state.parent,
+ :transaction => @transaction,
+ :message => self.to_s
+ )
+ rescue => detail
+ Blink.error "%s failed: %s" % [self.to_s,detail]
+ raise
+ # there should be a way to ask the state what type of event
+ # it would have generated, but...
+ pname = @state.parent.class.name.id2name
+ #if pname.is_a?(Symbol)
+ # pname = pname.id2name
+ #end
+ return Blink::Event.new(
+ :event => pname + "_failed",
+ :object => @state.parent,
+ :transaction => @transaction,
+ :message => "Failed: " + self.to_s
+ )
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def forward
+ #Blink.notice "moving change forward"
+
+ unless defined? @transaction
+ raise "StateChange '%s' tried to be executed outside of transaction" %
+ self
+ end
+
+ return self.go
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def backward
+ @state.should = @is
+ @state.retrieve
+
+ Blink.notice "Rolling %s backward" % self
+ return self.go
+
+ #raise "Moving statechanges backward is currently unsupported"
+ #@type.change(@path,@should,@is)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def noop
+ return @state.noop
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def to_s
+ return "%s: %s => %s" % [@state,@is,@should]
+ end
+ #---------------------------------------------------------------
+ end
+end
diff --git a/lib/puppet/storage.rb b/lib/puppet/storage.rb
new file mode 100644
index 000000000..9fc21d38b
--- /dev/null
+++ b/lib/puppet/storage.rb
@@ -0,0 +1,48 @@
+# $Id$
+
+module Blink
+ # a class for storing state
+ class Storage
+ include Singleton
+ @@state = Hash.new { |hash,key|
+ hash[key] = Hash.new(nil)
+ }
+ @@splitchar = "\t"
+
+ def initialize
+ self.class.load
+ end
+
+ def Storage.load
+ # XXX I should probably use a better default state dir
+ Blink[:statefile] ||= "/var/tmp/blinkstate"
+ return unless File.exists?(Blink[:statefile])
+ File.open(Blink[:statefile]) { |file|
+ file.gets { |line|
+ myclass, key, value = line.split(@@splitchar)
+
+ @@state[myclass][key] = Marshal::load(value)
+ }
+ }
+ end
+
+ def Storage.state(myclass)
+ unless myclass.is_a? Class
+ myclass = myclass.class
+ end
+ result = @@state[myclass]
+ return result
+ end
+
+ def Storage.store
+ File.open(Blink[:statefile], File::CREAT|File::WRONLY, 0600) { |file|
+ @@state.each { |klass, thash|
+ thash.each { |key,value|
+ mvalue = Marshal::dump(value)
+ file.puts([klass,key,mvalue].join(@@splitchar))
+ }
+ }
+ }
+ end
+ end
+end
diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb
new file mode 100644
index 000000000..87b2f950c
--- /dev/null
+++ b/lib/puppet/transaction.rb
@@ -0,0 +1,162 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# the class that actually walks our object/state tree, collects the changes,
+# and performs them
+
+# there are two directions of walking:
+# - first we recurse down the tree and collect changes
+# - then we walk back up the tree through 'refresh' after the changes
+
+require 'blink'
+require 'blink/statechange'
+
+#---------------------------------------------------------------
+module Blink
+class Transaction
+ attr_accessor :toplevel, :component
+
+ #---------------------------------------------------------------
+ # a bit of a gross hack; a global list of objects that have failed to sync,
+ # so that we can verify during later syncs that our dependencies haven't
+ # failed
+ def Transaction.init
+ @@failures = Hash.new(0)
+ @@changed = []
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # for now, just store the changes for executing linearly
+ # later, we might execute them as we receive them
+ def change(change)
+ @changes.push change
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # i don't need to worry about ordering, because it's not possible to specify
+ # an object as a dependency unless it's already been mentioned within the language
+ # thus, an object gets defined, then mentioned as a dependency, and the objects
+ # are synced in that order automatically
+ def evaluate
+ Blink.notice "executing %s changes or transactions" % @changes.length
+
+ return @changes.collect { |change|
+ if change.is_a?(Blink::StateChange)
+ change.transaction = self
+ events = nil
+ begin
+ events = [change.forward].flatten
+ #@@changed.push change.state.parent
+ rescue => detail
+ Blink.error("%s failed: %s" % [change,detail])
+ raise
+ # at this point, we would normally do error handling
+ # but i haven't decided what to do for that yet
+ # so just record that a sync failed for a given object
+ #@@failures[change.state.parent] += 1
+ # this still could get hairy; what if file contents changed,
+ # but a chmod failed? how would i handle that error? dern
+ end
+
+ if events.nil?
+ Blink.verbose "No events returned?"
+ else
+ # first handle the subscriptions on individual objects
+ events.each { |event|
+ change.state.parent.subscribers?(event).each { |sub|
+ sub.trigger(self)
+ }
+ }
+ end
+ events
+ elsif change.is_a?(Blink::Transaction)
+ change.evaluate
+ else
+ raise "Transactions cannot handle objects of type %s" % child.class
+ end
+ }.flatten.reject { |event|
+ event.nil?
+ }.each { |event|
+ # this handles subscriptions on the components, rather than
+ # on idividual objects
+ self.component.subscribers?(event).each { |sub|
+ sub.trigger(self)
+ }
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this should only be called by a Blink::Container object now
+ # and it should only receive an array
+ def initialize(tree)
+ @tree = tree
+ @toplevel = false
+
+ @triggered = Hash.new(0)
+
+ # of course, this won't work on the second run
+ unless defined? @@failures
+ @toplevel = true
+ self.class.init
+ end
+ # change collection is in-band, and message generation is out-of-band
+ # of course, exception raising is also out-of-band
+ @changes = @tree.collect { |child|
+ # these children are all Blink::Type instances
+ # not all of the children will return a change, and Containers
+ # return transactions
+ child.evaluate
+ }.flatten.reject { |child|
+ child.nil? # remove empties
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def rollback
+ @changes.each { |change|
+ if change.is_a?(Blink::StateChange)
+ next unless change.run
+ #change.transaction = self
+ begin
+ change.backward
+ #@@changed.push change.state.parent
+ rescue => detail
+ Blink.error("%s rollback failed: %s" % [change,detail])
+ # at this point, we would normally do error handling
+ # but i haven't decided what to do for that yet
+ # so just record that a sync failed for a given object
+ #@@failures[change.state.parent] += 1
+ # this still could get hairy; what if file contents changed,
+ # but a chmod failed? how would i handle that error? dern
+ end
+ elsif change.is_a?(Blink::Transaction)
+ # yay, recursion
+ change.rollback
+ else
+ raise "Transactions cannot handle objects of type %s" % child.class
+ end
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def triggercount(sub)
+ Blink.notice "Triggercount on %s is %s" % [sub,@triggered[sub]]
+ return @triggered[sub]
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def triggered(sub)
+ @triggered[sub] += 1
+ Blink.notice "%s was triggered; count is %s" % [sub,@triggered[sub]]
+ end
+ #---------------------------------------------------------------
+end
+end
+#---------------------------------------------------------------
diff --git a/lib/puppet/transportable.rb b/lib/puppet/transportable.rb
new file mode 100644
index 000000000..522a34d7c
--- /dev/null
+++ b/lib/puppet/transportable.rb
@@ -0,0 +1,200 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'blink'
+
+module Blink
+ #------------------------------------------------------------
+ class TransObject < Hash
+ attr_accessor :type
+
+ @@ohash = {}
+ @@oarray = []
+
+ def TransObject.add(object)
+ @@oarray.push object
+
+ # this is just so we can check, at parse time, whether a required
+ # object has already been mentioned when it is listed as required
+ # because we're ordered, as long as an object gets made before its
+ # dependent objects will get synced later
+ @@ohash[object.longname] = object
+ end
+
+ def TransObject.clear
+ @@oarray.clear
+ end
+
+ def TransObject.list
+ return @@oarray
+ end
+
+ def initialize(name,type)
+ self[:name] = name
+ @type = type
+ self.class.add(self)
+ end
+
+ def longname
+ return [self.type,self[:name]].join('--')
+ end
+
+ def name
+ return self[:name]
+ end
+
+ def to_s
+ return "%s(%s) => %s" % [@type,self[:name],super]
+ end
+
+ def to_type
+ retobj = nil
+ if type = Blink::Type.type(self.type)
+ #begin
+ # this will fail if the type already exists
+ # which may or may not be a good thing...
+ retobj = type.new(self)
+ #rescue => detail
+ # Blink.error "Failed to create %s: %s" % [type.name,detail]
+ # puts self.class
+ # puts self.inspect
+ # exit
+ #end
+ else
+ raise "Could not find object type %s" % self.type
+ end
+
+ return retobj
+ end
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ class TransSetting
+ attr_accessor :type, :name, :args, :evalcount
+
+ def initialize
+ @evalcount = 0
+ end
+
+ def evaluate
+ @evalcount += 0
+ if type = Blink::Type.type(self.type)
+ # call the settings
+ name = self.name
+ unless name.is_a?(Symbol)
+ name = name.intern
+ end
+ if type.allowedmethod(name)
+ type.send(self.name,self.args)
+ else
+ Blink.error("%s does not respond to %s" % [self.type,self.name])
+ end
+ else
+ raise "Could not find object type %s" % setting.type
+ end
+ end
+ end
+ #------------------------------------------------------------
+
+ #------------------------------------------------------------
+ # just a linear container for objects
+ class TransBucket < Array
+ attr_accessor :name, :type
+
+ def push(*args)
+ args.each { |arg|
+ case arg
+ when Blink::TransBucket, Blink::TransObject, Blink::TransSetting
+ # nada
+ else
+ raise "TransBuckets cannot handle objects of type %s" %
+ arg.class
+ end
+ }
+ super
+ end
+
+ def to_type
+ # this container will contain the equivalent of all objects at
+ # this level
+ #container = Blink::Component.new(:name => @name, :type => @type)
+ unless defined? @name
+ raise "TransBuckets must have names"
+ end
+ unless defined? @type
+ Blink.verbose "TransBucket '%s' has no type" % @name
+ end
+ hash = {
+ :name => @name,
+ :type => @type
+ }
+ if defined? @parameters
+ @parameters.each { |param,value|
+ Blink.warning "Defining %s on %s of type %s" %
+ [param,@name,@type]
+ hash[param] = value
+ }
+ else
+ Blink.warning "%s has no parameters" % @name
+ end
+ container = Blink::Component.new(hash)
+ nametable = {}
+
+ self.each { |child|
+ # the fact that we descend here means that we are
+ # always going to execute depth-first
+ # which is _probably_ a good thing, but one never knows...
+ if child.is_a?(Blink::TransBucket)
+ # just perform the same operation on any children
+ container.push(child.to_type)
+ elsif child.is_a?(Blink::TransSetting)
+ # XXX this is wrong, but for now just evaluate the settings
+ child.evaluate
+ elsif child.is_a?(Blink::TransObject)
+ # do a simple little naming hack to see if the object already
+ # exists in our scope
+ # this assumes that type/name combinations are globally
+ # unique
+ name = [child[:name],child.type].join("--")
+
+ if nametable.include?(name)
+ object = nametable[name]
+ child.each { |var,value|
+ # don't rename; this shouldn't be possible anyway
+ next if var == :name
+
+ Blink.notice "Adding %s to %s" % [var,name]
+ # override any existing values
+ object[var] = value
+ }
+ else # the object does not exist yet in our scope
+ # now we have the object instantiated, in our scope
+ object = child.to_type
+ nametable[name] = object
+
+ # this sets the order of the object
+ container.push object
+ end
+ else
+ raise "TransBucket#to_type cannot handle objects of type %s" %
+ child.class
+ end
+ }
+
+ # at this point, no objects at are level are still Transportable
+ # objects
+ return container
+ end
+
+ def param(param,value)
+ unless defined? @parameters
+ @parameters = {}
+ end
+ @parameters[param] = value
+ end
+
+ end
+ #------------------------------------------------------------
+end
diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb
new file mode 100644
index 000000000..51cd904e7
--- /dev/null
+++ b/lib/puppet/type.rb
@@ -0,0 +1,851 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# included so we can test object types
+require 'blink'
+require 'blink/element'
+require 'blink/event'
+require 'blink/type/state'
+
+
+# XXX see the bottom of the file for the rest of the inclusions
+
+#---------------------------------------------------------------
+# This class is the abstract base class for the mechanism for organizing
+# work. No work is actually done by this class or its subclasses; rather,
+# the subclasses include states which do the actual work.
+# See state.rb for how work is actually done.
+
+# our duck type interface -- if your object doesn't match this interface,
+# it won't work
+
+# all of our first-class objects (objects, states, and components) will
+# respond to these methods
+# although states don't inherit from Blink::Type
+# although maybe Blink::State should...
+
+# the default behaviour that this class provides is to just call a given
+# method on each contained object, e.g., in calling 'sync', we just run:
+# object.each { |subobj| subobj.sync() }
+
+# to use this interface, just define an 'each' method and 'include Blink::Type'
+
+module Blink
+class Type < Blink::Element
+ attr_accessor :children, :parameters, :parent
+ include Enumerable
+
+ @@allobjects = Array.new # an array for all objects
+ @abstract = true
+
+ @name = :blink # a little fakery, since Blink itself isn't a type
+ @namevar = :notused
+
+ @states = []
+ @parameters = [:notused]
+
+ @allowedmethods = [:noop,:debug,:statefile]
+
+ @@metaparams = [
+ :onerror,
+ :schedule,
+ :check,
+ :require
+ ]
+
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+ # class methods dealing with Type management
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # these objects are used for mapping type names (e.g., 'file')
+ # to actual object classes; because Type.inherited is
+ # called before the <subclass>.name method is defined, we need
+ # to store each class in an array, and then later actually iterate
+ # across that array and make a map
+ @@typeary = [self] # so that the allowedmethods stuff works
+ @@typehash = Hash.new { |hash,key|
+ if key.is_a?(String)
+ key = key.intern
+ end
+ if hash.include?(key)
+ hash[key]
+ else
+ raise "Object type %s not found" % key
+ end
+ }
+
+ #---------------------------------------------------------------
+ # a test for whether this type is allowed to have instances
+ # on clients
+ # subclasses can just set '@abstract = true' to mark themselves
+ # as abstract
+ def Type.abstract
+ if defined? @abstract
+ return @abstract
+ else
+ return false
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.allowedmethod(method)
+ if defined? @allowedmethods and @allowedmethods.include?(method)
+ return true
+ else
+ return false
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.statefile(file)
+ Blink[:statefile] = file
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # ill thought-out
+ # this needs to return @noop
+ #def noop(ary)
+ # Blink[:noop] = ary.shift
+ #end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #def debug(ary)
+ # Blink[:debug] = ary.shift
+ #end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this is meant to be run multiple times, e.g., when a new
+ # type is defined at run-time
+ def Type.buildtypehash
+ @@typeary.each { |otype|
+ if @@typehash.include?(otype.name)
+ if @@typehash[otype.name] != otype
+ Blink.warning("Object type %s is already defined (%s vs %s)" %
+ [otype.name,@@typehash[otype.name],otype])
+ end
+ else
+ @@typehash[otype.name] = otype
+ end
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.eachtype
+ @@typeary.each { |type| yield type }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this should make it so our subclasses don't have to worry about
+ # defining these class instance variables
+ def Type.inherited(sub)
+ sub.initvars
+
+ #Blink.notice("subtype %s(%s) just created" % [sub,sub.superclass])
+ # add it to the master list
+ # unfortunately we can't yet call sub.name, because the #inherited
+ # method gets called before any commands in the class definition
+ # get executed, which, um, sucks
+ @@typeary.push(sub)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this is so we don't have to eval this code
+ # init all of our class instance variables
+ def Type.initvars
+ @objects = Hash.new
+ @actions = Hash.new
+ #Blink.verbose "initing validstates for %s" % self
+ @validstates = {}
+ @validparameters = {}
+
+ unless defined? @states
+ @states = {}
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.metaclass
+ if defined? @metaclass
+ return @metaclass
+ else
+ return false
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this is used for mapping object types (e.g., Blink::Type::File)
+ # to names (e.g., "file")
+ def Type.name
+ return @name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.newtype(type)
+ raise "Type.newtype called, but I don't know why"
+ @@typeary.push(type)
+ if @@typehash.has_key?(type.name)
+ Blink.notice("Redefining object type %s" % type.name)
+ end
+ @@typehash[type.name] = type
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.type(type)
+ unless @@typeary.length == @@typehash.length
+ Type.buildtypehash
+ end
+ @@typehash[type]
+ end
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+ # class methods dealing with type instance management
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # retrieve a named object
+ def Type.[](name)
+ if @objects.has_key?(name)
+ return @objects[name]
+ else
+ return nil
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.[]=(name,object)
+ newobj = nil
+ if object.is_a?(Blink::Type)
+ newobj = object
+ else
+ raise "must pass a Blink::Type object"
+ end
+
+ if @objects.has_key?(newobj.name)
+ raise "Object '%s' of type '%s' already exists with id '%s' vs. '%s'" %
+ [newobj.name,newobj.class.name,
+ @objects[newobj.name].object_id,newobj.object_id]
+ else
+ #Blink.debug("adding %s of type %s to class list" %
+ # [object.name,object.class])
+ @objects[newobj.name] = newobj
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # remove all type instances
+ def Type.allclear
+ @@typeary.each { |subtype|
+ Blink.notice "Clearing %s of objects" % subtype
+ subtype.clear
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # per-type clearance
+ def Type.clear
+ if defined? @objects
+ @objects.clear
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # all objects total
+ def Type.push(object)
+ @@allobjects.push object
+ #Blink.debug("adding %s of type %s to master list" %
+ # [object.name,object.class])
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # some simple stuff to make it easier to get a name from everyone
+ def Type.namevar
+ unless defined? @namevar and ! @namevar.nil?
+ raise "Class %s has no namevar defined" % self
+ end
+ return @namevar
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.has_key?(name)
+ return @objects.has_key?(name)
+ end
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+ # class and instance methods dealing with parameters and states
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.buildstatehash
+ unless defined? @validstates
+ @validstates = Hash.new(false)
+ end
+ @states.each { |stateklass|
+ name = stateklass.name
+ if @validstates.include?(name)
+ if @validstates[name] != stateklass
+ raise "Redefining state %s(%s) in %s" % [name,stateklass,self]
+ else
+ # it's already there, so don't bother
+ end
+ else
+ @validstates[name] = stateklass
+ end
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # Is the parameter in question a meta-parameter?
+ def Type.metaparam(param)
+ @@metaparams.include?(param)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this is probably only used by FileRecord objects
+ def Type.parameters=(params)
+ Blink.notice "setting parameters to [%s]" % params.join(" ")
+ @parameters = params.collect { |param|
+ if param.class == Symbol
+ param
+ else
+ param.intern
+ end
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.states
+ return @states
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.validstates
+ return @validstates
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.validstate(name)
+ unless @validstates.length == @states.length
+ self.buildstatehash
+ end
+ if @validstates.include?(name)
+ return @validstates[name]
+ else
+ return false
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.validparameter(name)
+ unless defined? @parameters
+ raise "Class %s has not defined parameters" % self
+ end
+ return @parameters.include?(name)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def Type.validparam(name)
+ self.validstate(name) or self.validparameter(name) or false
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this abstracts accessing parameters and states, and normalizes
+ # access to always be symbols, not strings
+ def [](name)
+ mname = name
+ if name.is_a?(String)
+ mname = name.intern
+ end
+ unless self.class.validparam(name)
+ raise "Invalid parameter %s" % [mname]
+ end
+ if @states.include?(mname)
+ # if they're using [], they don't know if we're a state or a string
+ # thus, return a string
+ # if they want the actual state object, they should use state()
+ return @states[mname].is
+ elsif @parameters.include?(mname)
+ return @parameters[mname]
+ else
+ return nil
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this abstracts setting parameters and states, and normalizes
+ # access to always be symbols, not strings
+ def []=(name,value)
+ mname = name
+ if name.is_a?(String)
+ mname = name.intern
+ end
+
+ if Blink::Type.metaparam(mname)
+ # call the metaparam method
+ self.send(("meta" + mname.id2name),value)
+ elsif stateklass = self.class.validstate(mname)
+ if value.is_a?(Blink::State)
+ Blink.debug "'%s' got handed a state for '%s'" % [self,mname]
+ @states[mname] = value
+ else
+ if @states.include?(mname)
+ @states[mname].should = value
+ else
+ @states[mname] = stateklass.new(
+ :parent => self,
+ :should => value
+ )
+ #Blink.notice "Adding parent to %s" % mname
+ #@states[mname].parent = self
+ end
+ end
+ elsif self.class.validparameter(mname)
+ @parameters[mname] = value
+ else
+ raise "Invalid parameter %s" % [mname]
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # removing states
+ def delete(attr)
+ if @states.has_key?(attr)
+ @states.delete(attr)
+ else
+ raise "Undefined state '#{attr}' in #{self}"
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def state(name)
+ return @states[name]
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def parameter(name)
+ return @parameters[name]
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+ # instance methods related to instance intrinsics
+ # e.g., initialize() and name()
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def initialize(hash)
+ @children = []
+ @evalcount = 0
+
+ @subscriptions = []
+
+ # states and parameters are treated equivalently from the outside:
+ # as name-value pairs (using [] and []=)
+ # internally, however, parameters are merely a hash, while states
+ # point to State objects
+ # further, the lists of valid states and parameters are defined
+ # at the class level
+ @states = Hash.new(false)
+ @parameters = Hash.new(false)
+
+ @noop = false
+
+ # which objects to notify when we change
+ @notify = []
+
+ # keeping stats for the total number of changes, and how many were
+ # completely sync'ed
+ # this isn't really sufficient either, because it adds lots of special cases
+ # such as failed changes
+ # it also doesn't distinguish between changes from the current transaction
+ # vs. changes over the process lifetime
+ @totalchanges = 0
+ @syncedchanges = 0
+ @failedchanges = 0
+
+ hash.each { |var,value|
+ unless var.is_a? Symbol
+ hash[var.intern] = value
+ hash.delete(var)
+ end
+ }
+
+ if hash.include?(:noop)
+ @noop = hash[:noop]
+ hash.delete(:noop)
+ end
+
+ # we have to set the name of our object before anything else,
+ # because it might be used in creating the other states
+ namevar = self.class.namevar
+
+ # if they're not using :name for the namevar but we got :name (probably
+ # from the parser)
+ if namevar != :name and hash.include?(:name) and ! hash[:name].nil?
+ self[namevar] = hash[:name]
+ hash.delete(:name)
+ # else if we got the namevar
+ elsif hash.has_key?(namevar) and ! hash[namevar].nil?
+ self[namevar] = hash[namevar]
+ hash.delete(namevar)
+ # else something's screwy
+ else
+ p hash
+ p namevar
+ raise TypeError.new("A name must be provided to %s at initialization time" %
+ self.class)
+ end
+
+ hash.each { |param,value|
+ #Blink.debug("adding param '%s' with value '%s'" %
+ # [param,value])
+ self[param] = value
+ }
+
+ # add this object to the specific class's list of objects
+ #Blink.notice("Adding [%s] to %s" % [self.name,self.class])
+ self.class[self.name] = self
+
+ # and then add it to the master list
+ Blink::Type.push(self)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # return the full path to us, for logging and rollback
+ # some classes (e.g., FileTypeRecords) will have to override this
+ def fqpath
+ return self.class, self.name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this might result from a state or from a parameter
+ def name
+ return self[self.class.namevar]
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def retrieve
+ # it's important to use the method here, so we always get
+ # them back in the right order
+ self.states.collect { |state|
+ state.retrieve
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def sync
+ self.collect { |child|
+ child.sync
+ }.reject { |event|
+ ! (event.is_a?(Symbol) or event.is_a?(String))
+ }.flatten
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def to_s
+ self.name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+ # instance methods dealing with contained states
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def states
+ Blink.debug "%s has %s states" % [self,@states.length]
+ tmpstates = []
+ self.class.states.each { |state|
+ if @states.include?(state.name)
+ tmpstates.push(@states[state.name])
+ end
+ }
+ unless tmpstates.length == @states.length
+ raise "Something went very wrong with tmpstates creation"
+ end
+ return tmpstates
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def eachstate
+ self.states.each { |state|
+ yield state
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # iterate across all children, and then iterate across states
+ # we do children first so we're sure that all dependent objects
+ # are checked first
+ # we ignore parameters here, because they only modify how work gets
+ # done, they don't ever actually result in work specifically
+ def each
+ # we want to return the states in the order that each type
+ # specifies it, because it may (as in the case of File#create)
+ # be important
+ @children.each { |child|
+ yield child
+ }
+ self.eachstate { |state|
+ yield state
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def push(*child)
+ @children.push(*child)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+ # instance methods dealing with actually doing work
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def newchange
+ @totalchanges += 1
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this method is responsible for collecting state changes
+ # we always descend into the children before we evaluate our current
+ # states
+ # this returns any changes resulting from testing, thus 'collect'
+ # rather than 'each'
+ def evaluate
+ unless defined? @evalcount
+ Blink.error "No evalcount defined on '%s' of type '%s'" %
+ [self.name,self.class]
+ end
+ # if we're a metaclass and we've already evaluated once...
+ if self.metaclass and @evalcount > 0
+ return
+ end
+ @evalcount += 1
+
+ changes = @children.collect { |child|
+ child.evaluate
+ }
+
+ # this only operates on states, not states + children
+ self.retrieve
+ unless self.insync?
+ changes << self.states.find_all { |state|
+ ! state.insync?
+ }.collect { |state|
+ Blink::StateChange.new(state)
+ }
+ end
+ # collect changes and return them
+ # these changes could be from child objects or from contained states
+ #self.collect { |child|
+ # child.evaluate
+ #}
+ return changes
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # if all contained objects are in sync, then we're in sync
+ def insync?
+ insync = true
+
+ self.states.each { |state|
+ unless state.insync?
+ Blink.debug("%s is not in sync" % state)
+ insync = false
+ end
+ }
+
+ Blink.debug("%s sync status is %s" % [self,insync])
+ return insync
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # do we actually do work, or do we modify the system instead?
+ # instances of a metaclass only get executed once per client process,
+ # while instances of normal classes get run every time
+ def metaclass
+ return self.class.metaclass
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+ # Meta-parameter methods: These methods deal with the results
+ # of specifying metaparameters
+ #---------------------------------------------------------------
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this just marks states that we definitely want to retrieve values
+ # on
+ def metacheck(args)
+ unless args.is_a?(Array)
+ args = [args]
+ end
+
+ # these are states that we might not have values for but we want to retrieve
+ # values for anyway
+ args.each { |state|
+ unless state.is_a?(Symbol)
+ state = state.intern
+ end
+ next if @states.include?(state)
+
+ stateklass = nil
+ unless stateklass = self.class.validstate(state)
+ raise "%s is not a valid state for %s" % [state,self.class]
+ end
+
+ # XXX it's probably a bad idea to have code this important in
+ # two places
+ @states[state] = stateklass.new(
+ :parent => self
+ )
+ #@states[state] = stateklass.new()
+ #@states[state].parent = self
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def subscribe(hash)
+ if hash[:event] == '*'
+ hash[:event] = :ALL_EVENTS
+ end
+
+ hash[:source] = self
+ sub = Blink::Event::Subscription.new(hash)
+
+ # add to the correct area
+ @subscriptions.push sub
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # return all of the subscriptions to a given event
+ def subscribers?(event)
+ @subscriptions.find_all { |sub|
+ sub.event == event.event or
+ sub.event == :ALL_EVENTS
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # for each object we require, subscribe to all events that it
+ # generates
+ # we might reduce the level of subscription eventually, but for now...
+ def metarequire(requires)
+ unless requires.is_a?(Array)
+ requires = [requires]
+ end
+ requires.each { |rname|
+ # we just have a name and a type, and we need to convert it
+ # to an object...
+ type = nil
+ object = nil
+ tname = rname[0]
+ unless type = Blink::Type.type(tname)
+ raise "Could not find type %s" % tname
+ end
+ name = rname[1]
+ unless object = type[name]
+ raise "Could not retrieve object '%s' of type '%s'" %
+ [name,type]
+ end
+ Blink.debug("%s requires %s" % [self.name,object])
+
+ # for now, we only support this one method, 'refresh'
+ object.subscribe(
+ :event => '*',
+ :target => self,
+ :method => :refresh
+ )
+ #object.addnotify(self)
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def metaonerror(response)
+ Blink.debug("Would have called metaonerror")
+ @onerror = response
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def metaschedule(schedule)
+ @schedule = schedule
+ end
+ #---------------------------------------------------------------
+end # Blink::Type
+end
+
+require 'blink/type/service'
+require 'blink/type/file'
+require 'blink/type/symlink'
+require 'blink/type/package'
+require 'blink/type/component'
+require 'blink/statechange'
+#require 'blink/type/typegen'
+#require 'blink/type/typegen/filetype'
+#require 'blink/type/typegen/filerecord'
diff --git a/lib/puppet/type/component.rb b/lib/puppet/type/component.rb
new file mode 100644
index 000000000..38bf3326d
--- /dev/null
+++ b/lib/puppet/type/component.rb
@@ -0,0 +1,74 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# the object allowing us to build complex structures
+# this thing contains everything else, including itself
+
+require 'blink'
+require 'blink/type'
+require 'blink/transaction'
+
+module Blink
+ class Component < Blink::Type
+ include Enumerable
+
+ @name = :component
+ @namevar = :name
+
+ @states = []
+ @parameters = [:name,:type]
+
+ def each
+ @children.each { |child| yield child }
+ end
+
+ def initialize(args)
+ @children = []
+ super(args)
+ Blink.verbose "Made component with name %s" % self.name
+ end
+
+ # now we decide whether a transaction is dumb, and just accepts
+ # changes from the container, or whether it collects them itself
+ # for now, because i've already got this implemented, let transactions
+ # collect the changes themselves
+ def evaluate
+ transaction = Blink::Transaction.new(@children)
+ transaction.component = self
+ return transaction
+ end
+
+ def push(*ary)
+ ary.each { |child|
+ unless child.is_a?(Blink::Element)
+ Blink.notice "Got object of type %s" % child.class
+ raise "Containers can only contain Blink::Elements"
+ end
+ @children.push child
+ }
+ end
+
+ def name
+ return "%s[%s]" % [@parameters[:type],@parameters[:name]]
+ end
+
+ def refresh
+ @children.collect { |child|
+ if child.respond_to?(:refresh)
+ child.refresh
+ end
+ }
+ end
+
+ def retrieve
+ self.collect { |child|
+ child.retrieve
+ }
+ end
+
+ def to_s
+ return "component(%s)" % self.name
+ end
+ end
+end
diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb
new file mode 100644
index 000000000..9d0f98c61
--- /dev/null
+++ b/lib/puppet/type/file.rb
@@ -0,0 +1,450 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'digest/md5'
+require 'etc'
+require 'blink/type/state'
+
+module Blink
+ # we first define all of the state that our file will use
+ # because the objects must be defined for us to use them in our
+ # definition of the file object
+ class State
+ class FileCreate < Blink::State
+ require 'etc'
+ attr_accessor :file
+ @name = :create
+ @event = :file_created
+
+ def should=(value)
+ # default to just about anything meaning 'true'
+ if value == false or value.nil?
+ @should = false
+ else
+ @should = true
+ end
+ end
+
+ def retrieve
+ stat = nil
+
+ self.is = FileTest.exist?(self.parent[:path])
+ Blink.debug "'exists' state is %s" % self.is
+ end
+
+
+ def sync
+ begin
+ File.open(self.parent[:path],"w") { # just create an empty file
+ }
+ rescue => detail
+ raise detail
+ end
+ return :file_created
+ end
+ end
+
+ class FileChecksum < Blink::State
+ @name = :checksum
+ @event = :file_modified
+
+ def should=(value)
+ @checktype = value
+ state = Blink::Storage.state(self)
+ if hash = state[self.parent[:path]]
+ if hash.include?(@checktype)
+ @should = hash[@checktype]
+ else
+ Blink.verbose "Found checksum for %s but not of type %s" %
+ [self.parent[:path],@checktype]
+ @should = nil
+ end
+ else
+ Blink.debug "No checksum for %s" % self.parent[:path]
+ end
+ end
+
+ def retrieve
+ unless defined? @checktype
+ @checktype = "md5"
+ end
+
+ sum = ""
+ case @checktype
+ when "md5":
+ File.open(self.parent[:path]) { |file|
+ sum = Digest::MD5.hexdigest(file.read)
+ }
+ when "md5lite":
+ File.open(self.parent[:path]) { |file|
+ sum = Digest::MD5.hexdigest(file.read(512))
+ }
+ when "timestamp","mtime":
+ sum = File.stat(self.parent[:path]).mtime
+ when "time":
+ sum = File.stat(self.parent[:path]).ctime
+ end
+
+ self.is = sum
+
+ Blink.debug "checksum state is %s" % self.is
+ end
+
+
+ # at this point, we don't actually modify the system, we just kick
+ # off an event if we detect a change
+ def sync
+ if self.updatesum
+ return :file_modified
+ else
+ return nil
+ end
+ end
+
+ def updatesum
+ state = Blink::Storage.state(self)
+ unless state.include?(self.parent[:path])
+ state[self.parent[:path]] = Hash.new
+ end
+ # if we're replacing, vs. updating
+ if state[self.parent[:path]].include?(@checktype)
+ Blink.debug "Replacing checksum %s with %s" %
+ [state[self.parent[:path]][@checktype],@is]
+ result = true
+ else
+ Blink.verbose "Creating checksum %s for %s of type %s" %
+ [@is,self.parent[:path],@checktype]
+ result = false
+ end
+ state[self.parent[:path]][@checktype] = @is
+ return result
+ end
+ end
+
+ class FileUID < Blink::State
+ require 'etc'
+ attr_accessor :file
+ @name = :owner
+ @event = :inode_changed
+
+ def retrieve
+ stat = self.parent.stat(true)
+
+ self.is = stat.uid
+ if defined? @should
+ unless @should.is_a?(Integer)
+ begin
+ user = Etc.getpwnam(@should)
+ if user.gid == ""
+ raise "Could not retrieve uid for '%s'" % self.parent
+ end
+ Blink.debug "converting %s to integer '%d'" %
+ [@should,user.uid]
+ @should = user.uid
+ rescue
+ raise "Could not get any info on user '%s'" % @should
+ end
+ end
+ end
+
+ Blink.debug "chown state is %d" % self.is
+ end
+
+ def sync
+ if @is == -1
+ self.parent.stat(true)
+ self.retrieve
+ Blink.notice "%s: after refresh, is '%s'" % [self.class.name,@is]
+ end
+
+ unless self.parent.stat
+ Blink.error "File '%s' does not exist; cannot chown" %
+ self.parent[:path]
+ end
+
+ begin
+ File.chown(self.should,-1,self.parent[:path])
+ rescue => detail
+ raise "failed to chown '%s' to '%s': %s" %
+ [self.parent[:path],self.should,detail]
+ end
+
+ return :inode_changed
+ end
+ end
+
+ # this state should actually somehow turn into many states,
+ # one for each bit in the mode
+ # I think MetaStates are the answer, but I'm not quite sure
+ class FileMode < Blink::State
+ require 'etc'
+
+ @name = :mode
+ @event = :inode_changed
+
+ def should=(should)
+ # this is pretty hackish, but i need to make sure the number is in
+ # octal, yet the number can only be specified as a string right now
+ unless should.is_a?(Integer) # i've already converted it correctly
+ unless should =~ /^0/
+ should = "0" + should
+ end
+ should = Integer(should)
+ end
+ @should = should
+ end
+
+ def retrieve
+ stat = self.parent.stat(true)
+ self.is = stat.mode & 007777
+
+ Blink.debug "chmod state is %o" % self.is
+ end
+
+ def sync
+ if @is == -1
+ self.parent.stat(true)
+ self.retrieve
+ Blink.notice "%s: after refresh, is '%s'" % [self.class.name,@is]
+ end
+
+ unless self.parent.stat
+ Blink.error "File '%s' does not exist; cannot chmod" %
+ self.parent[:path]
+ return
+ end
+
+ begin
+ File.chmod(self.should,self.parent[:path])
+ rescue
+ raise "failed to chmod #{self.parent[:path]}: #{$!}"
+ end
+ return :inode_changed
+ end
+ end
+
+ # not used until I can figure out how to solve the problem with
+ # metastates
+ class FileSetUID < Blink::State
+ require 'etc'
+
+ @parent = Blink::State::FileMode
+
+ @name = :setuid
+ @event = :inode_changed
+
+ def <=>(other)
+ self.is <=> @parent.value[11]
+ end
+
+ # this just doesn't seem right...
+ def sync
+ unless defined? @is or @is == -1
+ self.parent.stat(true)
+ self.retrieve
+ Blink.notice "%s: should is '%s'" % [self.class.name,self.should]
+ end
+ tmp = 0
+ if self.is == true
+ tmp = 1
+ end
+ @parent.value[11] = tmp
+ return :inode_changed
+ end
+ end
+
+ class FileGroup < Blink::State
+ require 'etc'
+
+ @name = :group
+ @event = :inode_changed
+
+ def retrieve
+ stat = self.parent.stat(true)
+
+ self.is = stat.gid
+
+ # we probably shouldn't actually modify the 'should' value
+ # but i don't see a good way around it right now
+ # mmmm, should
+ if defined? @should
+ unless self.should.is_a?(Integer)
+ begin
+ require 'blink/fact'
+ group = Etc.getgrnam(self.should)
+ # apparently os x is six shades of weird
+ os = Blink::Fact["Operatingsystem"]
+
+ gid = ""
+ case os
+ when "Darwin":
+ gid = group.passwd
+ else
+ gid = group.gid
+ end
+ if gid == ""
+ raise "Could not retrieve gid for %s" % self.parent
+ end
+ Blink.debug "converting %s to integer %d" %
+ [self.should,gid]
+ self.should = gid
+ rescue
+ #raise "Could not get any info on group %s" % self.should
+ raise
+ end
+ end
+ end
+ Blink.debug "chgrp state is %d" % self.is
+ end
+
+ def sync
+ Blink.debug "setting chgrp state to %s" % self.should
+ if @is == -1
+ self.parent.stat(true)
+ self.retrieve
+ Blink.notice "%s: after refresh, is '%s'" % [self.class.name,@is]
+ end
+
+ unless self.parent.stat
+ Blink.error "File '%s' does not exist; cannot chgrp" %
+ self.parent[:path]
+ return
+ end
+
+ begin
+ # set owner to nil so it's ignored
+ File.chown(nil,self.should,self.parent[:path])
+ rescue
+ raise "failed to chgrp %s to %s: %s" %
+ [self.parent[:path], self.should, $!]
+ end
+ return :inode_changed
+ end
+ end
+ end
+ class Type
+ class File < Type
+ attr_reader :params
+ # class instance variable
+ @states = [
+ Blink::State::FileCreate,
+ Blink::State::FileUID,
+ Blink::State::FileGroup,
+ Blink::State::FileMode,
+ Blink::State::FileChecksum,
+ Blink::State::FileSetUID
+ ]
+
+ @parameters = [
+ :path,
+ :recurse
+ ]
+
+ @name = :file
+ @namevar = :path
+
+ # a wrapper method to make sure the file exists before doing anything
+ def retrieve
+ unless stat = self.stat(true)
+ Blink.verbose "File %s does not exist" % self[:path]
+ @states.each { |name,state|
+ state.is = -1
+ }
+ return
+ end
+ super
+ end
+
+ def stat(refresh = false)
+ if @stat.nil? or refresh == true
+ begin
+ @stat = ::File.stat(self[:path])
+ rescue => error
+ Blink.debug "Failed to stat %s: %s" %
+ [self[:path],error]
+ @stat = nil
+ end
+ end
+
+ return @stat
+ end
+
+ def initialize(hash)
+ arghash = hash.dup
+ super
+ @stat = nil
+
+ # if recursion is enabled and we're a directory...
+ if @parameters[:recurse] and self.stat.directory?
+ recurse = self[:recurse]
+ # we might have a string, rather than a number
+ if recurse.is_a?(String)
+ if recurse =~ /^[0-9]+$/
+ recurse = Integer(recurse)
+ elsif recurse =~ /^inf/ # infinite recursion
+ recurse = true
+ end
+ end
+
+ # unless we're at the end of the recursion
+ if recurse != 0
+ arghash.delete("recurse")
+ if recurse.is_a?(Integer)
+ recurse -= 1 # reduce the level of recursion
+ end
+
+ arghash[:recurse] = recurse
+
+ # now make each contained file/dir a child
+ unless defined? @children
+ @children = []
+ end
+
+ Dir.foreach(self[:path]) { |file|
+ next if file =~ /^\.\.?/ # skip . and ..
+
+ arghash[:path] = ::File.join(self[:path],file)
+
+ child = nil
+ # if the file already exists...
+ if child = self.class[arghash[:path]]
+ arghash.each { |var,value|
+ next if var == :path
+ child[var] = value
+ }
+ else # create it anew
+ child = self.class.new(arghash)
+ end
+ @children.push child
+ }
+ end
+ end
+ end
+ end # Blink::Type::File
+ end # Blink::Type
+
+ class FileSource
+ attr_accessor :name
+
+ @sources = Hash.new(nil)
+
+ def FileSource.[]=(name,sub)
+ @sources[name] = sub
+ end
+
+ def FileSource.[](name)
+ return @sources[name]
+ end
+
+ def initialize(name)
+ @name = name
+
+ if block_given?
+ yield self
+ end
+
+ FileSource[name] = self
+ end
+ end
+end
diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb
new file mode 100644
index 000000000..77bcd76b2
--- /dev/null
+++ b/lib/puppet/type/package.rb
@@ -0,0 +1,366 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'blink/type/state'
+require 'blink/fact'
+
+module Blink
+ class State
+ class PackageInstalled < Blink::State
+ @name = :install
+
+ def retrieve
+ #self.is = Blink::PackageTyping[@object.format][@object.name]
+ unless @parent.class.listed
+ @parent.class.getpkglist
+ end
+ Blink.debug "package install state is %s" % self.is
+ end
+
+ def sync
+ #begin
+ raise "cannot sync package states yet"
+ #rescue
+ # raise "failed to sync #{@params[:file]}: #{$!}"
+ #end
+
+ #return :package_installed
+ end
+ end
+ end
+
+ class Type
+ # packages are complicated because each package format has completely
+ # different commands. We need some way to convert specific packages
+ # into the general package object...
+ class Package < Type
+ attr_reader :version, :format
+ @states = [
+ Blink::State::PackageInstalled
+ ]
+ @parameters = [
+ :format,
+ :name,
+ :status,
+ :version,
+ :category,
+ :platform,
+ :root,
+ :vendor,
+ :description
+ ]
+
+ @name = :package
+ @namevar = :name
+ @listed = false
+
+ @allowedmethods = [:types]
+ @@types = nil
+
+ def Package.clear
+ @listed = false
+ super
+ end
+
+ def Package.listed
+ return @listed
+ end
+
+ def Package.types(array)
+ unless array.is_a?(Array)
+ array = [array]
+ end
+ @@types = array
+ Blink.warning "Types are %s" % array.join(" ")
+ end
+
+ def Package.getpkglist
+ if @@types.nil?
+ case Blink::Fact["operatingsystem"]
+ when "SunOS": @@types = ["sunpkg"]
+ when "Linux":
+ case Blink::Fact["distro"]
+ when "Debian": @@types = ["dpkg"]
+ else
+ raise "No default type for " + Blink::Fact["distro"]
+ end
+ else
+ raise "No default type for " + Blink::Fact["operatingsystem"]
+ end
+ end
+
+ list = @@types.collect { |type|
+ if typeobj = Blink::PackagingType[type]
+ # pull all of the objects
+ typeobj.list
+ else
+ raise "Could not find package type '%s'" % type
+ end
+ }.flatten
+ @listed = true
+ return list
+ end
+
+ def Package.installedpkg(hash)
+ # this is from code, so we don't have to do as much checking
+ name = hash[:name]
+
+ # if it already exists, modify the existing one
+ if object = Package[name]
+ states = {}
+ object.states.each { |state|
+ Blink.warning "Adding %s" % state.name.inspect
+ states[state.name] = state
+ }
+ hash.each { |var,value|
+ if states.include?(var)
+ Blink.verbose "%s is a set state" % var.inspect
+ states[var].is = value
+ else
+ Blink.verbose "%s is not a set state" % var.inspect
+ if object[var] and object[var] != value
+ Blink.warning "Overriding %s => %s on %s with %s" %
+ [var,object[var],name,value]
+ end
+
+ object[var] = value
+
+ # swap the values if we're a state
+ if states.include?(var)
+ Blink.verbose "Swapping %s because it's a state" % var
+ states[var].is = value
+ states[var].should = nil
+ else
+ Blink.verbose "%s is not a state" % var.inspect
+ Blink.verbose "States are %s" % states.keys.collect { |st|
+ st.inspect
+ }.join(" ")
+ end
+ end
+ }
+ return object
+ else # just create it
+ return self.new(hash)
+ end
+ end
+
+ # okay, there are two ways that a package could be created...
+ # either through the language, in which case the hash's values should
+ # be set in 'should', or through comparing against the system, in which
+ # case the hash's values should be set in 'is'
+ def initialize(hash)
+ super
+ end
+
+ end # Blink::Type::Package
+ end
+
+ class PackagingType
+ attr_writer :list, :install, :remove, :check
+
+ @@types = Hash.new(false)
+
+ def PackagingType.[](name)
+ if @@types.include?(name)
+ return @@types[name]
+ else
+ Blink.warning name.inspect
+ Blink.warning @@types.keys.collect { |key|
+ key.inspect
+ }.join(" ")
+ return nil
+ end
+ end
+
+ # whether a package is installed or not
+ def [](name)
+ return @packages[name]
+ end
+
+ [:list, :install, :remove, :check].each { |method|
+ self.send(:define_method, method) {
+ # retrieve the variable
+ var = eval("@" + method.id2name)
+ if var.is_a?(Proc)
+ var.call()
+ else
+ raise "only blocks are supported right now"
+ end
+ }
+ }
+
+ def initialize(name)
+ if block_given?
+ yield self
+ end
+
+ @packages = Hash.new(false)
+ @@types[name] = self
+ end
+
+ def retrieve
+ @packages.clear()
+
+ @packages = self.list()
+ end
+ end
+
+ PackagingType.new("dpkg") { |type|
+ type.list = proc {
+ packages = []
+
+ # dpkg only prints as many columns as you have available
+ # which means we don't get all of the info
+ # stupid stupid
+ oldcol = ENV["COLUMNS"]
+ ENV["COLUMNS"] = "500"
+
+ # list out all of the packages
+ open("| dpkg -l") { |process|
+ # our regex for matching dpkg output
+ regex = %r{^(\S+)\s+(\S+)\s+(\S+)\s+(.+)$}
+ fields = [:status, :name, :install, :description]
+ hash = {}
+
+ 5.times { process.gets } # throw away the header
+
+ # now turn each returned line into a package object
+ process.each { |line|
+ if match = regex.match(line)
+ hash.clear
+
+ fields.zip(match.captures) { |field,value|
+ hash[field] = value
+ }
+ packages.push Blink::Type::Package.installedpkg(hash)
+ else
+ raise "failed to match dpkg line %s" % line
+ end
+ }
+ }
+ ENV["COLUMNS"] = oldcol
+
+ return packages
+ }
+
+ # we need package retrieval mechanisms before we can have package
+ # installation mechanisms...
+ #type.install = proc { |pkg|
+ # raise "installation not implemented yet"
+ #}
+
+ type.remove = proc { |pkg|
+ cmd = "dpkg -r %s" % pkg.name
+ output = %x{#{cmd}}
+ if $? != 0
+ raise output
+ end
+ }
+ }
+
+ PackagingType.new("sunpkg") { |type|
+ type.list = proc {
+ packages = []
+ hash = {}
+ names = {
+ "PKGINST" => :name,
+ "NAME" => nil,
+ "CATEGORY" => :category,
+ "ARCH" => :platform,
+ "VERSION" => :install,
+ "BASEDIR" => :root,
+ "HOTLINE" => nil,
+ "EMAIL" => nil,
+ "VENDOR" => :vendor,
+ "DESC" => :description,
+ "PSTAMP" => nil,
+ "INSTDATE" => nil,
+ "STATUS" => nil,
+ "FILES" => nil
+ }
+
+ # list out all of the packages
+ open("| pkginfo -l") { |process|
+ # we're using the long listing, so each line is a separate piece
+ # of information
+ process.each { |line|
+ case line
+ when /^$/ then
+ packages.push Blink::Type::Package.installedpkg(hash)
+ hash.clear
+ when /\s*(\w+):\s+(.+)/
+ name = $1
+ value = $2
+ if names.include?(name)
+ hash[names[name]] = value
+ else
+ raise "Could not find %s" % name
+ end
+ when /\s+\d+.+/
+ # nothing; we're ignoring the FILES info
+ end
+ }
+ }
+ return packages
+ }
+
+ # we need package retrieval mechanisms before we can have package
+ # installation mechanisms...
+ #type.install = proc { |pkg|
+ # raise "installation not implemented yet"
+ #}
+
+ type.remove = proc { |pkg|
+ cmd = "pkgrm -n %s" % pkg.name
+ output = %x{#{cmd}}
+ if $? != 0
+ raise output
+ end
+ }
+ }
+
+ # this is how we retrieve packages
+ class PackageSource
+ attr_accessor :uri
+ attr_writer :retrieve
+
+ @@sources = Hash.new(false)
+
+ def PackageSource.get(file)
+ type = file.sub(%r{:.+},'')
+ source = nil
+ if source = @@sources[type]
+ return source.retrieve(file)
+ else
+ raise "Unknown package source: %s" % type
+ end
+ end
+
+ def initialize(name)
+ if block_given?
+ yield self
+ end
+
+ @@sources[name] = self
+ end
+
+ def retrieve(path)
+ @retrieve.call(path)
+ end
+
+ end
+
+ PackageSource.new("file") { |obj|
+ obj.retrieve = proc { |path|
+ # this might not work for windows...
+ file = path.sub(%r{^file://},'')
+
+ if FileTest.exists?(file)
+ return file
+ else
+ raise "File %s does not exist" % file
+ end
+ }
+ }
+end
diff --git a/lib/puppet/type/process.rb b/lib/puppet/type/process.rb
new file mode 100644
index 000000000..b42f75f26
--- /dev/null
+++ b/lib/puppet/type/process.rb
@@ -0,0 +1,83 @@
+#!/usr/local/bin/ruby -w
+
+# DISABLED
+# I'm only working on services, not processes, right now
+
+module Blink
+ class State
+ class ProcessRunning < State
+ def retrieve
+ running = 0
+ regex = Regexp.new(@params[:pattern])
+ begin
+ # this ps is only tested on Solaris
+ # XXX yeah, this definitely needs to be fixed...
+ %x{ps -ef -U #{@params[:user]}}.split("\n").each { |process|
+ if regex.match(process)
+ running += 1
+ end
+ }
+ rescue
+ # this isn't correct, but what the hell
+ Blink::Message.new(
+ :level => :error,
+ :source => self.parent,
+ :message => "Failed to run ps"
+ )
+ end
+
+ self.state = running
+ Blink.debug "there are #{running} #{self.parent} processes for start"
+ end
+
+ def <=>(other)
+ self.state < 1
+ end
+
+ def fix
+ require 'etc'
+ # ruby is really cool
+ uid = 0
+ if @params[:user].is_a? Integer
+ uid = @params[:user]
+ else
+ uid = Etc.getpwnam(@params[:user]).uid
+ end
+ Kernel.fork {
+ Process.uid = uid
+ Process.euid = uid
+ string = @params[:binary] + (@params[:arguments] || "")
+ Blink::Message.new(
+ :level => :notice,
+ :source => self.parent,
+ :message => "starting"
+ )
+ Kernel.exec(string)
+ }
+ end
+ end
+ end
+ class Type
+ class BProcess < Type
+ attr_reader :stat, :path
+ @parameters = [:start, :stop, :user, :pattern, :binary, :arguments]
+ @name = :process
+
+ @namevar = :pattern
+
+ Blink::Relation.new(self, Blink::Operation::Start, {
+ :user => :user,
+ :pattern => :pattern,
+ :binary => :binary,
+ :arguments => :arguments
+ })
+
+ Blink::Relation.new(self, Blink::Operation::Stop, {
+ :user => :user,
+ :pattern => :pattern
+ })
+
+ end # Blink::Type::BProcess
+ end # Blink::Type
+
+end
diff --git a/lib/puppet/type/service.rb b/lib/puppet/type/service.rb
new file mode 100644
index 000000000..f17f17e3f
--- /dev/null
+++ b/lib/puppet/type/service.rb
@@ -0,0 +1,186 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# this is our main way of managing processes right now
+#
+# a service is distinct from a process in that services
+# can only be managed through the interface of an init script
+# which is why they have a search path for initscripts and such
+
+module Blink
+ class State
+ class ServiceRunning < State
+ @name = :running
+ #@event = :file_created
+
+ # this whole thing is annoying
+ # i should probably just be using booleans, but for now, i'm not...
+ def should=(should)
+ case should
+ when false,0,"0":
+ should = 0
+ when true,1,"1":
+ should = 1
+ else
+ Blink.warning "%s: interpreting '%s' as false" %
+ [self.class,should]
+ should = 0
+ end
+ @should = should
+ end
+
+ def retrieve
+ self.is = self.running()
+ Blink.debug "Running value for '%s' is '%s'" %
+ [self.parent.name,self.is]
+ end
+
+ # should i cache this info?
+ def running
+ begin
+ status = self.parent.initcmd("status")
+ Blink.debug "initcmd status for '%s' is '%s'" %
+ [self.parent.name,status]
+
+ if status # the command succeeded
+ return 1
+ else
+ return 0
+ end
+ rescue SystemCallError
+ raise "Could not execute %s" % initscript
+ end
+
+ end
+
+ def sync
+ if self.running > 0
+ status = 1
+ else
+ status = 0
+ end
+ Blink.debug "'%s' status is '%s' and should be '%s'" %
+ [self,status,should]
+ event = nil
+ if self.should > 0
+ if status < 1
+ Blink.debug "Starting '%s'" % self
+ if self.parent.initcmd("start")
+ event = :service_started
+ else
+ raise "Failed to start '%s'" % self.parent.name
+ end
+ else
+ Blink.debug "'%s' is already running, yo" % self
+ #Blink.debug "Starting '%s'" % self
+ #unless self.parent.initcmd("start")
+ # raise "Failed to start %s" % self.name
+ #end
+ end
+ elsif status > 0
+ Blink.debug "Stopping '%s'" % self
+ if self.parent.initcmd("stop")
+ event = :service_stopped
+ else
+ raise "Failed to stop %s" % self.name
+ end
+ else
+ Blink.debug "Not running '%s' and shouldn't be running" % self
+ end
+
+ return event
+ end
+ end
+ end
+ class Type
+ class Service < Type
+ attr_reader :stat
+ @states = [
+ Blink::State::ServiceRunning
+ ]
+ @parameters = [
+ :name,
+ :pattern
+ ]
+
+ @functions = [
+ :setpath
+ ]
+
+ @name = :service
+ @namevar = :name
+
+ @searchpaths = Array.new
+ @allowedmethods = [:setpath]
+
+ def Service.search(name)
+ @searchpaths.each { |path|
+ # must specify that we want the top-level File, not Blink::...::File
+ fqname = ::File.join(path,name)
+ begin
+ stat = ::File.stat(fqname)
+ rescue
+ # should probably rescue specific errors...
+ Blink.debug("Could not find %s in %s" % [name,path])
+ next
+ end
+
+ # if we've gotten this far, we found a valid script
+ return fqname
+ }
+ raise "Could not find init script for '%s'" % name
+ end
+
+ def Service.setpath(ary)
+ # verify each of the paths exists
+ #ary.flatten!
+ @searchpaths = ary.find_all { |dir|
+ retvalue = false
+ begin
+ retvalue = ::File.stat(dir).directory?
+ rescue => detail
+ Blink.verbose("Directory %s does not exist: %s" % [dir,detail])
+ # just ignore it
+ end
+ # disallow relative paths
+ #if dir !~ /^\//
+ # retvalue = false
+ #end
+ retvalue
+ }
+ end
+
+ # it'd be nice if i didn't throw the output away...
+ # this command returns true if the exit code is 0, and returns
+ # false otherwise
+ def initcmd(cmd)
+ script = self.initscript
+
+ Blink.debug "Executing '%s %s' as initcmd for '%s'" %
+ [script,cmd,self]
+
+ rvalue = Kernel.system("%s %s" %
+ [script,cmd])
+
+ Blink.debug "'%s' ran with exit status '%s'" %
+ [cmd,rvalue]
+
+
+ rvalue
+ end
+
+ def initscript
+ if defined? @initscript
+ return @initscript
+ else
+ @initscript = Service.search(self.name)
+ end
+ end
+
+ def refresh
+ self.initcmd("restart")
+ end
+ end # Blink::Type::Service
+ end # Blink::Type
+end
diff --git a/lib/puppet/type/state.rb b/lib/puppet/type/state.rb
new file mode 100644
index 000000000..4706b04b7
--- /dev/null
+++ b/lib/puppet/type/state.rb
@@ -0,0 +1,135 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'blink'
+require 'blink/element'
+require 'blink/statechange'
+
+#---------------------------------------------------------------
+# this is a virtual base class for states
+# states are self-contained building blocks for objects
+
+# States can currently only be used for comparing a virtual "should" value
+# against the real state of the system. For instance, you could verify that
+# a file's owner is what you want, but you could not create two file objects
+# and use these methods to verify that they have the same owner
+module Blink
+class State < Blink::Element
+ attr_accessor :is, :should, :parent
+
+ @virtual = true
+
+ #---------------------------------------------------------------
+ # every state class must tell us what its name will be (as a symbol)
+ # this determines how we will refer to the state during usage
+ # e.g., the Owner state for Files might say its name is :owner;
+ # this means that we can say "file[:owner] = 'yayness'"
+ def State.name
+ return @name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # which event gets generated if this state change happens
+ def State.generates
+ return @event
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # if we're not in sync, return a statechange capable of putting us
+ # in sync
+ def evaluate
+ Blink.verbose "evaluating %s" % self
+ self.retrieve
+ if self.insync?
+ Blink.verbose "%s is in sync" % self
+ return nil
+ else
+ return Blink::StateChange.new(self)
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # return the full path to us, for logging and rollback
+ def fqpath
+ return @parent.fqpath, self.name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # we aren't actually comparing the states themselves, we're only
+ # comparing the "should" value with the "is" value
+ def insync?
+ Blink.debug "%s value is '%s', should be '%s'" %
+ [self,self.is.inspect,self.should.inspect]
+ self.is == self.should
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def initialize(hash)
+ @is = nil
+
+ unless hash.include?(:parent)
+ raise "State %s was not passed a parent" % self
+ end
+ @parent = hash[:parent]
+
+ if hash.include?(:should)
+ self.should = hash[:should]
+ else # we got passed no argument
+ # leave @should undefined
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # for testing whether we should actually do anything
+ def noop
+ unless defined? @noop
+ @noop = false
+ end
+ tmp = @noop || self.parent.noop || Blink[:noop] || false
+ Blink.notice "noop is %s" % tmp
+ return tmp
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #def refresh(transaction)
+ # self.retrieve
+
+ # we definitely need some way to batch these refreshes, so a
+ # given object doesn't get refreshed multiple times in a single
+ # run
+ # @parent.refresh
+ #end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # each state class must define the name() method, and state instances
+ # do not change that name
+ # this implicitly means that a given object can only have one state
+ # instance of a given state class
+ def name
+ return self.class.name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # retrieve the current state from the running system
+ def retrieve
+ raise "'retrieve' method was not overridden by %s" % self.class
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def to_s
+ return "%s(%s)" % [@parent.name,self.name]
+ end
+ #---------------------------------------------------------------
+end
+end
diff --git a/lib/puppet/type/symlink.rb b/lib/puppet/type/symlink.rb
new file mode 100644
index 000000000..772b5a831
--- /dev/null
+++ b/lib/puppet/type/symlink.rb
@@ -0,0 +1,109 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+require 'etc'
+require 'blink/type/state'
+require 'blink/type/file'
+
+module Blink
+ # okay, how do we deal with parameters that don't have operations
+ # associated with them?
+ class State
+ class SymlinkTarget < Blink::State
+ require 'etc'
+ attr_accessor :file
+
+ @name = :target
+
+ def create
+ begin
+ Blink.debug("Creating symlink '%s' to '%s'" %
+ [self.parent[:path],self.should])
+ unless File.symlink(self.should,self.parent[:path])
+ raise TypeError.new("Could not create symlink '%s'" %
+ self.parent[:path])
+ end
+ rescue => detail
+ raise TypeError.new("Cannot create symlink '%s': %s" %
+ [self.parent[:path],detail])
+ end
+ end
+
+ def remove
+ if FileTest.symlink?(self.parent[:path])
+ Blink.debug("Removing symlink '%s'" % self.parent[:path])
+ begin
+ File.unlink(self.parent[:path])
+ rescue
+ raise TypeError.new("Failed to remove symlink '%s'" %
+ self.parent[:path])
+ end
+ elsif FileTest.exists?(self.parent[:path])
+ raise TypeError.new("Cannot remove normal file '%s'" %
+ self.parent[:path])
+ else
+ Blink.debug("Symlink '%s' does not exist" %
+ self.parent[:path])
+ end
+ end
+
+ def retrieve
+ stat = nil
+
+ if FileTest.symlink?(self.parent[:path])
+ self.is = File.readlink(self.parent[:path])
+ Blink.debug("link value is '%s'" % self.is)
+ return
+ else
+ self.is = nil
+ return
+ end
+ end
+
+ # this is somewhat complicated, because it could exist and be
+ # a file
+ def sync
+ if self.should.nil?
+ self.remove()
+ else # it should exist and be a symlink
+ if FileTest.symlink?(self.parent[:path])
+ path = File.readlink(self.parent[:path])
+ if path != self.should
+ self.remove()
+ self.create()
+ end
+ elsif FileTest.exists?(self.parent[:path])
+ raise TypeError.new("Cannot replace normal file '%s'" %
+ self.parent[:path])
+ else
+ self.create()
+ end
+ end
+
+ #self.parent.newevent(:event => :inode_changed)
+ end
+ end
+ end
+
+ class Type
+ class Symlink < Type
+ attr_reader :stat, :path, :params
+ # class instance variable
+ @states = [
+ Blink::State::FileUID,
+ Blink::State::FileGroup,
+ Blink::State::FileMode,
+ Blink::State::SymlinkTarget
+ ]
+
+ @parameters = [
+ :path
+ ]
+
+ @name = :symlink
+ @namevar = :path
+ end # Blink::Type::Symlink
+ end # Blink::Type
+
+end
diff --git a/lib/puppet/type/typegen.rb b/lib/puppet/type/typegen.rb
new file mode 100644
index 000000000..85f04912c
--- /dev/null
+++ b/lib/puppet/type/typegen.rb
@@ -0,0 +1,146 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# parse and write configuration files using objects with minimal parsing abilities
+
+require 'etc'
+require 'blink/type'
+
+module Blink
+ class Type
+class TypeGenerator < Blink::Type
+ include Enumerable
+
+ @namevar = :name
+ @name = :typegen
+ @abstract = true
+
+ @parameters = [:name]
+ @states = []
+
+ #---------------------------------------------------------------
+ def TypeGenerator.[](name)
+ return @subclasses[name]
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def TypeGenerator.inherited(subclass)
+ #subclass.initvars
+ super(subclass)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # we don't need to 'super' here because type.rb already runs initvars
+ # in Type#inherited
+ def TypeGenerator.initvars
+ @subclasses = Hash.new(nil)
+ super
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def TypeGenerator.name
+ return @name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def TypeGenerator.name=(name)
+ @name = name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def TypeGenerator.namevar
+ return @namevar || :name
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def TypeGenerator.namevar=(namevar)
+ Blink.debug "Setting namevar for %s to %s" % [self,namevar]
+ unless namevar.is_a? Symbol
+ namevar = namevar.intern
+ end
+ @namevar = namevar
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def TypeGenerator.newtype(arghash)
+ unless defined? @parameters
+ raise "Type %s is set up incorrectly" % self
+ end
+
+ arghash.each { |key,value|
+ if key.class != Symbol
+ # convert to a symbol
+ arghash[key.intern] = value
+ arghash.delete key
+ key = key.intern
+ end
+ unless @parameters.include?(key)
+ raise "Invalid argument %s on class %s" %
+ [key,self]
+ end
+
+ }
+
+ # turn off automatically checking all arguments
+ #@parameters.each { |option|
+ # unless arghash.include?(option)
+ # p arghash
+ # raise "Must pass %s to class %s" %
+ # [option,self]
+ # end
+ #}
+
+ if @subclasses.include?(arghash[:name])
+ raise "File type %s already exists" % arghash[:name]
+ end
+
+ klassname = arghash[:name].capitalize
+
+ # create the file type
+ Blink::Type.module_eval "
+ class %s < %s
+ end" % [klassname,self]
+ klass = eval(klassname)
+ klass.name = arghash[:name]
+
+ @subclasses[arghash[:name]] = klass
+
+ arghash.each { |option,value|
+ method = option.id2name + "="
+ if klass.respond_to?(method)
+ #Blink.debug "Setting %s on %s to '%s'" % [option,klass,arghash[option]]
+ klass.send(method,arghash[option])
+ else
+ Blink.debug "%s does not respond to %s" % [klass,method]
+ end
+ }
+
+ # i couldn't get the method definition stuff to work
+ # oh well
+ # probably wouldn't want it in the end anyway
+ #@parameters.each { |option|
+ # writer = option.id2name + "="
+ # readproc = proc { eval("@" + option.id2name) }
+ # klass.send(:define_method,option,readproc)
+ # writeproc = proc { |value| module_eval("@" + option.id2name) = value }
+ # klass.send(:define_method,writer,writeproc)
+ # klass.send(writer,hash[option])
+ #}
+
+ #Blink::Type.inherited(klass)
+ Blink::Type.buildtypehash
+ return klass
+ end
+ #---------------------------------------------------------------
+end
+#---------------------------------------------------------------
+end
+end
diff --git a/lib/puppet/type/typegen/filerecord.rb b/lib/puppet/type/typegen/filerecord.rb
new file mode 100644
index 000000000..fb9030c12
--- /dev/null
+++ b/lib/puppet/type/typegen/filerecord.rb
@@ -0,0 +1,243 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# parse and write configuration files using objects with minimal parsing abilities
+
+require 'etc'
+require 'blink/type'
+require 'blink/type/typegen'
+
+#---------------------------------------------------------------
+class Blink::Type::FileRecord < Blink::Type::TypeGenerator
+ attr_accessor :fields, :namevar, :splitchar, :object
+
+ @parameters = [:name, :splitchar, :fields, :namevar, :filetype, :regex, :joinchar]
+ @abstract = true
+ @metaclass = true
+
+ @namevar = :name
+ @name = :filerecord
+
+ #---------------------------------------------------------------
+ def FileRecord.newtype(hash)
+ #shortname = hash[:name]
+ #hash[:name] = hash[:filetype].name.capitalize + hash[:name].capitalize
+ klass = super(hash)
+ #klass.name = shortname
+ klass.parameters = hash[:fields]
+ #klass.namevar = hash[:namevar]
+ klass.filetype = hash[:filetype]
+ hash.delete(:fields)
+ hash.delete(:namevar)
+ return klass
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.fields=(ary)
+ @fields = ary
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.fields
+ return @fields
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.filetype
+ @filetype
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.filetype=(filetype)
+ if filetype.is_a?(String)
+ @filetype = Blink::Type::FileType[filetype]
+ elsif filetype.is_a?(Blink::Type::FileType)
+ @filetype = filetype
+ else
+ raise "Cannot use objects of type %s as filetypes" % filetype
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.joinchar=(char)
+ @joinchar = char
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.joinchar
+ unless defined? @joinchar
+ @joinchar = nil
+ end
+ @joinchar
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.match(object,line)
+ matchobj = nil
+ begin
+ matchobj = self.regex.match(line)
+ rescue RegexpError => detail
+ raise
+ end
+
+ if matchobj.nil?
+ return nil
+ else
+ child = self.new(object)
+ child.match = matchobj
+ return child
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.regex=(regex)
+ @regex = regex
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.regex
+ # the only time @regex is allowed to be nil is if @splitchar is defined
+ if @regex.nil?
+ if @splitchar.nil?
+ raise "%s defined incorrectly -- splitchar or regex must be specified" %
+ self
+ else
+ ary = []
+ text = @fields.collect { |field|
+ "([^%s]*)" % @splitchar
+ }.join(@splitchar)
+ begin
+ @regex = Regexp.new(text)
+ rescue RegexpError => detail
+ raise "Could not create splitregex from %s" % @splitchar
+ end
+ Blink.debug("Created regexp %s" % @regex)
+ end
+ elsif @regex.is_a?(String)
+ begin
+ @regex = Regexp.new(@regex)
+ rescue RegexpError => detail
+ raise "Could not create splitregex from %s" % @regex
+ end
+ end
+ return @regex
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.splitchar=(char)
+ @splitchar = char
+ #@regex = %r{#{char}}
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileRecord.splitchar
+ return @splitchar
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #def [](field)
+ # @parameters[field]
+ #end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #def []=(field,value)
+ # @parameters[field] = value
+ #end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def ==(other)
+ unless self.class == other.class
+ return false
+ end
+
+ unless self.name == other.name
+ return false
+ end
+ @parameters.keys { |field|
+ unless self[field] == other[field]
+ Blink.debug("%s -> %s has changed" % [self.name, field])
+ return false
+ end
+ }
+ return true
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def initialize(hash)
+ if self.class == Blink::Type::FileRecord
+ self.class.newtype(hash)
+ return
+ end
+ @parameters = {}
+ #if block_given?
+ # yield self
+ #end
+ super(hash)
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def match=(matchobj)
+ @match = matchobj
+ #puts "captures are [%s]" % [matchobj.captures]
+ self.class.fields.zip(matchobj.captures) { |field,value|
+ @parameters[field] = value
+ #puts "%s => %s" % [field,@parameters[field]]
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def record=(record)
+ begin
+ ary = record.split(self.class.regex)
+ rescue RegexpError=> detail
+ raise RegexpError.new(detail)
+ end
+ self.class.fields.each { |field|
+ @parameters[field] = ary.shift
+ #puts "%s => %s" % [field,@parameters[field]]
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def name
+ if @parameters.include?(self.class.namevar)
+ return @parameters[self.class.namevar]
+ else
+ raise "No namevar '%s' for objects of type %s" %
+ [self.class.namevar,self.class.to_s]
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def to_s
+ ary = self.class.fields.collect { |field|
+ if ! @parameters.include?(field)
+ raise "Object %s is missing field %s" % [self.name,field]
+ else
+ @parameters[field]
+ end
+ }.join(self.class.joinchar || self.class.splitchar)
+ end
+ #---------------------------------------------------------------
+end
+#---------------------------------------------------------------
diff --git a/lib/puppet/type/typegen/filetype.rb b/lib/puppet/type/typegen/filetype.rb
new file mode 100644
index 000000000..4b3b89db8
--- /dev/null
+++ b/lib/puppet/type/typegen/filetype.rb
@@ -0,0 +1,316 @@
+#!/usr/local/bin/ruby -w
+
+# $Id$
+
+# parse and write configuration files using objects with minimal parsing abilities
+
+require 'blink/type'
+require 'blink/type/typegen'
+
+class Blink::Type::FileType < Blink::Type::TypeGenerator
+ attr_accessor :childtype
+
+ @parameters = [:name, :linesplit, :escapednewlines]
+ #@abstract = true
+ @metaclass = true
+
+ @namevar = :name
+ @name = :filetype
+
+ @modsystem = true
+
+ #---------------------------------------------------------------
+ def FileType.newtype(hash)
+ unless hash.include?(:linesplit)
+ hash[:linesplit] = "\n"
+ end
+
+ # i don't think there's any reason to 'super' this
+ #klass = Blink::Type::TypeGenerator.newtype(hash)
+ klass = super(hash)
+
+ klass.escapednewlines = true
+ klass.namevar = :name
+ klass.parameters = [:name, :path, :complete]
+
+ #klass.childtype = Blink::Type::FileRecord.newtype(
+ # :name => hash[:name] + "_record",
+ # :splitchar => hash[:recordsplit],
+ # :fields => hash[:fields],
+ # :namevar => hash[:namevar]
+ #)
+ #klass.addrecord(
+ # :name => hash[:name] + "_record",
+ # :splitchar => hash[:recordsplit],
+ # :fields => hash[:fields],
+ # :namevar => hash[:namevar]
+ #)
+
+ return klass
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # currently not used
+ def FileType.addrecord(hash)
+ unless defined? @records
+ @records = {}
+ end
+ hash[:filetype] = self
+
+ # default to the naming field being the first field provided
+ unless hash.include?(:namevar)
+ hash[:namevar] = hash[:fields][0]
+ end
+
+ recordtype = Blink::Type::FileRecord.newtype(hash)
+ @records[recordtype.name] = recordtype
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileType.records
+ return @records
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileType.escapednewlines=(value)
+ @escnlines = value
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileType.escapednewlines
+ if ! defined? @escnlines or @escnlines.nil?
+ return false
+ else
+ return @escnlines
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileType.childtype
+ unless defined? @childtype
+ @childtype = nil
+ end
+ return @childtype
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileType.childtype=(childtype)
+ @childtype = childtype
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileType.regex
+ return @regex
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileType.linesplit=(linesplit)
+ @regex = %r{#{linesplit}}
+ @linesplit = linesplit
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def FileType.linesplit
+ return @linesplit
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #def [](name)
+ # return @childhash[name]
+ #end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #def []=(name,value)
+ #end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # we don't really have a 'less-than/greater-than' sense here
+ # so i'm sticking with 'equals' until those make sense
+ def ==(other)
+ unless self.children.length == other.children.length
+ Blink.debug("file has %s records instead of %s" %
+ [self.children.length, other.children.length])
+ return self.children.length == other.children.length
+ end
+ equal = true
+ self.zip(other.children) { |schild,ochild|
+ unless schild == ochild
+ Blink.debug("%s has changed in %s" %
+ [schild.name,self.name])
+ equal = false
+ break
+ end
+ }
+
+ return equal
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # create a new record with a block
+ def add(type,&block)
+ obj = self.class.records[type].new(self,&block)
+ Blink.debug("adding %s" % obj.name)
+ @childary.push(obj)
+ @childhash[obj.name] = obj
+
+ return obj
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def children
+ return @childary
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # remove a record
+ def delete(name)
+ if @childhash.has_key?(name)
+ child = @childhash[name]
+
+ @childhash.delete(child)
+ @childary.delete(child)
+ else
+ raise "No such entry %s" % name
+ end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def each
+ @childary.each { |child|
+ yield child
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # create a new file
+ def initialize(hash)
+ # if we are the FileType object itself, we create a new type
+ # otherwise, we create an instance of an existing type
+ # yes, this should be more straightforward
+ if self.class == Blink::Type::FileType
+ self.class.newtype(hash)
+ return
+ end
+ Blink.debug "Creating new '%s' file with path '%s' and name '%s'" %
+ [self.class.name,hash["path"],hash[:name]]
+ Blink.debug hash.inspect
+ @file = hash["path"]
+
+ @childary = []
+ @childhash = {}
+ super
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # this is where we're pretty different from other objects
+ # we can choose to either reparse the existing file and compare
+ # the objects, or we can write our file out and do an
+ # text comparison
+ def insync?
+ tmp = self.class.new(@file)
+ tmp.retrieve
+
+ return self == tmp
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ #def name
+ # return @file
+ #end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ # read the whole file in and turn it into each of the appropriate
+ # objects
+ def retrieve
+ str = ""
+ ::File.open(@file) { |fname|
+ fname.each { |line|
+ str += line
+ }
+ }
+
+ if self.class.escapednewlines
+ endreg = %r{\\\n\s*}
+ str.gsub!(endreg,'')
+ end
+ @childary = str.split(self.class.regex).collect { |line|
+ childobj = nil
+ self.class.records.each { |name,recordtype|
+ if childobj = recordtype.match(self,line)
+ break
+ end
+ }
+ if childobj.nil?
+ Blink.warning("%s: could not match %s" % [self.name,line])
+ #Blink.warning("could not match %s" % line)
+ next
+ end
+
+ begin
+ Blink.debug("got child: %s(%s)" % [childobj.class,childobj.to_s])
+ rescue NoMethodError
+ Blink.warning "Failed: %s" % childobj
+ end
+ childobj
+ }.reject { |child|
+ child.nil?
+ }
+
+ @childary.each { |child|
+ begin
+ @childhash[child.name] = child
+ rescue NoMethodError => detail
+ p child
+ p child.class
+ puts detail
+ exit
+ end
+ }
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def sync
+ #unless self.insync?
+ self.write
+ #end
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def to_s
+ return @childary.collect { |child|
+ child.to_s
+ }.join(self.class.linesplit) + self.class.linesplit
+ end
+ #---------------------------------------------------------------
+
+ #---------------------------------------------------------------
+ def write
+ ::File.open(@file, "w") { |file|
+ file.write(self.to_s)
+ }
+ end
+ #---------------------------------------------------------------
+end
+#---------------------------------------------------------------