diff --git a/lib/puppet/application/apply.rb b/lib/puppet/application/apply.rb index 274d01af5..cb5ba744a 100644 --- a/lib/puppet/application/apply.rb +++ b/lib/puppet/application/apply.rb @@ -1,290 +1,290 @@ require 'puppet/application' require 'puppet/configurer' class Puppet::Application::Apply < Puppet::Application option("--debug","-d") option("--execute EXECUTE","-e") do |arg| options[:code] = arg end option("--loadclasses","-L") option("--test","-t") option("--verbose","-v") option("--use-nodes") option("--detailed-exitcodes") option("--write-catalog-summary") option("--catalog catalog", "-c catalog") do |arg| options[:catalog] = arg end option("--logdest LOGDEST", "-l") do |arg| handle_logdest_arg(arg) end option("--parseonly") do |args| puts "--parseonly has been removed. Please use 'puppet parser validate '" exit 1 end def help <<-'HELP' puppet-apply(8) -- Apply Puppet manifests locally ======== SYNOPSIS -------- Applies a standalone Puppet manifest to the local system. USAGE ----- puppet apply [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] [-e|--execute] [--detailed-exitcodes] [-L|--loadclasses] [-l|--logdest ] [--noop] [--catalog ] [--write-catalog-summary] DESCRIPTION ----------- This is the standalone puppet execution tool; use it to apply individual manifests. When provided with a modulepath, via command line or config file, puppet apply can effectively mimic the catalog that would be served by puppet master with access to the same modules, although there are some subtle differences. When combined with scheduling and an automated system for pushing manifests, this can be used to implement a serverless Puppet site. Most users should use 'puppet agent' and 'puppet master' for site-wide manifests. OPTIONS ------- Note that any setting that's valid in the configuration file is also a valid long argument. For example, 'tags' is a valid setting, so you can specify '--tags ,' as an argument. See the configuration file documentation at http://docs.puppetlabs.com/references/stable/configuration.html for the full list of acceptable parameters. A commented list of all configuration options can also be generated by running puppet with '--genconfig'. * --debug: Enable full debugging. * --detailed-exitcodes: Provide transaction information via exit codes. If this is enabled, an exit code of '2' means there were changes, an exit code of '4' means there were failures during the transaction, and an exit code of '6' means there were both changes and failures. * --help: Print this help message * --loadclasses: Load any stored classes. 'puppet agent' caches configured classes (usually at /etc/puppet/classes.txt), and setting this option causes all of those classes to be set in your puppet manifest. * --logdest: Where to send messages. Choose between syslog, the console, and a log file. Defaults to sending messages to the console. * --noop: Use 'noop' mode where Puppet runs in a no-op or dry-run mode. This is useful for seeing what changes Puppet will make without actually executing the changes. * --execute: Execute a specific piece of Puppet code * --test: Enable the most common options used for testing. These are 'verbose', 'detailed-exitcodes' and 'show_diff'. * --verbose: Print extra information. * --catalog: Apply a JSON catalog (such as one generated with 'puppet master --compile'). You can either specify a JSON file or pipe in JSON from standard input. * --write-catalog-summary After compiling the catalog saves the resource list and classes list to the node in the state directory named classes.txt and resources.txt EXAMPLE ------- $ puppet apply -l /tmp/manifest.log manifest.pp $ puppet apply --modulepath=/root/dev/modules -e "include ntpd::server" $ puppet apply --catalog catalog.json AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def app_defaults super.merge({ :default_file_terminus => :file_server, }) end def run_command if options[:catalog] apply else main end end def apply if options[:catalog] == "-" text = $stdin.read else text = ::File.read(options[:catalog]) end catalog = read_catalog(text) apply_catalog(catalog) end def main # Set our code or file to use. if options[:code] or command_line.args.length == 0 Puppet[:code] = options[:code] || STDIN.read else manifest = command_line.args.shift raise "Could not find file #{manifest}" unless Puppet::FileSystem.exist?(manifest) Puppet.warning("Only one file can be applied per run. Skipping #{command_line.args.join(', ')}") if command_line.args.size > 0 end unless Puppet[:node_name_fact].empty? # Collect our facts. unless facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value]) raise "Could not find facts for #{Puppet[:node_name_value]}" end Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]] facts.name = Puppet[:node_name_value] end configured_environment = Puppet.lookup(:current_environment) apply_environment = manifest ? configured_environment.override_with(:manifest => manifest) : configured_environment Puppet.override(:current_environment => apply_environment) do # Find our Node unless node = Puppet::Node.indirection.find(Puppet[:node_name_value]) raise "Could not find node #{Puppet[:node_name_value]}" end # Merge in the facts. node.merge(facts.values) if facts # Allow users to load the classes that puppet agent creates. if options[:loadclasses] file = Puppet[:classfile] if Puppet::FileSystem.exist?(file) unless FileTest.readable?(file) $stderr.puts "#{file} is not readable" exit(63) end node.classes = ::File.read(file).split(/[\s\n]+/) end end begin # Compile our catalog starttime = Time.now catalog = Puppet::Resource::Catalog.indirection.find(node.name, :use_node => node) # Translate it to a RAL catalog catalog = catalog.to_ral catalog.finalize catalog.retrieval_duration = Time.now - starttime if options[:write_catalog_summary] catalog.write_class_file catalog.write_resource_file end exit_status = apply_catalog(catalog) if not exit_status exit(1) elsif options[:detailed_exitcodes] then exit(exit_status) else exit(0) end rescue => detail Puppet.log_exception(detail) exit(1) end end end # Enable all of the most common test options. def setup_test Puppet.settings.handlearg("--show_diff") options[:verbose] = true options[:detailed_exitcodes] = true end def setup setup_test if options[:test] exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? Puppet::Util::Log.newdestination(:console) unless options[:setdest] Signal.trap(:INT) do $stderr.puts "Exiting" exit(1) end # we want the last report to be persisted locally Puppet::Transaction::Report.indirection.cache_class = :yaml set_log_level if Puppet[:profile] - Puppet::Util::Profiler.current = Puppet::Util::Profiler::WallClock.new(Puppet.method(:debug), "apply") + Puppet::Util::Profiler.add_profiler(Puppet::Util::Profiler::WallClock.new(Puppet.method(:debug), "apply")) end end private def read_catalog(text) begin catalog = Puppet::Resource::Catalog.convert_from(Puppet::Resource::Catalog.default_format,text) catalog = Puppet::Resource::Catalog.pson_create(catalog) unless catalog.is_a?(Puppet::Resource::Catalog) rescue => detail raise Puppet::Error, "Could not deserialize catalog from pson: #{detail}", detail.backtrace end catalog.to_ral end def apply_catalog(catalog) configurer = Puppet::Configurer.new configurer.run(:catalog => catalog, :pluginsync => false) end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index 82e873ea0..f6148f752 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -1,180 +1,185 @@ module Puppet::Network::HTTP end require 'puppet/network/http' require 'puppet/network/http/api/v1' require 'puppet/network/authentication' require 'puppet/network/rights' require 'puppet/util/profiler' require 'resolv' module Puppet::Network::HTTP::Handler include Puppet::Network::Authentication 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 - configure_profiler(request_headers, request_params) + profiler = configure_profiler(request_headers, request_params) warn_if_near_expiration(new_request.client_cert) Puppet::Util::Profiler.profile("Processed request #{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 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) case value when /^---/ Puppet.debug("Found YAML while processing request parameter #{param} (value: <#{value}>)") Puppet.deprecation_warning("YAML in network requests is deprecated and will be removed in a future version. See http://links.puppetlabs.com/deprecate_yaml_on_network") YAML.load(value, :safe => true, :deserialize_symbols => true) when 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.current = Puppet::Util::Profiler::WallClock.new(Puppet.method(:debug), request_params.object_id) - else - Puppet::Util::Profiler.current = Puppet::Util::Profiler::NONE + Puppet::Util::Profiler.add_profiler(Puppet::Util::Profiler::WallClock.new(Puppet.method(:debug), request_params.object_id)) end end + + def remove_profiler(profiler) + Puppet::Util::Profiler.remove_profiler(profiler) + end end diff --git a/lib/puppet/util/profiler.rb b/lib/puppet/util/profiler.rb index 4246181b4..d0d373cc2 100644 --- a/lib/puppet/util/profiler.rb +++ b/lib/puppet/util/profiler.rb @@ -1,45 +1,52 @@ require 'benchmark' # A simple profiling callback system. # # @api public module Puppet::Util::Profiler require 'puppet/util/profiler/wall_clock' require 'puppet/util/profiler/object_counts' - require 'puppet/util/profiler/none' + require 'puppet/util/profiler/around_profiler' - NONE = Puppet::Util::Profiler::None.new + @profiler = Puppet::Util::Profiler::AroundProfiler.new # Reset the profiling system to the original state # # @api private def self.clear - @profiler = nil + @profiler.clear end - # @return This thread's configured profiler + # Retrieve the current list of profilers + # # @api private def self.current - @profiler || NONE + @profiler.current end # @param profiler [#profile] A profiler for the current thread # @api private - def self.current=(profiler) - @profiler = profiler + def self.add_profiler(profiler) + @profiler.add_profiler(profiler) + end + + # @param profiler [#profile] A profiler to remove from the current thread + # @api private + def self.remove_profiler(profiler) + @profiler.remove_profiler(profiler) end # Profile a block of code and log the time it took to execute. # # This outputs logs entries to the Puppet masters logging destination # providing the time it took, a message describing the profiled code # and a leaf location marking where the profile method was called # in the profiled hierachy. # # @param message [String] A description of the profiled event # @param block [Block] The segment of code to profile # @api public def self.profile(message, &block) - current.profile(message, &block) + @profiler.profile(message, &block) end end diff --git a/lib/puppet/util/profiler/around_profiler.rb b/lib/puppet/util/profiler/around_profiler.rb new file mode 100644 index 000000000..5ee546640 --- /dev/null +++ b/lib/puppet/util/profiler/around_profiler.rb @@ -0,0 +1,66 @@ +# A Profiler that can be used to wrap around blocks of code. It is configured +# with other profilers and controls them to start before the block is executed +# and finish after the block is executed. +# +# @api private +class Puppet::Util::Profiler::AroundProfiler + + def initialize + @profilers = [] + end + + # Reset the profiling system to the original state + # + # @api private + def clear + @profilers = [] + end + + # Retrieve the current list of profilers + # + # @api private + def current + @profilers + end + + # @param profiler [#profile] A profiler for the current thread + # @api private + def add_profiler(profiler) + @profilers << profiler + profiler + end + + # @param profiler [#profile] A profiler to remove from the current thread + # @api private + def remove_profiler(profiler) + @profilers.delete(profiler) + end + + # Profile a block of code and log the time it took to execute. + # + # This outputs logs entries to the Puppet masters logging destination + # providing the time it took, a message describing the profiled code + # and a leaf location marking where the profile method was called + # in the profiled hierachy. + # + # @param message [String] A description of the profiled event + # @param block [Block] The segment of code to profile + # @api private + def profile(message) + retval = nil + contexts = {} + @profilers.each do |profiler| + contexts[profiler] = profiler.start(message) + end + + begin + retval = yield + ensure + @profilers.each do |profiler| + profiler.finish(contexts[profiler], message) + end + end + + retval + end +end diff --git a/lib/puppet/util/profiler/logging.rb b/lib/puppet/util/profiler/logging.rb index c0de09e25..e4806c3f8 100644 --- a/lib/puppet/util/profiler/logging.rb +++ b/lib/puppet/util/profiler/logging.rb @@ -1,47 +1,44 @@ class Puppet::Util::Profiler::Logging def initialize(logger, identifier) @logger = logger @identifier = identifier @sequence = Sequence.new end - def profile(description, &block) - retval = nil + def start(description) @sequence.next @sequence.down - context = start - begin - retval = yield - ensure - profile_explanation = finish(context) - @sequence.up - @logger.call("PROFILE [#{@identifier}] #{@sequence} #{description}: #{profile_explanation}") - end - retval + do_start + end + + def finish(context, description) + profile_explanation = do_finish(context) + @sequence.up + @logger.call("PROFILE [#{@identifier}] #{@sequence} #{description}: #{profile_explanation}") end class Sequence INITIAL = 0 SEPARATOR = '.' def initialize @elements = [INITIAL] end def next @elements[-1] += 1 end def down @elements << INITIAL end def up @elements.pop end def to_s @elements.join(SEPARATOR) end end end diff --git a/lib/puppet/util/profiler/none.rb b/lib/puppet/util/profiler/none.rb deleted file mode 100644 index 7d4ad716d..000000000 --- a/lib/puppet/util/profiler/none.rb +++ /dev/null @@ -1,8 +0,0 @@ -# A no-op profiler. Used when there is no profiling wanted. -# -# @api private -class Puppet::Util::Profiler::None - def profile(description, &block) - yield - end -end diff --git a/lib/puppet/util/profiler/wall_clock.rb b/lib/puppet/util/profiler/wall_clock.rb index 2ba47ca3e..ae0a151c3 100644 --- a/lib/puppet/util/profiler/wall_clock.rb +++ b/lib/puppet/util/profiler/wall_clock.rb @@ -1,34 +1,34 @@ require 'puppet/util/profiler/logging' # A profiler implementation that measures the number of seconds a segment of # code takes to execute and provides a callback with a string representation of # the profiling information. # # @api private class Puppet::Util::Profiler::WallClock < Puppet::Util::Profiler::Logging - def start + def do_start Timer.new end - def finish(context) + def do_finish(context) context.stop "took #{context} seconds" end class Timer FOUR_DECIMAL_DIGITS = '%0.4f' def initialize @start = Time.now end def stop @finish = Time.now end def to_s format(FOUR_DECIMAL_DIGITS, @finish - @start) end end end diff --git a/spec/unit/application/apply_spec.rb b/spec/unit/application/apply_spec.rb index de4784fbe..ec6afee66 100755 --- a/spec/unit/application/apply_spec.rb +++ b/spec/unit/application/apply_spec.rb @@ -1,458 +1,460 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/application/apply' require 'puppet/file_bucket/dipper' require 'puppet/configurer' require 'fileutils' describe Puppet::Application::Apply do before :each do @apply = Puppet::Application[:apply] Puppet::Util::Log.stubs(:newdestination) Puppet[:reports] = "none" end after :each do Puppet::Node::Facts.indirection.reset_terminus_class Puppet::Node::Facts.indirection.cache_class = nil Puppet::Node.indirection.reset_terminus_class Puppet::Node.indirection.cache_class = nil end [:debug,:loadclasses,:test,:verbose,:use_nodes,:detailed_exitcodes,:catalog, :write_catalog_summary].each do |option| it "should declare handle_#{option} method" do @apply.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @apply.options.expects(:[]=).with(option, 'arg') @apply.send("handle_#{option}".to_sym, 'arg') end end it "should set the code to the provided code when :execute is used" do @apply.options.expects(:[]=).with(:code, 'arg') @apply.send("handle_execute".to_sym, 'arg') end describe "when applying options" do it "should set the log destination with --logdest" do Puppet::Log.expects(:newdestination).with("console") @apply.handle_logdest("console") end it "should set the setdest options to true" do @apply.options.expects(:[]=).with(:setdest,true) @apply.handle_logdest("console") end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) Puppet::FileBucket::Dipper.stubs(:new) STDIN.stubs(:read) Puppet::Transaction::Report.indirection.stubs(:cache_class=) end describe "with --test" do it "should call setup_test" do @apply.options[:test] = true @apply.expects(:setup_test) @apply.setup end it "should set options[:verbose] to true" do @apply.setup_test @apply.options[:verbose].should == true end it "should set options[:show_diff] to true" do Puppet.settings.override_default(:show_diff, false) @apply.setup_test Puppet[:show_diff].should == true end it "should set options[:detailed_exitcodes] to true" do @apply.setup_test @apply.options[:detailed_exitcodes].should == true end end it "should set console as the log destination if logdest option wasn't provided" do Puppet::Log.expects(:newdestination).with(:console) @apply.setup end it "should set INT trap" do Signal.expects(:trap).with(:INT) @apply.setup end it "should set log level to debug if --debug was passed" do @apply.options[:debug] = true @apply.setup Puppet::Log.level.should == :debug end it "should set log level to info if --verbose was passed" do @apply.options[:verbose] = true @apply.setup Puppet::Log.level.should == :info end it "should print puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns true Puppet.settings.expects(:print_configs).returns true expect { @apply.setup }.to exit_with 0 end it "should exit after printing puppet config if asked to in Puppet config" do Puppet.settings.stubs(:print_configs?).returns(true) expect { @apply.setup }.to exit_with 1 end it "should tell the report handler to cache locally as yaml" do Puppet::Transaction::Report.indirection.expects(:cache_class=).with(:yaml) @apply.setup end it "configures a profiler when profiling is enabled" do Puppet[:profile] = true @apply.setup - expect(Puppet::Util::Profiler.current).to be_a(Puppet::Util::Profiler::WallClock) + expect(Puppet::Util::Profiler.current).to satisfy do |ps| + ps.any? {|p| p.is_a? Puppet::Util::Profiler::WallClock } + end end it "does not have a profiler if profiling is disabled" do Puppet[:profile] = false @apply.setup - expect(Puppet::Util::Profiler.current).to eq(Puppet::Util::Profiler::NONE) + expect(Puppet::Util::Profiler.current.length).to be 0 end it "should set default_file_terminus to `file_server` to be local" do @apply.app_defaults[:default_file_terminus].should == :file_server end end describe "when executing" do it "should dispatch to 'apply' if it was called with 'apply'" do @apply.options[:catalog] = "foo" @apply.expects(:apply) @apply.run_command end it "should dispatch to main otherwise" do @apply.stubs(:options).returns({}) @apply.expects(:main) @apply.run_command end describe "the main command" do include PuppetSpec::Files before :each do Puppet[:prerun_command] = '' Puppet[:postrun_command] = '' Puppet::Node::Facts.indirection.terminus_class = :memory Puppet::Node::Facts.indirection.cache_class = :memory Puppet::Node.indirection.terminus_class = :memory Puppet::Node.indirection.cache_class = :memory @facts = Puppet::Node::Facts.new(Puppet[:node_name_value]) Puppet::Node::Facts.indirection.save(@facts) @node = Puppet::Node.new(Puppet[:node_name_value]) Puppet::Node.indirection.save(@node) @catalog = Puppet::Resource::Catalog.new("testing", Puppet.lookup(:environments).get(Puppet[:environment])) @catalog.stubs(:to_ral).returns(@catalog) Puppet::Resource::Catalog.indirection.stubs(:find).returns(@catalog) STDIN.stubs(:read) @transaction = stub('transaction') @catalog.stubs(:apply).returns(@transaction) Puppet::Util::Storage.stubs(:load) Puppet::Configurer.any_instance.stubs(:save_last_run_summary) # to prevent it from trying to write files end after :each do Puppet::Node::Facts.indirection.reset_terminus_class Puppet::Node::Facts.indirection.cache_class = nil end around :each do |example| Puppet.override(:current_environment => Puppet::Node::Environment.create(:production, [])) do example.run end end it "should set the code to run from --code" do @apply.options[:code] = "code to run" Puppet.expects(:[]=).with(:code,"code to run") expect { @apply.main }.to exit_with 0 end it "should set the code to run from STDIN if no arguments" do @apply.command_line.stubs(:args).returns([]) STDIN.stubs(:read).returns("code to run") Puppet.expects(:[]=).with(:code,"code to run") expect { @apply.main }.to exit_with 0 end it "should raise an error if a file is passed on command line and the file does not exist" do noexist = tmpfile('noexist.pp') @apply.command_line.stubs(:args).returns([noexist]) lambda { @apply.main }.should raise_error(RuntimeError, "Could not find file #{noexist}") end it "should set the manifest to the first file and warn other files will be skipped" do manifest = tmpfile('starwarsIV') FileUtils.touch(manifest) @apply.command_line.stubs(:args).returns([manifest, 'starwarsI', 'starwarsII']) expect { @apply.main }.to exit_with 0 msg = @logs.find {|m| m.message =~ /Only one file can be applied per run/ } msg.message.should == 'Only one file can be applied per run. Skipping starwarsI, starwarsII' msg.level.should == :warning end it "should raise an error if we can't find the node" do Puppet::Node.indirection.expects(:find).returns(nil) lambda { @apply.main }.should raise_error(RuntimeError, /Could not find node/) end it "should load custom classes if loadclasses" do @apply.options[:loadclasses] = true classfile = tmpfile('classfile') File.open(classfile, 'w') { |c| c.puts 'class' } Puppet[:classfile] = classfile @node.expects(:classes=).with(['class']) expect { @apply.main }.to exit_with 0 end it "should compile the catalog" do Puppet::Resource::Catalog.indirection.expects(:find).returns(@catalog) expect { @apply.main }.to exit_with 0 end it "should transform the catalog to ral" do @catalog.expects(:to_ral).returns(@catalog) expect { @apply.main }.to exit_with 0 end it "should finalize the catalog" do @catalog.expects(:finalize) expect { @apply.main }.to exit_with 0 end it "should not save the classes or resource file by default" do @catalog.expects(:write_class_file).never @catalog.expects(:write_resource_file).never expect { @apply.main }.to exit_with 0 end it "should save the classes and resources files when requested" do @apply.options[:write_catalog_summary] = true @catalog.expects(:write_class_file).once @catalog.expects(:write_resource_file).once expect { @apply.main }.to exit_with 0 end it "should call the prerun and postrun commands on a Configurer instance" do Puppet::Configurer.any_instance.expects(:execute_prerun_command).returns(true) Puppet::Configurer.any_instance.expects(:execute_postrun_command).returns(true) expect { @apply.main }.to exit_with 0 end it "should apply the catalog" do @catalog.expects(:apply).returns(stub_everything('transaction')) expect { @apply.main }.to exit_with 0 end it "should save the last run summary" do Puppet[:noop] = false report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.stubs(:new).returns(report) Puppet::Configurer.any_instance.expects(:save_last_run_summary).with(report) expect { @apply.main }.to exit_with 0 end describe "when using node_name_fact" do before :each do @facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name') Puppet::Node::Facts.indirection.save(@facts) @node = Puppet::Node.new('other_node_name') Puppet::Node.indirection.save(@node) Puppet[:node_name_fact] = 'my_name_fact' end it "should set the facts name based on the node_name_fact" do expect { @apply.main }.to exit_with 0 @facts.name.should == 'other_node_name' end it "should set the node_name_value based on the node_name_fact" do expect { @apply.main }.to exit_with 0 Puppet[:node_name_value].should == 'other_node_name' end it "should merge in our node the loaded facts" do @facts.values.merge!('key' => 'value') expect { @apply.main }.to exit_with 0 @node.parameters['key'].should == 'value' end it "should raise an error if we can't find the facts" do Puppet::Node::Facts.indirection.expects(:find).returns(nil) lambda { @apply.main }.should raise_error end end describe "with detailed_exitcodes" do before :each do @apply.options[:detailed_exitcodes] = true end it "should exit with report's computed exit status" do Puppet[:noop] = false Puppet::Transaction::Report.any_instance.stubs(:exit_status).returns(666) expect { @apply.main }.to exit_with 666 end it "should exit with report's computed exit status, even if --noop is set" do Puppet[:noop] = true Puppet::Transaction::Report.any_instance.stubs(:exit_status).returns(666) expect { @apply.main }.to exit_with 666 end it "should always exit with 0 if option is disabled" do Puppet[:noop] = false report = stub 'report', :exit_status => 666 @transaction.stubs(:report).returns(report) expect { @apply.main }.to exit_with 0 end it "should always exit with 0 if --noop" do Puppet[:noop] = true report = stub 'report', :exit_status => 666 @transaction.stubs(:report).returns(report) expect { @apply.main }.to exit_with 0 end end end describe "the 'apply' command" do # We want this memoized, and to be able to adjust the content, so we # have to do it ourselves. def temporary_catalog(content = '"something"') @tempfile = Tempfile.new('catalog.pson') @tempfile.write(content) @tempfile.close @tempfile.path end it "should read the catalog in from disk if a file name is provided" do @apply.options[:catalog] = temporary_catalog catalog = Puppet::Resource::Catalog.new("testing", Puppet::Node::Environment::NONE) Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'"something"').returns(catalog) @apply.apply end it "should read the catalog in from stdin if '-' is provided" do @apply.options[:catalog] = "-" $stdin.expects(:read).returns '"something"' catalog = Puppet::Resource::Catalog.new("testing", Puppet::Node::Environment::NONE) Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'"something"').returns(catalog) @apply.apply end it "should deserialize the catalog from the default format" do @apply.options[:catalog] = temporary_catalog Puppet::Resource::Catalog.stubs(:default_format).returns :rot13_piglatin catalog = Puppet::Resource::Catalog.new("testing", Puppet::Node::Environment::NONE) Puppet::Resource::Catalog.stubs(:convert_from).with(:rot13_piglatin,'"something"').returns(catalog) @apply.apply end it "should fail helpfully if deserializing fails" do @apply.options[:catalog] = temporary_catalog('something syntactically invalid') lambda { @apply.apply }.should raise_error(Puppet::Error) end it "should convert plain data structures into a catalog if deserialization does not do so" do @apply.options[:catalog] = temporary_catalog Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'"something"').returns({:foo => "bar"}) catalog = Puppet::Resource::Catalog.new("testing", Puppet::Node::Environment::NONE) Puppet::Resource::Catalog.expects(:pson_create).with({:foo => "bar"}).returns(catalog) @apply.apply end it "should convert the catalog to a RAL catalog and use a Configurer instance to apply it" do @apply.options[:catalog] = temporary_catalog catalog = Puppet::Resource::Catalog.new("testing", Puppet::Node::Environment::NONE) Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'"something"').returns catalog catalog.expects(:to_ral).returns "mycatalog" configurer = stub 'configurer' Puppet::Configurer.expects(:new).returns configurer configurer.expects(:run). with(:catalog => "mycatalog", :pluginsync => false) @apply.apply end end end describe "apply_catalog" do it "should call the configurer with the catalog" do catalog = "I am a catalog" Puppet::Configurer.any_instance.expects(:run). with(:catalog => catalog, :pluginsync => false) @apply.send(:apply_catalog, catalog) end end end diff --git a/spec/unit/network/http/handler_spec.rb b/spec/unit/network/http/handler_spec.rb index 345818b4a..1259b749b 100755 --- a/spec/unit/network/http/handler_spec.rb +++ b/spec/unit/network/http/handler_spec.rb @@ -1,222 +1,243 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/indirector_testing' require 'puppet/network/authorization' require 'puppet/network/authentication' 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/yaml", :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!")})) 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 check the client certificate for upcoming expiration" do request = a_request cert = mock 'cert' handler.expects(:client_cert).returns(cert).with(request) handler.expects(:warn_if_near_expiration).with(cert) handler.process(request, response) 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" - handler.process(request, response) + p = TestProfiler.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 + } - Puppet::Util::Profiler.current.should be_kind_of(Puppet::Util::Profiler::WallClock) + handler.process(request, response) end it "should not setup profiler when the profile parameter is missing" do request = a_request request[:params] = { } - handler.process(request, response) + Puppet::Util::Profiler.expects(:add_profiler).never - Puppet::Util::Profiler.current.should == Puppet::Util::Profiler::NONE + 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 it "should deserialize YAML parameters" do params = {'my_param' => [1,2,3].to_yaml} decoded_params = handler.send(:decode_params, params) decoded_params.should == {:my_param => [1,2,3]} end it "should ignore tags on YAML parameters" do params = {'my_param' => "--- !ruby/object:Array {}"} decoded_params = handler.send(:decode_params, params) decoded_params[:my_param].should be_a(Hash) 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 TestProfiler + attr_accessor :context, :description + + def start(description) + description + end + + def finish(context, description) + @context = context + @description = description + end + end end diff --git a/spec/unit/parser/functions_spec.rb b/spec/unit/parser/functions_spec.rb index cf8aecc87..3c6266752 100755 --- a/spec/unit/parser/functions_spec.rb +++ b/spec/unit/parser/functions_spec.rb @@ -1,132 +1,132 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Parser::Functions do def callable_functions_from(mod) Class.new { include mod }.new end let(:function_module) { Puppet::Parser::Functions.environment_module(Puppet.lookup(:current_environment)) } let(:environment) { Puppet::Node::Environment.create(:myenv, []) } before do Puppet::Parser::Functions.reset end it "should have a method for returning an environment-specific module" do Puppet::Parser::Functions.environment_module(environment).should be_instance_of(Module) end describe "when calling newfunction" do it "should create the function in the environment module" do Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| } function_module.should be_method_defined :function_name end it "should warn if the function already exists" do Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| } Puppet.expects(:warning) Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| } end it "should raise an error if the function type is not correct" do expect { Puppet::Parser::Functions.newfunction("name", :type => :unknown) { |args| } }.to raise_error Puppet::DevError, "Invalid statement type :unknown" end it "instruments the function to profile the execution" do messages = [] - Puppet::Util::Profiler.current = Puppet::Util::Profiler::WallClock.new(proc { |msg| messages << msg }, "id") + Puppet::Util::Profiler.add_profiler(Puppet::Util::Profiler::WallClock.new(proc { |msg| messages << msg }, "id")) Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| } callable_functions_from(function_module).function_name([]) messages.first.should =~ /Called name/ end end describe "when calling function to test function existence" do it "should return false if the function doesn't exist" do Puppet::Parser::Functions.autoloader.stubs(:load) Puppet::Parser::Functions.function("name").should be_false end it "should return its name if the function exists" do Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| } Puppet::Parser::Functions.function("name").should == "function_name" end it "should try to autoload the function if it doesn't exist yet" do Puppet::Parser::Functions.autoloader.expects(:load) Puppet::Parser::Functions.function("name") end it "combines functions from the root with those from the current environment" do Puppet.override(:current_environment => Puppet.lookup(:root_environment)) do Puppet::Parser::Functions.newfunction("onlyroot", :type => :rvalue) do |args| end end Puppet.override(:current_environment => Puppet::Node::Environment.create(:other, [])) do Puppet::Parser::Functions.newfunction("other_env", :type => :rvalue) do |args| end expect(Puppet::Parser::Functions.function("onlyroot")).to eq("function_onlyroot") expect(Puppet::Parser::Functions.function("other_env")).to eq("function_other_env") end expect(Puppet::Parser::Functions.function("other_env")).to be_false end end describe "when calling function to test arity" do let(:function_module) { Puppet::Parser::Functions.environment_module(Puppet.lookup(:current_environment)) } it "should raise an error if the function is called with too many arguments" do Puppet::Parser::Functions.newfunction("name", :arity => 2) { |args| } expect { callable_functions_from(function_module).function_name([1,2,3]) }.to raise_error ArgumentError end it "should raise an error if the function is called with too few arguments" do Puppet::Parser::Functions.newfunction("name", :arity => 2) { |args| } expect { callable_functions_from(function_module).function_name([1]) }.to raise_error ArgumentError end it "should not raise an error if the function is called with correct number of arguments" do Puppet::Parser::Functions.newfunction("name", :arity => 2) { |args| } expect { callable_functions_from(function_module).function_name([1,2]) }.to_not raise_error end it "should raise an error if the variable arg function is called with too few arguments" do Puppet::Parser::Functions.newfunction("name", :arity => -3) { |args| } expect { callable_functions_from(function_module).function_name([1]) }.to raise_error ArgumentError end it "should not raise an error if the variable arg function is called with correct number of arguments" do Puppet::Parser::Functions.newfunction("name", :arity => -3) { |args| } expect { callable_functions_from(function_module).function_name([1,2]) }.to_not raise_error end it "should not raise an error if the variable arg function is called with more number of arguments" do Puppet::Parser::Functions.newfunction("name", :arity => -3) { |args| } expect { callable_functions_from(function_module).function_name([1,2,3]) }.to_not raise_error end end describe "::arity" do it "returns the given arity of a function" do Puppet::Parser::Functions.newfunction("name", :arity => 4) { |args| } Puppet::Parser::Functions.arity(:name).should == 4 end it "returns -1 if no arity is given" do Puppet::Parser::Functions.newfunction("name") { |args| } Puppet::Parser::Functions.arity(:name).should == -1 end end end diff --git a/spec/unit/util/profiler/logging_spec.rb b/spec/unit/util/profiler/logging_spec.rb index 5316e5ae9..08d83cde3 100644 --- a/spec/unit/util/profiler/logging_spec.rb +++ b/spec/unit/util/profiler/logging_spec.rb @@ -1,81 +1,67 @@ require 'spec_helper' require 'puppet/util/profiler' describe Puppet::Util::Profiler::Logging do let(:logger) { SimpleLog.new } let(:identifier) { "Profiling ID" } - let(:profiler) { TestLoggingProfiler.new(logger, identifier) } - - it "returns the value of the profiled segment" do - retval = profiler.profile("Testing") { "the return value" } - - retval.should == "the return value" + let(:logging_profiler) { TestLoggingProfiler.new(logger, identifier) } + let(:profiler) do + p = Puppet::Util::Profiler::AroundProfiler.new + p.add_profiler(logging_profiler) + p end - it "propogates any errors raised in the profiled segment" do - expect do - profiler.profile("Testing") { raise "a problem" } - end.to raise_error("a problem") - end it "logs the explanation of the profile results" do profiler.profile("Testing") { } logger.messages.first.should =~ /the explanation/ end - it "logs results even when an error is raised" do - begin - profiler.profile("Testing") { raise "a problem" } - rescue - logger.messages.first.should =~ /the explanation/ - end - end - it "describes the profiled segment" do profiler.profile("Tested measurement") { } logger.messages.first.should =~ /PROFILE \[#{identifier}\] \d Tested measurement/ end it "indicates the order in which segments are profiled" do profiler.profile("Measurement") { } profiler.profile("Another measurement") { } logger.messages[0].should =~ /1 Measurement/ logger.messages[1].should =~ /2 Another measurement/ end it "indicates the nesting of profiled segments" do profiler.profile("Measurement") { profiler.profile("Nested measurement") { } } profiler.profile("Another measurement") { profiler.profile("Another nested measurement") { } } logger.messages[0].should =~ /1.1 Nested measurement/ logger.messages[1].should =~ /1 Measurement/ logger.messages[2].should =~ /2.1 Another nested measurement/ logger.messages[3].should =~ /2 Another measurement/ end class TestLoggingProfiler < Puppet::Util::Profiler::Logging - def start + def do_start "the start" end - def finish(context) + def do_finish(context) "the explanation of #{context}" end end class SimpleLog attr_reader :messages def initialize @messages = [] end def call(msg) @messages << msg end end end diff --git a/spec/unit/util/profiler/manager_spec.rb b/spec/unit/util/profiler/manager_spec.rb new file mode 100644 index 000000000..9dbc6a79c --- /dev/null +++ b/spec/unit/util/profiler/manager_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'puppet/util/profiler' + +describe Puppet::Util::Profiler::AroundProfiler do + let(:child) { TestProfiler.new() } + let(:profiler) { Puppet::Util::Profiler::AroundProfiler.new } + + before :each do + profiler.add_profiler(child) + end + + it "returns the value of the profiled segment" do + retval = profiler.profile("Testing") { "the return value" } + + retval.should == "the return value" + end + + it "propogates any errors raised in the profiled segment" do + expect do + profiler.profile("Testing") { raise "a problem" } + end.to raise_error("a problem") + end + + it "makes the description and the context available to the `start` and `finish` methods" do + profiler.profile("Testing") { } + + child.context.should == "Testing" + child.description.should == "Testing" + end + + it "calls finish even when an error is raised" do + begin + profiler.profile("Testing") { raise "a problem" } + rescue + child.context.should == "Testing" + end + end + + it "supports multiple profilers" do + profiler2 = TestProfiler.new + profiler.add_profiler(profiler2) + profiler.profile("Testing") {} + + child.context.should == "Testing" + profiler2.context.should == "Testing" + end + + class TestProfiler + attr_accessor :context, :description + + def start(description) + description + end + + def finish(context, description) + @context = context + @description = description + end + end +end + diff --git a/spec/unit/util/profiler/none_spec.rb b/spec/unit/util/profiler/none_spec.rb deleted file mode 100644 index 0cabfef6f..000000000 --- a/spec/unit/util/profiler/none_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' -require 'puppet/util/profiler' - -describe Puppet::Util::Profiler::None do - let(:profiler) { Puppet::Util::Profiler::None.new } - - it "returns the value of the profiled block" do - retval = profiler.profile("Testing") { "the return value" } - - retval.should == "the return value" - end -end diff --git a/spec/unit/util/profiler/wall_clock_spec.rb b/spec/unit/util/profiler/wall_clock_spec.rb index 668f63221..8d95a3ce9 100644 --- a/spec/unit/util/profiler/wall_clock_spec.rb +++ b/spec/unit/util/profiler/wall_clock_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' require 'puppet/util/profiler' describe Puppet::Util::Profiler::WallClock do it "logs the number of seconds it took to execute the segment" do profiler = Puppet::Util::Profiler::WallClock.new(nil, nil) - message = profiler.finish(profiler.start) + message = profiler.do_finish(profiler.start("Testing")) message.should =~ /took \d\.\d{4} seconds/ end end diff --git a/spec/unit/util/profiler_spec.rb b/spec/unit/util/profiler_spec.rb new file mode 100644 index 000000000..e9ed568aa --- /dev/null +++ b/spec/unit/util/profiler_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require 'puppet/util/profiler' + +describe Puppet::Util::Profiler do + let(:profiler) { TestProfiler.new() } + + it "supports adding profilers" do + subject.add_profiler(profiler) + subject.current[0].should == profiler + end + + it "supports removing profilers" do + subject.add_profiler(profiler) + subject.remove_profiler(profiler) + subject.current.length.should == 0 + end + + it "supports clearing profiler list" do + subject.add_profiler(profiler) + subject.clear + subject.current.length.should == 0 + end + + it "supports profiling" do + subject.add_profiler(profiler) + subject.profile("hi") {} + profiler.context = "hi" + profiler.description = "hi" + end + + class TestProfiler + attr_accessor :context, :description + + def start(description) + description + end + + def finish(context, description) + @context = context + @description = description + end + end +end +