diff --git a/ext/regexp_nodes/regexp_nodes.rb b/ext/regexp_nodes/regexp_nodes.rb index 2d0de8e4b..b8861eafe 100644 --- a/ext/regexp_nodes/regexp_nodes.rb +++ b/ext/regexp_nodes/regexp_nodes.rb @@ -1,271 +1,271 @@ #!/usr/bin/env ruby # = Synopsis # This is an external node classifier script, after # http://docs.puppetlabs.com/guides/external_nodes.html # # = Usage # regexp_nodes.rb # # = Description # This classifier implements filesystem autoloading: It looks through classes, # parameters, and environment subdirectories, looping through each file it # finds. Each file's contents are a regexp-per-line which, if they match the # hostname passed to the program as ARGV[0], sets a class, parameter value # or environment named the same thing as the file itself. At the end, the # resultant data structure is returned back to the puppet master process as # yaml. # # = Caveats # Since the files are read in directory order, multiple matches for a given # hostname in the parameters/ and environment/ subdirectories will return the # last-read value. (Multiple classes/ matches don't cause a problem; the # class is either incuded or it isn't) # # Unmatched hostnames in any of the environment/ files will cause 'production' # to be emitted; be aware of the complexity surrounding the interaction between # ENC and environments as discussed in https://projects.puppetlabs.com/issues/3910 # # = Examples # Based on the example files in the classes/ and parameters/ subdirectories # in the distribution, classes/database will set the 'database' class for # hosts matching %r{db\d{2}} (that is, 'db' followed by two digits) or with # 'mysql' anywhere in the hostname. Similarly, hosts beginning with 'www' or # 'web' or the hostname 'leterel' (my workstation) will be assigned the # 'webserver' class. # # Under parameters/ there is one subdirectory 'service' which # sets the a parameter called 'service' to the value 'prod' for production # hosts (whose hostnames always end with a three-digit code), 'qa' for # anything that starts with 'qa-' 'qa2-' or 'qa3-', and 'sandbox' for any # development machines whose hostnames start with 'dev-'. # # In the environment/ subdirectory, any hosts matching '^dev-' and a single # production host which serves as 'canary in the coal mine' will be put into # the development environment # # = Author # Eric Sorenson # we need yaml or there's not much point in going on require 'yaml' # Sets are like arrays but automatically de-duplicate elements require 'set' # set up some syslog logging require 'syslog' Syslog.open('extnodes', Syslog::LOG_PID | Syslog::LOG_NDELAY, Syslog::LOG_DAEMON) # change this to LOG_UPTO(Sysslog::LOG_DEBUG) if you want to see everything # but remember your syslog.conf needs to match this or messages will be filtered Syslog.mask = Syslog::LOG_UPTO(Syslog::LOG_INFO) # Helper method to log to syslog; we log at level debug if no level is specified # since those are the most frequent calls to this method def log(message,level=:debug) Syslog.send(level,message) end # set our workingdir to be the directory we're executed from, regardless # of parent's cwd, symlinks, etc. via handy Pathname.realpath method require 'pathname' p = Pathname.new(File.dirname(__FILE__)) WORKINGDIR = "#{p.realpath}" # This class holds all the methods for creating and accessing the properties # of an external node. There are really only two public methods: initialize # and a special version of to_yaml class ExternalNode # Make these instance variables get/set-able with eponymous methods attr_accessor :classes, :parameters, :environment, :hostname # initialize takes three arguments: # hostname:: usually passed in via ARGV[0] but it could be anything # classdir:: directory under WORKINGDIR to look for files named after # classes # parameterdir:: directory under WORKINGDIR to look for directories to set # parameters def initialize(hostname, classdir = 'classes/', parameterdir = 'parameters/', environmentdir = 'environment/') # instance variables that contain the lists of classes and parameters @hostname @classes = Set.new @parameters = Hash.new("unknown") # sets a default value of "unknown" @environment = "production" self.parse_argv(hostname) self.match_classes("#{WORKINGDIR}/#{classdir}") self.match_parameters("#{WORKINGDIR}/#{parameterdir}") self.match_environment("#{WORKINGDIR}/#{environmentdir}") end # private method called by initialize which sanity-checks our hostname. # good candidate for overriding in a subclass if you need different checks def parse_argv(hostname) if hostname =~ /^([-\w]+?)\.([-\w\.]+)/ # non-greedy up to the first . is hostname @hostname = $1 elsif hostname =~ /^([-\w]+)$/ # sometimes puppet's @name is just a name @hostname = hostname log("got shortname for [#{hostname}]") else log("didn't receive parsable hostname, got: [#{hostname}]",:err) exit(1) end end # to_yaml massages a copy of the object and outputs clean yaml so we don't # feed weird things back to puppet []< def to_yaml classes = self.classes.to_a if self.parameters.empty? # otherwise to_yaml prints "parameters: {}" parameters = nil else parameters = self.parameters end ({ 'classes' => classes, 'parameters' => parameters, 'environment' => environment}).to_yaml end # Private method that expects an absolute path to a file and a string to # match - it returns true if the string was matched by any of the lines in # the file def matched_in_patternfile?(filepath, matchthis) patternlist = [] begin open(filepath).each { |l| l.chomp! next if l =~ /^$/ next if l =~ /^#/ if l =~ /^\s*(\S+)/ m = Regexp.last_match log("found a non-comment line, transforming [#{l}] into [#{m[1]}]") l.gsub!(l,m[1]) else next l end pattern = %r{#{l}} patternlist << pattern log("appending [#{pattern}] to patternlist for [#{filepath}]") } - rescue Exception + rescue StandardError log("Problem reading #{filepath}: #{$!}",:err) exit(1) end log("list of patterns for #{filepath}: #{patternlist}") if matchthis =~ Regexp.union(patternlist) log("matched #{$~.to_s} in #{matchthis}, returning true") return true else # hostname didn't match anything in patternlist log("#{matchthis} unmatched, returning false") return nil end end # def # private method - takes a path to look for files, iterates through all # readable, regular files it finds, and matches this instance's @hostname # against each line; if any match, the class will be set for this node. def match_classes(fullpath) Dir.foreach(fullpath) do |patternfile| filepath = "#{fullpath}/#{patternfile}" next unless File.file?(filepath) and File.readable?(filepath) next if patternfile =~ /^\./ log("Attempting to match [#{@hostname}] in [#{filepath}]") if matched_in_patternfile?(filepath,@hostname) @classes << patternfile.to_s log("Appended #{patternfile.to_s} to classes instance variable") end end end # match_environment is similar to match_classes but it overwrites # any previously set value (usually just the default, 'production') # with a match def match_environment(fullpath) Dir.foreach(fullpath) do |patternfile| filepath = "#{fullpath}/#{patternfile}" next unless File.file?(filepath) and File.readable?(filepath) next if patternfile =~ /^\./ log("Attempting to match [#{@hostname}] in [#{filepath}]") if matched_in_patternfile?(filepath,@hostname) @environment = patternfile.to_s log("Wrote #{patternfile.to_s} to environment instance variable") end end end # Parameters are handled slightly differently; we make another level of # directories to get the parameter name, then use the names of the files # contained in there for the values of those parameters. # # ex: cat ./parameters/service/production # ^prodweb # would set parameters["service"] = "production" for prodweb001 def match_parameters(fullpath) Dir.foreach(fullpath) do |parametername| filepath = "#{fullpath}/#{parametername}" next if File.basename(filepath) =~ /^\./ # skip over dotfiles next unless File.directory?(filepath) and File.readable?(filepath) # skip over non-directories log("Considering contents of #{filepath}") Dir.foreach("#{filepath}") do |patternfile| secondlevel = "#{filepath}/#{patternfile}" log("Found parameters patternfile at #{secondlevel}") next unless File.file?(secondlevel) and File.readable?(secondlevel) log("Attempting to match [#{@hostname}] in [#{secondlevel}]") if matched_in_patternfile?(secondlevel, @hostname) @parameters[ parametername.to_s ] = patternfile.to_s log("Set @parameters[#{parametername.to_s}] = #{patternfile.to_s}") end end end end end # Logic for local hacks that don't fit neatly into the autoloading model can # happen as we initialize a subclass class MyExternalNode < ExternalNode def initialize(hostname, classdir = 'classes/', parameterdir = 'parameters/') super # Set "hostclass" parameter based on hostname, # stripped of leading environment prefix and numeric suffix if @hostname =~ /^(\w*?)-?(\D+)(\d{2,3})$/ match = Regexp.last_match hostclass = match[2] log("matched hostclass #{hostclass}") @parameters[ "hostclass" ] = hostclass else log("couldn't figure out class from #{@hostname}",:warning) end end end # Here we begin actual execution by calling methods defined above mynode = MyExternalNode.new(ARGV[0]) puts mynode.to_yaml diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb index f6172d22e..80a97efde 100644 --- a/lib/puppet/agent.rb +++ b/lib/puppet/agent.rb @@ -1,121 +1,117 @@ require 'puppet/application' # A general class for triggering a run of another # class. class Puppet::Agent require 'puppet/agent/locker' include Puppet::Agent::Locker require 'puppet/agent/disabler' include Puppet::Agent::Disabler attr_reader :client_class, :client, :splayed, :should_fork def initialize(client_class, should_fork=true) @splayed = false @should_fork = can_fork? && should_fork @client_class = client_class end def can_fork? Puppet.features.posix? && RUBY_PLATFORM != 'java' end def needing_restart? Puppet::Application.restart_requested? end # Perform a run with our client. def run(client_options = {}) if running? Puppet.notice "Run of #{client_class} already in progress; skipping (#{lockfile_path} exists)" return end if disabled? Puppet.notice "Skipping run of #{client_class}; administratively disabled (Reason: '#{disable_message}');\nUse 'puppet agent --enable' to re-enable." return end result = nil block_run = Puppet::Application.controlled_run do splay client_options.fetch :splay, Puppet[:splay] result = run_in_fork(should_fork) do with_client do |client| begin client_args = client_options.merge(:pluginsync => Puppet[:pluginsync]) lock { client.run(client_args) } - rescue SystemExit,NoMemoryError - raise - rescue Exception => detail + rescue StandardError => detail Puppet.log_exception(detail, "Could not run #{client_class}: #{detail}") end end end true end Puppet.notice "Shutdown/restart in progress (#{Puppet::Application.run_status.inspect}); skipping run" unless block_run result end def stopping? Puppet::Application.stop_requested? end # Have we splayed already? def splayed? splayed end # Sleep when splay is enabled; else just return. def splay(do_splay = Puppet[:splay]) return unless do_splay return if splayed? time = rand(Puppet[:splaylimit] + 1) Puppet.info "Sleeping for #{time} seconds (splay is enabled)" sleep(time) @splayed = true end def run_in_fork(forking = true) return yield unless forking or Puppet.features.windows? child_pid = Kernel.fork do $0 = "puppet agent: applying configuration" begin exit(yield) rescue SystemExit exit(-1) rescue NoMemoryError exit(-2) end end exit_code = Process.waitpid2(child_pid) case exit_code[1].exitstatus when -1 raise SystemExit when -2 raise NoMemoryError end exit_code[1].exitstatus end private # Create and yield a client instance, keeping a reference # to it during the yield. def with_client begin @client = client_class.new - rescue SystemExit,NoMemoryError - raise - rescue Exception => detail + rescue StandardError => detail Puppet.log_exception(detail, "Could not create instance of #{client_class}: #{detail}") return end yield @client ensure @client = nil end end diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb index f42355122..90dfd49da 100644 --- a/lib/puppet/configurer.rb +++ b/lib/puppet/configurer.rb @@ -1,308 +1,304 @@ # The client for interacting with the puppetmaster config server. require 'sync' require 'timeout' require 'puppet/network/http_pool' require 'puppet/util' require 'securerandom' class Puppet::Configurer require 'puppet/configurer/fact_handler' require 'puppet/configurer/plugin_handler' require 'puppet/configurer/downloader_factory' include Puppet::Configurer::FactHandler # For benchmarking include Puppet::Util attr_reader :compile_time, :environment # Provide more helpful strings to the logging that the Agent does def self.to_s "Puppet configuration client" end def execute_postrun_command execute_from_setting(:postrun_command) end def execute_prerun_command execute_from_setting(:prerun_command) end # Initialize and load storage def init_storage Puppet::Util::Storage.load @compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time] rescue => detail Puppet.log_exception(detail, "Removing corrupt state file #{Puppet[:statefile]}: #{detail}") begin Puppet::FileSystem.unlink(Puppet[:statefile]) retry rescue => detail raise Puppet::Error.new("Cannot remove #{Puppet[:statefile]}: #{detail}", detail) end end def initialize(factory = Puppet::Configurer::DownloaderFactory.new) Puppet.settings.use(:main, :ssl, :agent) @running = false @splayed = false @environment = Puppet[:environment] @transaction_uuid = SecureRandom.uuid @handler = Puppet::Configurer::PluginHandler.new(factory) end # Get the remote catalog, yo. Returns nil if no catalog can be found. def retrieve_catalog(query_options) query_options ||= {} # First try it with no cache, then with the cache. unless (Puppet[:use_cached_catalog] and result = retrieve_catalog_from_cache(query_options)) or result = retrieve_new_catalog(query_options) if ! Puppet[:usecacheonfailure] Puppet.warning "Not using cache on failed catalog" return nil end result = retrieve_catalog_from_cache(query_options) end return nil unless result convert_catalog(result, @duration) end # Convert a plain resource catalog into our full host catalog. def convert_catalog(result, duration) catalog = result.to_ral catalog.finalize catalog.retrieval_duration = duration catalog.write_class_file catalog.write_resource_file catalog end def get_facts(options) if options[:pluginsync] remote_environment_for_plugins = Puppet::Node::Environment.remote(@environment) download_plugins(remote_environment_for_plugins) end facts_hash = {} if Puppet::Resource::Catalog.indirection.terminus_class == :rest # This is a bit complicated. We need the serialized and escaped facts, # and we need to know which format they're encoded in. Thus, we # get a hash with both of these pieces of information. # # facts_for_uploading may set Puppet[:node_name_value] as a side effect facts_hash = facts_for_uploading end facts_hash end def prepare_and_retrieve_catalog(options, query_options) # set report host name now that we have the fact options[:report].host = Puppet[:node_name_value] unless catalog = (options.delete(:catalog) || retrieve_catalog(query_options)) Puppet.err "Could not retrieve catalog; skipping run" return end catalog end # Retrieve (optionally) and apply a catalog. If a catalog is passed in # the options, then apply that one, otherwise retrieve it. def apply_catalog(catalog, options) report = options[:report] report.configuration_version = catalog.version benchmark(:notice, "Applied catalog") do catalog.apply(options) end report.finalize_report report end # The code that actually runs the catalog. # This just passes any options on to the catalog, # which accepts :tags and :ignoreschedules. def run(options = {}) pool = Puppet::Network::HTTP::Pool.new(Puppet[:http_keepalive_timeout]) begin Puppet.override(:http_pool => pool) do run_internal(options) end ensure pool.close end end def run_internal(options) # We create the report pre-populated with default settings for # environment and transaction_uuid very early, this is to ensure # they are sent regardless of any catalog compilation failures or # exceptions. options[:report] ||= Puppet::Transaction::Report.new("apply", nil, @environment, @transaction_uuid) report = options[:report] init_storage Puppet::Util::Log.newdestination(report) begin unless Puppet[:node_name_fact].empty? query_options = get_facts(options) end # We only need to find out the environment to run in if we don't already have a catalog unless options[:catalog] begin if node = Puppet::Node.indirection.find(Puppet[:node_name_value], :environment => Puppet::Node::Environment.remote(@environment), :ignore_cache => true, :transaction_uuid => @transaction_uuid, :fail_on_404 => true) # If we have deserialized a node from a rest call, we want to set # an environment instance as a simple 'remote' environment reference. if !node.has_environment_instance? && node.environment_name node.environment = Puppet::Node::Environment.remote(node.environment_name) end if node.environment.to_s != @environment Puppet.warning "Local environment: \"#{@environment}\" doesn't match server specified node environment \"#{node.environment}\", switching agent to \"#{node.environment}\"." @environment = node.environment.to_s report.environment = @environment query_options = nil end end - rescue SystemExit,NoMemoryError - raise - rescue Exception => detail + rescue StandardError => detail Puppet.warning("Unable to fetch my node definition, but the agent run will continue:") Puppet.warning(detail) end end current_environment = Puppet.lookup(:current_environment) local_node_environment = if current_environment.name == @environment.intern current_environment else Puppet::Node::Environment.create(@environment, current_environment.modulepath, current_environment.manifest, current_environment.config_version) end Puppet.push_context({:current_environment => local_node_environment}, "Local node environment for configurer transaction") query_options = get_facts(options) unless query_options query_options[:transaction_uuid] = @transaction_uuid unless catalog = prepare_and_retrieve_catalog(options, query_options) return nil end # Here we set the local environment based on what we get from the # catalog. Since a change in environment means a change in facts, and # facts may be used to determine which catalog we get, we need to # rerun the process if the environment is changed. tries = 0 while catalog.environment and not catalog.environment.empty? and catalog.environment != @environment if tries > 3 raise Puppet::Error, "Catalog environment didn't stabilize after #{tries} fetches, aborting run" end Puppet.warning "Local environment: \"#{@environment}\" doesn't match server specified environment \"#{catalog.environment}\", restarting agent run with environment \"#{catalog.environment}\"" @environment = catalog.environment report.environment = @environment query_options = get_facts(options) query_options[:transaction_uuid] = @transaction_uuid return nil unless catalog = prepare_and_retrieve_catalog(options, query_options) tries += 1 end execute_prerun_command or return nil apply_catalog(catalog, options) report.exit_status rescue => detail Puppet.log_exception(detail, "Failed to apply catalog: #{detail}") return nil ensure execute_postrun_command or return nil end ensure # Between Puppet runs we need to forget the cached values. This lets us # pick up on new functions installed by gems or new modules being added # without the daemon being restarted. $env_module_directories = nil Puppet::Util::Log.close(report) send_report(report) Puppet.pop_context end private :run_internal def send_report(report) puts report.summary if Puppet[:summarize] save_last_run_summary(report) Puppet::Transaction::Report.indirection.save(report, nil, :environment => Puppet::Node::Environment.remote(@environment)) if Puppet[:report] rescue => detail Puppet.log_exception(detail, "Could not send report: #{detail}") end def save_last_run_summary(report) mode = Puppet.settings.setting(:lastrunfile).mode Puppet::Util.replace_file(Puppet[:lastrunfile], mode) do |fh| fh.print YAML.dump(report.raw_summary) end rescue => detail Puppet.log_exception(detail, "Could not save last run local report: #{detail}") end private def execute_from_setting(setting) return true if (command = Puppet[setting]) == "" begin Puppet::Util::Execution.execute([command]) true rescue => detail Puppet.log_exception(detail, "Could not run command from #{setting}: #{detail}") false end end def retrieve_catalog_from_cache(query_options) result = nil @duration = thinmark do result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], query_options.merge(:ignore_terminus => true, :environment => Puppet::Node::Environment.remote(@environment))) end Puppet.notice "Using cached catalog" result rescue => detail Puppet.log_exception(detail, "Could not retrieve catalog from cache: #{detail}") return nil end def retrieve_new_catalog(query_options) result = nil @duration = thinmark do result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], query_options.merge(:ignore_cache => true, :environment => Puppet::Node::Environment.remote(@environment), :fail_on_404 => true)) end result - rescue SystemExit,NoMemoryError - raise - rescue Exception => detail + rescue StandardError => detail Puppet.log_exception(detail, "Could not retrieve catalog from remote server: #{detail}") return nil end def download_plugins(remote_environment_for_plugins) @handler.download_plugins(remote_environment_for_plugins) end end diff --git a/lib/puppet/face/help.rb b/lib/puppet/face/help.rb index e9dfe9037..4fa555e82 100644 --- a/lib/puppet/face/help.rb +++ b/lib/puppet/face/help.rb @@ -1,194 +1,193 @@ require 'puppet/face' require 'puppet/application/face_base' require 'puppet/util/constant_inflector' require 'pathname' require 'erb' Puppet::Face.define(:help, '0.0.1') do copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" summary "Display Puppet help." action(:help) do summary "Display help about Puppet subcommands and their actions." arguments "[] []" returns "Short help text for the specified subcommand or action." examples <<-'EOT' Get help for an action: $ puppet help EOT option "--version VERSION" do summary "The version of the subcommand for which to show help." end default when_invoked do |*args| # Check our invocation, because we want varargs and can't do defaults # yet. REVISIT: when we do option defaults, and positional options, we # should rewrite this to use those. --daniel 2011-04-04 options = args.pop if options.nil? or args.length > 2 then if args.select { |x| x == 'help' }.length > 2 then c = "\n %'(),-./=ADEFHILORSTUXY\\_`gnv|".split('') i = <<-'EOT'.gsub(/\s*/, '').to_i(36) 3he6737w1aghshs6nwrivl8mz5mu9nywg9tbtlt081uv6fq5kvxse1td3tj1wvccmte806nb cy6de2ogw0fqjymbfwi6a304vd56vlq71atwmqsvz3gpu0hj42200otlycweufh0hylu79t3 gmrijm6pgn26ic575qkexyuoncbujv0vcscgzh5us2swklsp5cqnuanlrbnget7rt3956kam j8adhdrzqqt9bor0cv2fqgkloref0ygk3dekiwfj1zxrt13moyhn217yy6w4shwyywik7w0l xtuevmh0m7xp6eoswin70khm5nrggkui6z8vdjnrgdqeojq40fya5qexk97g4d8qgw0hvokr pli1biaz503grqf2ycy0ppkhz1hwhl6ifbpet7xd6jjepq4oe0ofl575lxdzjeg25217zyl4 nokn6tj5pq7gcdsjre75rqylydh7iia7s3yrko4f5ud9v8hdtqhu60stcitirvfj6zphppmx 7wfm7i9641d00bhs44n6vh6qvx39pg3urifgr6ihx3e0j1ychzypunyou7iplevitkyg6gbg wm08oy1rvogcjakkqc1f7y1awdfvlb4ego8wrtgu9vzw4vmj59utwifn2ejcs569dh1oaavi sc581n7jjg1dugzdu094fdobtx6rsvk3sfctvqnr36xctold EOT 353.times{i,x=i.divmod(1184);a,b=x.divmod(37);print(c[a]*b)} end raise ArgumentError, "Puppet help only takes two (optional) arguments: a subcommand and an action" end version = :current if options.has_key? :version then if options[:version].to_s !~ /^current$/i then version = options[:version] else if args.length == 0 then raise ArgumentError, "Version only makes sense when a Faces subcommand is given" end end end return erb('global.erb').result(binding) if args.empty? facename, actionname = args if legacy_applications.include? facename then if actionname then raise ArgumentError, "Legacy subcommands don't take actions" end return render_application_help(facename) else return render_face_help(facename, actionname, version) end end end def render_application_help(applicationname) return Puppet::Application[applicationname].help end def render_face_help(facename, actionname, version) face, action = load_face_help(facename, actionname, version) return template_for(face, action).result(binding) end def load_face_help(facename, actionname, version) begin face = Puppet::Face[facename.to_sym, version] rescue Puppet::Error => detail msg = <<-MSG Could not load help for the face #{facename}. Please check the error logs for more information. Detail: "#{detail.message}" MSG fail ArgumentError, msg, detail.backtrace end if actionname action = face.get_action(actionname.to_sym) if not action fail ArgumentError, "Unable to load action #{actionname} from #{face}" end end [face, action] end def template_for(face, action) if action.nil? erb('face.erb') else erb('action.erb') end end def erb(name) template = (Pathname(__FILE__).dirname + "help" + name) erb = ERB.new(template.read, nil, '-') erb.filename = template.to_s return erb end # Return a list of applications that are not simply just stubs for Faces. def legacy_applications Puppet::Application.available_application_names.reject do |appname| (is_face_app?(appname)) or (exclude_from_docs?(appname)) end.sort end # Return a list of all applications (both legacy and Face applications), along with a summary # of their functionality. # @return [Array] An Array of Arrays. The outer array contains one entry per application; each # element in the outer array is a pair whose first element is a String containing the application # name, and whose second element is a String containing the summary for that application. def all_application_summaries() Puppet::Application.available_application_names.sort.inject([]) do |result, appname| next result if exclude_from_docs?(appname) if (is_face_app?(appname)) begin face = Puppet::Face[appname, :current] result << [appname, face.summary] rescue Puppet::Error result << [ "! #{appname}", "! Subcommand unavailable due to error. Check error logs." ] end else result << [appname, horribly_extract_summary_from(appname)] end end end def horribly_extract_summary_from(appname) begin help = Puppet::Application[appname].help.split("\n") # Now we find the line with our summary, extract it, and return it. This # depends on the implementation coincidence of how our pages are # formatted. If we can't match the pattern we expect we return the empty # string to ensure we don't blow up in the summary. --daniel 2011-04-11 while line = help.shift do if md = /^puppet-#{appname}\([^\)]+\) -- (.*)$/.match(line) then return md[1] end end - rescue Exception - # Damn, but I hate this: we just ignore errors here, no matter what - # class they are. Meh. + rescue StandardError + result << [ "! #{appname}", "! Subcommand unavailable due to error. Check error logs." ] end return '' end # This should absolutely be a private method, but for some reason it appears # that you can't use the 'private' keyword inside of a Face definition. # See #14205. #private :horribly_extract_summary_from def exclude_from_docs?(appname) %w{face_base indirection_base}.include? appname end # This should absolutely be a private method, but for some reason it appears # that you can't use the 'private' keyword inside of a Face definition. # See #14205. #private :exclude_from_docs? def is_face_app?(appname) clazz = Puppet::Application.find(appname) clazz.ancestors.include?(Puppet::Application::FaceBase) end # This should probably be a private method, but for some reason it appears # that you can't use the 'private' keyword inside of a Face definition. # See #14205. #private :is_face_app? end diff --git a/lib/puppet/network/http/api/v1.rb b/lib/puppet/network/http/api/v1.rb index 67623e534..dbde63f25 100644 --- a/lib/puppet/network/http/api/v1.rb +++ b/lib/puppet/network/http/api/v1.rb @@ -1,220 +1,220 @@ require 'puppet/network/authorization' class Puppet::Network::HTTP::API::V1 include Puppet::Network::Authorization # How we map http methods and the indirection name in the URI # to an indirection method. METHOD_MAP = { "GET" => { :plural => :search, :singular => :find }, "POST" => { :singular => :find, }, "PUT" => { :singular => :save }, "DELETE" => { :singular => :destroy }, "HEAD" => { :singular => :head } } def self.routes Puppet::Network::HTTP::Route.path(/.*/).any(new) end # handle an HTTP request def call(request, response) indirection_name, method, key, params = uri2indirection(request.method, request.path, request.params) certificate = request.client_cert check_authorization(method, "/#{indirection_name}/#{key}", params) indirection = Puppet::Indirector::Indirection.instance(indirection_name.to_sym) raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless indirection if !indirection.allow_remote_requests? # TODO: should we tell the user we found an indirection but it doesn't # allow remote requests, or just pretend there's no handler at all? what # are the security implications for the former? raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("No handler for #{indirection.name}", :NO_INDIRECTION_REMOTE_REQUESTS) end trusted = Puppet::Context::TrustedInformation.remote(params[:authenticated], params[:node], certificate) Puppet.override(:trusted_information => trusted) do send("do_#{method}", indirection, key, params, request, response) end rescue Puppet::Network::HTTP::Error::HTTPError => e return do_http_control_exception(response, e) - rescue Exception => e + rescue StandardError => e return do_exception(response, e) end 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 '#{environment}'" unless Puppet::Node::Environment.valid_name?(environment) raise ArgumentError, "The indirection name must be purely alphanumeric, not '#{indirection}'" unless indirection =~ /^\w+$/ method = indirection_method(http_method, indirection) configured_environment = Puppet.lookup(:environments).get(environment) if configured_environment.nil? raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find environment '#{environment}'", Puppet::Network::HTTP::Issues::ENVIRONMENT_NOT_FOUND) else configured_environment = configured_environment.override_from_commandline(Puppet.settings) params[:environment] = configured_environment end params.delete(:bucket_path) raise ArgumentError, "No request key specified in #{uri}" if key == "" or key.nil? key = URI.unescape(key) [indirection, method, key, params] end private def do_http_control_exception(response, exception) msg = exception.message Puppet.info(msg) response.respond_with(exception.status, "text/plain", msg) end def do_exception(response, exception, status=400) if exception.is_a?(Puppet::Network::AuthorizationError) # make sure we return the correct status code # for authorization issues status = 403 if status == 400 end Puppet.log_exception(exception) response.respond_with(status, "text/plain", exception.to_s) end # Execute our find. def do_find(indirection, key, params, request, response) unless result = indirection.find(key, params) raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find #{indirection.name} #{key}", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND) end format = accepted_response_formatter_for(indirection.model, request) rendered_result = result if result.respond_to?(:render) Puppet::Util::Profiler.profile("Rendered result in #{format}", [:http, :v1_render, format]) do rendered_result = result.render(format) end end Puppet::Util::Profiler.profile("Sent response", [:http, :v1_response]) do response.respond_with(200, format, rendered_result) end end # Execute our head. def do_head(indirection, key, params, request, response) unless indirection.head(key, params) raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find #{indirection.name} #{key}", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND) end # No need to set a response because no response is expected from a # HEAD request. All we need to do is not die. end # Execute our search. def do_search(indirection, key, params, request, response) result = indirection.search(key, params) if result.nil? raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find instances in #{indirection.name} with '#{key}'", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND) end format = accepted_response_formatter_for(indirection.model, request) response.respond_with(200, format, indirection.model.render_multiple(format, result)) end # Execute our destroy. def do_destroy(indirection, key, params, request, response) formatter = accepted_response_formatter_or_pson_for(indirection.model, request) result = indirection.destroy(key, params) response.respond_with(200, formatter, formatter.render(result)) end # Execute our save. def do_save(indirection, key, params, request, response) formatter = accepted_response_formatter_or_pson_for(indirection.model, request) sent_object = read_body_into_model(indirection.model, request) result = indirection.save(sent_object, key) response.respond_with(200, formatter, formatter.render(result)) end def accepted_response_formatter_for(model_class, request) accepted_formats = request.headers['accept'] or raise Puppet::Network::HTTP::Error::HTTPNotAcceptableError.new("Missing required Accept header", Puppet::Network::HTTP::Issues::MISSING_HEADER_FIELD) request.response_formatter_for(model_class.supported_formats, accepted_formats) end def accepted_response_formatter_or_pson_for(model_class, request) accepted_formats = request.headers['accept'] || "text/pson" request.response_formatter_for(model_class.supported_formats, accepted_formats) end def read_body_into_model(model_class, request) data = request.body.to_s format = request.format model_class.convert_from(format, data) end def indirection_method(http_method, indirection) raise ArgumentError, "No support for http method #{http_method}" unless METHOD_MAP[http_method] unless method = METHOD_MAP[http_method][plurality(indirection)] raise ArgumentError, "No support for plurality #{plurality(indirection)} for #{http_method} operations" end method end def self.indirection2uri(request) 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 self.request_to_uri_and_body(request) 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.sub(/^\?/,'')] end def self.pluralize(indirection) return(indirection == "status" ? "statuses" : indirection + "s") end def plurality(indirection) # NOTE These specific hooks for paths are 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 == "status" return :singular if indirection == "certificate_status" result = (indirection =~ /s$|_search$/) ? :plural : :singular indirection.sub!(/s$|_search$/, '') indirection.sub!(/statuse$/, 'status') result end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index fe9793e71..e26973e76 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,179 +1,179 @@ module Puppet::Network::HTTP end require 'puppet/network/http' require 'puppet/network/http/api/v1' require 'puppet/network/rights' require 'puppet/util/profiler' require 'puppet/util/profiler/aggregate' require 'resolv' module Puppet::Network::HTTP::Handler include Puppet::Network::HTTP::Issues # These shouldn't be allowed to be set by clients # in the query string, for security reasons. DISALLOWED_KEYS = ["node", "ip"] def register(routes) # There's got to be a simpler way to do this, right? dupes = {} routes.each { |r| dupes[r.path_matcher] = (dupes[r.path_matcher] || 0) + 1 } dupes = dupes.collect { |pm, count| pm if count > 1 }.compact if dupes.count > 0 raise ArgumentError, "Given multiple routes with identical path regexes: #{dupes.map{ |rgx| rgx.inspect }.join(', ')}" end @routes = routes Puppet.debug("Routes Registered:") @routes.each do |route| Puppet.debug(route.inspect) end end # Retrieve all headers from the http request, as a hash with the header names # (lower-cased) as the keys def headers(request) raise NotImplementedError end def format_to_mime(format) format.is_a?(Puppet::Network::Format) ? format.mime : format end # handle an HTTP request def process(request, response) new_response = Puppet::Network::HTTP::Response.new(self, response) request_headers = headers(request) request_params = params(request) request_method = http_method(request) request_path = path(request) new_request = Puppet::Network::HTTP::Request.new(request_headers, request_params, request_method, request_path, request_path, client_cert(request), body(request)) response[Puppet::Network::HTTP::HEADER_PUPPET_VERSION] = Puppet.version profiler = configure_profiler(request_headers, request_params) Puppet::Util::Profiler.profile("Processed request #{request_method} #{request_path}", [:http, request_method, request_path]) do if route = @routes.find { |route| route.matches?(new_request) } route.process(new_request, new_response) else raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("No route for #{new_request.method} #{new_request.path}", HANDLER_NOT_FOUND) end end rescue Puppet::Network::HTTP::Error::HTTPError => e Puppet.info(e.message) new_response.respond_with(e.status, "application/json", e.to_json) - rescue Exception => e + rescue StandardError => e http_e = Puppet::Network::HTTP::Error::HTTPServerError.new(e) Puppet.err(http_e.message) new_response.respond_with(http_e.status, "application/json", http_e.to_json) ensure if profiler remove_profiler(profiler) end cleanup(request) end # Set the response up, with the body and status. def set_response(response, body, status = 200) raise NotImplementedError end # Set the specified format as the content type of the response. def set_content_type(response, format) raise NotImplementedError end # resolve node name from peer's ip address # this is used when the request is unauthenticated def resolve_node(result) begin return Resolv.getname(result[:ip]) rescue => detail Puppet.err "Could not resolve #{result[:ip]}: #{detail}" end result[:ip] end private # methods to be overridden by the including web server class def http_method(request) raise NotImplementedError end def path(request) raise NotImplementedError end def request_key(request) raise NotImplementedError end def body(request) raise NotImplementedError end def params(request) raise NotImplementedError end def client_cert(request) raise NotImplementedError end def cleanup(request) # By default, there is nothing to cleanup. end def decode_params(params) params.select { |key, _| allowed_parameter?(key) }.inject({}) do |result, ary| param, value = ary result[param.to_sym] = parse_parameter_value(param, value) result end end def allowed_parameter?(name) not (name.nil? || name.empty? || DISALLOWED_KEYS.include?(name)) end def parse_parameter_value(param, value) if value.is_a?(Array) value.collect { |v| parse_primitive_parameter_value(v) } else parse_primitive_parameter_value(value) end end def parse_primitive_parameter_value(value) case value when "true" true when "false" false when /^\d+$/ Integer(value) when /^\d+\.\d+$/ value.to_f else value end end def configure_profiler(request_headers, request_params) if (request_headers.has_key?(Puppet::Network::HTTP::HEADER_ENABLE_PROFILING.downcase) or Puppet[:profile]) Puppet::Util::Profiler.add_profiler(Puppet::Util::Profiler::Aggregate.new(Puppet.method(:debug), request_params.object_id)) end end def remove_profiler(profiler) profiler.shutdown Puppet::Util::Profiler.remove_profiler(profiler) end end diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index 5f11c34c2..31b1a6bdc 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -1,509 +1,505 @@ # # Copyright 2011 Bryan Kearney # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require 'augeas' if Puppet.features.augeas? require 'strscan' require 'puppet/util' require 'puppet/util/diff' require 'puppet/util/package' Puppet::Type.type(:augeas).provide(:augeas) do include Puppet::Util include Puppet::Util::Diff include Puppet::Util::Package confine :feature => :augeas has_features :parse_commands, :need_to_run?,:execute_changes SAVE_NOOP = "noop" SAVE_OVERWRITE = "overwrite" SAVE_NEWFILE = "newfile" SAVE_BACKUP = "backup" COMMANDS = { "set" => [ :path, :string ], "setm" => [ :path, :string, :string ], "rm" => [ :path ], "clear" => [ :path ], "clearm" => [ :path, :string ], "mv" => [ :path, :path ], "insert" => [ :string, :string, :path ], "get" => [ :path, :comparator, :string ], "defvar" => [ :string, :path ], "defnode" => [ :string, :path, :string ], "match" => [ :path, :glob ], "size" => [:comparator, :int], "include" => [:string], "not_include" => [:string], "==" => [:glob], "!=" => [:glob] } COMMANDS["ins"] = COMMANDS["insert"] COMMANDS["remove"] = COMMANDS["rm"] COMMANDS["move"] = COMMANDS["mv"] attr_accessor :aug # Extracts an 2 dimensional array of commands which are in the # form of command path value. # The input can be # - A string with one command # - A string with many commands per line # - An array of strings. def parse_commands(data) context = resource[:context] # Add a trailing / if it is not there if (context.length > 0) context << "/" if context[-1, 1] != "/" end data = data.split($/) if data.is_a?(String) data = data.flatten args = [] data.each do |line| line.strip! next if line.nil? || line.empty? argline = [] sc = StringScanner.new(line) cmd = sc.scan(/\w+|==|!=/) formals = COMMANDS[cmd] fail("Unknown command #{cmd}") unless formals argline << cmd narg = 0 formals.each do |f| sc.skip(/\s+/) narg += 1 if f == :path start = sc.pos nbracket = 0 inSingleTick = false inDoubleTick = false begin sc.skip(/([^\]\[\s\\'"]|\\.)+/) ch = sc.getch nbracket += 1 if ch == "[" nbracket -= 1 if ch == "]" inSingleTick = !inSingleTick if ch == "'" inDoubleTick = !inDoubleTick if ch == "\"" fail("unmatched [") if nbracket < 0 end until ((nbracket == 0 && !inSingleTick && !inDoubleTick && (ch =~ /\s/)) || sc.eos?) len = sc.pos - start len -= 1 unless sc.eos? unless p = sc.string[start, len] fail("missing path argument #{narg} for #{cmd}") end # Rip off any ticks if they are there. p = p[1, (p.size - 2)] if p[0,1] == "'" || p[0,1] == "\"" p.chomp!("/") if p[0,1] != '$' && p[0,1] != "/" argline << context + p else argline << p end elsif f == :string delim = sc.peek(1) if delim == "'" || delim == "\"" sc.getch argline << sc.scan(/([^\\#{delim}]|(\\.))*/) sc.getch else argline << sc.scan(/[^\s]+/) end fail("missing string argument #{narg} for #{cmd}") unless argline[-1] elsif f == :comparator argline << sc.scan(/(==|!=|=~|<=|>=|<|>)/) unless argline[-1] puts sc.rest fail("invalid comparator for command #{cmd}") end elsif f == :int argline << sc.scan(/\d+/).to_i elsif f== :glob argline << sc.rest end end args << argline end args end def open_augeas unless @aug flags = Augeas::NONE flags = Augeas::TYPE_CHECK if resource[:type_check] == :true if resource[:incl] flags |= Augeas::NO_MODL_AUTOLOAD else flags |= Augeas::NO_LOAD end root = resource[:root] load_path = get_load_path(resource) debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}") @aug = Augeas::open(root, load_path,flags) debug("Augeas version #{get_augeas_version} is installed") if versioncmp(get_augeas_version, "0.3.6") >= 0 # Optimize loading if the context is given and it's a simple path, # requires the glob function from Augeas 0.8.2 or up glob_avail = !aug.match("/augeas/version/pathx/functions/glob").empty? opt_ctx = resource[:context].match("^/files/[^'\"\\[\\]]+$") if resource[:context] restricted = false if resource[:incl] aug.set("/augeas/load/Xfm/lens", resource[:lens]) aug.set("/augeas/load/Xfm/incl", resource[:incl]) restricted_metadata = "/augeas//error" elsif glob_avail and opt_ctx # Optimize loading if the context is given, requires the glob function # from Augeas 0.8.2 or up ctx_path = resource[:context].sub(/^\/files(.*?)\/?$/, '\1/') load_path = "/augeas/load/*['%s' !~ glob(incl) + regexp('/.*')]" % ctx_path if aug.match(load_path).size < aug.match("/augeas/load/*").size aug.rm(load_path) restricted_metadata = "/augeas/files#{ctx_path}/error" else # This will occur if the context is less specific than any glob debug("Unable to optimize files loaded by context path, no glob matches") end end aug.load print_load_errors(restricted_metadata) end @aug end def close_augeas if @aug @aug.close debug("Closed the augeas connection") @aug = nil end end def is_numeric?(s) case s when Fixnum true when String s.match(/\A[+-]?\d+?(\.\d+)?\Z/n) == nil ? false : true else false end end # Used by the need_to_run? method to process get filters. Returns # true if there is a match, false if otherwise # Assumes a syntax of get /files/path [COMPARATOR] value def process_get(cmd_array) return_value = false #validate and tear apart the command fail ("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 4 cmd = cmd_array.shift path = cmd_array.shift comparator = cmd_array.shift arg = cmd_array.join(" ") #check the value in augeas result = @aug.get(path) || '' if ['<', '<=', '>=', '>'].include? comparator and is_numeric?(result) and is_numeric?(arg) resultf = result.to_f argf = arg.to_f return_value = (resultf.send(comparator, argf)) elsif comparator == "!=" return_value = (result != arg) elsif comparator == "=~" regex = Regexp.new(arg) return_value = (result =~ regex) else return_value = (result.send(comparator, arg)) end !!return_value end # Used by the need_to_run? method to process match filters. Returns # true if there is a match, false if otherwise def process_match(cmd_array) return_value = false #validate and tear apart the command fail("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 3 cmd = cmd_array.shift path = cmd_array.shift # Need to break apart the clause clause_array = parse_commands(cmd_array.shift)[0] verb = clause_array.shift #Get the values from augeas result = @aug.match(path) || [] fail("Error trying to match path '#{path}'") if (result == -1) # Now do the work case verb when "size" fail("Invalid command: #{cmd_array.join(" ")}") if clause_array.length != 2 comparator = clause_array.shift arg = clause_array.shift case comparator when "!=" return_value = !(result.size.send(:==, arg)) else return_value = (result.size.send(comparator, arg)) end when "include" arg = clause_array.shift return_value = result.include?(arg) when "not_include" arg = clause_array.shift return_value = !result.include?(arg) when "==" begin arg = clause_array.shift new_array = eval arg return_value = (result == new_array) rescue fail("Invalid array in command: #{cmd_array.join(" ")}") end when "!=" begin arg = clause_array.shift new_array = eval arg return_value = (result != new_array) rescue fail("Invalid array in command: #{cmd_array.join(" ")}") end end !!return_value end # Generate lens load paths from user given paths and local pluginsync dir def get_load_path(resource) load_path = [] # Permits colon separated strings or arrays if resource[:load_path] load_path = [resource[:load_path]].flatten load_path.map! { |path| path.split(/:/) } load_path.flatten! end if Puppet::FileSystem.exist?("#{Puppet[:libdir]}/augeas/lenses") load_path << "#{Puppet[:libdir]}/augeas/lenses" end load_path.join(":") end def get_augeas_version @aug.get("/augeas/version") || "" end def set_augeas_save_mode(mode) @aug.set("/augeas/save", mode) end def print_load_errors(path) errors = @aug.match("/augeas//error") unless errors.empty? if path && !@aug.match(path).empty? warning("Loading failed for one or more files, see debug for /augeas//error output") else debug("Loading failed for one or more files, output from /augeas//error:") end end print_errors(errors) end def print_put_errors errors = @aug.match("/augeas//error[. = 'put_failed']") debug("Put failed on one or more files, output from /augeas//error:") unless errors.empty? print_errors(errors) end def print_errors(errors) errors.each do |errnode| error = @aug.get(errnode) debug("#{errnode} = #{error}") unless error.nil? @aug.match("#{errnode}/*").each do |subnode| subvalue = @aug.get(subnode) debug("#{subnode} = #{subvalue}") end end end # Determines if augeas actually needs to run. def need_to_run? force = resource[:force] return_value = true begin open_augeas filter = resource[:onlyif] unless filter == "" cmd_array = parse_commands(filter)[0] command = cmd_array[0]; begin case command when "get"; return_value = process_get(cmd_array) when "match"; return_value = process_match(cmd_array) end - rescue SystemExit,NoMemoryError - raise - rescue Exception => e + rescue StandardError => e fail("Error sending command '#{command}' with params #{cmd_array[1..-1].inspect}/#{e.message}") end end unless force # If we have a verison of augeas which is at least 0.3.6 then we # can make the changes now and see if changes were made. if return_value and versioncmp(get_augeas_version, "0.3.6") >= 0 debug("Will attempt to save and only run if files changed") # Execute in NEWFILE mode so we can show a diff set_augeas_save_mode(SAVE_NEWFILE) do_execute_changes save_result = @aug.save unless save_result print_put_errors fail("Saving failed, see debug") end saved_files = @aug.match("/augeas/events/saved") if saved_files.size > 0 root = resource[:root].sub(/^\/$/, "") saved_files.map! {|key| @aug.get(key).sub(/^\/files/, root) } saved_files.uniq.each do |saved_file| if Puppet[:show_diff] && @resource[:show_diff] self.send(@resource[:loglevel], "\n" + diff(saved_file, saved_file + ".augnew")) end File.delete(saved_file + ".augnew") end debug("Files changed, should execute") return_value = true else debug("Skipping because no files were changed") return_value = false end end end ensure if not return_value or resource.noop? or not save_result close_augeas end end return_value end def execute_changes # Workaround Augeas bug where changing the save mode doesn't trigger a # reload of the previously saved file(s) when we call Augeas#load @aug.match("/augeas/events/saved").each do |file| @aug.rm("/augeas#{@aug.get(file)}/mtime") end # Reload augeas, and execute the changes for real set_augeas_save_mode(SAVE_OVERWRITE) if versioncmp(get_augeas_version, "0.3.6") >= 0 @aug.load do_execute_changes unless @aug.save print_put_errors fail("Save failed, see debug") end :executed ensure close_augeas end # Actually execute the augeas changes. def do_execute_changes commands = parse_commands(resource[:changes]) commands.each do |cmd_array| fail("invalid command #{cmd_array.join[" "]}") if cmd_array.length < 2 command = cmd_array[0] cmd_array.shift begin case command when "set" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.set(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "setm" if aug.respond_to?(command) debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.setm(cmd_array[0], cmd_array[1], cmd_array[2]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) else fail("command '#{command}' not supported in installed version of ruby-augeas") end when "rm", "remove" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.rm(cmd_array[0]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) when "clear" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.clear(cmd_array[0]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "clearm" # Check command exists ... doesn't currently in ruby-augeas 0.4.1 if aug.respond_to?(command) debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.clearm(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) else fail("command '#{command}' not supported in installed version of ruby-augeas") end when "insert", "ins" label = cmd_array[0] where = cmd_array[1] path = cmd_array[2] case where when "before"; before = true when "after"; before = false else fail("Invalid value '#{where}' for where param") end debug("sending command '#{command}' with params #{[label, where, path].inspect}") rv = aug.insert(path, label, before) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) when "defvar" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.defvar(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "defnode" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.defnode(cmd_array[0], cmd_array[1], cmd_array[2]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv) when "mv", "move" debug("sending command '#{command}' with params #{cmd_array.inspect}") rv = aug.mv(cmd_array[0], cmd_array[1]) fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1) else fail("Command '#{command}' is not supported") end - rescue SystemExit,NoMemoryError - raise - rescue Exception => e + rescue StandardError => e fail("Error sending command '#{command}' with params #{cmd_array.inspect}/#{e.message}") end end end end diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb index c680546a1..94b210f2a 100644 --- a/lib/puppet/ssl/host.rb +++ b/lib/puppet/ssl/host.rb @@ -1,367 +1,365 @@ require 'puppet/indirector' require 'puppet/ssl' require 'puppet/ssl/key' require 'puppet/ssl/certificate' require 'puppet/ssl/certificate_request' require 'puppet/ssl/certificate_revocation_list' require 'puppet/ssl/certificate_request_attributes' # The class that manages all aspects of our SSL certificates -- # private keys, public keys, requests, etc. class Puppet::SSL::Host # Yay, ruby's strange constant lookups. Key = Puppet::SSL::Key CA_NAME = Puppet::SSL::CA_NAME Certificate = Puppet::SSL::Certificate CertificateRequest = Puppet::SSL::CertificateRequest CertificateRevocationList = Puppet::SSL::CertificateRevocationList extend Puppet::Indirector indirects :certificate_status, :terminus_class => :file, :doc => < :file, :disabled_ca => nil, :file => nil, :rest => :rest} if term = host_map[terminus] self.indirection.terminus_class = term else self.indirection.reset_terminus_class end if cache # This is weird; we don't actually cache our keys, we # use what would otherwise be the cache as our normal # terminus. Key.indirection.terminus_class = cache else Key.indirection.terminus_class = terminus end if cache Certificate.indirection.cache_class = cache CertificateRequest.indirection.cache_class = cache CertificateRevocationList.indirection.cache_class = cache else # Make sure we have no cache configured. puppet master # switches the configurations around a bit, so it's important # that we specify the configs for absolutely everything, every # time. Certificate.indirection.cache_class = nil CertificateRequest.indirection.cache_class = nil CertificateRevocationList.indirection.cache_class = nil end end CA_MODES = { # Our ca is local, so we use it as the ultimate source of information # And we cache files locally. :local => [:ca, :file], # We're a remote CA client. :remote => [:rest, :file], # We are the CA, so we don't have read/write access to the normal certificates. :only => [:ca], # We have no CA, so we just look in the local file store. :none => [:disabled_ca] } # Specify how we expect to interact with our certificate authority. def self.ca_location=(mode) modes = CA_MODES.collect { |m, vals| m.to_s }.join(", ") raise ArgumentError, "CA Mode can only be one of: #{modes}" unless CA_MODES.include?(mode) @ca_location = mode configure_indirection(*CA_MODES[@ca_location]) end # Puppet::SSL::Host is actually indirected now so the original implementation # has been moved into the certificate_status indirector. This method is in-use # in `puppet cert -c `. def self.destroy(name) indirection.destroy(name) end def self.from_data_hash(data) instance = new(data["name"]) if data["desired_state"] instance.desired_state = data["desired_state"] end instance end # Puppet::SSL::Host is actually indirected now so the original implementation # has been moved into the certificate_status indirector. This method does not # appear to be in use in `puppet cert -l`. def self.search(options = {}) indirection.search("*", options) end # Is this a ca host, meaning that all of its files go in the CA location? def ca? ca end def key @key ||= Key.indirection.find(name) end # This is the private key; we can create it from scratch # with no inputs. def generate_key @key = Key.new(name) @key.generate begin Key.indirection.save(@key) rescue @key = nil raise end true end def certificate_request @certificate_request ||= CertificateRequest.indirection.find(name) end # Our certificate request requires the key but that's all. def generate_certificate_request(options = {}) generate_key unless key # If this CSR is for the current machine... if name == Puppet[:certname].downcase # ...add our configured dns_alt_names if Puppet[:dns_alt_names] and Puppet[:dns_alt_names] != '' options[:dns_alt_names] ||= Puppet[:dns_alt_names] elsif Puppet::SSL::CertificateAuthority.ca? and fqdn = Facter.value(:fqdn) and domain = Facter.value(:domain) options[:dns_alt_names] = "puppet, #{fqdn}, puppet.#{domain}" end end csr_attributes = Puppet::SSL::CertificateRequestAttributes.new(Puppet[:csr_attributes]) if csr_attributes.load options[:csr_attributes] = csr_attributes.custom_attributes options[:extension_requests] = csr_attributes.extension_requests end @certificate_request = CertificateRequest.new(name) @certificate_request.generate(key.content, options) begin CertificateRequest.indirection.save(@certificate_request) rescue @certificate_request = nil raise end true end def certificate unless @certificate generate_key unless key # get the CA cert first, since it's required for the normal cert # to be of any use. return nil unless Certificate.indirection.find("ca", :fail_on_404 => true) unless ca? return nil unless @certificate = Certificate.indirection.find(name) validate_certificate_with_key end @certificate end def validate_certificate_with_key raise Puppet::Error, "No certificate to validate." unless certificate raise Puppet::Error, "No private key with which to validate certificate with fingerprint: #{certificate.fingerprint}" unless key unless certificate.content.check_private_key(key.content) raise Puppet::Error, < name } my_state = state result[:state] = my_state result[:desired_state] = desired_state if desired_state thing_to_use = (my_state == 'requested') ? certificate_request : my_cert # this is for backwards-compatibility # we should deprecate it and transition people to using # pson[:fingerprints][:default] # It appears that we have no internal consumers of this api # --jeffweiss 30 aug 2012 result[:fingerprint] = thing_to_use.fingerprint # The above fingerprint doesn't tell us what message digest algorithm was used # No problem, except that the default is changing between 2.7 and 3.0. Also, as # we move to FIPS 140-2 compliance, MD5 is no longer allowed (and, gasp, will # segfault in rubies older than 1.9.3) # So, when we add the newer fingerprints, we're explicit about the hashing # algorithm used. # --jeffweiss 31 july 2012 result[:fingerprints] = {} result[:fingerprints][:default] = thing_to_use.fingerprint suitable_message_digest_algorithms.each do |md| result[:fingerprints][md] = thing_to_use.fingerprint md end result[:dns_alt_names] = thing_to_use.subject_alt_names result end # eventually we'll probably want to move this somewhere else or make it # configurable # --jeffweiss 29 aug 2012 def suitable_message_digest_algorithms [:SHA1, :SHA256, :SHA512] end # Attempt to retrieve a cert, if we don't already have one. def wait_for_cert(time) begin return if certificate generate return if certificate - rescue SystemExit,NoMemoryError - raise - rescue Exception => detail + rescue StandardError => detail Puppet.log_exception(detail, "Could not request certificate: #{detail.message}") if time < 1 puts "Exiting; failed to retrieve certificate and waitforcert is disabled" exit(1) else sleep(time) end retry end if time < 1 puts "Exiting; no certificate found and waitforcert is disabled" exit(1) end while true sleep time begin break if certificate Puppet.notice "Did not receive certificate" rescue StandardError => detail Puppet.log_exception(detail, "Could not request certificate: #{detail.message}") end end end def state if certificate_request return 'requested' end begin Puppet::SSL::CertificateAuthority.new.verify(name) return 'signed' rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError return 'revoked' end end end require 'puppet/ssl/certificate_authority' diff --git a/lib/puppet/util/feature.rb b/lib/puppet/util/feature.rb index 23d00edde..59ad3ec3f 100644 --- a/lib/puppet/util/feature.rb +++ b/lib/puppet/util/feature.rb @@ -1,97 +1,95 @@ require 'puppet' class Puppet::Util::Feature attr_reader :path # Create a new feature test. You have to pass the feature name, # and it must be unique. You can either provide a block that # will get executed immediately to determine if the feature # is present, or you can pass an option to determine it. # Currently, the only supported option is 'libs' (must be # passed as a symbol), which will make sure that each lib loads # successfully. def add(name, options = {}) method = name.to_s + "?" @results.delete(name) if block_given? begin result = yield - rescue Exception => detail + rescue StandardError,ScriptError => detail warn "Failed to load feature test for #{name}: #{detail}" result = false end @results[name] = result end meta_def(method) do # we return a cached result if: # * if a block is given (and we just evaluated it above) # * if we already have a positive result # * if we've tested this feature before and it failed, but we're # configured to always cache if block_given? || @results[name] || (@results.has_key?(name) and Puppet[:always_cache_features]) @results[name] else @results[name] = test(name, options) @results[name] end end end # Create a new feature collection. def initialize(path) @path = path @results = {} @loader = Puppet::Util::Autoload.new(self, @path) end def load @loader.loadall end def method_missing(method, *args) return super unless method.to_s =~ /\?$/ feature = method.to_s.sub(/\?$/, '') @loader.load(feature) respond_to?(method) && self.send(method) end # Actually test whether the feature is present. We only want to test when # someone asks for the feature, so we don't unnecessarily load # files. def test(name, options) return true unless ary = options[:libs] ary = [ary] unless ary.is_a?(Array) ary.each do |lib| return false unless load_library(lib, name) end # We loaded all of the required libraries true end private def load_library(lib, name) raise ArgumentError, "Libraries must be passed as strings not #{lib.class}" unless lib.is_a?(String) @rubygems ||= Puppet::Util::RubyGems::Source.new @rubygems.clear_paths begin require lib - rescue SystemExit,NoMemoryError - raise - rescue Exception + rescue ScriptError Puppet.debug "Failed to load library '#{lib}' for feature '#{name}'" return false end true end end diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb index 5c7761b7a..215bb6202 100644 --- a/lib/puppet/util/monkey_patches.rb +++ b/lib/puppet/util/monkey_patches.rb @@ -1,132 +1,132 @@ module Puppet::Util::MonkeyPatches end begin Process.maxgroups = 1024 -rescue Exception +rescue NotImplementedError # Actually, I just want to ignore it, since various platforms - JRuby, # Windows, and so forth - don't support it, but only because it isn't a # meaningful or implementable concept there. end module RDoc def self.caller(skip=nil) in_gem_wrapper = false Kernel.caller.reject { |call| in_gem_wrapper ||= call =~ /#{Regexp.escape $0}:\d+:in `load'/ } end end class Symbol def <=> (other) self.to_s <=> other.to_s end unless method_defined? '<=>' def intern self end unless method_defined? 'intern' end class Object # ActiveSupport 2.3.x mixes in a dangerous method # that can cause rspec to fork bomb # and other strange things like that. def daemonize raise NotImplementedError, "Kernel.daemonize is too dangerous, please don't try to use it." end end require 'fcntl' class IO def self.binwrite(name, string, offset = nil) # Determine if we should truncate or not. Since the truncate method on a # file handle isn't implemented on all platforms, safer to do this in what # looks like the libc / POSIX flag - which is usually pretty robust. # --daniel 2012-03-11 mode = Fcntl::O_CREAT | Fcntl::O_WRONLY | (offset.nil? ? Fcntl::O_TRUNC : 0) # We have to duplicate the mode because Ruby on Windows is a bit precious, # and doesn't actually carry over the mode. It won't work to just use # open, either, because that doesn't like our system modes and the default # open bits don't do what we need, which is awesome. --daniel 2012-03-30 IO.open(IO::sysopen(name, mode), mode) do |f| # ...seek to our desired offset, then write the bytes. Don't try to # seek past the start of the file, eh, because who knows what platform # would legitimately blow up if we did that. # # Double-check the positioning, too, since destroying data isn't my idea # of a good time. --daniel 2012-03-11 target = [0, offset.to_i].max unless (landed = f.sysseek(target, IO::SEEK_SET)) == target raise "unable to seek to target offset #{target} in #{name}: got to #{landed}" end f.syswrite(string) end end unless singleton_methods.include?(:binwrite) end class Range def intersection(other) raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range) return unless other === self.first || self === other.first start = [self.first, other.first].max if self.exclude_end? && self.last <= other.last start ... self.last elsif other.exclude_end? && self.last >= other.last start ... other.last else start .. [ self.last, other.last ].min end end unless method_defined? :intersection alias_method :&, :intersection unless method_defined? :& end # (#19151) Reject all SSLv2 ciphers and handshakes require 'openssl' class OpenSSL::SSL::SSLContext if DEFAULT_PARAMS[:options] DEFAULT_PARAMS[:options] |= OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 else DEFAULT_PARAMS[:options] = OpenSSL::SSL::OP_NO_SSLv2 | OpenSSL::SSL::OP_NO_SSLv3 end DEFAULT_PARAMS[:ciphers] << ':!SSLv2' alias __original_initialize initialize private :__original_initialize def initialize(*args) __original_initialize(*args) params = { :options => DEFAULT_PARAMS[:options], :ciphers => DEFAULT_PARAMS[:ciphers], } set_params(params) end end require 'puppet/util/platform' if Puppet::Util::Platform.windows? require 'puppet/util/windows' require 'openssl' class OpenSSL::X509::Store alias __original_set_default_paths set_default_paths def set_default_paths # This can be removed once openssl integrates with windows # cert store, see http://rt.openssl.org/Ticket/Display.html?id=2158 Puppet::Util::Windows::RootCerts.instance.to_a.uniq.each do |x509| begin add_cert(x509) rescue OpenSSL::X509::StoreError => e warn "Failed to add #{x509.subject.to_s}" end end __original_set_default_paths end end end diff --git a/spec/unit/network/http/handler_spec.rb b/spec/unit/network/http/handler_spec.rb index 7adec758c..9b0b754e5 100755 --- a/spec/unit/network/http/handler_spec.rb +++ b/spec/unit/network/http/handler_spec.rb @@ -1,228 +1,228 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/indirector_testing' require 'puppet/network/authorization' require 'puppet/network/http' describe Puppet::Network::HTTP::Handler do before :each do Puppet::IndirectorTesting.indirection.terminus_class = :memory end let(:indirection) { Puppet::IndirectorTesting.indirection } def a_request(method = "HEAD", path = "/production/#{indirection.name}/unknown") { :accept_header => "pson", :content_type_header => "text/pson", :http_method => method, :path => path, :params => {}, :client_cert => nil, :headers => {}, :body => nil } end let(:handler) { TestingHandler.new() } describe "the HTTP Handler" do def respond(text) lambda { |req, res| res.respond_with(200, "text/plain", text) } end it "hands the request to the first route that matches the request path" do handler = TestingHandler.new( Puppet::Network::HTTP::Route.path(%r{^/foo}).get(respond("skipped")), Puppet::Network::HTTP::Route.path(%r{^/vtest}).get(respond("used")), Puppet::Network::HTTP::Route.path(%r{^/vtest/foo}).get(respond("ignored"))) req = a_request("GET", "/vtest/foo") res = {} handler.process(req, res) expect(res[:body]).to eq("used") end it "raises an error if multiple routes with the same path regex are registered" do expect do handler = TestingHandler.new( Puppet::Network::HTTP::Route.path(%r{^/foo}).get(respond("ignored")), Puppet::Network::HTTP::Route.path(%r{^/foo}).post(respond("also ignored"))) end.to raise_error(ArgumentError) end it "raises an HTTP not found error if no routes match" do handler = TestingHandler.new req = a_request("GET", "/vtest/foo") res = {} handler.process(req, res) res_body = JSON(res[:body]) expect(res[:content_type_header]).to eq("application/json") expect(res_body["issue_kind"]).to eq("HANDLER_NOT_FOUND") expect(res_body["message"]).to eq("Not Found: No route for GET /vtest/foo") expect(res[:status]).to eq(404) end it "returns a structured error response with a stacktrace when the server encounters an internal error" do handler = TestingHandler.new( - Puppet::Network::HTTP::Route.path(/.*/).get(lambda { |_, _| raise Exception.new("the sky is falling!")})) + Puppet::Network::HTTP::Route.path(/.*/).get(lambda { |_, _| raise StandardError.new("the sky is falling!")})) req = a_request("GET", "/vtest/foo") res = {} handler.process(req, res) res_body = JSON(res[:body]) expect(res[:content_type_header]).to eq("application/json") expect(res_body["issue_kind"]).to eq(Puppet::Network::HTTP::Issues::RUNTIME_ERROR.to_s) expect(res_body["message"]).to eq("Server Error: the sky is falling!") expect(res_body["stacktrace"].is_a?(Array) && !res_body["stacktrace"].empty?).to be_true expect(res_body["stacktrace"][0]).to match("spec/unit/network/http/handler_spec.rb") expect(res[:status]).to eq(500) end end describe "when processing a request" do let(:response) do { :status => 200 } end before do handler.stubs(:check_authorization) handler.stubs(:warn_if_near_expiration) end it "should setup a profiler when the puppet-profiling header exists" do request = a_request request[:headers][Puppet::Network::HTTP::HEADER_ENABLE_PROFILING.downcase] = "true" p = HandlerTestProfiler.new Puppet::Util::Profiler.expects(:add_profiler).with { |profiler| profiler.is_a? Puppet::Util::Profiler::WallClock }.returns(p) Puppet::Util::Profiler.expects(:remove_profiler).with { |profiler| profiler == p } handler.process(request, response) end it "should not setup profiler when the profile parameter is missing" do request = a_request request[:params] = { } Puppet::Util::Profiler.expects(:add_profiler).never handler.process(request, response) end it "should raise an error if the request is formatted in an unknown format" do handler.stubs(:content_type_header).returns "unknown format" lambda { handler.request_format(request) }.should raise_error end it "should still find the correct format if content type contains charset information" do request = Puppet::Network::HTTP::Request.new({ 'content-type' => "text/plain; charset=UTF-8" }, {}, 'GET', '/', nil) request.format.should == "s" end # PUP-3272 # This used to be for YAML, and doing a to_yaml on an array. # The result with to_pson is something different, the result is a string # Which seems correct. Looks like this was some kind of nesting option "yaml inside yaml" ? # Removing the test # it "should deserialize PSON parameters" do # params = {'my_param' => [1,2,3].to_pson} # # decoded_params = handler.send(:decode_params, params) # # decoded_params.should == {:my_param => [1,2,3]} # end end describe "when resolving node" do it "should use a look-up from the ip address" do Resolv.expects(:getname).with("1.2.3.4").returns("host.domain.com") handler.resolve_node(:ip => "1.2.3.4") end it "should return the look-up result" do Resolv.stubs(:getname).with("1.2.3.4").returns("host.domain.com") handler.resolve_node(:ip => "1.2.3.4").should == "host.domain.com" end it "should return the ip address if resolving fails" do Resolv.stubs(:getname).with("1.2.3.4").raises(RuntimeError, "no such host") handler.resolve_node(:ip => "1.2.3.4").should == "1.2.3.4" end end class TestingHandler include Puppet::Network::HTTP::Handler def initialize(* routes) register(routes) end def set_content_type(response, format) response[:content_type_header] = format end def set_response(response, body, status = 200) response[:body] = body response[:status] = status end def http_method(request) request[:http_method] end def path(request) request[:path] end def params(request) request[:params] end def client_cert(request) request[:client_cert] end def body(request) request[:body] end def headers(request) request[:headers] || {} end end class HandlerTestProfiler def start(metric, description) end def finish(context, metric, description) end def shutdown() end end end diff --git a/spec/unit/network/http/rack/rest_spec.rb b/spec/unit/network/http/rack/rest_spec.rb index 764a0bf56..f4125f170 100755 --- a/spec/unit/network/http/rack/rest_spec.rb +++ b/spec/unit/network/http/rack/rest_spec.rb @@ -1,318 +1,318 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/http/rack' if Puppet.features.rack? require 'puppet/network/http/rack/rest' describe "Puppet::Network::HTTP::RackREST", :if => Puppet.features.rack? do it "should include the Puppet::Network::HTTP::Handler module" do Puppet::Network::HTTP::RackREST.ancestors.should be_include(Puppet::Network::HTTP::Handler) end describe "when serving a request" do before :all do @model_class = stub('indirected model class') Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@model_class) end before :each do @response = Rack::Response.new @handler = Puppet::Network::HTTP::RackREST.new(:handler => :foo) end def mk_req(uri, opts = {}) env = Rack::MockRequest.env_for(uri, opts) Rack::Request.new(env) end let(:minimal_certificate) do key = OpenSSL::PKey::RSA.new(512) signer = Puppet::SSL::CertificateSigner.new cert = OpenSSL::X509::Certificate.new cert.version = 2 cert.serial = 0 cert.not_before = Time.now cert.not_after = Time.now + 3600 cert.public_key = key cert.subject = OpenSSL::X509::Name.parse("/CN=testing") signer.sign(cert, key) cert end describe "#headers" do it "should return the headers (parsed from env with prefix 'HTTP_')" do req = mk_req('/', {'HTTP_Accept' => 'myaccept', 'HTTP_X_Custom_Header' => 'mycustom', 'NOT_HTTP_foo' => 'not an http header'}) @handler.headers(req).should == {"accept" => 'myaccept', "x-custom-header" => 'mycustom', "content-type" => nil } end end describe "and using the HTTP Handler interface" do it "should return the CONTENT_TYPE parameter as the content type header" do req = mk_req('/', 'CONTENT_TYPE' => 'mycontent') @handler.headers(req)['content-type'].should == "mycontent" end it "should use the REQUEST_METHOD as the http method" do req = mk_req('/', :method => 'MYMETHOD') @handler.http_method(req).should == "MYMETHOD" end it "should return the request path as the path" do req = mk_req('/foo/bar') @handler.path(req).should == "/foo/bar" end it "should return the request body as the body" do req = mk_req('/foo/bar', :input => 'mybody') @handler.body(req).should == "mybody" end it "should return the an Puppet::SSL::Certificate instance as the client_cert" do req = mk_req('/foo/bar', 'SSL_CLIENT_CERT' => minimal_certificate.to_pem) expect(@handler.client_cert(req).content.to_pem).to eq(minimal_certificate.to_pem) end it "returns nil when SSL_CLIENT_CERT is empty" do req = mk_req('/foo/bar', 'SSL_CLIENT_CERT' => '') @handler.client_cert(req).should be_nil end it "should set the response's content-type header when setting the content type" do @header = mock 'header' @response.expects(:header).returns @header @header.expects(:[]=).with('Content-Type', "mytype") @handler.set_content_type(@response, "mytype") end it "should set the status and write the body when setting the response for a request" do @response.expects(:status=).with(400) @response.expects(:write).with("mybody") @handler.set_response(@response, "mybody", 400) end describe "when result is a File" do before :each do stat = stub 'stat', :size => 100 @file = stub 'file', :stat => stat, :path => "/tmp/path" @file.stubs(:is_a?).with(File).returns(true) end it "should set the Content-Length header as a string" do @response.expects(:[]=).with("Content-Length", '100') @handler.set_response(@response, @file, 200) end it "should return a RackFile adapter as body" do @response.expects(:body=).with { |val| val.is_a?(Puppet::Network::HTTP::RackREST::RackFile) } @handler.set_response(@response, @file, 200) end end it "should ensure the body has been read on success" do req = mk_req('/production/report/foo', :method => 'PUT') req.body.expects(:read).at_least_once Puppet::Transaction::Report.stubs(:save) @handler.process(req, @response) end it "should ensure the body has been partially read on failure" do req = mk_req('/production/report/foo') req.body.expects(:read).with(1) - @handler.stubs(:headers).raises(Exception) + @handler.stubs(:headers).raises(StandardError) @handler.process(req, @response) end end describe "and determining the request parameters" do it "should include the HTTP request parameters, with the keys as symbols" do req = mk_req('/?foo=baz&bar=xyzzy') result = @handler.params(req) result[:foo].should == "baz" result[:bar].should == "xyzzy" end it "should return multi-values params as an array of the values" do req = mk_req('/?foo=baz&foo=xyzzy') result = @handler.params(req) result[:foo].should == ["baz", "xyzzy"] end it "should return parameters from the POST body" do req = mk_req("/", :method => 'POST', :input => 'foo=baz&bar=xyzzy') result = @handler.params(req) result[:foo].should == "baz" result[:bar].should == "xyzzy" end it "should not return multi-valued params in a POST body as an array of values" do req = mk_req("/", :method => 'POST', :input => 'foo=baz&foo=xyzzy') result = @handler.params(req) result[:foo].should be_one_of("baz", "xyzzy") end it "should CGI-decode the HTTP parameters" do encoding = CGI.escape("foo bar") req = mk_req("/?foo=#{encoding}") result = @handler.params(req) result[:foo].should == "foo bar" end it "should convert the string 'true' to the boolean" do req = mk_req("/?foo=true") result = @handler.params(req) result[:foo].should be_true end it "should convert the string 'false' to the boolean" do req = mk_req("/?foo=false") result = @handler.params(req) result[:foo].should be_false end it "should convert integer arguments to Integers" do req = mk_req("/?foo=15") result = @handler.params(req) result[:foo].should == 15 end it "should convert floating point arguments to Floats" do req = mk_req("/?foo=1.5") result = @handler.params(req) result[:foo].should == 1.5 end it "should treat YAML encoded parameters like it was any string" do escaping = CGI.escape(YAML.dump(%w{one two})) req = mk_req("/?foo=#{escaping}") @handler.params(req)[:foo].should == "---\n- one\n- two\n" end it "should not allow the client to set the node via the query string" do req = mk_req("/?node=foo") @handler.params(req)[:node].should be_nil end it "should not allow the client to set the IP address via the query string" do req = mk_req("/?ip=foo") @handler.params(req)[:ip].should be_nil end it "should pass the client's ip address to model find" do req = mk_req("/", 'REMOTE_ADDR' => 'ipaddress') @handler.params(req)[:ip].should == "ipaddress" end it "should set 'authenticated' to false if no certificate is present" do req = mk_req('/') @handler.params(req)[:authenticated].should be_false end end describe "with pre-validated certificates" do it "should retrieve the hostname by finding the CN given in :ssl_client_header, in the format returned by Apache (RFC2253)" do Puppet[:ssl_client_header] = "myheader" req = mk_req('/', "myheader" => "O=Foo\\, Inc,CN=host.domain.com") @handler.params(req)[:node].should == "host.domain.com" end it "should retrieve the hostname by finding the CN given in :ssl_client_header, in the format returned by nginx" do Puppet[:ssl_client_header] = "myheader" req = mk_req('/', "myheader" => "/CN=host.domain.com") @handler.params(req)[:node].should == "host.domain.com" end it "should retrieve the hostname by finding the CN given in :ssl_client_header, ignoring other fields" do Puppet[:ssl_client_header] = "myheader" req = mk_req('/', "myheader" => 'ST=Denial,CN=host.domain.com,O=Domain\\, Inc.') @handler.params(req)[:node].should == "host.domain.com" end it "should use the :ssl_client_header to determine the parameter for checking whether the host certificate is valid" do Puppet[:ssl_client_header] = "certheader" Puppet[:ssl_client_verify_header] = "myheader" req = mk_req('/', "myheader" => "SUCCESS", "certheader" => "CN=host.domain.com") @handler.params(req)[:authenticated].should be_true end it "should consider the host unauthenticated if the validity parameter does not contain 'SUCCESS'" do Puppet[:ssl_client_header] = "certheader" Puppet[:ssl_client_verify_header] = "myheader" req = mk_req('/', "myheader" => "whatever", "certheader" => "CN=host.domain.com") @handler.params(req)[:authenticated].should be_false end it "should consider the host unauthenticated if no certificate information is present" do Puppet[:ssl_client_header] = "certheader" Puppet[:ssl_client_verify_header] = "myheader" req = mk_req('/', "myheader" => nil, "certheader" => "CN=host.domain.com") @handler.params(req)[:authenticated].should be_false end it "should resolve the node name with an ip address look-up if no certificate is present" do Puppet[:ssl_client_header] = "myheader" req = mk_req('/', "myheader" => nil) @handler.expects(:resolve_node).returns("host.domain.com") @handler.params(req)[:node].should == "host.domain.com" end it "should resolve the node name with an ip address look-up if a certificate without a CN is present" do Puppet[:ssl_client_header] = "myheader" req = mk_req('/', "myheader" => "O=no CN") @handler.expects(:resolve_node).returns("host.domain.com") @handler.params(req)[:node].should == "host.domain.com" end it "should not allow authentication via the verify header if there is no CN available" do Puppet[:ssl_client_header] = "dn_header" Puppet[:ssl_client_verify_header] = "verify_header" req = mk_req('/', "dn_header" => "O=no CN", "verify_header" => 'SUCCESS') @handler.expects(:resolve_node).returns("host.domain.com") @handler.params(req)[:authenticated].should be_false end end end end describe Puppet::Network::HTTP::RackREST::RackFile do before(:each) do stat = stub 'stat', :size => 100 @file = stub 'file', :stat => stat, :path => "/tmp/path" @rackfile = Puppet::Network::HTTP::RackREST::RackFile.new(@file) end it "should have an each method" do @rackfile.should be_respond_to(:each) end it "should yield file chunks by chunks" do @file.expects(:read).times(3).with(8192).returns("1", "2", nil) i = 1 @rackfile.each do |chunk| chunk.to_i.should == i i += 1 end end it "should have a close method" do @rackfile.should be_respond_to(:close) end it "should delegate close to File close" do @file.expects(:close) @rackfile.close end end