diff options
| author | Luke Kanies <luke@madstop.com> | 2005-06-27 21:44:46 +0000 |
|---|---|---|
| committer | Luke Kanies <luke@madstop.com> | 2005-06-27 21:44:46 +0000 |
| commit | 8f95084cd854aef4e3493854e58cefd352cdc68d (patch) | |
| tree | f31288d1cbbd60c0fdc7c04bbd6960516a6893be /lib/puppet | |
| parent | 6f074138779e558fd7017880f606dcf3527233f9 (diff) | |
| download | puppet-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.rb | 120 | ||||
| -rw-r--r-- | lib/puppet/element.rb | 41 | ||||
| -rw-r--r-- | lib/puppet/event.rb | 201 | ||||
| -rw-r--r-- | lib/puppet/fact.rb | 65 | ||||
| -rw-r--r-- | lib/puppet/function.rb | 72 | ||||
| -rw-r--r-- | lib/puppet/message.rb | 64 | ||||
| -rw-r--r-- | lib/puppet/selector.rb | 82 | ||||
| -rw-r--r-- | lib/puppet/statechange.rb | 111 | ||||
| -rw-r--r-- | lib/puppet/storage.rb | 48 | ||||
| -rw-r--r-- | lib/puppet/transaction.rb | 162 | ||||
| -rw-r--r-- | lib/puppet/transportable.rb | 200 | ||||
| -rw-r--r-- | lib/puppet/type.rb | 851 | ||||
| -rw-r--r-- | lib/puppet/type/component.rb | 74 | ||||
| -rw-r--r-- | lib/puppet/type/file.rb | 450 | ||||
| -rw-r--r-- | lib/puppet/type/package.rb | 366 | ||||
| -rw-r--r-- | lib/puppet/type/process.rb | 83 | ||||
| -rw-r--r-- | lib/puppet/type/service.rb | 186 | ||||
| -rw-r--r-- | lib/puppet/type/state.rb | 135 | ||||
| -rw-r--r-- | lib/puppet/type/symlink.rb | 109 | ||||
| -rw-r--r-- | lib/puppet/type/typegen.rb | 146 | ||||
| -rw-r--r-- | lib/puppet/type/typegen/filerecord.rb | 243 | ||||
| -rw-r--r-- | lib/puppet/type/typegen/filetype.rb | 316 |
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 +#--------------------------------------------------------------- |
