diff --git a/lib/puppet.rb b/lib/puppet.rb index f53e664be..1fa51c3b4 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,163 +1,164 @@ # Try to load rubygems. Hey rubygems, I hate you. begin require 'rubygems' rescue LoadError end # see the bottom of the file for further inclusions require 'singleton' require 'facter' require 'puppet/error' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/util/settings' require 'puppet/util/feature' require 'puppet/util/suidmanager' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' module Puppet PUPPETVERSION = '0.25.4' def Puppet.version return PUPPETVERSION end class << self include Puppet::Util attr_reader :features attr_writer :name end # the hash that determines how our system behaves @@settings = Puppet::Util::Settings.new # The services running in this process. @services ||= [] require 'puppet/util/logging' extend Puppet::Util::Logging # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. def self.setdefaults(section, hash) @@settings.setdefaults(section, hash) end # configuration parameter access and stuff def self.[](param) case param when :debug if Puppet::Util::Log.level == :debug return true else return false end else return @@settings[param] end end # configuration parameter access and stuff def self.[]=(param,value) @@settings[param] = value end def self.clear @@settings.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.settings @@settings end # Load all of the configuration parameters. require 'puppet/defaults' def self.genmanifest if Puppet[:genmanifest] puts Puppet.settings.to_manifest exit(0) end end # Parse the config file for this process. def self.parse_config Puppet.settings.parse end # XXX this should all be done using puppet objects, not using # normal mkdir def self.recmkdir(dir,mode = 0755) if FileTest.exist?(dir) return false else tmp = dir.sub(/^\//,'') path = [File::SEPARATOR] tmp.split(File::SEPARATOR).each { |dir| path.push dir if ! FileTest.exist?(File.join(path)) begin Dir.mkdir(File.join(path), mode) rescue Errno::EACCES => detail Puppet.err detail.to_s return false rescue => detail Puppet.err "Could not create %s: %s" % [path, detail.to_s] return false end elsif FileTest.directory?(File.join(path)) next else FileTest.exist?(File.join(path)) raise Puppet::Error, "Cannot create %s: basedir %s is a file" % [dir, File.join(path)] end } return true end end # Create a new type. Just proxy to the Type class. def self.newtype(name, options = {}, &block) Puppet::Type.newtype(name, options, &block) end # Retrieve a type by name. Just proxy to the Type class. def self.type(name) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet.type is deprecated; use Puppet::Type.type" Puppet::Type.type(name) end end require 'puppet/type' require 'puppet/parser' require 'puppet/network' require 'puppet/ssl' require 'puppet/module' require 'puppet/util/storage' +require 'puppet/status' if Puppet[:storeconfigs] require 'puppet/rails' end diff --git a/lib/puppet/indirector/status.rb b/lib/puppet/indirector/status.rb new file mode 100644 index 000000000..f40bbc4d8 --- /dev/null +++ b/lib/puppet/indirector/status.rb @@ -0,0 +1,3 @@ +# A stub class, so our constants work. +class Puppet::Indirector::Status +end diff --git a/lib/puppet/indirector/status/local.rb b/lib/puppet/indirector/status/local.rb new file mode 100644 index 000000000..377be89df --- /dev/null +++ b/lib/puppet/indirector/status/local.rb @@ -0,0 +1,7 @@ +require 'puppet/indirector/status' + +class Puppet::Indirector::Status::Local < Puppet::Indirector::Code + def find( *anything ) + return model.new + end +end diff --git a/lib/puppet/indirector/status/rest.rb b/lib/puppet/indirector/status/rest.rb new file mode 100644 index 000000000..22e70429b --- /dev/null +++ b/lib/puppet/indirector/status/rest.rb @@ -0,0 +1,5 @@ +require 'puppet/indirector/status' +require 'puppet/indirector/rest' + +class Puppet::Indirector::Status::Rest < Puppet::Indirector::REST +end diff --git a/lib/puppet/network/http/api/v1.rb b/lib/puppet/network/http/api/v1.rb index 13df7c3d0..6a5ff156a 100644 --- a/lib/puppet/network/http/api/v1.rb +++ b/lib/puppet/network/http/api/v1.rb @@ -1,65 +1,73 @@ require 'puppet/network/http/api' module Puppet::Network::HTTP::API::V1 # How we map http methods and the indirection name in the URI # to an indirection method. METHOD_MAP = { "GET" => { :plural => :search, :singular => :find }, "PUT" => { :singular => :save }, "DELETE" => { :singular => :destroy } } def uri2indirection(http_method, uri, params) environment, indirection, key = uri.split("/", 4)[1..-1] # the first field is always nil because of the leading slash raise ArgumentError, "The environment must be purely alphanumeric, not '%s'" % environment unless environment =~ /^\w+$/ raise ArgumentError, "The indirection name must be purely alphanumeric, not '%s'" % indirection unless indirection =~ /^\w+$/ method = indirection_method(http_method, indirection) params[:environment] = environment raise ArgumentError, "No request key specified in %s" % uri if key == "" or key.nil? key = URI.unescape(key) Puppet::Indirector::Request.new(indirection, method, key, params) end def indirection2uri(request) - indirection = request.method == :search ? request.indirection_name.to_s + "s" : request.indirection_name.to_s + indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s "/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}#{request.query_string}" end def indirection_method(http_method, indirection) unless METHOD_MAP[http_method] raise ArgumentError, "No support for http method %s" % http_method end unless method = METHOD_MAP[http_method][plurality(indirection)] raise ArgumentError, "No support for plural %s operations" % http_method end return method end + def pluralize(indirection) + return "statuses" if indirection == "status" + return indirection + "s" + end + def plurality(indirection) # NOTE This specific hook for facts is ridiculous, but it's a *many*-line # fix to not need this, and our goal is to move away from the complication # that leads to the fix being too long. return :singular if indirection == "facts" + # "status" really is singular + return :singular if indirection == "status" + result = (indirection =~ /s$/) ? :plural : :singular indirection.sub!(/s$/, '') if result result end end diff --git a/lib/puppet/network/rest_authconfig.rb b/lib/puppet/network/rest_authconfig.rb index 635ed1b8d..01ed412cd 100644 --- a/lib/puppet/network/rest_authconfig.rb +++ b/lib/puppet/network/rest_authconfig.rb @@ -1,89 +1,90 @@ require 'puppet/network/authconfig' module Puppet class Network::RestAuthConfig < Network::AuthConfig attr_accessor :rights DEFAULT_ACL = [ { :acl => "~ ^\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, # this one will allow all file access, and thus delegate # to fileserver.conf { :acl => "/file" }, { :acl => "/certificate_revocation_list/ca", :method => :find, :authenticated => true }, { :acl => "/report", :method => :save, :authenticated => true }, { :acl => "/certificate/ca", :method => :find, :authenticated => false }, { :acl => "/certificate/", :method => :find, :authenticated => false }, { :acl => "/certificate_request", :method => [:find, :save], :authenticated => false }, + { :acl => "/status", :method => [:find], :authenticated => true }, ] def self.main add_acl = @main.nil? super @main.insert_default_acl if add_acl and !@main.exists? @main end # check wether this request is allowed in our ACL # raise an Puppet::Network::AuthorizedError if the request # is denied. def allowed?(request) read() # we're splitting the request in part because # fail_on_deny could as well be called in the XMLRPC context # with a ClientRequest. @rights.fail_on_deny(build_uri(request), :node => request.node, :ip => request.ip, :method => request.method, :environment => request.environment, :authenticated => request.authenticated) end def initialize(file = nil, parsenow = true) super(file || Puppet[:rest_authconfig], parsenow) # if we didn't read a file (ie it doesn't exist) # make sure we can create some default rights @rights ||= Puppet::Network::Rights.new end def parse() super() insert_default_acl end # force regular ACLs to be present def insert_default_acl DEFAULT_ACL.each do |acl| unless rights[acl[:acl]] Puppet.info "Inserting default '#{acl[:acl]}'(%s) acl because %s" % [acl[:authenticated] ? "auth" : "non-auth" , ( !exists? ? "#{Puppet[:rest_authconfig]} doesn't exist" : "none where found in '#{@file}'")] mk_acl(acl) end end # queue an empty (ie deny all) right for every other path # actually this is not strictly necessary as the rights system # denies not explicitely allowed paths unless rights["/"] rights.newright("/") rights.restrict_authenticated("/", :any) end end def mk_acl(acl) @rights.newright(acl[:acl]) @rights.allow(acl[:acl], acl[:allow] || "*") if method = acl[:method] method = [method] unless method.is_a?(Array) method.each { |m| @rights.restrict_method(acl[:acl], m) } end @rights.restrict_authenticated(acl[:acl], acl[:authenticated]) unless acl[:authenticated].nil? end def build_uri(request) "/#{request.indirection_name}/#{request.key}" end end end diff --git a/lib/puppet/status.rb b/lib/puppet/status.rb new file mode 100644 index 000000000..f587a5a2a --- /dev/null +++ b/lib/puppet/status.rb @@ -0,0 +1,20 @@ +require 'puppet/indirector' + +class Puppet::Status + extend Puppet::Indirector + indirects :status, :terminus_class => :local + + attr :status, true + + def initialize( status = nil ) + @status = status || {"is_alive" => true} + end + + def to_pson + @status.to_pson + end + + def self.from_pson( pson ) + self.new( pson ) + end +end diff --git a/spec/integration/bin/puppetmasterd.rb b/spec/integration/bin/puppetmasterd.rb new file mode 100755 index 000000000..a9a8321e9 --- /dev/null +++ b/spec/integration/bin/puppetmasterd.rb @@ -0,0 +1,155 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +describe "puppetmasterd" do + before do + # Get a safe temporary file + file = Tempfile.new("puppetmaster_integration_testing") + @dir = file.path + file.delete + + Dir.mkdir(@dir) + + Puppet.settings[:confdir] = @dir + Puppet.settings[:vardir] = @dir + Puppet[:certdnsnames] = "localhost" + + @@port = 12345 + + Puppet::SSL::Host.instance_eval{ + @value_cache = {} + } + end + + after { + stop + + Puppet::SSL::Host.ca_location = :none + + system("rm -rf %s" % @dir) + Puppet.settings.clear + } + + def arguments + rundir = File.join(Puppet[:vardir], "run") + @pidfile = File.join(rundir, "testing.pid") + args = "" + args += " --confdir %s" % Puppet[:confdir] + args += " --rundir %s" % rundir + args += " --pidfile %s" % @pidfile + args += " --vardir %s" % Puppet[:vardir] + args += " --certdnsnames %s" % Puppet[:certdnsnames] + args += " --masterport %s" % @@port + args += " --user %s" % Puppet::Util::SUIDManager.uid + args += " --group %s" % Puppet::Util::SUIDManager.gid + args += " --autosign true" + end + + def start(addl_args = "") + Puppet.settings.mkdir(:manifestdir) + Puppet.settings.write(:manifest) do |f| + f.puts { "notify { testing: }" } + end + + args = arguments + " " + addl_args + + bin = File.join(File.dirname(__FILE__), "..", "..", "..", "sbin", "puppetmasterd") + lib = File.join(File.dirname(__FILE__), "..", "..", "..", "lib") + output = %x{/usr/bin/env ruby -I #{lib} #{bin} #{args}}.chomp + end + + def stop + if @pidfile and File.exist?(@pidfile) + pid = File.read(@pidfile).chomp.to_i + Process.kill(:TERM, pid) + 10.times do + break unless File.exist?(@pidfile) + sleep 1 + end + begin + # sigkill and report if process was still running + Process.kill(:KILL, pid) + + raise "Process didn't die from SIGTERM after 10 seconds" + rescue Errno::ESRCH + # process wasn't running. good. + end + + end + end + + it "should create a PID file" do + start + + FileTest.exist?(@pidfile).should be_true + end + + it "should be serving status information over REST" do + start + sleep 6 + + Puppet::Status.indirection.terminus_class = :rest + status = Puppet::Status.find("https://localhost:#{@@port}/production/status/default") + + status.status["is_alive"].should == true + + Puppet::Status.indirection.terminus_class = :local + end + + it "should be serving status information over xmlrpc" do + start + + sleep 6 + + client = Puppet::Network::Client.status.new(:Server => "localhost", :Port => @@port) + + retval = client.status + + retval.should == 1 + end + + it "should exit with return code 0 after parsing if --parseonly is set and there are no errors" do + start(" --parseonly > /dev/null") + sleep(1) + + ps = Facter["ps"].value || "ps -ef" + pid = nil + %x{#{ps}}.chomp.split(/\n/).each { |line| + next if line =~ /^puppet/ # skip normal master procs + if line =~ /puppetmasterd.+--manifest/ + ary = line.split(" ") + pid = ary[1].to_i + end + } + + $?.should == 0 + + pid.should be_nil + end + + it "should exit with return code 1 after parsing if --parseonly is set and there are errors" + + describe "when run for the first time" do + before do + @ssldir = File.join(@dir, 'ssl') + FileUtils.rm_r(@ssldir) if File.exists?(@ssldir) + end + + describe "with noop" do + it "should create its ssl directory" do + File.directory?(@ssldir).should be_false + start(' --noop') + File.directory?(@ssldir).should be_true + end + end + + describe "without noop" do + it "should create its ssl directory" do + File.directory?(@ssldir).should be_false + start + File.directory?(@ssldir).should be_true + end + end + end +end diff --git a/spec/unit/indirector/status/rest.rb b/spec/unit/indirector/status/rest.rb new file mode 100755 index 000000000..8f803a253 --- /dev/null +++ b/spec/unit/indirector/status/rest.rb @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby + +Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } + +require 'puppet/indirector/status/rest' + +describe Puppet::Indirector::Status::Rest do + it "should be a sublcass of Puppet::Indirector::REST" do + Puppet::Indirector::Status::Rest.superclass.should equal(Puppet::Indirector::REST) + end +end diff --git a/spec/unit/network/rest_authconfig.rb b/spec/unit/network/rest_authconfig.rb index 20a51ba87..407fc43f4 100755 --- a/spec/unit/network/rest_authconfig.rb +++ b/spec/unit/network/rest_authconfig.rb @@ -1,146 +1,147 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/network/rest_authconfig' describe Puppet::Network::RestAuthConfig do DEFAULT_ACL = [ { :acl => "~ ^\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true }, # this one will allow all file access, and thus delegate # to fileserver.conf { :acl => "/file" }, { :acl => "/certificate_revocation_list/ca", :method => :find, :authenticated => true }, { :acl => "/report", :method => :save, :authenticated => true }, { :acl => "/certificate/ca", :method => :find, :authenticated => false }, { :acl => "/certificate/", :method => :find, :authenticated => false }, { :acl => "/certificate_request", :method => [:find, :save], :authenticated => false }, + { :acl => "/status", :method => [:find], :authenticated => true }, ] before :each do FileTest.stubs(:exists?).returns(true) File.stubs(:stat).returns(stub('stat', :ctime => :now)) Time.stubs(:now).returns :now @authconfig = Puppet::Network::RestAuthConfig.new("dummy", false) @authconfig.stubs(:read) @acl = stub_everything 'rights' @authconfig.rights = @acl @request = stub 'request', :indirection_name => "path", :key => "to/resource", :ip => "127.0.0.1", :node => "me", :method => :save, :environment => :env, :authenticated => true end it "should use the puppet default rest authorization file" do Puppet.expects(:[]).with(:rest_authconfig).returns("dummy") Puppet::Network::RestAuthConfig.new(nil, false) end it "should read the config file when needed" do @authconfig.expects(:read) @authconfig.allowed?(@request) end it "should ask for authorization to the ACL subsystem" do @acl.expects(:fail_on_deny).with("/path/to/resource", :node => "me", :ip => "127.0.0.1", :method => :save, :environment => :env, :authenticated => true) @authconfig.allowed?(@request) end describe "when defining an acl with mk_acl" do it "should create a new right for each default acl" do @acl.expects(:newright).with(:path) @authconfig.mk_acl(:acl => :path) end it "should allow everyone for each default right" do @acl.expects(:allow).with(:path, "*") @authconfig.mk_acl(:acl => :path) end it "should restrict the ACL to a method" do @acl.expects(:restrict_method).with(:path, :method) @authconfig.mk_acl(:acl => :path, :method => :method) end it "should restrict the ACL to a specific authentication state" do @acl.expects(:restrict_authenticated).with(:path, :authentication) @authconfig.mk_acl(:acl => :path, :authenticated => :authentication) end end describe "when parsing the configuration file" do it "should check for missing ACL after reading the authconfig file" do File.stubs(:open) @authconfig.expects(:insert_default_acl) @authconfig.parse() end end DEFAULT_ACL.each do |acl| it "should insert #{acl[:acl]} if not present" do @authconfig.rights.stubs(:[]).returns(true) @authconfig.rights.stubs(:[]).with(acl[:acl]).returns(nil) @authconfig.expects(:mk_acl).with { |h| h[:acl] == acl[:acl] } @authconfig.insert_default_acl end it "should not insert #{acl[:acl]} if present" do @authconfig.rights.stubs(:[]).returns(true) @authconfig.rights.stubs(:[]).with(acl).returns(true) @authconfig.expects(:mk_acl).never @authconfig.insert_default_acl end end it "should create default ACL entries if no file have been read" do Puppet::Network::RestAuthConfig.any_instance.stubs(:exists?).returns(false) Puppet::Network::RestAuthConfig.any_instance.expects(:insert_default_acl) Puppet::Network::RestAuthConfig.main end describe "when adding default ACLs" do DEFAULT_ACL.each do |acl| it "should create a default right for #{acl[:acl]}" do @authconfig.stubs(:mk_acl) @authconfig.expects(:mk_acl).with(acl) @authconfig.insert_default_acl end end it "should log at info loglevel" do Puppet.expects(:info).at_least_once @authconfig.insert_default_acl end it "should create a last catch-all deny all rule" do @authconfig.stubs(:mk_acl) @acl.expects(:newright).with("/") @authconfig.insert_default_acl end it "should create a last catch-all deny all rule for any authenticated request state" do @authconfig.stubs(:mk_acl) @acl.stubs(:newright).with("/") @acl.expects(:restrict_authenticated).with("/", :any) @authconfig.insert_default_acl end end end diff --git a/spec/unit/status.rb b/spec/unit/status.rb new file mode 100644 index 000000000..b13b246af --- /dev/null +++ b/spec/unit/status.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../spec_helper' + +describe Puppet::Status do + it "should implement find" do + Puppet::Status.find( :default ).should be_is_a(Puppet::Status) + Puppet::Status.find( :default ).status["is_alive"].should == true + end + + it "should default to is_alive is true" do + Puppet::Status.new.status["is_alive"].should == true + end + + it "should return a pson hash" do + Puppet::Status.new.status.to_pson.should == '{"is_alive":true}' + end + + it "should accept a hash from pson" do + status = Puppet::Status.new( { "is_alive" => false } ) + status.status.should == { "is_alive" => false } + end +end