diff --git a/ext/rack/README b/ext/rack/README new file mode 100644 index 000000000..33f958e36 --- /dev/null +++ b/ext/rack/README @@ -0,0 +1,70 @@ + +PUPPETMASTER AS A RACK APPLICATION +================================== + +puppetmaster can now be hosted as a standard Rack application. A proper +config.ru is provided for this. + +For more details about rack, see http://rack.rubyforge.org/ . + +Getting started +=============== + +You'll need rack installed, version 1.0.0. Older versions are known not +to work. + + +WEBrick +------- + +WEBrick is currently not supported as a Rack host. You'll be better off +just running puppetmasterd directly. + + +Mongrel +------- + +If you like Mongrel, and want to replicate wiki:UsingMongrel, you could +probably start your backend mongrels this way: + +cd ext/rack +for port in `seq 18140 18150`; do + rackup --server mongrel --port $port & +done + +rackup is part of the rack gem. Make sure it's in your path. + + + +Apache with Passenger (aka mod_rails) +------------------------------------- + +Make sure puppetmasterd ran at least once, so the SSL certificates +got set up. + +Requirements: + Passenger version 2.2.2 or newer [1] + Rack version 1.0.0 + Apache 2.x + SSL Module loaded + +Apache configuration snippet is in files/apache2.conf. You need to +edit it to reflect your servername. + +Required puppet.conf settings: + [puppetmasterd] + ssl_client_header = SSL_CLIENT_S_DN + ssl_client_verify_header = SSL_CLIENT_VERIFY + +To set up most of the boring stuff, you can use this command: + puppet --verbose --modulepath ./ext ext/rack/manifest.pp +Or use manifest.pp as a starting point for your own module. + +Note: Passenger will not let applications run as root or the Apache user, +instead an implicit setuid will be done, to the user whom owns +config.ru. Therefore, config.ru shall be owned by the puppet user. + + +[1] http://www.modrails.com/install.html + + diff --git a/ext/rack/files/apache2.conf b/ext/rack/files/apache2.conf new file mode 100644 index 000000000..88c0f052d --- /dev/null +++ b/ext/rack/files/apache2.conf @@ -0,0 +1,36 @@ +Listen 8140 + + SSLEngine on + SSLProtocol -ALL +SSLv3 +TLSv1 + SSLCipherSuite ALL:!ADH:RC4+RSA:+HIGH:+MEDIUM:-LOW:-SSLv2:-EXP + + SSLCertificateFile /etc/puppet/ssl/certs/squigley.namespace.at.pem + SSLCertificateKeyFile /etc/puppet/ssl/private_keys/squigley.namespace.at.pem + SSLCertificateChainFile /etc/puppet/ssl/ca/ca_crt.pem + SSLCACertificateFile /etc/puppet/ssl/ca/ca_crt.pem + # If Apache complains about invalid signatures on the CRL, you can try disabling + # CRL checking by commenting the next line, but this is not recommended. + SSLCARevocationFile /etc/puppet/ssl/ca/ca_crl.pem + SSLVerifyClient optional + SSLVerifyDepth 1 + SSLOptions +StdEnvVars + + # you probably want to tune these settings + PassengerHighPerformance on + PassengerMaxPoolSize 12 + PassengerPoolIdleTime 1500 + # PassengerMaxRequests 1000 + PassengerStatThrottleRate 120 + RackAutoDetect Off + RailsAutoDetect Off + + DocumentRoot /etc/puppet/rack/public/ + RackBaseURI / + + Options None + AllowOverride None + Order allow,deny + allow from all + + + diff --git a/ext/rack/files/config.ru b/ext/rack/files/config.ru new file mode 100644 index 000000000..5607bab72 --- /dev/null +++ b/ext/rack/files/config.ru @@ -0,0 +1,18 @@ +# a config.ru, for use with every rack-compatible webserver. +# SSL needs to be handled outside this, though. + +# if puppet is not in your RUBYLIB: +# $:.push('/opt/puppet/lib') + +$0 = "puppetmasterd" +require 'puppet' + +# if you want debugging: +# ARGV << "--debug" + +ARGV << "--rack" +require 'puppet/application/puppetmasterd' +# we're usually running inside a Rack::Builder.new {} block, +# therefore we need to call run *here*. +run Puppet::Application[:puppetmasterd].run + diff --git a/ext/rack/manifest.pp b/ext/rack/manifest.pp new file mode 100644 index 000000000..5145e05c8 --- /dev/null +++ b/ext/rack/manifest.pp @@ -0,0 +1,59 @@ + +file { ["/etc/puppet/rack", "/etc/puppet/rack/public"]: + ensure => directory, + mode => 0755, + owner => root, + group => root, +} +file { "/etc/puppet/rack/config.ru": + ensure => present, + source => "puppet:///modules/rack/config.ru", + mode => 0644, + owner => puppet, + group => root, +} +file { "/etc/apache2/conf.d/puppetmasterd": + ensure => present, + source => "puppet:///modules/rack/apache2.conf", + mode => 0644, + owner => root, + group => root, + require => [File["/etc/puppet/rack/config.ru"], File["/etc/puppet/rack/public"], Package["apache2"], Package["passenger"]], + notify => Service["apache2"], +} + +package { ["rack", "passenger"]: + ensure => installed, + provider => "gem", +} + +service { "apache2": +} + +case $lsbdistid { + "Debian": { + package { ["apache2-mpm-worker", "apache2-threaded-dev", "apache2"]: + ensure => installed, + } + file { "/etc/apache2/mods-enabled/ssl.load": + ensure => "../mods-available/ssl.load", + notify => Service["apache2"], + require => Package["apache2"], + } + Service["apache2"] { + require => Package["apache2"], + } + exec { "/var/lib/gems/1.8/bin/passenger-install-apache2-module --auto": + subscribe => Package["passenger"], + before => Service["apache2"], + require => Package[["passenger", "apache2-threaded-dev"]], + } + } +} + +notice("You need to manually enable mod_passenger.so for Apache.") +notice("Usually, you put these config stanzas into httpd.conf:") +notice(" LoadModule passenger_module /var/lib/gems/1.8/gems/passenger-2.2.2/ext/apache2/mod_passenger.so") +notice(" PassengerRoot /var/lib/gems/1.8/gems/passenger-2.2.2") +notice(" PassengerRuby /usr/bin/ruby1.8") +notice("--------------------------------------------------------") diff --git a/lib/puppet/application/puppetmasterd.rb b/lib/puppet/application/puppetmasterd.rb index fe92bca7a..52f33cba9 100644 --- a/lib/puppet/application/puppetmasterd.rb +++ b/lib/puppet/application/puppetmasterd.rb @@ -1,130 +1,142 @@ require 'puppet' require 'puppet/application' require 'puppet/daemon' require 'puppet/network/server' +require 'puppet/network/http/rack' if Puppet.features.rack? Puppet::Application.new(:puppetmasterd) do should_parse_config option("--debug", "-d") option("--verbose", "-v") + # internal option, only to be used by ext/rack/config.ru + option("--rack") + option("--logdest", "-l") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail if Puppet[:debug] puts detail.backtrace end $stderr.puts detail.to_s end end preinit do trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end # Create this first-off, so we have ARGV @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup end dispatch do return Puppet[:parseonly] ? :parseonly : :main end command(:parseonly) do begin Puppet::Parser::Interpreter.new.parser(Puppet[:environment]) rescue => detail Puppet.err detail exit 1 end exit(0) end command(:main) do require 'etc' require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' require 'puppet/checksum' xmlrpc_handlers = [:Status, :FileServer, :Master, :Report, :Filebucket] if Puppet[:ca] xmlrpc_handlers << :CA end - @daemon.server = Puppet::Network::Server.new(:xmlrpc_handlers => xmlrpc_handlers) - # Make sure we've got a localhost ssl cert Puppet::SSL::Host.localhost # And now configure our server to *only* hit the CA for data, because that's # all it will have write access to. if Puppet::SSL::CertificateAuthority.ca? Puppet::SSL::Host.ca_location = :only end if Process.uid == 0 begin Puppet::Util.chuser rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Could not change user to %s: %s" % [Puppet[:user], detail] exit(39) end end - @daemon.daemonize if Puppet[:daemonize] + unless options[:rack] + @daemon.server = Puppet::Network::Server.new(:xmlrpc_handlers => xmlrpc_handlers) + @daemon.daemonize if Puppet[:daemonize] + else + require 'puppet/network/http/rack' + @app = Puppet::Network::HTTP::Rack.new(:xmlrpc_handlers => xmlrpc_handlers, :protocols => [:rest, :xmlrpc]) + end Puppet.notice "Starting Puppet server version %s" % [Puppet.version] - @daemon.start + unless options[:rack] + @daemon.start + else + return @app + end end setup do # Handle the logging settings. if options[:debug] or options[:verbose] if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end - unless Puppet[:daemonize] + unless Puppet[:daemonize] or options[:rack] Puppet::Util::Log.newdestination(:console) options[:setdest] = true end end unless options[:setdest] Puppet::Util::Log.newdestination(:syslog) end if Puppet.settings.print_configs? exit(Puppet.settings.print_configs ? 0 : 1) end Puppet.settings.use :main, :puppetmasterd, :ssl # A temporary solution, to at least make the master work for now. Puppet::Node::Facts.terminus_class = :yaml # Cache our nodes in yaml. Currently not configurable. Puppet::Node.cache_class = :yaml # Configure all of the SSL stuff. if Puppet::SSL::CertificateAuthority.ca? Puppet::SSL::Host.ca_location = :local Puppet.settings.use :ca Puppet::SSL::CertificateAuthority.instance else Puppet::SSL::Host.ca_location = :none end end end diff --git a/spec/unit/application/puppetmasterd.rb b/spec/unit/application/puppetmasterd.rb index 5b193ebbf..df6f87895 100644 --- a/spec/unit/application/puppetmasterd.rb +++ b/spec/unit/application/puppetmasterd.rb @@ -1,330 +1,371 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/application/puppetmasterd' describe "PuppetMaster" do before :each do @puppetmasterd = Puppet::Application[:puppetmasterd] @daemon = stub_everything 'daemon' Puppet::Daemon.stubs(:new).returns(@daemon) Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) Puppet::Node.stubs(:terminus_class=) Puppet::Node.stubs(:cache_class=) Puppet::Node::Facts.stubs(:terminus_class=) Puppet::Node::Facts.stubs(:cache_class=) Puppet::Transaction::Report.stubs(:terminus_class=) Puppet::Resource::Catalog.stubs(:terminus_class=) end it "should ask Puppet::Application to parse Puppet configuration file" do @puppetmasterd.should_parse_config?.should be_true end it "should declare a main command" do @puppetmasterd.should respond_to(:main) end it "should declare a parseonly command" do @puppetmasterd.should respond_to(:parseonly) end it "should declare a preinit block" do @puppetmasterd.should respond_to(:run_preinit) end describe "during preinit" do before :each do @puppetmasterd.stubs(:trap) end it "should catch INT" do @puppetmasterd.stubs(:trap).with { |arg,block| arg == :INT } @puppetmasterd.run_preinit end it "should create a Puppet Daemon" do Puppet::Daemon.expects(:new).returns(@daemon) @puppetmasterd.run_preinit end it "should give ARGV to the Daemon" do argv = stub 'argv' ARGV.stubs(:dup).returns(argv) @daemon.expects(:argv=).with(argv) @puppetmasterd.run_preinit end end [:debug,:verbose].each do |option| it "should declare handle_#{option} method" do @puppetmasterd.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @puppetmasterd.options.expects(:[]=).with(option, 'arg') @puppetmasterd.send("handle_#{option}".to_sym, 'arg') end end describe "when applying options" do it "should set the log destination with --logdest" do Puppet::Log.expects(:newdestination).with("console") @puppetmasterd.handle_logdest("console") end it "should put the setdest options to true" do @puppetmasterd.options.expects(:[]=).with(:setdest,true) @puppetmasterd.handle_logdest("console") end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) Puppet.stubs(:settraps) Puppet::Log.stubs(:level=) Puppet::SSL::CertificateAuthority.stubs(:instance) Puppet::SSL::CertificateAuthority.stubs(:ca?) Puppet.settings.stubs(:use) @puppetmasterd.options.stubs(:[]).with(any_parameters) end it "should set log level to debug if --debug was passed" do @puppetmasterd.options.stubs(:[]).with(:debug).returns(true) Puppet::Log.expects(:level=).with(:debug) @puppetmasterd.run_setup end it "should set log level to info if --verbose was passed" do @puppetmasterd.options.stubs(:[]).with(:verbose).returns(true) Puppet::Log.expects(:level=).with(:info) @puppetmasterd.run_setup end it "should set console as the log destination if no --logdest and --daemonize" do @puppetmasterd.stubs(:[]).with(:daemonize).returns(:false) Puppet::Log.expects(:newdestination).with(:syslog) @puppetmasterd.run_setup end it "should set syslog as the log destination if no --logdest and not --daemonize" do Puppet::Log.expects(:newdestination).with(:syslog) @puppetmasterd.run_setup end + it "should set syslog as the log destination if --rack" do + @puppetmasterd.options.stubs(:[]).with(:rack).returns(:true) + + Puppet::Log.expects(:newdestination).with(:syslog) + + @puppetmasterd.run_setup + end + it "should print puppet config if asked to in Puppet config" do @puppetmasterd.stubs(:exit) Puppet.settings.stubs(:print_configs?).returns(true) Puppet.settings.expects(:print_configs) @puppetmasterd.run_setup end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) lambda { @puppetmasterd.run_setup }.should raise_error(SystemExit) end it "should tell Puppet.settings to use :main,:ssl and :puppetmasterd category" do Puppet.settings.expects(:use).with(:main,:puppetmasterd,:ssl) @puppetmasterd.run_setup end it "should set node facst terminus to yaml" do Puppet::Node::Facts.expects(:terminus_class=).with(:yaml) @puppetmasterd.run_setup end it "should cache class in yaml" do Puppet::Node.expects(:cache_class=).with(:yaml) @puppetmasterd.run_setup end describe "with no ca" do it "should set the ca_location to none" do Puppet::SSL::Host.expects(:ca_location=).with(:none) @puppetmasterd.run_setup end end describe "with a ca configured" do before :each do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(true) end it "should set the ca_location to local" do Puppet::SSL::Host.expects(:ca_location=).with(:local) @puppetmasterd.run_setup end it "should tell Puppet.settings to use :ca category" do Puppet.settings.expects(:use).with(:ca) @puppetmasterd.run_setup end it "should instantiate the CertificateAuthority singleton" do Puppet::SSL::CertificateAuthority.expects(:instance) @puppetmasterd.run_setup end end end describe "when running" do it "should dispatch to parseonly if parseonly is set" do Puppet.stubs(:[]).with(:parseonly).returns(true) @puppetmasterd.get_command.should == :parseonly end it "should dispatch to main if parseonly is not set" do Puppet.stubs(:[]).with(:parseonly).returns(false) @puppetmasterd.get_command.should == :main end describe "the parseonly command" do before :each do Puppet.stubs(:[]).with(:environment) Puppet.stubs(:[]).with(:manifest).returns("site.pp") @interpreter = stub_everything Puppet.stubs(:err) @puppetmasterd.stubs(:exit) Puppet::Parser::Interpreter.stubs(:new).returns(@interpreter) end it "should delegate to the Puppet Parser" do @interpreter.expects(:parser) @puppetmasterd.parseonly end it "should exit with exit code 0 if no error" do @puppetmasterd.expects(:exit).with(0) @puppetmasterd.parseonly end it "should exit with exit code 1 if error" do @interpreter.stubs(:parser).raises(Puppet::ParseError) @puppetmasterd.expects(:exit).with(1) @puppetmasterd.parseonly end end describe "the main command" do before :each do @puppetmasterd.run_preinit @server = stub_everything 'server' Puppet::Network::Server.stubs(:new).returns(@server) + @app = stub_everything 'app' + Puppet::Network::HTTP::Rack.stubs(:new).returns(@app) Puppet::SSL::Host.stubs(:localhost) Puppet::SSL::CertificateAuthority.stubs(:ca?) Process.stubs(:uid).returns(1000) Puppet.stubs(:service) Puppet.stubs(:[]) Puppet.stubs(:notice) Puppet.stubs(:start) end it "should create a Server" do Puppet::Network::Server.expects(:new) @puppetmasterd.main end it "should give the server to the daemon" do @daemon.expects(:server=).with(@server) @puppetmasterd.main end it "should create the server with the right XMLRPC handlers" do Puppet::Network::Server.expects(:new).with { |args| args[:xmlrpc_handlers] == [:Status, :FileServer, :Master, :Report, :Filebucket]} @puppetmasterd.main end it "should create the server with a :ca xmlrpc handler if needed" do Puppet.stubs(:[]).with(:ca).returns(true) Puppet::Network::Server.expects(:new).with { |args| args[:xmlrpc_handlers].include?(:CA) } @puppetmasterd.main end it "should generate a SSL cert for localhost" do Puppet::SSL::Host.expects(:localhost) @puppetmasterd.main end it "should make sure to *only* hit the CA for data" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(true) Puppet::SSL::Host.expects(:ca_location=).with(:only) @puppetmasterd.main end it "should drop privileges if running as root" do Process.stubs(:uid).returns(0) Puppet::Util.expects(:chuser) @puppetmasterd.main end it "should daemonize if needed" do Puppet.stubs(:[]).with(:daemonize).returns(true) @daemon.expects(:daemonize) @puppetmasterd.main end it "should start the service" do @daemon.expects(:start) @puppetmasterd.main end + describe "with --rack" do + confine "Rack is not available" => Puppet.features.rack? + + it "it should create the app with REST and XMLRPC support" do + @puppetmasterd.options.stubs(:[]).with(:rack).returns(:true) + + Puppet::Network::HTTP::Rack.expects(:new).with { |args| + args[:xmlrpc_handlers] == [:Status, :FileServer, :Master, :Report, :Filebucket] and + args[:protocols] == [:rest, :xmlrpc] + } + + @puppetmasterd.main + end + + it "it should not start a daemon" do + @puppetmasterd.options.stubs(:[]).with(:rack).returns(:true) + + @daemon.expects(:start).never + + @puppetmasterd.main + end + + it "it should return the app" do + @puppetmasterd.options.stubs(:[]).with(:rack).returns(:true) + + app = @puppetmasterd.main + app.should equal(@app) + end + + end + end end end