summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2006-05-18 20:41:54 +0000
committerluke <luke@980ebf18-57e1-0310-9a29-db15c13687c0>2006-05-18 20:41:54 +0000
commitb3ea53cd99df84a93fe2f093d2224e711f49e5dd (patch)
tree848e3b2bc03865709e4b6e38f1536ebec99ba8b0
parent93771b7935e544630c3416fda928a3820c615df2 (diff)
downloadpuppet-b3ea53cd99df84a93fe2f093d2224e711f49e5dd.tar.gz
puppet-b3ea53cd99df84a93fe2f093d2224e711f49e5dd.tar.xz
puppet-b3ea53cd99df84a93fe2f093d2224e711f49e5dd.zip
Adding a lot of structure to puppet.rb to make it easier to manage multiple objects in a single process, including making it easy to add threads. Added some testing for all of that.
Also added a "runner" server, meant to be started within puppetd, so that clients can have runs triggered from a central host git-svn-id: https://reductivelabs.com/svn/puppet/trunk@1212 980ebf18-57e1-0310-9a29-db15c13687c0
-rwxr-xr-xbin/puppetd55
-rwxr-xr-xbin/puppetmasterd25
-rw-r--r--lib/puppet.rb116
-rw-r--r--lib/puppet/client.rb18
-rw-r--r--lib/puppet/client/master.rb27
-rw-r--r--lib/puppet/client/runner.rb17
-rwxr-xr-xlib/puppet/server/runner.rb63
-rw-r--r--lib/puppet/transaction.rb9
-rw-r--r--lib/puppet/type/pfile.rb3
-rwxr-xr-xtest/other/puppet.rb93
-rwxr-xr-xtest/server/runner.rb75
11 files changed, 426 insertions, 75 deletions
diff --git a/bin/puppetd b/bin/puppetd
index 928ae5ba3..703bb8a21 100755
--- a/bin/puppetd
+++ b/bin/puppetd
@@ -139,6 +139,11 @@
# Copyright (c) 2005, 2006 Reductive Labs, LLC
# Licensed under the GNU Public License
+# Do an initial trap, so that cancels don't get a stack trace.
+trap(:INT) do
+ $stderr.puts "Cancelling startup"
+ exit(0)
+end
require 'puppet'
require 'puppet/server'
@@ -367,10 +372,6 @@ if options[:listen]
objects << server
end
-if options[:daemonize]
- client.daemonize
-end
-
# now set up the network client with the certs, now that we have them
client.setcerts
@@ -378,16 +379,8 @@ if options[:client]
objects << client
end
-
-[:INT, :TERM].each do |signal|
- trap(signal) do
- Puppet.notice "Caught #{signal}; shutting down"
- objects.each do |obj|
- obj.shutdown
- end
- exit(0)
- end
-end
+# Set traps for INT and TERM
+Puppet.settraps
if options[:onetime]
unless options[:client]
@@ -399,6 +392,9 @@ if options[:onetime]
Puppet.notice "Ignoring --listen on onetime run"
end
+ # Add the service, so the traps work correctly.
+ Puppet.newservice(client)
+
begin
client.run
rescue => detail
@@ -409,37 +405,24 @@ if options[:onetime]
end
exit(0)
else
- threads = []
if server
- threads << Thread.new do
- begin
- server.start
- rescue => detail
- Puppet.err "Could not start server: %s" % [detail]
- server.shutdown
- exit(1)
- end
- end
+ Puppet.newservice(server)
end
if options[:client]
Puppet.notice "Starting Puppet client version %s" % [Puppet.version]
- begin
- client.start
- rescue => detail
- puts detail.backtrace
- Puppet.err "Could not start client: %s" % [detail]
- client.shutdown
- exit(1)
- end
+ Puppet.newservice(client)
end
- threads << Thread.new do
- # Mmm, hackish
- Puppet.start
+ Puppet.settraps
+
+ # Daemonize as late as possible.
+ if options[:daemonize]
+ client.daemonize
end
- threads.each do |th| th.join end
+ Puppet.start
end
+
# $Id$
diff --git a/bin/puppetmasterd b/bin/puppetmasterd
index 82dd45b99..6d6077f70 100755
--- a/bin/puppetmasterd
+++ b/bin/puppetmasterd
@@ -69,6 +69,12 @@
# Copyright (c) 2005 Reductive Labs, LLC
# Licensed under the GNU Public License
+# Do an initial trap, so that cancels don't get a stack trace.
+trap(:INT) do
+ $stderr.puts "Cancelling startup"
+ exit(0)
+end
+
require 'getoptlong'
require 'puppet'
require 'puppet/server'
@@ -234,25 +240,14 @@ if Puppet[:parseonly]
exit(0)
end
+Puppet.newservice(server)
+Puppet.settraps
+
if options[:daemonize]
server.daemonize
end
-
-[:INT, :TERM].each do |signal|
- trap(signal) do
- Puppet.notice "Caught #{signal}; shutting down"
- server.shutdown
- end
-end
-
Puppet.notice "Starting Puppet server version %s" % [Puppet.version]
-
-begin
- server.start
-rescue => detail
- Puppet.err "Could not start puppetmaster: %s" % detail
- exit(1)
-end
+Puppet.start
# $Id$
diff --git a/lib/puppet.rb b/lib/puppet.rb
index b089fc96c..ffcadfa9e 100644
--- a/lib/puppet.rb
+++ b/lib/puppet.rb
@@ -20,6 +20,11 @@ module Puppet
return PUPPETVERSION
end
+ # So we can monitor signals and such.
+ class << self
+ include SignalObserver
+ end
+
class Error < RuntimeError
attr_accessor :stack, :line, :file
attr_writer :backtrace
@@ -282,11 +287,114 @@ module Puppet
end
end
- # Start our event loop. This blocks, waiting for someone, somewhere,
- # to generate events of some kind.
- def self.start
+ # Run all threads to their ends
+ def self.join
+ defined? @threads and @threads.each do |t| t.join end
+ end
+
+ # Create a new service that we're supposed to run
+ def self.newservice(service)
+ @services ||= []
+
+ @services << service
+ end
+
+ def self.newthread(&block)
+ @threads ||= []
+
+ @threads << Thread.new do
+ yield
+ end
+ end
+
+ def self.newtimer(hash, &block)
+ @timers ||= []
+ timer = EventLoop::Timer.new(hash)
+ @timers << timer
+
+ if block_given?
+ observe_signal(timer, :alarm, &block)
+ end
+
+ # In case they need it for something else.
+ timer
+ end
+
+ # Trap a couple of the main signals. This should probably be handled
+ # in a way that anyone else can register callbacks for traps, but, eh.
+ def self.settraps
+ [:INT, :TERM].each do |signal|
+ trap(signal) do
+ Puppet.notice "Caught #{signal}; shutting down"
+ Puppet.shutdown
+ end
+ end
+ end
+
+ # Shutdown our server process, meaning stop all services and all threads.
+ # Optionally, exit.
+ def self.shutdown(leave = true)
+ # Unmonitor our timers
+ defined? @timers and @timers.each do |timer|
+ EventLoop.current.ignore_timer timer
+ end
+
+ if EventLoop.current.running?
+ EventLoop.current.quit
+ end
+
+ # Stop our services
+ defined? @services and @services.each do |svc|
+ begin
+ timeout(20) do
+ svc.shutdown
+ end
+ rescue TimeoutError
+ Puppet.err "%s could not shut down within 20 seconds" % svc.class
+ end
+ end
+
+ # And wait for them all to die, giving a decent amount of time
+ defined? @threads and @threads.each do |thr|
+ begin
+ timeout(20) do
+ thr.join
+ end
+ rescue TimeoutError
+ # Just ignore this, since we can't intelligently provide a warning
+ end
+ end
+
+ if leave
+ exit(0)
+ end
+ end
+
+ # Start all of our services and optionally our event loop, which blocks,
+ # waiting for someone, somewhere, to generate events of some kind.
+ def self.start(block = true)
#Puppet.info "Starting loop"
- EventLoop.current.run
+ # Starting everything in its own thread, fwiw
+ defined? @timers and @timers.each do |timer|
+ EventLoop.current.monitor_timer timer
+ end
+
+ defined? @services and @services.each do |svc|
+ newthread do
+ begin
+ svc.start
+ rescue => detail
+ if Puppet[:debug]
+ puts detail.backtrace
+ end
+ Puppet.err "Could not start %s: %s" % [svc.class, detail]
+ end
+ end
+ end
+
+ if block
+ EventLoop.current.run
+ end
end
# Create the timer that our different objects (uh, mostly the client)
diff --git a/lib/puppet/client.rb b/lib/puppet/client.rb
index 7f6ed2592..f1884ac85 100644
--- a/lib/puppet/client.rb
+++ b/lib/puppet/client.rb
@@ -145,25 +145,19 @@ module Puppet
# Start listening for events. We're pretty much just listening for
# timer events here.
def start
- # Create our timer
- timer = EventLoop::Timer.new(
+ # Create our timer. Puppet will handle observing it and such.
+ timer = Puppet.newtimer(
:interval => Puppet[:runinterval],
:tolerance => 1,
:start? => true
- )
-
- # Stick it in the loop
- EventLoop.current.monitor_timer timer
-
- # Run once before we start following the timer
- self.runnow
-
- # And run indefinitely
- observe_signal timer, :alarm do
+ ) do
if self.scheduled?
self.runnow
end
end
+
+ # Run once before we start following the timer
+ self.runnow
end
require 'puppet/client/proxy'
diff --git a/lib/puppet/client/master.rb b/lib/puppet/client/master.rb
index b1da61405..9194b86be 100644
--- a/lib/puppet/client/master.rb
+++ b/lib/puppet/client/master.rb
@@ -25,6 +25,12 @@ class Puppet::Client::MasterClient < Puppet::Client
attr_accessor :objects
+ class << self
+ # Puppetd should only have one instance running, and we need a way
+ # to retrieve it.
+ attr_accessor :instance
+ end
+
def self.facts
facts = {}
Facter.each { |name,fact|
@@ -39,7 +45,7 @@ class Puppet::Client::MasterClient < Puppet::Client
end
# This method actually applies the configuration.
- def apply
+ def apply(tags = nil, ignoreschedules = false)
dostorage()
unless defined? @objects
raise Puppet::Error, "Cannot apply; objects not defined"
@@ -52,6 +58,14 @@ class Puppet::Client::MasterClient < Puppet::Client
transaction = @objects.evaluate
transaction.toplevel = true
+ if tags
+ transaction.tags = tags
+ end
+
+ if ignoreschedules
+ transaction.ignoreschedules = true
+ end
+
begin
transaction.evaluate
rescue Puppet::Error => detail
@@ -267,6 +281,13 @@ class Puppet::Client::MasterClient < Puppet::Client
return @objects
end
+ # Just so we can specify that we are "the" instance.
+ def initialize(*args)
+ super
+
+ self.class.instance = self
+ end
+
# Make sure only one client runs at a time, and make sure only one thread
# runs at a time. However, this does not lock local clients -- you could have
# as many separate puppet scripts running as you want.
@@ -308,7 +329,7 @@ class Puppet::Client::MasterClient < Puppet::Client
end
# The code that actually runs the configuration.
- def run
+ def run(tags = nil, ignoreschedules = false)
if pid = locked?
t = ""
if pid == true
@@ -325,7 +346,7 @@ class Puppet::Client::MasterClient < Puppet::Client
Puppet.notice "Starting configuration run"
end
benchmark(:notice, "Finished configuration run") do
- self.apply
+ self.apply(tags, ignoreschedules)
end
end
end
diff --git a/lib/puppet/client/runner.rb b/lib/puppet/client/runner.rb
new file mode 100644
index 000000000..9bedf2374
--- /dev/null
+++ b/lib/puppet/client/runner.rb
@@ -0,0 +1,17 @@
+class Puppet::Client::Runner < Puppet::Client::ProxyClient
+ @drivername = :Runner
+
+ # set up the appropriate interface methods
+ @handler = Puppet::Server::Runner
+ self.mkmethods
+
+ def initialize(hash = {})
+ if hash.include?(:Runner)
+ hash[:Runner] = Puppet::Server::Runner.new()
+ end
+
+ super(hash)
+ end
+end
+
+# $Id$
diff --git a/lib/puppet/server/runner.rb b/lib/puppet/server/runner.rb
new file mode 100755
index 000000000..d53b6c9b1
--- /dev/null
+++ b/lib/puppet/server/runner.rb
@@ -0,0 +1,63 @@
+module Puppet
+class Server
+ class MissingMasterError < RuntimeError # Cannot find the master client
+ end
+ # A simple server for triggering a new run on a Puppet client.
+ class Runner < Handler
+ @interface = XMLRPC::Service::Interface.new("puppetrunner") { |iface|
+ iface.add_method("string run(string, string)")
+ }
+
+ # Run the client configuration right now, optionally specifying
+ # tags and whether to ignore schedules
+ def run(tags = [], ignoreschedules = false, bg = true, client = nil, clientip = nil)
+ # We need to retrieve the client
+ master = Puppet::Client::MasterClient.instance
+
+ unless master
+ raise MissingMasterError, "Could not find the master client"
+ end
+
+ if master.locked?
+ Puppet.notice "Could not trigger run; already running"
+ return "running"
+ end
+
+ if tags == ""
+ tags = nil
+ end
+
+ if ignoreschedules == ""
+ ignoreschedules == nil
+ end
+
+ if client
+ msg = "%s(%s) triggered run" % [client, clientip]
+ if tags
+ msg += " with tags %s" % tags.join(", ")
+ end
+
+ if ignoreschedules
+ msg += " without schedules"
+ end
+
+ Puppet.notice msg
+ end
+
+ # And then we need to tell it to run, with this extra info.
+ # By default, stick it in a thread
+ if bg
+ Puppet.newthread do
+ master.run(tags, ignoreschedules)
+ end
+ else
+ master.run(tags, ignoreschedules)
+ end
+
+ return "success"
+ end
+ end
+end
+end
+
+# $Id$
diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb
index 23d384b38..a451d85d4 100644
--- a/lib/puppet/transaction.rb
+++ b/lib/puppet/transaction.rb
@@ -6,7 +6,7 @@ require 'puppet/statechange'
module Puppet
class Transaction
- attr_accessor :toplevel, :component, :objects
+ attr_accessor :toplevel, :component, :objects, :tags, :ignoreschedules
Puppet.config.setdefaults(:transaction,
@@ -103,8 +103,9 @@ class Transaction
# [self.object_id, @changes.length]
@count = 0
- tags = Puppet[:tags]
- if tags == ""
+ # Allow the tags to be overriden
+ tags = self.tags || Puppet[:tags]
+ if tags.nil? or tags == ""
tags = nil
else
tags = tags.split(/\s*,\s*/)
@@ -113,7 +114,7 @@ class Transaction
allevents = @objects.collect { |child|
events = nil
if (tags.nil? or child.tagged?(tags))
- if child.scheduled?
+ if self.ignoreschedules or child.scheduled?
# Perform the actual changes
events = apply(child)
diff --git a/lib/puppet/type/pfile.rb b/lib/puppet/type/pfile.rb
index 63481566c..9124c5c54 100644
--- a/lib/puppet/type/pfile.rb
+++ b/lib/puppet/type/pfile.rb
@@ -34,7 +34,8 @@ module Puppet
desc "Whether files should be backed up before
being replaced. If a filebucket_ is specified, files will be
backed up there; else, they will be backed up in the same directory
- with a ``.puppet-bak`` extension.
+ with a ``.puppet-bak`` extension,, and no backups
+ will be made if backup is ``false``.
To use filebuckets, you must first create a filebucket in your
configuration::
diff --git a/test/other/puppet.rb b/test/other/puppet.rb
new file mode 100755
index 000000000..966c574d4
--- /dev/null
+++ b/test/other/puppet.rb
@@ -0,0 +1,93 @@
+if __FILE__ == $0
+ $:.unshift '..'
+ $:.unshift '../../lib'
+ $puppetbase = "../.."
+end
+
+require 'puppet'
+require 'puppet/parsedfile'
+require 'puppettest'
+require 'test/unit'
+
+# Test the different features of the main puppet module
+class TestPuppetModule < Test::Unit::TestCase
+ include TestPuppet
+ include SignalObserver
+ def mktestclass
+ Class.new do
+ def initialize(file)
+ @file = file
+ end
+
+ def started?
+ FileTest.exists?(@file)
+ end
+
+ def start
+ File.open(@file, "w") do |f| f.puts "" end
+ end
+
+ def shutdown
+ File.unlink(@file)
+ end
+ end
+ end
+
+ # Make sure that services get correctly started and stopped
+ def test_servicehandling
+ file = tempfile()
+ testclass = mktestclass()
+
+ obj = testclass.new(file)
+
+ assert_nothing_raised {
+ Puppet.newservice(obj)
+ }
+
+ assert_nothing_raised {
+ Puppet.start(false)
+ }
+
+ # Give it a sec or so
+ sleep 0.3
+
+ assert(obj.started?, "Object was not started")
+
+ assert_nothing_raised {
+ Puppet.shutdown(false)
+ }
+ # Give it a sec or so
+ sleep 0.3
+
+ assert(!obj.started?, "Object is still running")
+
+ end
+
+ # Make sure timers are being handled correctly
+ def test_timerhandling
+ timer = nil
+ file = tempfile()
+ assert_nothing_raised {
+ timer = Puppet.newtimer(
+ :interval => 0.1,
+ :tolerance => 1,
+ :start? => true
+ ) do
+ File.open(file, "w") do |f| f.puts "" end
+ Puppet.shutdown(false)
+ end
+ }
+
+ assert(timer, "Did not get timer back from Puppet")
+
+ assert_nothing_raised {
+ timeout(1) do
+ Puppet.start()
+ end
+ }
+
+ assert(FileTest.exists?(file), "timer never got triggered")
+ end
+end
+
+# $Id$
diff --git a/test/server/runner.rb b/test/server/runner.rb
new file mode 100755
index 000000000..7f362dc17
--- /dev/null
+++ b/test/server/runner.rb
@@ -0,0 +1,75 @@
+if __FILE__ == $0
+ $:.unshift '../../lib'
+ $:.unshift '..'
+ $puppetbase = "../.."
+end
+
+require 'puppet'
+require 'puppet/server/runner'
+require 'test/unit'
+require 'puppettest.rb'
+
+class TestServerRunner < Test::Unit::TestCase
+ include TestPuppet
+
+ def mkclient(file)
+ master = nil
+ client = nil
+ # create our master
+ assert_nothing_raised() {
+ # this is the default server setup
+ master = Puppet::Server::Master.new(
+ :Manifest => file,
+ :UseNodes => false,
+ :Local => true
+ )
+ }
+
+ # and our client
+ assert_nothing_raised() {
+ client = Puppet::Client::MasterClient.new(
+ :Master => master
+ )
+ }
+
+ client
+ end
+
+ def test_runner
+ # Okay, make our manifest
+ file = tempfile()
+ created = tempfile()
+ File.open(file, "w") do |f|
+ f.puts %{file { "#{created}": ensure => file }}
+ end
+
+ client = mkclient(file)
+
+ runner = nil
+ assert_nothing_raised {
+ runner = Puppet::Server::Runner.new
+ }
+
+ assert_nothing_raised {
+ # Try it without backgrounding
+ runner.run(nil, nil, false)
+ }
+
+ assert(FileTest.exists?(created), "File did not get created")
+
+ # Now background it
+ File.unlink(created)
+
+ assert_nothing_raised {
+ runner.run(nil, nil, true)
+ }
+
+ Puppet.join
+
+ assert(FileTest.exists?(created), "File did not get created")
+
+ end
+end
+
+# $Id$
+