diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb index f85344e36..1b060224c 100644 --- a/lib/puppet/configurer.rb +++ b/lib/puppet/configurer.rb @@ -1,270 +1,273 @@ # 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' include Puppet::Configurer::FactHandler include Puppet::Configurer::PluginHandler # 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 Puppet.settings.use(:main, :ssl, :agent) @running = false @splayed = false @environment = Puppet[:environment] @transaction_uuid = SecureRandom.uuid 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 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 return facts_for_uploading end 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, "Finished catalog run") 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 = {}) # 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 => @environment, :ignore_cache => true, :transaction_uuid => @transaction_uuid) + :environment => @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 Puppet.warning("Unable to fetch my node definition, but the agent run will continue:") Puppet.warning(detail) end end query_options = get_facts(options) unless query_options # get_facts returns nil during puppet apply 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 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) end def send_report(report) puts report.summary if Puppet[:summarize] save_last_run_summary(report) Puppet::Transaction::Report.indirection.save(report, nil, :environment => @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 => @environment)) + result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], + query_options.merge(:ignore_terminus => true, :environment => @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 => @environment)) + result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], + query_options.merge(:ignore_cache => true, :environment => @environment, :fail_on_404 => true)) end result rescue SystemExit,NoMemoryError raise rescue Exception => detail Puppet.log_exception(detail, "Could not retrieve catalog from remote server: #{detail}") return nil end end diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index e16c5118e..efef8f664 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -1,260 +1,259 @@ require 'net/http' require 'uri' require 'puppet/network/http' require 'puppet/network/http_pool' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' # Access objects via REST class Puppet::Indirector::REST < Puppet::Indirector::Terminus include Puppet::Network::HTTP::Compression.module class << self attr_reader :server_setting, :port_setting end # Specify the setting that we should use to get the server name. def self.use_server_setting(setting) @server_setting = setting end # Specify the setting that we should use to get the port. def self.use_port_setting(setting) @port_setting = setting end # Specify the service to use when doing SRV record lookup def self.use_srv_service(service) @srv_service = service end def self.srv_service @srv_service || :puppet end def self.server Puppet.settings[server_setting || :server] end def self.port Puppet.settings[port_setting || :masterport].to_i end # Provide appropriate headers. def headers add_accept_encoding({"Accept" => model.supported_formats.join(", ")}) end def add_profiling_header(headers) if (Puppet[:profile]) headers[Puppet::Network::HTTP::HEADER_ENABLE_PROFILING] = "true" end headers end def network(request) Puppet::Network::HttpPool.http_instance(request.server || self.class.server, request.port || self.class.port) end def http_get(request, path, headers = nil, *args) http_request(:get, request, path, add_profiling_header(headers), *args) end def http_post(request, path, data, headers = nil, *args) http_request(:post, request, path, data, add_profiling_header(headers), *args) end def http_head(request, path, headers = nil, *args) http_request(:head, request, path, add_profiling_header(headers), *args) end def http_delete(request, path, headers = nil, *args) http_request(:delete, request, path, add_profiling_header(headers), *args) end def http_put(request, path, data, headers = nil, *args) http_request(:put, request, path, data, add_profiling_header(headers), *args) end def http_request(method, request, *args) conn = network(request) conn.send(method, *args) end def find(request) uri, body = Puppet::Network::HTTP::API::V1.request_to_uri_and_body(request) uri_with_query_string = "#{uri}?#{body}" response = do_request(request) do |request| # WEBrick in Ruby 1.9.1 only supports up to 1024 character lines in an HTTP request # http://redmine.ruby-lang.org/issues/show/3991 if "GET #{uri_with_query_string} HTTP/1.1\r\n".length > 1024 http_post(request, uri, body, headers) else http_get(request, uri_with_query_string, headers) end end if is_http_200?(response) check_master_version(response) content_type, body = parse_response(response) result = deserialize_find(content_type, body) result.name = request.key if result.respond_to?(:name=) result elsif is_http_404?(response) - # 404 gets special treatment as the indirector API can not produce a meaningful + return nil unless request.options[:fail_on_404] + + # 404 can get special treatment as the indirector API can not produce a meaningful # reason to why something is not found - it may not be the thing the user is # expecting to find that is missing, but something else (like the environment). - # While this way of handling the issue is not perfect, there is at least a warning + # While this way of handling the issue is not perfect, there is at least an error # that makes a user aware of the reason for the failure. # content_type, body = parse_response(response) msg = "Find #{uri_with_query_string} resulted in 404 with the message: #{body}" - # warn_once - Puppet::Util::Warnings.maybe_log(msg, self.class){ Puppet.warning msg } - nil - + raise Puppet::Error, msg else nil end end def head(request) response = do_request(request) do |request| http_head(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers) end if is_http_200?(response) check_master_version(response) true else false end end def search(request) response = do_request(request) do |request| http_get(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers) end if is_http_200?(response) check_master_version(response) content_type, body = parse_response(response) deserialize_search(content_type, body) || [] else [] end end def destroy(request) raise ArgumentError, "DELETE does not accept options" unless request.options.empty? response = do_request(request) do |request| http_delete(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers) end if is_http_200?(response) check_master_version(response) content_type, body = parse_response(response) deserialize_destroy(content_type, body) else nil end end def save(request) raise ArgumentError, "PUT does not accept options" unless request.options.empty? response = do_request(request) do |request| http_put(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), request.instance.render, headers.merge({ "Content-Type" => request.instance.mime })) end if is_http_200?(response) check_master_version(response) content_type, body = parse_response(response) deserialize_save(content_type, body) else nil end end # Encapsulate call to request.do_request with the arguments from this class # Then yield to the code block that was called in # We certainly could have retained the full request.do_request(...) { |r| ... } # but this makes the code much cleaner and we only then actually make the call # to request.do_request from here, thus if we change what we pass or how we # get it, we only need to change it here. def do_request(request) request.do_request(self.class.srv_service, self.class.server, self.class.port) { |request| yield(request) } end def validate_key(request) # Validation happens on the remote end end private def is_http_200?(response) case response.code when "404" false when /^2/ true else # Raise the http error if we didn't get a 'success' of some kind. raise convert_to_http_error(response) end end def is_http_404?(response) response.code == "404" end def convert_to_http_error(response) message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" Net::HTTPError.new(message, response) end def check_master_version response if !response[Puppet::Network::HTTP::HEADER_PUPPET_VERSION] && (Puppet[:legacy_query_parameter_serialization] == false || Puppet[:report_serialization_format] != "yaml") Puppet.notice "Using less secure serialization of reports and query parameters for compatibility" Puppet.notice "with older puppet master. To remove this notice, please upgrade your master(s) " Puppet.notice "to Puppet 3.3 or newer." Puppet.notice "See http://links.puppetlabs.com/deprecate_yaml_on_network for more information." Puppet[:legacy_query_parameter_serialization] = true Puppet[:report_serialization_format] = "yaml" end end # Returns the content_type, stripping any appended charset, and the # body, decompressed if necessary (content-encoding is checked inside # uncompress_body) def parse_response(response) if response['content-type'] [ response['content-type'].gsub(/\s*;.*$/,''), body = uncompress_body(response) ] else raise "No content type in http response; cannot parse" end end def deserialize_find(content_type, body) model.convert_from(content_type, body) end def deserialize_search(content_type, body) model.convert_from_multiple(content_type, body) end def deserialize_destroy(content_type, body) model.convert_from(content_type, body) end def deserialize_save(content_type, body) nil end end diff --git a/lib/puppet/parser/functions/epp.rb b/lib/puppet/parser/functions/epp.rb index da6d5f3ff..616d61422 100644 --- a/lib/puppet/parser/functions/epp.rb +++ b/lib/puppet/parser/functions/epp.rb @@ -1,41 +1,41 @@ Puppet::Parser::Functions::newfunction(:epp, :type => :rvalue, :arity => -2, :doc => "Evaluates an Embedded Puppet Template (EPP) file and returns the rendered text result as a String. EPP support the following tags: * `<%= puppet expression %>` - This tag renders the value of the expression it contains. * `<% puppet expression(s) %>` - This tag will execute the expression(s) it contains, but renders nothing. * `<%# comment %>` - The tag and its content renders nothing. * `<%%` or `%%>` - Renders a literal `<%` or `%>` respectively. * `<%-` - Same as `<%` but suppresses any leading whitespace. * `-%>` - Same as `%>` but suppresses any trailing whitespace on the same line (including line break). * `<%-( parameters )-%>` - When placed as the first tag declares the template's parameters. File based EPP supports the following visibilities of variables in scope: * Global scope (i.e. top + node scopes) - global scope is always visible * Global + all given arguments - if the EPP template does not declare parameters, and arguments are given * Global + declared parameters - if the EPP declares parameters, given argument names must match EPP supports parameters by placing an optional parameter list as the very first element in the EPP. As an example, `<%- ($x, $y, $z='unicorn') -%>` when placed first in the EPP text declares that the parameters `x` and `y` must be given as template arguments when calling `inline_epp`, and that `z` if not given as a template argument defaults to `'unicorn'`. Template parameters are available as variables, e.g.arguments `$x`, `$y` and `$z` in the example. Note that `<%-` must be used or any leading whitespace will be interpreted as text Arguments are passed to the template by calling `epp` with a Hash as the last argument, where parameters are bound to values, e.g. `epp('...', {'x'=>10, 'y'=>20})`. Excess arguments may be given (i.e. undeclared parameters) only if the EPP templates does not declare any parameters at all. Template parameters shadow variables in outer scopes. File based epp does never have access to variables in the scope where the `epp` function is called from. - See function inline_epp for examples of EPP - Since 3.5 - Requires Future Parser") do |arguments| # Requires future parser unless Puppet[:parser] == "future" raise ArgumentError, "epp(): function is only available when --parser future is in effect" end - Puppet::Pops::Evaluator::EppEvaluator.epp(self, arguments[0], self.compiler.environment.to_s, arguments[1]) + Puppet::Pops::Evaluator::EppEvaluator.epp(self, arguments[0], self.compiler.environment, arguments[1]) end diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb index 79851619e..442c648bf 100644 --- a/lib/puppet/ssl/host.rb +++ b/lib/puppet/ssl/host.rb @@ -1,372 +1,372 @@ 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 def self.from_pson(pson) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(pson) 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") unless ca? + 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 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/spec/unit/indirector/rest_spec.rb b/spec/unit/indirector/rest_spec.rb index a038853f9..34d5fb726 100755 --- a/spec/unit/indirector/rest_spec.rb +++ b/spec/unit/indirector/rest_spec.rb @@ -1,551 +1,565 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/indirector' require 'puppet/indirector/errors' require 'puppet/indirector/rest' HTTP_ERROR_CODES = [300, 400, 500] # Just one from each category since the code makes no real distinctions shared_examples_for "a REST terminus method" do |terminus_method| describe "when talking to an older master" do it "should set backward compatibility settings" do response.stubs(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).returns nil terminus.send(terminus_method, request) Puppet[:report_serialization_format].should == 'yaml' Puppet[:legacy_query_parameter_serialization].should == true end end describe "when talking to a 3.3.1 master" do it "should not set backward compatibility settings" do response.stubs(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).returns "3.3.1" terminus.send(terminus_method, request) Puppet[:report_serialization_format].should == 'pson' Puppet[:legacy_query_parameter_serialization].should == false end end HTTP_ERROR_CODES.each do |code| describe "when the response code is #{code}" do let(:response) { mock_response(code, 'error messaged!!!') } it "raises an http error with the body of the response" do expect { terminus.send(terminus_method, request) }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{response.body}") end it "does not attempt to deserialize the response" do model.expects(:convert_from).never expect { terminus.send(terminus_method, request) }.to raise_error(Net::HTTPError) end # I'm not sure what this means or if it's used it "if the body is empty raises an http error with the response header" do response.stubs(:body).returns "" response.stubs(:message).returns "fhqwhgads" expect { terminus.send(terminus_method, request) }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{response.message}") end describe "and the body is compressed" do it "raises an http error with the decompressed body of the response" do uncompressed_body = "why" compressed_body = Zlib::Deflate.deflate(uncompressed_body) response = mock_response(code, compressed_body, 'text/plain', 'deflate') connection.expects(http_method).returns(response) expect { terminus.send(terminus_method, request) }.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{uncompressed_body}") end end end end end shared_examples_for "a deserializing terminus method" do |terminus_method| describe "when the response has no content-type" do let(:response) { mock_response(200, "body", nil, nil) } it "raises an error" do expect { terminus.send(terminus_method, request) }.to raise_error(RuntimeError, "No content type in http response; cannot parse") end end it "doesn't catch errors in deserialization" do model.expects(:convert_from).raises(Puppet::Error, "Whoa there") expect { terminus.send(terminus_method, request) }.to raise_error(Puppet::Error, "Whoa there") end end describe Puppet::Indirector::REST do before :all do class Puppet::TestModel extend Puppet::Indirector indirects :test_model attr_accessor :name, :data def initialize(name = "name", data = '') @name = name @data = data end def self.convert_from(format, string) new('', string) end def self.convert_from_multiple(format, string) string.split(',').collect { |s| convert_from(format, s) } end def to_data_hash { 'name' => @name, 'data' => @data } end def ==(other) other.is_a? Puppet::TestModel and other.name == name and other.data == data end end # The subclass must not be all caps even though the superclass is class Puppet::TestModel::Rest < Puppet::Indirector::REST end Puppet::TestModel.indirection.terminus_class = :rest end after :all do Puppet::TestModel.indirection.delete # Remove the class, unlinking it from the rest of the system. Puppet.send(:remove_const, :TestModel) end let(:terminus_class) { Puppet::TestModel::Rest } let(:terminus) { Puppet::TestModel.indirection.terminus(:rest) } let(:indirection) { Puppet::TestModel.indirection } let(:model) { Puppet::TestModel } around(:each) do |example| Puppet.override(:current_environment => Puppet::Node::Environment.create(:production, [])) do example.run end end def mock_response(code, body, content_type='text/plain', encoding=nil) obj = stub('http 200 ok', :code => code.to_s, :body => body) obj.stubs(:[]).with('content-type').returns(content_type) obj.stubs(:[]).with('content-encoding').returns(encoding) obj.stubs(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).returns(Puppet.version) obj end def find_request(key, options={}) Puppet::Indirector::Request.new(:test_model, :find, key, nil, options) end def head_request(key, options={}) Puppet::Indirector::Request.new(:test_model, :head, key, nil, options) end def search_request(key, options={}) Puppet::Indirector::Request.new(:test_model, :search, key, nil, options) end def delete_request(key, options={}) Puppet::Indirector::Request.new(:test_model, :destroy, key, nil, options) end def save_request(key, instance, options={}) Puppet::Indirector::Request.new(:test_model, :save, key, instance, options) end it "should have a method for specifying what setting a subclass should use to retrieve its server" do terminus_class.should respond_to(:use_server_setting) end it "should use any specified setting to pick the server" do terminus_class.expects(:server_setting).returns :inventory_server Puppet[:inventory_server] = "myserver" terminus_class.server.should == "myserver" end it "should default to :server for the server setting" do terminus_class.expects(:server_setting).returns nil Puppet[:server] = "myserver" terminus_class.server.should == "myserver" end it "should have a method for specifying what setting a subclass should use to retrieve its port" do terminus_class.should respond_to(:use_port_setting) end it "should use any specified setting to pick the port" do terminus_class.expects(:port_setting).returns :ca_port Puppet[:ca_port] = "321" terminus_class.port.should == 321 end it "should default to :port for the port setting" do terminus_class.expects(:port_setting).returns nil Puppet[:masterport] = "543" terminus_class.port.should == 543 end it 'should default to :puppet for the srv_service' do Puppet::Indirector::REST.srv_service.should == :puppet end describe "when creating an HTTP client" do it "should use the class's server and port if the indirection request provides neither" do @request = stub 'request', :key => "foo", :server => nil, :port => nil terminus.class.expects(:port).returns 321 terminus.class.expects(:server).returns "myserver" Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end it "should use the server from the indirection request if one is present" do @request = stub 'request', :key => "foo", :server => "myserver", :port => nil terminus.class.stubs(:port).returns 321 Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end it "should use the port from the indirection request if one is present" do @request = stub 'request', :key => "foo", :server => nil, :port => 321 terminus.class.stubs(:server).returns "myserver" Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end end describe "#find" do let(:http_method) { :get } let(:response) { mock_response(200, 'body') } let(:connection) { stub('mock http connection', :get => response, :verify_callback= => nil) } let(:request) { find_request('foo') } before :each do terminus.stubs(:network).returns(connection) end it_behaves_like 'a REST terminus method', :find it_behaves_like 'a deserializing terminus method', :find describe "with a long set of parameters" do it "calls post on the connection with the query params in the body" do params = {} 'aa'.upto('zz') do |s| params[s] = 'foo' end # The request special-cases this parameter, and it # won't be passed on to the server, so we remove it here # to avoid a failure. params.delete('ip') request = find_request('whoa', params) connection.expects(:post).with do |uri, body| body.split("&").sort == params.map {|key,value| "#{key}=#{value}"}.sort end.returns(mock_response(200, 'body')) terminus.find(request) end end describe "with no parameters" do it "calls get on the connection" do request = find_request('foo bar') connection.expects(:get).with('/production/test_model/foo%20bar?', anything).returns(mock_response('200', 'response body')) terminus.find(request).should == model.new('foo bar', 'response body') end end it "returns nil on 404" do response = mock_response('404', nil) connection.expects(:get).returns(response) terminus.find(request).should == nil end - it 'raises a warning for a 404' do + it 'raises no warning for a 404 (when not asked to do so)' do response = mock_response('404', 'this is the notfound you are looking for') connection.expects(:get).returns(response) expected_message = 'Find /production/test_model/foo? resulted in 404 with the message: this is the notfound you are looking for' - Puppet::Util::Warnings.expects(:maybe_log).with(expected_message, Puppet::TestModel::Rest) - terminus.find(request) + expect{terminus.find(request)}.to_not raise_error() + end + + context 'when fail_on_404 is used in request' do + let(:request) { find_request('foo', :fail_on_404 => true) } + + it 'raises an error for a 404 when asked to do so' do + response = mock_response('404', 'this is the notfound you are looking for') + connection.expects(:get).returns(response) + expected_message = [ + 'Find /production/test_model/foo?fail_on_404=true', + 'resulted in 404 with the message: this is the notfound you are looking for'].join( ' ') + expect do + terminus.find(request) + end.to raise_error(Puppet::Error, expected_message) + end end it "asks the model to deserialize the response body and sets the name on the resulting object to the find key" do connection.expects(:get).returns response model.expects(:convert_from).with(response['content-type'], response.body).returns( model.new('overwritten', 'decoded body') ) terminus.find(request).should == model.new('foo', 'decoded body') end it "doesn't require the model to support name=" do connection.expects(:get).returns response instance = model.new('name', 'decoded body') model.expects(:convert_from).with(response['content-type'], response.body).returns(instance) instance.expects(:respond_to?).with(:name=).returns(false) instance.expects(:name=).never terminus.find(request).should == model.new('name', 'decoded body') end it "provides an Accept header containing the list of supported formats joined with commas" do connection.expects(:get).with(anything, has_entry("Accept" => "supported, formats")).returns(response) terminus.model.expects(:supported_formats).returns %w{supported formats} terminus.find(request) end it "adds an Accept-Encoding header" do terminus.expects(:add_accept_encoding).returns({"accept-encoding" => "gzip"}) connection.expects(:get).with(anything, has_entry("accept-encoding" => "gzip")).returns(response) terminus.find(request) end it "uses only the mime-type from the content-type header when asking the model to deserialize" do response = mock_response('200', 'mydata', "text/plain; charset=utf-8") connection.expects(:get).returns(response) model.expects(:convert_from).with("text/plain", "mydata").returns "myobject" terminus.find(request).should == "myobject" end it "decompresses the body before passing it to the model for deserialization" do uncompressed_body = "Why hello there" compressed_body = Zlib::Deflate.deflate(uncompressed_body) response = mock_response('200', compressed_body, 'text/plain', 'deflate') connection.expects(:get).returns(response) model.expects(:convert_from).with("text/plain", uncompressed_body).returns "myobject" terminus.find(request).should == "myobject" end end describe "#head" do let(:http_method) { :head } let(:response) { mock_response(200, nil) } let(:connection) { stub('mock http connection', :head => response, :verify_callback= => nil) } let(:request) { head_request('foo') } before :each do terminus.stubs(:network).returns(connection) end it_behaves_like 'a REST terminus method', :head it "returns true if there was a successful http response" do connection.expects(:head).returns mock_response('200', nil) terminus.head(request).should == true end it "returns false on a 404 response" do connection.expects(:head).returns mock_response('404', nil) terminus.head(request).should == false end end describe "#search" do let(:http_method) { :get } let(:response) { mock_response(200, 'data1,data2,data3') } let(:connection) { stub('mock http connection', :get => response, :verify_callback= => nil) } let(:request) { search_request('foo') } before :each do terminus.stubs(:network).returns(connection) end it_behaves_like 'a REST terminus method', :search it_behaves_like 'a deserializing terminus method', :search it "should call the GET http method on a network connection" do connection.expects(:get).with('/production/test_models/foo', has_key('Accept')).returns mock_response(200, 'data3, data4') terminus.search(request) end it "returns an empty list on 404" do response = mock_response('404', nil) connection.expects(:get).returns(response) terminus.search(request).should == [] end it "asks the model to deserialize the response body into multiple instances" do terminus.search(request).should == [model.new('', 'data1'), model.new('', 'data2'), model.new('', 'data3')] end it "should provide an Accept header containing the list of supported formats joined with commas" do connection.expects(:get).with(anything, has_entry("Accept" => "supported, formats")).returns(mock_response(200, '')) terminus.model.expects(:supported_formats).returns %w{supported formats} terminus.search(request) end it "should return an empty array if serialization returns nil" do model.stubs(:convert_from_multiple).returns nil terminus.search(request).should == [] end end describe "#destroy" do let(:http_method) { :delete } let(:response) { mock_response(200, 'body') } let(:connection) { stub('mock http connection', :delete => response, :verify_callback= => nil) } let(:request) { delete_request('foo') } before :each do terminus.stubs(:network).returns(connection) end it_behaves_like 'a REST terminus method', :destroy it_behaves_like 'a deserializing terminus method', :destroy it "should call the DELETE http method on a network connection" do connection.expects(:delete).with('/production/test_model/foo', has_key('Accept')).returns(response) terminus.destroy(request) end it "should fail if any options are provided, since DELETE apparently does not support query options" do request = delete_request('foo', :one => "two", :three => "four") expect { terminus.destroy(request) }.to raise_error(ArgumentError) end it "should deserialize and return the http response" do connection.expects(:delete).returns response terminus.destroy(request).should == model.new('', 'body') end it "returns nil on 404" do response = mock_response('404', nil) connection.expects(:delete).returns(response) terminus.destroy(request).should == nil end it "should provide an Accept header containing the list of supported formats joined with commas" do connection.expects(:delete).with(anything, has_entry("Accept" => "supported, formats")).returns(response) terminus.model.expects(:supported_formats).returns %w{supported formats} terminus.destroy(request) end end describe "#save" do let(:http_method) { :put } let(:response) { mock_response(200, 'body') } let(:connection) { stub('mock http connection', :put => response, :verify_callback= => nil) } let(:instance) { model.new('the thing', 'some contents') } let(:request) { save_request(instance.name, instance) } before :each do terminus.stubs(:network).returns(connection) end it_behaves_like 'a REST terminus method', :save it "should call the PUT http method on a network connection" do connection.expects(:put).with('/production/test_model/the%20thing', anything, has_key("Content-Type")).returns response terminus.save(request) end it "should fail if any options are provided, since PUT apparently does not support query options" do request = save_request(instance.name, instance, :one => "two", :three => "four") expect { terminus.save(request) }.to raise_error(ArgumentError) end it "should serialize the instance using the default format and pass the result as the body of the request" do instance.expects(:render).returns "serial_instance" connection.expects(:put).with(anything, "serial_instance", anything).returns response terminus.save(request) end it "returns nil on 404" do response = mock_response('404', nil) connection.expects(:put).returns(response) terminus.save(request).should == nil end it "returns nil" do connection.expects(:put).returns response terminus.save(request).should be_nil end it "should provide an Accept header containing the list of supported formats joined with commas" do connection.expects(:put).with(anything, anything, has_entry("Accept" => "supported, formats")).returns(response) instance.expects(:render).returns('') model.expects(:supported_formats).returns %w{supported formats} instance.expects(:mime).returns "supported" terminus.save(request) end it "should provide a Content-Type header containing the mime-type of the sent object" do instance.expects(:mime).returns "mime" connection.expects(:put).with(anything, anything, has_entry('Content-Type' => "mime")).returns(response) terminus.save(request) end end context 'dealing with SRV settings' do [ :destroy, :find, :head, :save, :search ].each do |method| it "##{method} passes the SRV service, and fall-back server & port to the request's do_request method" do request = Puppet::Indirector::Request.new(:indirection, method, 'key', nil) stub_response = mock_response('200', 'body') request.expects(:do_request).with(terminus.class.srv_service, terminus.class.server, terminus.class.port).returns(stub_response) terminus.send(method, request) end end end end diff --git a/spec/unit/parser/functions/epp_spec.rb b/spec/unit/parser/functions/epp_spec.rb index 4ab200c18..962709703 100644 --- a/spec/unit/parser/functions/epp_spec.rb +++ b/spec/unit/parser/functions/epp_spec.rb @@ -1,88 +1,103 @@ require 'spec_helper' describe "the epp function" do include PuppetSpec::Files before :all do Puppet::Parser::Functions.autoloader.loadall end before :each do Puppet[:parser] = 'future' end let :node do Puppet::Node.new('localhost') end let :compiler do Puppet::Parser::Compiler.new(node) end let :scope do Puppet::Parser::Scope.new(compiler) end context "when accessing scope variables as $ variables" do it "looks up the value from the scope" do scope["what"] = "are belong" eval_template("all your base <%= $what %> to us").should == "all your base are belong to us" end it "get nil accessing a variable that does not exist" do eval_template("<%= $kryptonite == undef %>").should == "true" end it "get nil accessing a variable that is undef" do scope['undef_var'] = :undef eval_template("<%= $undef_var == undef %>").should == "true" end it "gets shadowed variable if args are given" do scope['phantom'] = 'of the opera' eval_template_with_args("<%= $phantom == dragos %>", 'phantom' => 'dragos').should == "true" end it "gets shadowed variable if args are given and parameters are specified" do scope['x'] = 'wrong one' eval_template_with_args("<%- |$x| -%><%= $x == correct %>", 'x' => 'correct').should == "true" end it "raises an error if required variable is not given" do scope['x'] = 'wrong one' expect { eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'y' => 'correct') }.to raise_error(/no value given for required parameters x/) end it "raises an error if too many arguments are given" do scope['x'] = 'wrong one' expect { eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'x' => 'correct', 'y' => 'surplus') }.to raise_error(/Too many arguments: 2 for 1/) end end # although never a problem with epp it "is not interfered with by having a variable named 'string' (#14093)" do scope['string'] = "this output should not be seen" eval_template("some text that is static").should == "some text that is static" end it "has access to a variable named 'string' (#14093)" do scope['string'] = "the string value" eval_template("string was: <%= $string %>").should == "string was: the string value" end + describe 'when loading from modules' do + include PuppetSpec::Files + it 'an epp template is found' do + modules_dir = dir_containing('modules', { + 'testmodule' => { + 'templates' => { + 'the_x.epp' => 'The x is <%= $x %>' + } + }}) + Puppet.override({:current_environment => (env = Puppet::Node::Environment.create(:testload, [ modules_dir ]))}, "test") do + node.environment = env + expect(scope.function_epp([ 'testmodule/the_x.epp', { 'x' => '3'} ])).to eql("The x is 3") + end + end + end def eval_template_with_args(content, args_hash) file_path = tmpdir('epp_spec_content') filename = File.join(file_path, "template.epp") File.open(filename, "w+") { |f| f.write(content) } Puppet::Parser::Files.stubs(:find_template).returns(filename) scope.function_epp(['template', args_hash]) end def eval_template(content) file_path = tmpdir('epp_spec_content') filename = File.join(file_path, "template.epp") File.open(filename, "w+") { |f| f.write(content) } Puppet::Parser::Files.stubs(:find_template).returns(filename) scope.function_epp(['template']) end end diff --git a/spec/unit/ssl/host_spec.rb b/spec/unit/ssl/host_spec.rb index a80fe9205..280880d1b 100755 --- a/spec/unit/ssl/host_spec.rb +++ b/spec/unit/ssl/host_spec.rb @@ -1,940 +1,940 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/ssl/host' require 'matchers/json' def base_pson_comparison(result, pson_hash) result["fingerprint"].should == pson_hash["fingerprint"] result["name"].should == pson_hash["name"] result["state"].should == pson_hash["desired_state"] end describe Puppet::SSL::Host do include JSONMatchers include PuppetSpec::Files before do Puppet::SSL::Host.indirection.terminus_class = :file # Get a safe temporary file dir = tmpdir("ssl_host_testing") Puppet.settings[:confdir] = dir Puppet.settings[:vardir] = dir Puppet.settings.use :main, :ssl @host = Puppet::SSL::Host.new("myname") end after do # Cleaned out any cached localhost instance. Puppet::SSL::Host.reset Puppet::SSL::Host.ca_location = :none end it "should use any provided name as its name" do @host.name.should == "myname" end it "should retrieve its public key from its private key" do realkey = mock 'realkey' key = stub 'key', :content => realkey Puppet::SSL::Key.indirection.stubs(:find).returns(key) pubkey = mock 'public_key' realkey.expects(:public_key).returns pubkey @host.public_key.should equal(pubkey) end it "should default to being a non-ca host" do @host.ca?.should be_false end it "should be a ca host if its name matches the CA_NAME" do Puppet::SSL::Host.stubs(:ca_name).returns "yayca" Puppet::SSL::Host.new("yayca").should be_ca end it "should have a method for determining the CA location" do Puppet::SSL::Host.should respond_to(:ca_location) end it "should have a method for specifying the CA location" do Puppet::SSL::Host.should respond_to(:ca_location=) end it "should have a method for retrieving the default ssl host" do Puppet::SSL::Host.should respond_to(:ca_location=) end it "should have a method for producing an instance to manage the local host's keys" do Puppet::SSL::Host.should respond_to(:localhost) end it "should allow to reset localhost" do previous_host = Puppet::SSL::Host.localhost Puppet::SSL::Host.reset Puppet::SSL::Host.localhost.should_not == previous_host end it "should generate the certificate for the localhost instance if no certificate is available" do host = stub 'host', :key => nil Puppet::SSL::Host.expects(:new).returns host host.expects(:certificate).returns nil host.expects(:generate) Puppet::SSL::Host.localhost.should equal(host) end it "should create a localhost cert if no cert is available and it is a CA with autosign and it is using DNS alt names", :unless => Puppet.features.microsoft_windows? do Puppet[:autosign] = true Puppet[:confdir] = tmpdir('conf') Puppet[:dns_alt_names] = "foo,bar,baz" ca = Puppet::SSL::CertificateAuthority.new Puppet::SSL::CertificateAuthority.stubs(:instance).returns ca localhost = Puppet::SSL::Host.localhost cert = localhost.certificate cert.should be_a(Puppet::SSL::Certificate) cert.subject_alt_names.should =~ %W[DNS:#{Puppet[:certname]} DNS:foo DNS:bar DNS:baz] end context "with dns_alt_names" do before :each do @key = stub('key content') key = stub('key', :generate => true, :content => @key) Puppet::SSL::Key.stubs(:new).returns key Puppet::SSL::Key.indirection.stubs(:save).with(key) @cr = stub('certificate request') Puppet::SSL::CertificateRequest.stubs(:new).returns @cr Puppet::SSL::CertificateRequest.indirection.stubs(:save).with(@cr) end describe "explicitly specified" do before :each do Puppet[:dns_alt_names] = 'one, two' end it "should not include subjectAltName if not the local node" do @cr.expects(:generate).with(@key, {}) Puppet::SSL::Host.new('not-the-' + Puppet[:certname]).generate end it "should include subjectAltName if I am a CA" do @cr.expects(:generate). with(@key, { :dns_alt_names => Puppet[:dns_alt_names] }) Puppet::SSL::Host.localhost end end describe "implicitly defaulted" do let(:ca) { stub('ca', :sign => nil) } before :each do Puppet[:dns_alt_names] = '' Puppet::SSL::CertificateAuthority.stubs(:instance).returns ca end it "should not include defaults if we're not the CA" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns false @cr.expects(:generate).with(@key, {}) Puppet::SSL::Host.localhost end it "should not include defaults if not the local node" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns true @cr.expects(:generate).with(@key, {}) Puppet::SSL::Host.new('not-the-' + Puppet[:certname]).generate end it "should not include defaults if we can't resolve our fqdn" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns true Facter.stubs(:value).with(:fqdn).returns nil @cr.expects(:generate).with(@key, {}) Puppet::SSL::Host.localhost end it "should provide defaults if we're bootstrapping the local master" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns true Facter.stubs(:value).with(:fqdn).returns 'web.foo.com' Facter.stubs(:value).with(:domain).returns 'foo.com' @cr.expects(:generate).with(@key, {:dns_alt_names => "puppet, web.foo.com, puppet.foo.com"}) Puppet::SSL::Host.localhost end end end it "should always read the key for the localhost instance in from disk" do host = stub 'host', :certificate => "eh" Puppet::SSL::Host.expects(:new).returns host host.expects(:key) Puppet::SSL::Host.localhost end it "should cache the localhost instance" do host = stub 'host', :certificate => "eh", :key => 'foo' Puppet::SSL::Host.expects(:new).once.returns host Puppet::SSL::Host.localhost.should == Puppet::SSL::Host.localhost end it "should be able to verify its certificate matches its key" do Puppet::SSL::Host.new("foo").should respond_to(:validate_certificate_with_key) end it "should consider the certificate invalid if it cannot find a key" do host = Puppet::SSL::Host.new("foo") certificate = mock('cert', :fingerprint => 'DEADBEEF') host.expects(:certificate).twice.returns certificate host.expects(:key).returns nil lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, "No private key with which to validate certificate with fingerprint: DEADBEEF") end it "should consider the certificate invalid if it cannot find a certificate" do host = Puppet::SSL::Host.new("foo") host.expects(:key).never host.expects(:certificate).returns nil lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, "No certificate to validate.") end it "should consider the certificate invalid if the SSL certificate's key verification fails" do host = Puppet::SSL::Host.new("foo") key = mock 'key', :content => "private_key" sslcert = mock 'sslcert' certificate = mock 'cert', {:content => sslcert, :fingerprint => 'DEADBEEF'} host.stubs(:key).returns key host.stubs(:certificate).returns certificate sslcert.expects(:check_private_key).with("private_key").returns false lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, /DEADBEEF/) end it "should consider the certificate valid if the SSL certificate's key verification succeeds" do host = Puppet::SSL::Host.new("foo") key = mock 'key', :content => "private_key" sslcert = mock 'sslcert' certificate = mock 'cert', :content => sslcert host.stubs(:key).returns key host.stubs(:certificate).returns certificate sslcert.expects(:check_private_key).with("private_key").returns true lambda{ host.validate_certificate_with_key }.should_not raise_error end describe "when specifying the CA location" do it "should support the location ':local'" do lambda { Puppet::SSL::Host.ca_location = :local }.should_not raise_error end it "should support the location ':remote'" do lambda { Puppet::SSL::Host.ca_location = :remote }.should_not raise_error end it "should support the location ':none'" do lambda { Puppet::SSL::Host.ca_location = :none }.should_not raise_error end it "should support the location ':only'" do lambda { Puppet::SSL::Host.ca_location = :only }.should_not raise_error end it "should not support other modes" do lambda { Puppet::SSL::Host.ca_location = :whatever }.should raise_error(ArgumentError) end describe "as 'local'" do before do Puppet::SSL::Host.ca_location = :local end it "should set the cache class for Certificate, CertificateRevocationList, and CertificateRequest as :file" do Puppet::SSL::Certificate.indirection.cache_class.should == :file Puppet::SSL::CertificateRequest.indirection.cache_class.should == :file Puppet::SSL::CertificateRevocationList.indirection.cache_class.should == :file end it "should set the terminus class for Key and Host as :file" do Puppet::SSL::Key.indirection.terminus_class.should == :file Puppet::SSL::Host.indirection.terminus_class.should == :file end it "should set the terminus class for Certificate, CertificateRevocationList, and CertificateRequest as :ca" do Puppet::SSL::Certificate.indirection.terminus_class.should == :ca Puppet::SSL::CertificateRequest.indirection.terminus_class.should == :ca Puppet::SSL::CertificateRevocationList.indirection.terminus_class.should == :ca end end describe "as 'remote'" do before do Puppet::SSL::Host.ca_location = :remote end it "should set the cache class for Certificate, CertificateRevocationList, and CertificateRequest as :file" do Puppet::SSL::Certificate.indirection.cache_class.should == :file Puppet::SSL::CertificateRequest.indirection.cache_class.should == :file Puppet::SSL::CertificateRevocationList.indirection.cache_class.should == :file end it "should set the terminus class for Key as :file" do Puppet::SSL::Key.indirection.terminus_class.should == :file end it "should set the terminus class for Host, Certificate, CertificateRevocationList, and CertificateRequest as :rest" do Puppet::SSL::Host.indirection.terminus_class.should == :rest Puppet::SSL::Certificate.indirection.terminus_class.should == :rest Puppet::SSL::CertificateRequest.indirection.terminus_class.should == :rest Puppet::SSL::CertificateRevocationList.indirection.terminus_class.should == :rest end end describe "as 'only'" do before do Puppet::SSL::Host.ca_location = :only end it "should set the terminus class for Key, Certificate, CertificateRevocationList, and CertificateRequest as :ca" do Puppet::SSL::Key.indirection.terminus_class.should == :ca Puppet::SSL::Certificate.indirection.terminus_class.should == :ca Puppet::SSL::CertificateRequest.indirection.terminus_class.should == :ca Puppet::SSL::CertificateRevocationList.indirection.terminus_class.should == :ca end it "should set the cache class for Certificate, CertificateRevocationList, and CertificateRequest to nil" do Puppet::SSL::Certificate.indirection.cache_class.should be_nil Puppet::SSL::CertificateRequest.indirection.cache_class.should be_nil Puppet::SSL::CertificateRevocationList.indirection.cache_class.should be_nil end it "should set the terminus class for Host to :file" do Puppet::SSL::Host.indirection.terminus_class.should == :file end end describe "as 'none'" do before do Puppet::SSL::Host.ca_location = :none end it "should set the terminus class for Key, Certificate, CertificateRevocationList, and CertificateRequest as :file" do Puppet::SSL::Key.indirection.terminus_class.should == :disabled_ca Puppet::SSL::Certificate.indirection.terminus_class.should == :disabled_ca Puppet::SSL::CertificateRequest.indirection.terminus_class.should == :disabled_ca Puppet::SSL::CertificateRevocationList.indirection.terminus_class.should == :disabled_ca end it "should set the terminus class for Host to 'none'" do lambda { Puppet::SSL::Host.indirection.terminus_class }.should raise_error(Puppet::DevError) end end end it "should have a class method for destroying all files related to a given host" do Puppet::SSL::Host.should respond_to(:destroy) end describe "when destroying a host's SSL files" do before do Puppet::SSL::Key.indirection.stubs(:destroy).returns false Puppet::SSL::Certificate.indirection.stubs(:destroy).returns false Puppet::SSL::CertificateRequest.indirection.stubs(:destroy).returns false end it "should destroy its certificate, certificate request, and key" do Puppet::SSL::Key.indirection.expects(:destroy).with("myhost") Puppet::SSL::Certificate.indirection.expects(:destroy).with("myhost") Puppet::SSL::CertificateRequest.indirection.expects(:destroy).with("myhost") Puppet::SSL::Host.destroy("myhost") end it "should return true if any of the classes returned true" do Puppet::SSL::Certificate.indirection.expects(:destroy).with("myhost").returns true Puppet::SSL::Host.destroy("myhost").should be_true end it "should report that nothing was deleted if none of the classes returned true" do Puppet::SSL::Host.destroy("myhost").should == "Nothing was deleted" end end describe "when initializing" do it "should default its name to the :certname setting" do Puppet[:certname] = "myname" Puppet::SSL::Host.new.name.should == "myname" end it "should downcase a passed in name" do Puppet::SSL::Host.new("Host.Domain.Com").name.should == "host.domain.com" end it "should indicate that it is a CA host if its name matches the ca_name constant" do Puppet::SSL::Host.stubs(:ca_name).returns "myca" Puppet::SSL::Host.new("myca").should be_ca end end describe "when managing its private key" do before do @realkey = "mykey" @key = Puppet::SSL::Key.new("mykey") @key.content = @realkey end it "should return nil if the key is not set and cannot be found" do Puppet::SSL::Key.indirection.expects(:find).with("myname").returns(nil) @host.key.should be_nil end it "should find the key in the Key class and return the Puppet instance" do Puppet::SSL::Key.indirection.expects(:find).with("myname").returns(@key) @host.key.should equal(@key) end it "should be able to generate and save a new key" do Puppet::SSL::Key.expects(:new).with("myname").returns(@key) @key.expects(:generate) Puppet::SSL::Key.indirection.expects(:save) @host.generate_key.should be_true @host.key.should equal(@key) end it "should not retain keys that could not be saved" do Puppet::SSL::Key.expects(:new).with("myname").returns(@key) @key.stubs(:generate) Puppet::SSL::Key.indirection.expects(:save).raises "eh" lambda { @host.generate_key }.should raise_error @host.key.should be_nil end it "should return any previously found key without requerying" do Puppet::SSL::Key.indirection.expects(:find).with("myname").returns(@key).once @host.key.should equal(@key) @host.key.should equal(@key) end end describe "when managing its certificate request" do before do @realrequest = "real request" @request = Puppet::SSL::CertificateRequest.new("myname") @request.content = @realrequest end it "should return nil if the key is not set and cannot be found" do Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myname").returns(nil) @host.certificate_request.should be_nil end it "should find the request in the Key class and return it and return the Puppet SSL request" do Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myname").returns @request @host.certificate_request.should equal(@request) end it "should generate a new key when generating the cert request if no key exists" do Puppet::SSL::CertificateRequest.expects(:new).with("myname").returns @request key = stub 'key', :public_key => mock("public_key"), :content => "mycontent" @host.expects(:key).times(2).returns(nil).then.returns(key) @host.expects(:generate_key).returns(key) @request.stubs(:generate) Puppet::SSL::CertificateRequest.indirection.stubs(:save) @host.generate_certificate_request end it "should be able to generate and save a new request using the private key" do Puppet::SSL::CertificateRequest.expects(:new).with("myname").returns @request key = stub 'key', :public_key => mock("public_key"), :content => "mycontent" @host.stubs(:key).returns(key) @request.expects(:generate).with("mycontent", {}) Puppet::SSL::CertificateRequest.indirection.expects(:save).with(@request) @host.generate_certificate_request.should be_true @host.certificate_request.should equal(@request) end it "should return any previously found request without requerying" do Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myname").returns(@request).once @host.certificate_request.should equal(@request) @host.certificate_request.should equal(@request) end it "should not keep its certificate request in memory if the request cannot be saved" do Puppet::SSL::CertificateRequest.expects(:new).with("myname").returns @request key = stub 'key', :public_key => mock("public_key"), :content => "mycontent" @host.stubs(:key).returns(key) @request.stubs(:generate) @request.stubs(:name).returns("myname") terminus = stub 'terminus' terminus.stubs(:validate) Puppet::SSL::CertificateRequest.indirection.expects(:prepare).returns(terminus) terminus.expects(:save).with { |req| req.instance == @request && req.key == "myname" }.raises "eh" lambda { @host.generate_certificate_request }.should raise_error @host.instance_eval { @certificate_request }.should be_nil end end describe "when managing its certificate" do before do @realcert = mock 'certificate' @cert = stub 'cert', :content => @realcert @host.stubs(:key).returns mock("key") @host.stubs(:validate_certificate_with_key) end it "should find the CA certificate if it does not have a certificate" do - Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert") + Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME, :fail_on_404 => true).returns mock("cacert") Puppet::SSL::Certificate.indirection.stubs(:find).with("myname").returns @cert @host.certificate end it "should not find the CA certificate if it is the CA host" do @host.expects(:ca?).returns true Puppet::SSL::Certificate.indirection.stubs(:find) - Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).never + Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME, :fail_on_404 => true).never @host.certificate end it "should return nil if it cannot find a CA certificate" do - Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns nil + Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME, :fail_on_404 => true).returns nil Puppet::SSL::Certificate.indirection.expects(:find).with("myname").never @host.certificate.should be_nil end it "should find the key if it does not have one" do Puppet::SSL::Certificate.indirection.stubs(:find) @host.expects(:key).returns mock("key") @host.certificate end it "should generate the key if one cannot be found" do Puppet::SSL::Certificate.indirection.stubs(:find) @host.expects(:key).returns nil @host.expects(:generate_key) @host.certificate end it "should find the certificate in the Certificate class and return the Puppet certificate instance" do - Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert") + Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME, :fail_on_404 => true).returns mock("cacert") Puppet::SSL::Certificate.indirection.expects(:find).with("myname").returns @cert @host.certificate.should equal(@cert) end it "should return any previously found certificate" do - Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert") + Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME, :fail_on_404 => true).returns mock("cacert") Puppet::SSL::Certificate.indirection.expects(:find).with("myname").returns(@cert).once @host.certificate.should equal(@cert) @host.certificate.should equal(@cert) end end it "should have a method for listing certificate hosts" do Puppet::SSL::Host.should respond_to(:search) end describe "when listing certificate hosts" do it "should default to listing all clients with any file types" do Puppet::SSL::Key.indirection.expects(:search).returns [] Puppet::SSL::Certificate.indirection.expects(:search).returns [] Puppet::SSL::CertificateRequest.indirection.expects(:search).returns [] Puppet::SSL::Host.search end it "should be able to list only clients with a key" do Puppet::SSL::Key.indirection.expects(:search).returns [] Puppet::SSL::Certificate.indirection.expects(:search).never Puppet::SSL::CertificateRequest.indirection.expects(:search).never Puppet::SSL::Host.search :for => Puppet::SSL::Key end it "should be able to list only clients with a certificate" do Puppet::SSL::Key.indirection.expects(:search).never Puppet::SSL::Certificate.indirection.expects(:search).returns [] Puppet::SSL::CertificateRequest.indirection.expects(:search).never Puppet::SSL::Host.search :for => Puppet::SSL::Certificate end it "should be able to list only clients with a certificate request" do Puppet::SSL::Key.indirection.expects(:search).never Puppet::SSL::Certificate.indirection.expects(:search).never Puppet::SSL::CertificateRequest.indirection.expects(:search).returns [] Puppet::SSL::Host.search :for => Puppet::SSL::CertificateRequest end it "should return a Host instance created with the name of each found instance" do key = stub 'key', :name => "key", :to_ary => nil cert = stub 'cert', :name => "cert", :to_ary => nil csr = stub 'csr', :name => "csr", :to_ary => nil Puppet::SSL::Key.indirection.expects(:search).returns [key] Puppet::SSL::Certificate.indirection.expects(:search).returns [cert] Puppet::SSL::CertificateRequest.indirection.expects(:search).returns [csr] returned = [] %w{key cert csr}.each do |name| result = mock(name) returned << result Puppet::SSL::Host.expects(:new).with(name).returns result end result = Puppet::SSL::Host.search returned.each do |r| result.should be_include(r) end end end it "should have a method for generating all necessary files" do Puppet::SSL::Host.new("me").should respond_to(:generate) end describe "when generating files" do before do @host = Puppet::SSL::Host.new("me") @host.stubs(:generate_key) @host.stubs(:generate_certificate_request) end it "should generate a key if one is not present" do @host.stubs(:key).returns nil @host.expects(:generate_key) @host.generate end it "should generate a certificate request if one is not present" do @host.expects(:certificate_request).returns nil @host.expects(:generate_certificate_request) @host.generate end describe "and it can create a certificate authority" do before do @ca = mock 'ca' Puppet::SSL::CertificateAuthority.stubs(:instance).returns @ca end it "should use the CA to sign its certificate request if it does not have a certificate" do @host.expects(:certificate).returns nil @ca.expects(:sign).with(@host.name, true) @host.generate end end describe "and it cannot create a certificate authority" do before do Puppet::SSL::CertificateAuthority.stubs(:instance).returns nil end it "should seek its certificate" do @host.expects(:certificate) @host.generate end end end it "should have a method for creating an SSL store" do Puppet::SSL::Host.new("me").should respond_to(:ssl_store) end it "should always return the same store" do host = Puppet::SSL::Host.new("foo") store = mock 'store' store.stub_everything OpenSSL::X509::Store.expects(:new).returns store host.ssl_store.should equal(host.ssl_store) end describe "when creating an SSL store" do before do @host = Puppet::SSL::Host.new("me") @store = mock 'store' @store.stub_everything OpenSSL::X509::Store.stubs(:new).returns @store Puppet[:localcacert] = "ssl_host_testing" Puppet::SSL::CertificateRevocationList.indirection.stubs(:find).returns(nil) end it "should accept a purpose" do @store.expects(:purpose=).with "my special purpose" @host.ssl_store("my special purpose") end it "should default to OpenSSL::X509::PURPOSE_ANY as the purpose" do @store.expects(:purpose=).with OpenSSL::X509::PURPOSE_ANY @host.ssl_store end it "should add the local CA cert file" do Puppet[:localcacert] = "/ca/cert/file" @store.expects(:add_file).with Puppet[:localcacert] @host.ssl_store end describe "and a CRL is available" do before do @crl = stub 'crl', :content => "real_crl" Puppet::SSL::CertificateRevocationList.indirection.stubs(:find).returns @crl end describe "and 'certificate_revocation' is true" do before do Puppet[:certificate_revocation] = true end it "should add the CRL" do @store.expects(:add_crl).with "real_crl" @host.ssl_store end it "should set the flags to OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK" do @store.expects(:flags=).with OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK @host.ssl_store end end describe "and 'certificate_revocation' is false" do before do Puppet[:certificate_revocation] = false end it "should not add the CRL" do @store.expects(:add_crl).never @host.ssl_store end it "should not set the flags" do @store.expects(:flags=).never @host.ssl_store end end end end describe "when waiting for a cert" do before do @host = Puppet::SSL::Host.new("me") end it "should generate its certificate request and attempt to read the certificate again if no certificate is found" do @host.expects(:certificate).times(2).returns(nil).then.returns "foo" @host.expects(:generate) @host.wait_for_cert(1) end it "should catch and log errors during CSR saving" do @host.expects(:certificate).times(2).returns(nil).then.returns "foo" @host.expects(:generate).raises(RuntimeError).then.returns nil @host.stubs(:sleep) @host.wait_for_cert(1) end it "should sleep and retry after failures saving the CSR if waitforcert is enabled" do @host.expects(:certificate).times(2).returns(nil).then.returns "foo" @host.expects(:generate).raises(RuntimeError).then.returns nil @host.expects(:sleep).with(1) @host.wait_for_cert(1) end it "should exit after failures saving the CSR of waitforcert is disabled" do @host.expects(:certificate).returns(nil) @host.expects(:generate).raises(RuntimeError) @host.expects(:puts) expect { @host.wait_for_cert(0) }.to exit_with 1 end it "should exit if the wait time is 0 and it can neither find nor retrieve a certificate" do @host.stubs(:certificate).returns nil @host.expects(:generate) @host.expects(:puts) expect { @host.wait_for_cert(0) }.to exit_with 1 end it "should sleep for the specified amount of time if no certificate is found after generating its certificate request" do @host.expects(:certificate).times(3).returns(nil).then.returns(nil).then.returns "foo" @host.expects(:generate) @host.expects(:sleep).with(1) @host.wait_for_cert(1) end it "should catch and log exceptions during certificate retrieval" do @host.expects(:certificate).times(3).returns(nil).then.raises(RuntimeError).then.returns("foo") @host.stubs(:generate) @host.stubs(:sleep) Puppet.expects(:err) @host.wait_for_cert(1) end end describe "when handling PSON", :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files before do Puppet[:vardir] = tmpdir("ssl_test_vardir") Puppet[:ssldir] = tmpdir("ssl_test_ssldir") # localcacert is where each client stores the CA certificate # cacert is where the master stores the CA certificate # Since we need to play the role of both for testing we need them to be the same and exist Puppet[:cacert] = Puppet[:localcacert] @ca=Puppet::SSL::CertificateAuthority.new end describe "when converting to PSON" do let(:host) do Puppet::SSL::Host.new("bazinga") end let(:pson_hash) do { "fingerprint" => host.certificate_request.fingerprint, "desired_state" => 'requested', "name" => host.name } end it "should be able to identify a host with an unsigned certificate request" do host.generate_certificate_request result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson) base_pson_comparison result, pson_hash end it "should validate against the schema" do host.generate_certificate_request expect(host.to_pson).to validate_against('api/schemas/host.json') end describe "explicit fingerprints" do [:SHA1, :SHA256, :SHA512].each do |md| it "should include #{md}" do mds = md.to_s host.generate_certificate_request pson_hash["fingerprints"] = {} pson_hash["fingerprints"][mds] = host.certificate_request.fingerprint(md) result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson) base_pson_comparison result, pson_hash result["fingerprints"][mds].should == pson_hash["fingerprints"][mds] end end end describe "dns_alt_names" do describe "when not specified" do it "should include the dns_alt_names associated with the certificate" do host.generate_certificate_request pson_hash["desired_alt_names"] = host.certificate_request.subject_alt_names result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson) base_pson_comparison result, pson_hash result["dns_alt_names"].should == pson_hash["desired_alt_names"] end end [ "", "test, alt, names" ].each do |alt_names| describe "when #{alt_names}" do before(:each) do host.generate_certificate_request :dns_alt_names => alt_names end it "should include the dns_alt_names associated with the certificate" do pson_hash["desired_alt_names"] = host.certificate_request.subject_alt_names result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson) base_pson_comparison result, pson_hash result["dns_alt_names"].should == pson_hash["desired_alt_names"] end it "should validate against the schema" do expect(host.to_pson).to validate_against('api/schemas/host.json') end end end end it "should be able to identify a host with a signed certificate" do host.generate_certificate_request @ca.sign(host.name) pson_hash = { "fingerprint" => Puppet::SSL::Certificate.indirection.find(host.name).fingerprint, "desired_state" => 'signed', "name" => host.name, } result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson) base_pson_comparison result, pson_hash end it "should be able to identify a host with a revoked certificate" do host.generate_certificate_request @ca.sign(host.name) @ca.revoke(host.name) pson_hash["fingerprint"] = Puppet::SSL::Certificate.indirection.find(host.name).fingerprint pson_hash["desired_state"] = 'revoked' result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson) base_pson_comparison result, pson_hash end end describe "when converting from PSON" do it "should return a Puppet::SSL::Host object with the specified desired state" do host = Puppet::SSL::Host.new("bazinga") host.desired_state="signed" pson_hash = { "name" => host.name, "desired_state" => host.desired_state, } generated_host = Puppet::SSL::Host.from_data_hash(pson_hash) generated_host.desired_state.should == host.desired_state generated_host.name.should == host.name end end end end