diff --git a/acceptance/Rakefile b/acceptance/Rakefile index 6933b319c..903dbef7f 100644 --- a/acceptance/Rakefile +++ b/acceptance/Rakefile @@ -1,353 +1,353 @@ require 'rake/clean' require 'pp' require 'yaml' $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), 'lib')) require 'puppet/acceptance/git_utils' extend Puppet::Acceptance::GitUtils ONE_DAY_IN_SECS = 24 * 60 * 60 REPO_CONFIGS_DIR = "repo-configs" CLEAN.include('*.tar', REPO_CONFIGS_DIR, 'merged_options.rb') module HarnessOptions DEFAULTS = { :type => 'git', :helper => ['lib/helper.rb'], :tests => ['tests'], :log_level => 'debug', :color => false, :root_keys => true, :ssh => { - :keys => ["id_rsa-acceptance"], + :keys => ["id_rsa_acceptance", "#{ENV['HOME']}/.ssh/id_rsa-acceptance"], }, :xml => true, :timesync => false, :repo_proxy => true, :add_el_extras => true, :preserve_hosts => 'onfail', :forge_host => 'forge-aio01-petest.puppetlabs.com', :'master-start-curl-retries' => 30, } class Aggregator attr_reader :mode def initialize(mode) @mode = mode end def get_options(file_path) puts file_path if File.exists? file_path options = eval(File.read(file_path), binding) else puts "No options file found at #{File.expand_path(file_path)}" end options || {} end def get_mode_options get_options("./config/#{mode}/options.rb") end def get_local_options get_options("./local_options.rb") end def final_options(intermediary_options = {}) mode_options = get_mode_options local_overrides = get_local_options final_options = DEFAULTS.merge(mode_options) final_options.merge!(intermediary_options) final_options.merge!(local_overrides) return final_options end end def self.options(mode, options) final_options = Aggregator.new(mode).final_options(options) final_options end end def beaker_test(mode = :packages, options = {}) delete_options = options.delete(:__delete_options__) || [] final_options = HarnessOptions.options(mode, options) preserve_config = final_options.delete(:__preserve_config__) if mode == :git # Build up project git urls based on git server and fork env variables or defaults final_options[:install].map! do |install| if md = /^(\w+)#(\w+)$/.match(install) project, project_sha = md.captures "#{build_giturl(project)}##{project_sha}" elsif md = /^(\w+)$/.match(install) project = md[1] "#{build_giturl(project)}##{sha}" end end end delete_options.each do |delete_me| final_options.delete(delete_me) end options_file = 'merged_options.rb' File.open(options_file, 'w') do |merged| merged.puts <<-EOS # Copy this file to local_options.rb and adjust as needed if you wish to run # with some local overrides. EOS merged.puts(final_options.pretty_inspect) end tests = ENV['TESTS'] || ENV['TEST'] tests_opt = "--tests=#{tests}" if tests config_opt = "--hosts=#{config}" if config overriding_options = ENV['OPTIONS'] args = ["--options-file", options_file, config_opt, tests_opt, overriding_options].compact begin sh("beaker", *args) ensure preserve_configuration(final_options, options_file) if preserve_config end end def preserve_configuration(final_options, options_file) if (hosts_file = config || final_options[:hosts_file]) && hosts_file !~ /preserved_config/ cp(hosts_file, "log/latest/config.yml") generate_config_for_latest_hosts end mv(options_file, "log/latest") end def generate_config_for_latest_hosts preserved_config_hash = { 'HOSTS' => {} } puts "\nPreserving configuration so that any preserved nodes can be tested again locally..." config_hash = YAML.load_file('log/latest/config.yml') if !config_hash || !config_hash.include?('HOSTS') puts "Warning: No HOSTS configuration found in log/latest/config.yml" return else nodes = config_hash['HOSTS'].map do |node_label,hash| { :node_label => node_label, :roles => hash['roles'], :platform => hash['platform'] } end pre_suite_log = File.read('log/latest/pre_suite-run.log') nodes.each do |node_info| host_regex = /^([\w.]+) \(#{node_info[:node_label]}\)/ if matched = host_regex.match(pre_suite_log) hostname = matched[1] fqdn = (hostname =~ /\./) ? hostname : "#{hostname}.delivery.puppetlabs.net" elsif /^#{node_info[:node_label]} /.match(pre_suite_log) fqdn = "#{node_info[:node_label]}" puts "* Couldn't find any log lines for #{host_regex}, assuming #{fqdn} is the fqdn" end if fqdn preserved_config_hash['HOSTS'][fqdn] = { 'roles' => node_info[:roles], 'platform' => node_info[:platform], } else puts "* Couldn't match #{node_info[:node_label]} in pre_suite-run.log" end end pp preserved_config_hash File.open('log/latest/preserved_config.yaml', 'w') do |config_file| YAML.dump(preserved_config_hash, config_file) end end rescue Errno::ENOENT => e puts "Warning: Couldn't generate preserved_config.yaml #{e}" end def list_preserved_configurations(secs_ago = ONE_DAY_IN_SECS) preserved = {} Dir.glob('log/*_*').each do |dir| preserved_config_path = "#{dir}/preserved_config.yaml" yesterday = Time.now - secs_ago.to_i if preserved_config = File.exists?(preserved_config_path) directory = File.new(dir) if directory.ctime > yesterday hosts = [] preserved_config = YAML.load_file(preserved_config_path).to_hash preserved_config['HOSTS'].each do |hostname,values| hosts << "#{hostname}: #{values['platform']}, #{values['roles']}" end preserved[hosts] = directory.to_path end end end preserved.map { |k,v| [v,k] }.sort { |a,b| a[0] <=> b[0] }.reverse end def list_preserved_hosts(secs_ago = ONE_DAY_IN_SECS) hosts = Set.new Dir.glob('log/**/pre*suite*run.log').each do |log| yesterday = Time.now - secs_ago.to_i File.open(log, 'r') do |file| if file.ctime > yesterday file.each_line do |line| matchdata = /^(\w+)(?:\.[\w.]+)? \(.*?\) \d\d:\d\d:\d\d\$/.match(line.encode!('UTF-8', 'UTF-8', :invalid => :replace)) hosts.add(matchdata[1]) if matchdata end end end end hosts end def release_hosts(hosts = nil, secs_ago = ONE_DAY_IN_SECS) secs_ago ||= ONE_DAY_IN_SECS hosts ||= list_preserved_hosts(secs_ago) hosts.each do |h| hostname = h.split('.').first puts "Releaseing '#{hostname}'" puts `curl -X DELETE --url http://vcloud.delivery.puppetlabs.net/vm/#{hostname}` end end def print_preserved(preserved) preserved.each_with_index do |entry,i| puts "##{i}: #{entry[0]}" entry[1].each { |h| puts " #{h}" } end end def beaker_run_type type = ENV['TYPE'] || :packages type = type.to_sym end def sha ENV['SHA'] end def config ENV['CONFIG'] end namespace :ci do task :check_env do raise(USAGE) unless sha end namespace :test do USAGE = <<-EOS Requires commit SHA to be put under test as environment variable: SHA=''. Also must set CONFIG=config/nodes/foo.yaml or include it in an options.rb for Beaker. You may set TESTS=path/to/test,and/more/tests. You may set additional Beaker OPTIONS='--more --options' If testing from git checkouts, you may optionally set the github fork to checkout from using PUPPET_FORK='some-other-puppet-fork' (you may change the HIERA_FORK and FACTER_FORK as well if you wish). You may also optionally set the git server to checkout repos from using GIT_SERVER='some.git.mirror'. Or you may set PUPPET_GIT_SERVER='my.host.with.git.daemon', specifically, if you have set up a `git daemon` to pull local commits from. (You will need to allow the git daemon to serve the repo (see `git help daemon` and the docs/acceptance_tests.md for more details)). If there is a Beaker options hash in a ./local_options.rb, it will be included. Commandline options set through the above environment variables will override settings in this file. EOS desc <<-EOS Run the acceptance tests through Beaker and install packages on the configuration targets. #{USAGE} EOS task :packages => 'ci:check_env' do beaker_test end desc <<-EOS Run the acceptance tests through Beaker and install from git on the configuration targets. #{USAGE} EOS task :git => 'ci:check_env' do beaker_test(:git) end end desc "Capture the master and agent hostname from the latest log and construct a preserved_config.yaml for re-running against preserved hosts without provisioning." task :extract_preserved_config do generate_config_for_latest_hosts end desc <<-EOS Run an acceptance test for a given node configuration and preserve the hosts. Defaults to a packages run, but you can set it to 'git' with TYPE='git'. #{USAGE} EOS task :test_and_preserve_hosts => 'ci:check_env' do beaker_test(beaker_run_type, :preserve_hosts => 'always', :__preserve_config__ => true) end desc "List acceptance runs from the past day which had hosts preserved." task :list_preserved do preserved = list_preserved_configurations print_preserved(preserved) end desc <<-EOS Shutdown and destroy any hosts that we have preserved for testing. These should be reaped daily by scripts, but this will free up resources immediately. Specify a list of comma separated HOST_NAMES if you have a set of dynamic vcloud host names you want to purge outside of what can be grepped from the logs. You can go back through the last SECS_AGO logs. Default is one day ago in secs. EOS task :release_hosts do host_names = ENV['HOST_NAMES'].split(',') if ENV['HOST_NAMES'] secs_ago = ENV['SECS_AGO'] release_hosts(host_names, secs_ago) end task :destroy_preserved_hosts => 'ci:release_hosts' do puts "Note: we are now releasing hosts back to the vcloud pooling api rather than destroying them directly. The rake task for this is ci:release_hosts" end desc <<-EOS Rerun an acceptance test using the last captured preserved_config.yaml to skip provisioning. Or specify a CONFIG_NUMBER from `rake ci:list_preserved`. Defaults to a packages run, but you can set it to 'git' with TYPE='git'. EOS task :test_against_preserved_hosts do config_number = (ENV['CONFIG_NUMBER'] || 0).to_i preserved = list_preserved_configurations print_preserved(preserved) config_path = preserved[config_number][0] puts "Using ##{config_number}: #{config_path}" options = { :hosts_file => "#{config_path}/preserved_config.yaml", :no_provision => true, :preserve_hosts => 'always', } run_type = beaker_run_type if run_type == :packages options.merge!(:pre_suite => [ 'setup/packages/pre-suite/015_PackageHostsPresets.rb', 'setup/packages/pre-suite/045_EnsureMasterStartedOnPassenger.rb', ]) else options.merge!(:__delete_options__ => [:pre_suite]) end beaker_test(beaker_run_type, options) end end task :default do sh('rake -T') end task :spec do sh('rspec lib') end diff --git a/acceptance/lib/puppet/acceptance/module_utils.rb b/acceptance/lib/puppet/acceptance/module_utils.rb index 5b6124998..05178336c 100644 --- a/acceptance/lib/puppet/acceptance/module_utils.rb +++ b/acceptance/lib/puppet/acceptance/module_utils.rb @@ -1,282 +1,273 @@ module Puppet module Acceptance module ModuleUtils # Return an array of module paths for a given host. # # Example return value: # # [ # "/etc/puppetlabs/puppet/environments/production/modules", # "/etc/puppetlabs/puppet/modules", # "/opt/puppet/share/puppet/modules", # ] # # @param host [String] hostname # @return [Array] paths for found modulepath def get_modulepaths_for_host (host) separator = ':' if host['platform'] =~ /windows/ separator = ';' end environment = on(host, puppet("config print environment")).stdout.chomp on(host, puppet("config print modulepath --environment #{environment}")).stdout.chomp.split(separator) end # Return a string of the default (first) path in modulepath for a given host. # # Example return value: # # "/etc/puppetlabs/puppet/environments/production/modules" # # @param host [String] hostname # @return [String] first path for found modulepath def get_default_modulepath_for_host (host) get_modulepaths_for_host(host)[0] end # Return an array of paths to installed modules for a given host. # # Example return value: # # [ # "/opt/puppet/share/puppet/modules/apt", # "/opt/puppet/share/puppet/modules/auth_conf", # "/opt/puppet/share/puppet/modules/concat", # ] # # @param host [String] hostname # @return [Array] paths for found modules def get_installed_modules_for_host (host) on host, puppet("module list --render-as pson") str = stdout.lines.to_a.last pat = /\(([^()]+)\)/ mods = str.scan(pat).flatten return mods end # Return a hash of array of paths to installed modules for a hosts. # The individual hostnames are the keys of the hash. The only value # for a given key is an array of paths for the found modules. # # Example return value: # # { # "my_master" => # [ # "/opt/puppet/share/puppet/modules/apt", # "/opt/puppet/share/puppet/modules/auth_conf", # "/opt/puppet/share/puppet/modules/concat", # ], # "my_agent01" => # [ # "/opt/puppet/share/puppet/modules/apt", # "/opt/puppet/share/puppet/modules/auth_conf", # "/opt/puppet/share/puppet/modules/concat", # ], # } # # @param hosts [Array] hostnames # @return [Hash] paths for found modules indexed by hostname def get_installed_modules_for_hosts (hosts) mods = {} hosts.each do |host| mods[host] = get_installed_modules_for_host host end return mods end # Compare the module paths in given hashes and remove paths that # are were not present in the first hash. The use case for this # method is to remove any modules that were installed during the # course of a test run. # # Installed module hashes would be gathered using the # `get_+installed_module_for_hosts` command in the setup stage # and teardown stages of a test. These hashes would be passed into # this method in order to find modules installed during the test # and delete them in order to return the SUT environments to their # initial state. # # TODO: Enhance to take versions into account, so that upgrade/ # downgrade events during a test does not persist in the SUT # environment. # # @param beginning_hash [Hash] paths for found modules indexed # by hostname. Taken in the setup stage of a test. # @param ending_hash [Hash] paths for found modules indexed # by hostname. Taken in the teardown stage of a test. def rm_installed_modules_from_hosts (beginning_hash, ending_hash) ending_hash.each do |host, mod_array| mod_array.each do |mod| if ! beginning_hash[host].include? mod on host, "rm -rf '#{mod}'" end end end end # Convert a semantic version number string to an integer. # # Example return value given an input of '1.2.42': # # 10242 # # @param semver [String] semantic version number def semver_to_i ( semver ) # semver assumed to be in format .. # calculation assumes that each segment is < 100 tmp = semver.split('.') tmp[0].to_i * 10000 + tmp[1].to_i * 100 + tmp[2].to_i end # Compare two given semantic version numbers. # # Returns an integer indicating the relationship between the two: # 0 indicates that both are equal # a value greater than 0 indicates that the semver1 is greater than semver2 # a value less than 0 indicates that the semver1 is less than semver2 # def semver_cmp ( semver1, semver2 ) semver_to_i(semver1) - semver_to_i(semver2) end # Assert that a module was installed according to the UI.. # # This is a wrapper to centralize the validation about how # the UI responded that a module was installed. # It is called after a call # to `on ( host )` and inspects # STDOUT for specific content. # # @param stdout [String] # @param module_author [String] the author portion of a module name # @param module_name [String] the name portion of a module name # @param module_verion [String] the version of the module to compare to # installed version # @param compare_op [String] the operator for comparing the verions of # the installed module def assert_module_installed_ui ( stdout, module_author, module_name, module_version = nil, compare_op = nil ) valid_compare_ops = {'==' => 'equal to', '>' => 'greater than', '<' => 'less than'} assert_match(/#{module_author}-#{module_name}/, stdout, "Notice that module '#{module_author}-#{module_name}' was installed was not displayed") if version /#{module_author}-#{module_name} \(.*v(\d+\.\d+\.\d+)/ =~ stdout installed_version = Regexp.last_match[1] if valid_compare_ops.include? compare_op assert_equal( true, semver_cmp(installed_version, module_version).send(compare_op, 0), "Installed version '#{installed_version}' of '#{module_name}' was not #{valid_compare_ops[compare_op]} '#{module_version}'") end end end # Assert that a module is installed on disk. # # @param host [HOST] the host object to make the remote call on # @param module_name [String] the name portion of a module name # @param optional moduledir [String, Array] the path where the module should be, will # iterate over components of the modulepath by default. def assert_module_installed_on_disk (host, module_name, moduledir=nil) moduledir ||= get_modulepaths_for_host(host) modulepath = moduledir.is_a?(Array) ? moduledir : [moduledir] moduledir= nil modulepath.each do |i| # module directory should exist if on(host, %Q{[ -d "#{i}/#{module_name}" ]}, :acceptable_exit_codes => (0..255)).exit_code == 0 moduledir = i end end fail_test('module directory not found') unless moduledir owner = '' group = '' on host, %Q{ls -ld "#{moduledir}"} do listing = stdout.split(' ') owner = listing[2] group = listing[3] end # A module's files should have: # * a mode of 444 (755, if they're a directory) # * owner == owner of moduledir # * group == group of moduledir on host, %Q{ls -alR "#{moduledir}/#{module_name}"} do listings = stdout.split("\n") listings = listings.grep(/^[bcdlsp-]/) listings = listings.reject { |l| l =~ /\.\.$/ } listings.each do |line| assert_match /(drwxr-xr-x|[^d]r--r--r--)[^\d]+\d+\s+#{owner}\s+#{group}/, line, "bad permissions for '#{line[/\S+$/]}' - expected 444/755, #{owner}, #{group}" end end end # Assert that a module is not installed on disk. # # @param host [HOST] the host object to make the remote call on # @param module_name [String] the name portion of a module name # @param optional moduledir [String, Array] the path where the module should be, will # iterate over components of the modulepath by default. def assert_module_not_installed_on_disk (host, module_name, moduledir=nil) moduledir ||= get_modulepaths_for_host(host) modulepath = moduledir.is_a?(Array) ? moduledir : [moduledir] moduledir= nil modulepath.each do |i| # module directory should not exist on host, %Q{[ ! -d "#{i}/#{module_name}" ]} end end - # Create a simple legacy and directory environment at :path_to_environments. + # Create a simple directory environment and puppet.conf at :tmpdir. # # @note Also registers a teardown block to remove generated files. # - # @param path_to_environments [String] directory to contain all the + # @param tmpdir [String] directory to contain all the # generated environment files # @return [String] path to the new puppet configuration file defining the # environments - def generate_base_legacy_and_directory_environments(path_to_environments) - puppet_conf = "#{path_to_environments}/puppet2.conf" - legacy_env = "#{path_to_environments}/legacyenv" - dir_envs = "#{path_to_environments}/environments" + def generate_base_directory_environments(tmpdir) + puppet_conf = "#{tmpdir}/puppet2.conf" + dir_envs = "#{tmpdir}/environments" - step "ensure we don't have left over bad state from another, possibly failed run" - on master, "rm -rf #{legacy_env} #{dir_envs} #{puppet_conf}" - - # and register to clean up afterwords - teardown do - on master, "rm -rf #{legacy_env} #{dir_envs} #{puppet_conf}" - end - - step 'Configure a non-default legacy and directory environment' + step 'Configure a the direnv directory environment' apply_manifest_on master, %Q{ + File { + ensure => directory, + owner => #{master.puppet['user']}, + group => #{master.puppet['group']}, + mode => "0750", + } file { [ - '#{legacy_env}', - '#{legacy_env}/modules', '#{dir_envs}', '#{dir_envs}/direnv', ]: - ensure => directory, } + file { '#{puppet_conf}': - source => $settings::config, + ensure => file, + content => " + [main] + environmentpath=#{dir_envs} + " } } - # remove environmentpath entry from config - on master, "sed '/environmentpath/d' #{puppet_conf} > #{path_to_environments}/tmp && mv #{path_to_environments}/tmp #{puppet_conf}" - - on master, puppet("config", "set", - "modulepath", "#{legacy_env}/modules", - "--section", "legacyenv", - "--config", puppet_conf) - return puppet_conf end end end end diff --git a/acceptance/tests/environment/agent_runs_pluginsync_with_proper_environment.rb b/acceptance/tests/environment/agent_runs_pluginsync_with_proper_environment.rb index 828344988..99103b16b 100644 --- a/acceptance/tests/environment/agent_runs_pluginsync_with_proper_environment.rb +++ b/acceptance/tests/environment/agent_runs_pluginsync_with_proper_environment.rb @@ -1,63 +1,63 @@ # We noticed some strange behavior if an environment was changed between the # time where the node retrieved facts for itself and the catalog retrieved # facts puppet could pluginsync with the incorrect environment. For more # details see PUP-3591. test_name "Agent should pluginsync with the environment the agent resolves to" testdir = create_tmpdir_for_user master, 'environment_resolve' create_remote_file master, "#{testdir}/enc.rb", < true) File { ensure => directory, mode => "0770", owner => #{master.puppet['user']}, group => #{master.puppet['group']}, } file { '#{testdir}/environments':; '#{testdir}/environments/production':; '#{testdir}/environments/correct/':; '#{testdir}/environments/correct/modules':; '#{testdir}/environments/correct/modules/amod':; '#{testdir}/environments/correct/modules/amod/lib':; '#{testdir}/environments/correct/modules/amod/lib/puppet':; } file { '#{testdir}/environments/correct/modules/amod/lib/puppet/foo.rb': ensure => file, mode => "0640", content => "#correct_version", } MANIFEST master_opts = { 'main' => { 'environmentpath' => "#{testdir}/environments", }, 'master' => { 'node_terminus' => 'exec', 'external_nodes' => "#{testdir}/enc.rb" }, } with_puppet_running_on master, master_opts, testdir do agents.each do |agent| - run_agent_on(agent, "--no-daemonize --onetime --server #{master}") - on agent, "cat \"#{agent.puppet['vardir']}/lib/puppet/foo.rb\"" + on(agent, puppet("agent", "-t", "--server #{master}")) + on(agent, "cat \"#{agent.puppet['vardir']}/lib/puppet/foo.rb\"") assert_match(/#correct_version/, stdout, "The plugin from environment 'correct' was not synced") - on agent, "rm -rf \"#{agent.puppet['vardir']}/lib\"" + on(agent, "rm -rf \"#{agent.puppet['vardir']}/lib\"") end end diff --git a/acceptance/tests/environment/cmdline_overrides_environment.rb b/acceptance/tests/environment/cmdline_overrides_environment.rb deleted file mode 100644 index ad3652c7f..000000000 --- a/acceptance/tests/environment/cmdline_overrides_environment.rb +++ /dev/null @@ -1,330 +0,0 @@ -test_name "Commandline modulepath and manifest settings override environment" - -skip_test "CLI-master tests are not applicable" if @options[:is_puppetserver] - -require 'puppet/acceptance/classifier_utils' -extend Puppet::Acceptance::ClassifierUtils - -classify_nodes_as_agent_specified_if_classifer_present - -testdir = create_tmpdir_for_user master, 'cmdline_and_environment' -environmentpath = "#{testdir}/environments" -modulepath = "#{testdir}/modules" -manifests = "#{testdir}/manifests" -sitepp = "#{manifests}/site.pp" -other_manifestdir = "#{testdir}/other_manifests" -other_sitepp = "#{other_manifestdir}/site.pp" -other_modulepath = "#{testdir}/some_other_modulepath" -cmdline_manifest = "#{testdir}/cmdline.pp" - -step "Prepare manifests and modules" -apply_manifest_on(master, <<-MANIFEST, :catch_failures => true) -File { - ensure => directory, - owner => #{master['user']}, - group => #{master['group']}, - mode => "0750", -} - -############################################## -# A production directory environment -file { - "#{testdir}":; - "#{environmentpath}":; - "#{environmentpath}/production":; - "#{environmentpath}/production/manifests":; - "#{environmentpath}/production/modules":; - "#{environmentpath}/production/modules/amod":; - "#{environmentpath}/production/modules/amod/manifests":; -} - -file { "#{environmentpath}/production/modules/amod/manifests/init.pp": - ensure => file, - mode => "0640", - content => 'class amod { - notify { "amod from production environment": } - }' -} - -file { "#{environmentpath}/production/manifests/production.pp": - ensure => file, - mode => "0640", - content => ' - notify { "in production.pp": } - include amod - ' -} - -############################################################## -# To be set as default manifests and modulepath in puppet.conf -file { - "#{modulepath}":; - "#{modulepath}/amod/":; - "#{modulepath}/amod/manifests":; -} - -file { "#{modulepath}/amod/manifests/init.pp": - ensure => file, - mode => "0640", - content => 'class amod { - notify { "amod from modulepath": } - }' -} - -file { "#{manifests}": } -file { "#{sitepp}": - ensure => file, - mode => "0640", - content => ' - notify { "in site.pp": } - include amod - ' -} - -file { "#{other_manifestdir}": } -file { "#{other_sitepp}": - ensure => file, - mode => "0640", - content => ' - notify { "in other manifestdir site.pp": } - include amod - ' -} - -################################ -# To be specified on commandline -file { - "#{other_modulepath}":; - "#{other_modulepath}/amod/":; - "#{other_modulepath}/amod/manifests":; -} - -file { "#{other_modulepath}/amod/manifests/init.pp": - ensure => file, - mode => "0640", - content => 'class amod { - notify { "amod from commandline modulepath": } - }' -} - -file { "#{cmdline_manifest}": - ensure => file, - mode => "0640", - content => ' - notify { "in cmdline.pp": } - include amod - ' -} -MANIFEST - -def shutdown_puppet_if_running_as_a_service - if master.use_service_scripts? - # Beaker defaults to leaving puppet running when using service scripts, - # Need to shut it down so we can start up with commandline options - on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=stopped')) - end -end - -teardown do - if master.use_service_scripts? - # Beaker defaults to leaving puppet running when using service scripts, - on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=running')) - end -end - -# Note: this is the semantics seen with legacy environments if commandline -# manifest/modulepath are set. -step "CASE 1: puppet master with --manifest and --modulepath overrides set production directory environment" do - if master.is_using_passenger? - step "Skipping for Passenger (PE) setup; since the equivalent of a commandline override would be adding the setting to config.ru, which seems like a very odd thing to do." - else - - shutdown_puppet_if_running_as_a_service - - master_opts = { - 'master' => { - 'environmentpath' => environmentpath, - 'manifest' => sitepp, - 'modulepath' => modulepath, - }, - :__service_args__ => { - :bypass_service_script => true, - }, - } - - master_opts_with_cmdline = master_opts.merge(:__commandline_args__ => "--manifest=#{cmdline_manifest} --modulepath=#{other_modulepath}") - with_puppet_running_on master, master_opts_with_cmdline, testdir do - agents.each do |agent| - on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2] ) do - assert_match(/in cmdline\.pp/, stdout) - assert_match(/amod from commandline modulepath/, stdout) - assert_no_match(/production/, stdout) - end - - step "CASE 1a: even if environment is specified" - on(agent, puppet("agent -t --server #{master} --environment production"), :acceptable_exit_codes => [2]) do - assert_match(/in cmdline\.pp/, stdout) - assert_match(/amod from commandline modulepath/, stdout) - assert_no_match(/production/, stdout) - end - end - end - - step "CASE 2: or if you set --manifestdir" do - master_opts_with_cmdline = master_opts.merge(:__commandline_args__ => "--manifestdir=#{other_manifestdir} --modulepath=#{other_modulepath}") - step "CASE 2: it is ignored if manifest is set in puppet.conf to something not using $manifestdir" - with_puppet_running_on master, master_opts_with_cmdline, testdir do - agents.each do |agent| - on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2]) do - assert_match(/in production\.pp/, stdout) - assert_match(/amod from commandline modulepath/, stdout) - end - end - end - - step "CASE 2a: but does pull in the default manifest via manifestdir if manifest is not set" - master_opts_with_cmdline = master_opts.merge(:__commandline_args__ => "--manifestdir=#{other_manifestdir} --modulepath=#{other_modulepath}") - master_opts_with_cmdline['master'].delete('manifest') - with_puppet_running_on master, master_opts_with_cmdline, testdir do - agents.each do |agent| - on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2]) do - assert_match(/in other manifestdir site\.pp/, stdout) - assert_match(/amod from commandline modulepath/, stdout) - assert_no_match(/production/, stdout) - end - end - end - end - end -end - -step "CASE 3: puppet master with manifest and modulepath set in puppet.conf is overriden by an existing and set production directory environment" do - master_opts = { - 'master' => { - 'environmentpath' => environmentpath, - 'manifest' => sitepp, - 'modulepath' => modulepath, - } - } - if master.is_pe? - master_opts['master']['basemodulepath'] = master['sitemoduledir'] - end - - with_puppet_running_on master, master_opts, testdir do - agents.each do |agent| - step "CASE 3: this case is unfortunate, but will be irrelevant when we remove legacyenv in 4.0" - on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2] ) do - assert_match(/in production\.pp/, stdout) - assert_match(/amod from production environment/, stdout) - end - - step "CASE 3a: if environment is specified" - on(agent, puppet("agent -t --server #{master} --environment production"), :acceptable_exit_codes => [2]) do - assert_match(/in production\.pp/, stdout) - assert_match(/amod from production environment/, stdout) - end - end - end -end - -step "CASE 4: puppet master with default manifest, modulepath, environment, environmentpath and an existing '#{environmentpath}/production' directory environment that has not been set" do - - if master.is_using_passenger? - step "Skipping for PE because PE requires most of the existing puppet.conf and /etc/puppetlabs/puppet configuration, and we cannot simply point to a new conf directory." - else - - shutdown_puppet_if_running_as_a_service - - ssldir = on(master, puppet("master --configprint ssldir")).stdout.chomp - master_opts = { - :__service_args__ => { - :bypass_service_script => true, - }, - :__commandline_args__ => "--confdir=#{testdir} --ssldir=#{ssldir}" - } - - with_puppet_running_on master, master_opts, testdir do - agents.each do |agent| - step "CASE 4: #{environmentpath}/production directory environment does not take precedence because default environmentpath is ''" - on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2] ) do - assert_match(/in site\.pp/, stdout) - assert_match(/amod from modulepath/, stdout) - end - on(agent, puppet("agent -t --server #{master} --environment production"), :acceptable_exit_codes => [2]) do - assert_match(/in site\.pp/, stdout) - assert_match(/amod from modulepath/, stdout) - end - end - end - end -end - -step "CASE 5: puppet master with explicit dynamic environment settings and empty environmentpath" do - step "CASE 5: Prepare an additional modulepath module" - apply_manifest_on(master, <<-MANIFEST, :catch_failures => true) - File { - ensure => directory, - owner => #{master['user']}, - group => #{master['group']}, - mode => "0750", - } - - # A second module in another modules dir - file { - "#{other_modulepath}":; - "#{other_modulepath}/bmod/":; - "#{other_modulepath}/bmod/manifests":; - } - - file { "#{other_modulepath}/bmod/manifests/init.pp": - ensure => file, - mode => "0640", - content => 'class bmod { - notify { "bmod from other modulepath": } - }' - } - - file { "#{environmentpath}/production/manifests/production.pp": - ensure => file, - mode => "0640", - content => ' - notify { "in production.pp": } - include amod - include bmod - ' - } - MANIFEST - - master_opts = { - 'main' => { - 'environmentpath' => '', - }, - 'master' => { - 'manifest' => "#{environmentpath}/$environment/manifests", - 'modulepath' => "#{environmentpath}/$environment/modules:#{other_modulepath}", - } - } - if master.is_pe? - master_opts['master']['modulepath'] << ":#{master['sitemoduledir']}" - end - - with_puppet_running_on master, master_opts, testdir do - agents.each do |agent| - step "CASE 5: pulls in the production environment based on $environment default" - on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2] ) do - assert_match(/in production\.pp/, stdout) - assert_match(/amod from production environment/, stdout) - step "CASE 5: and sees modules located in later elements of the modulepath (which would not be seen by a directory env (PUP-2158)" - assert_match(/bmod from other modulepath/, stdout) - end - - step "CASE 5a: pulls in the production environment when explicitly set" - on(agent, puppet("agent -t --server #{master} --environment production"), :acceptable_exit_codes => [2] ) do - assert_match(/in production\.pp/, stdout) - assert_match(/amod from production environment/, stdout) - step "CASE 5a: and sees modules located in later elements of the modulepath (which would not be seen by a directory env (PUP-2158)" - assert_match(/bmod from other modulepath/, stdout) - end - end - end -end diff --git a/acceptance/tests/environment/dynamic.rb b/acceptance/tests/environment/dynamic.rb deleted file mode 100644 index 46b89db16..000000000 --- a/acceptance/tests/environment/dynamic.rb +++ /dev/null @@ -1,131 +0,0 @@ -test_name "dynamic environments" -require 'puppet/acceptance/environment_utils' -extend Puppet::Acceptance::EnvironmentUtils -require 'puppet/acceptance/classifier_utils' -extend Puppet::Acceptance::ClassifierUtils - -hosts.each do |host| - skip_test "skip tests requiring forge certs on solaris and aix" if host['platform'] =~ /solaris/ -end - -classify_nodes_as_agent_specified_if_classifer_present - -step "setup environments" - -stub_forge_on(master) - -testdir = create_tmpdir_for_user master, "confdir" -puppet_conf_backup_dir = create_tmpdir_for_user(master, "puppet-conf-backup-dir") - -apply_manifest_on(master, environment_manifest(testdir), :catch_failures => true) - -results = {} -review = {} - -#################### -step "[ Run Tests ]" - -existing_dynamic_scenario = "Test a specific, existing dynamic environment configuration" -step existing_dynamic_scenario -master_opts = { - 'main' => { - 'environmentpath' => '', - 'manifest' => '$confdir/dynamic/$environment/manifests', - 'modulepath' => '$confdir/dynamic/$environment/modules', - 'config_version' => '$confdir/static-version.sh', - } -} -if master.is_pe? - master_opts['main']['modulepath'] << ":#{master['sitemoduledir']}" -end - -results[existing_dynamic_scenario] = use_an_environment("testing", "dynamic testing", master_opts, testdir, puppet_conf_backup_dir) - -default_environment_scenario = "Test behavior of default environment" -step default_environment_scenario -results[default_environment_scenario] = use_an_environment(nil, "default environment", master_opts, testdir, puppet_conf_backup_dir) - -non_existent_environment_scenario = "Test for an environment that does not exist" -step non_existent_environment_scenario -results[non_existent_environment_scenario] = use_an_environment("doesnotexist", "non existent environment", master_opts, testdir, puppet_conf_backup_dir) - -######################################## -step "[ Report on Environment Results ]" - -confdir = master.puppet['confdir'] - -step "Reviewing: #{existing_dynamic_scenario}" -review[existing_dynamic_scenario] = review_results(results[existing_dynamic_scenario], - :puppet_config => { - :exit_code => 0, - :matches => [%r{manifest.*#{confdir}/dynamic/testing/manifests$}, - %r{modulepath.*#{confdir}/dynamic/testing/modules(?::#{master['sitemoduledir']})?$}, - %r{config_version.*#{confdir}/static-version.sh$}] - }, - :puppet_module_install => { - :exit_code => 0, - :matches => [%r{Preparing to install into #{confdir}/dynamic/testing/modules}, - %r{pmtacceptance-nginx}], - }, - :puppet_module_uninstall => { - :exit_code => 0, - :matches => [%r{Removed.*pmtacceptance-nginx.*from #{confdir}/dynamic/testing/modules}], - }, - :puppet_apply => { - :exit_code => 0, - :matches => [%r{include dynamic testing environment testing_mod}], - }, - :puppet_agent => { - :exit_code => 2, - :matches => [%r{Applying configuration version 'static'}, - %r{in dynamic testing environment site.pp}, - %r{include dynamic testing environment testing_mod}], - } -) - -step "Reviewing: #{default_environment_scenario}" -default_expectations = lambda do |env| - { - :puppet_config => { - :exit_code => 0, - :matches => [%r{manifest.*#{confdir}/dynamic/#{env}/manifests$}, - %r{modulepath.*#{confdir}/dynamic/#{env}/modules(?::#{master['sitemoduledir']})?$}, - %r{^config_version.*#{confdir}/static-version.sh$}] - }, - :puppet_module_install => { - :exit_code => 0, - :matches => [%r{Preparing to install into #{confdir}/dynamic/#{env}/modules}, - %r{pmtacceptance-nginx}], - }, - :puppet_module_uninstall => { - :exit_code => 0, - :matches => [%r{Removed.*pmtacceptance-nginx.*from #{confdir}/dynamic/#{env}/modules}], - }, - :puppet_apply => { - :exit_code => 1, - :matches => [%r{Error:.*Could not find class ::testing_mod}], - }, - :puppet_agent => { - :exit_code => 0, - :matches => [%r{Applying configuration version 'static'}], - :does_not_match => [%r{in default environment site.pp}, - %r{include default environment testing_mod}, - %r{Notice: include}], - }, - } -end -review[default_environment_scenario] = review_results( - results[default_environment_scenario], - default_expectations.call('production') -) - -step "Reviewing: #{non_existent_environment_scenario}" -review[non_existent_environment_scenario] = review_results( - results[non_existent_environment_scenario], - default_expectations.call('doesnotexist') -) - -######################### -step "[ Assert Success ]" - -assert_review(review) diff --git a/acceptance/tests/environment/dynamic_environments.rb b/acceptance/tests/environment/dynamic_environments.rb deleted file mode 100644 index 7d9e43697..000000000 --- a/acceptance/tests/environment/dynamic_environments.rb +++ /dev/null @@ -1,143 +0,0 @@ -test_name "Dynamic Environments" -require 'puppet/acceptance/classifier_utils' -extend Puppet::Acceptance::ClassifierUtils - -classify_nodes_as_agent_specified_if_classifer_present - -testdir = create_tmpdir_for_user master, 'dynamic-environment' -environmentsdir = "#{testdir}/environments" - -step "Prepare manifests and modules" -def an_environment(envdir, env) - content = <<-ENVIRONMENT - -#################### -# #{env} environment -file { - "#{envdir}/#{env}":; - "#{envdir}/#{env}/hiera":; - "#{envdir}/#{env}/manifests":; - "#{envdir}/#{env}/modules":; - "#{envdir}/#{env}/modules/amod":; - "#{envdir}/#{env}/modules/amod/manifests":; -} - -file { "#{envdir}/#{env}/hiera/#{env}.yaml": - ensure => file, - mode => "0640", - content => 'foo: foo-#{env}', -} -file { "#{envdir}/#{env}/hiera/common.yaml": - ensure => file, - mode => "0640", - content => 'foo: foo-common', -} -file { "#{envdir}/#{env}/manifests/site.pp": - ensure => file, - mode => "0640", - content => ' - notify { "#{env}-site.pp": } - notify { "hiera": - message => hiera(foo), - } - include amod - ' -} -file { "#{envdir}/#{env}/modules/amod/manifests/init.pp": - ensure => file, - mode => "0640", - content => ' - class amod { - notify { "#{env}-amod": } - } - ' -} - ENVIRONMENT -end - -manifest = <<-MANIFEST -File { - ensure => directory, - owner => #{master['user']}, - group => #{master['group']}, - mode => "0750", -} - -file { - "#{testdir}":; - "#{environmentsdir}":; -} - -file { "#{testdir}/hiera.yaml": - ensure => file, - mode => "0640", - content => ' ---- -:backends: yaml -:yaml: - :datadir: "#{environmentsdir}/%{environment}/hiera" -:hierarchy: - - "%{environment}" - - common - ', -} - -#{an_environment(environmentsdir, 'production')} -#{an_environment(environmentsdir, 'testing')} -MANIFEST - -apply_manifest_on(master, manifest, :catch_failures => true) - -def test_on_agents(environment, default_env = false) - agents.each do |agent| - environment_switch = "--environment #{environment}" if !default_env - on(agent, puppet("agent -t --server #{master}", environment_switch), :acceptable_exit_codes => [2] ) do - assert_match(/#{environment}-site.pp/, stdout) - assert_match(/foo-#{environment}/, stdout) - assert_match(/#{environment}-amod/, stdout) - end - end -end - -ssldir = on(master, puppet("master --configprint ssldir")).stdout.chomp - -common_opts = { - 'environmentpath' => '', - 'modulepath' => "#{testdir}/environments/$environment/modules", - 'hiera_config' => "#{testdir}/hiera.yaml", -} -if master.is_pe? - common_opts['modulepath'] << ":#{master['sitemoduledir']}" -end - -master_opts = { - 'main' => { - 'manifest' => "#{testdir}/environments/$environment/manifests/site.pp", - }.merge(common_opts) -} -with_puppet_running_on master, master_opts, testdir do - step "Agent run with default environment" - test_on_agents('production', true) -end - -master_opts = { - 'main' => { - 'manifest' => "#{testdir}/environments/$environment/manifests/site.pp", - }.merge(common_opts) -} -with_puppet_running_on master, master_opts, testdir do - step "Agent run with testing environment" - test_on_agents('testing') - step "And then agent run with another environment but the same master process" - test_on_agents('production') -end - -master_opts = { - 'main' => { - 'manifestdir' => "#{testdir}/environments/$environment/manifests", - }.merge(common_opts) -} -with_puppet_running_on master, master_opts, testdir do - step "Agent run with testing environment and manifestdir set instead of manifest" - test_on_agents('testing') -end diff --git a/acceptance/tests/environment/environment_scenario-bad.rb b/acceptance/tests/environment/environment_scenario-bad.rb index 1d4b8f5fc..a96c9f660 100644 --- a/acceptance/tests/environment/environment_scenario-bad.rb +++ b/acceptance/tests/environment/environment_scenario-bad.rb @@ -1,55 +1,54 @@ test_name "Test behavior of directory environments when environmentpath is set to a non-existent directory" require 'puppet/acceptance/environment_utils' extend Puppet::Acceptance::EnvironmentUtils require 'puppet/acceptance/classifier_utils' extend Puppet::Acceptance::ClassifierUtils classify_nodes_as_agent_specified_if_classifer_present step "setup environments" stub_forge_on(master) testdir = create_tmpdir_for_user master, "confdir" puppet_conf_backup_dir = create_tmpdir_for_user(master, "puppet-conf-backup-dir") apply_manifest_on(master, environment_manifest(testdir), :catch_failures => true) step "Test" master_opts = { 'main' => { 'environmentpath' => '/doesnotexist', - 'config_version' => '$confdir/static-version.sh', } } general = [ master_opts, testdir, puppet_conf_backup_dir, { :directory_environments => true } ] env = 'doesnotexist' path = master['puppetpath'] results = use_an_environment("testing", "bad environmentpath", master_opts, testdir, puppet_conf_backup_dir, :directory_environments => true) expectations = { :puppet_config => { :exit_code => 1, :matches => [%r{Could not find a directory environment named '#{env}' anywhere in the path.*#{path}}], }, :puppet_module_install => { :exit_code => 1, :matches => [%r{Could not find a directory environment named '#{env}' anywhere in the path.*#{path}}], }, :puppet_module_uninstall => { :exit_code => 1, :matches => [%r{Could not find a directory environment named '#{env}' anywhere in the path.*#{path}}], }, :puppet_apply => { :exit_code => 1, :matches => [%r{Could not find a directory environment named '#{env}' anywhere in the path.*#{path}}], }, :puppet_agent => { :exit_code => 1, :matches => [%r{Warning.*404.*Could not find environment '#{env}'}, %r{Could not retrieve catalog; skipping run}], }, } review_results(results,expectations) diff --git a/acceptance/tests/environment/environment_scenario-default.rb b/acceptance/tests/environment/environment_scenario-default.rb index f5c968738..6df7b82f9 100644 --- a/acceptance/tests/environment/environment_scenario-default.rb +++ b/acceptance/tests/environment/environment_scenario-default.rb @@ -1,60 +1,59 @@ test_name "Test behavior of default environment" require 'puppet/acceptance/environment_utils' extend Puppet::Acceptance::EnvironmentUtils require 'puppet/acceptance/classifier_utils' extend Puppet::Acceptance::ClassifierUtils classify_nodes_as_agent_specified_if_classifer_present step "setup environments" stub_forge_on(master) testdir = create_tmpdir_for_user master, "confdir" puppet_conf_backup_dir = create_tmpdir_for_user(master, "puppet-conf-backup-dir") apply_manifest_on(master, environment_manifest(testdir), :catch_failures => true) step "Test" master_opts = { 'main' => { 'environmentpath' => '$confdir/environments', - 'config_version' => '$confdir/static-version.sh', } } general = [ master_opts, testdir, puppet_conf_backup_dir, { :directory_environments => true } ] env = nil results = use_an_environment(env, "default environment", *general) expectations = { :puppet_config => { :exit_code => 0, :matches => [%r{manifest.*#{master['puppetpath']}/environments/#{env}/manifests$}, %r{modulepath.*#{master['puppetpath']}/environments/#{env}/modules:.+}, %r{config_version = $}] }, :puppet_module_install => { :exit_code => 0, :matches => [%r{Preparing to install into #{master['puppetpath']}/environments/#{env}/modules}, %r{pmtacceptance-nginx}], }, :puppet_module_uninstall => { :exit_code => 0, :matches => [%r{Removed.*pmtacceptance-nginx.*from #{master['puppetpath']}/environments/#{env}/modules}], }, :puppet_apply => { :exit_code => 0, :matches => [%r{include default environment testing_mod}], :notes => "The production directory environment is empty, but the inclusion of basemodulepath in the directory environment modulepath picks up the default testing_mod class in $confdir/modules" }, :puppet_agent => { :exit_code => 0, :matches => [ %r{Applying configuration version '\d+'}], :does_not_match => [%r{include.*testing_mod}, %r{Warning.*404}], :notes => "The master automatically creates an empty production env dir." } } review_results(results,expectations) diff --git a/acceptance/tests/environment/environment_scenario-existing.rb b/acceptance/tests/environment/environment_scenario-existing.rb index de1cb88ed..2c2e5a649 100644 --- a/acceptance/tests/environment/environment_scenario-existing.rb +++ b/acceptance/tests/environment/environment_scenario-existing.rb @@ -1,59 +1,58 @@ test_name "Test a specific, existing directory environment configuration" require 'puppet/acceptance/environment_utils' extend Puppet::Acceptance::EnvironmentUtils require 'puppet/acceptance/classifier_utils' extend Puppet::Acceptance::ClassifierUtils classify_nodes_as_agent_specified_if_classifer_present step "setup environments" stub_forge_on(master) testdir = create_tmpdir_for_user master, "confdir" puppet_conf_backup_dir = create_tmpdir_for_user(master, "puppet-conf-backup-dir") apply_manifest_on(master, environment_manifest(testdir), :catch_failures => true) step "Test" master_opts = { 'main' => { 'environmentpath' => '$confdir/environments', - 'config_version' => '$confdir/static-version.sh', } } general = [ master_opts, testdir, puppet_conf_backup_dir, { :directory_environments => true } ] env = 'testing' results = use_an_environment(env, "directory testing", *general) expectations = { :puppet_config => { :exit_code => 0, :matches => [%r{manifest.*#{master['puppetpath']}/environments/#{env}/manifests$}, %r{modulepath.*#{master['puppetpath']}/environments/#{env}/modules:.+}, %r{config_version = $}] }, :puppet_module_install => { :exit_code => 0, :matches => [%r{Preparing to install into #{master['puppetpath']}/environments/#{env}/modules}, %r{pmtacceptance-nginx}], }, :puppet_module_uninstall => { :exit_code => 0, :matches => [%r{Removed.*pmtacceptance-nginx.*from #{master['puppetpath']}/environments/#{env}/modules}], }, :puppet_apply => { :exit_code => 0, :matches => [%r{include directory #{env} environment testing_mod}], }, :puppet_agent => { :exit_code => 2, :matches => [%r{Applying configuration version '\d+'}, %r{in directory #{env} environment site.pp}, %r{include directory #{env} environment testing_mod}], }, } review_results(results, expectations) diff --git a/acceptance/tests/environment/environment_scenario-master_environmentpath.rb b/acceptance/tests/environment/environment_scenario-master_environmentpath.rb index aace209aa..e922b1cf3 100644 --- a/acceptance/tests/environment/environment_scenario-master_environmentpath.rb +++ b/acceptance/tests/environment/environment_scenario-master_environmentpath.rb @@ -1,66 +1,62 @@ test_name "Test behavior of a directory environment when environmentpath is set in the master section" require 'puppet/acceptance/environment_utils' extend Puppet::Acceptance::EnvironmentUtils require 'puppet/acceptance/classifier_utils' extend Puppet::Acceptance::ClassifierUtils classify_nodes_as_agent_specified_if_classifer_present step "setup environments" stub_forge_on(master) testdir = create_tmpdir_for_user master, "confdir" puppet_conf_backup_dir = create_tmpdir_for_user(master, "puppet-conf-backup-dir") apply_manifest_on(master, environment_manifest(testdir), :catch_failures => true) step "Test" master_opts = { - 'main' => { - 'environmentpath' => '', - }, 'master' => { 'environmentpath' => '$confdir/environments', - 'config_version' => '$confdir/static-version.sh', } } env = 'testing' results = use_an_environment("testing", "master environmentpath", master_opts, testdir, puppet_conf_backup_dir, :directory_environments => true, :config_print => '--section=master') expectations = { :puppet_config => { :exit_code => 0, :matches => [%r{manifest.*#{master['puppetpath']}/environments/#{env}/manifests$}, %r{modulepath.*#{master['puppetpath']}/environments/#{env}/modules:.+}, %r{config_version = $}] }, :puppet_module_install => { :exit_code => 0, :matches => [%r{Preparing to install into #{master['puppetpath']}/modules}, %r{pmtacceptance-nginx}], :expect_failure => true, :notes => "Runs in user mode and doesn't see the master environmenetpath setting.", }, :puppet_module_uninstall => { :exit_code => 0, :matches => [%r{Removed.*pmtacceptance-nginx.*from #{master['puppetpath']}/modules}], :expect_failure => true, :notes => "Runs in user mode and doesn't see the master environmenetpath setting.", }, :puppet_apply => { :exit_code => 0, :matches => [%r{include default environment testing_mod}], :expect_failure => true, :notes => "Runs in user mode and doesn't see the master environmenetpath setting.", }, :puppet_agent => { :exit_code => 2, :matches => [%r{Applying configuration version '\d+'}, %r{in directory #{env} environment site.pp}, %r{include directory #{env} environment testing_mod}], }, } review_results(results,expectations) diff --git a/acceptance/tests/environment/environment_scenario-non_existent.rb b/acceptance/tests/environment/environment_scenario-non_existent.rb index 609dcaa50..f57cf5d0e 100644 --- a/acceptance/tests/environment/environment_scenario-non_existent.rb +++ b/acceptance/tests/environment/environment_scenario-non_existent.rb @@ -1,55 +1,54 @@ test_name "Test for an environment that does not exist" require 'puppet/acceptance/environment_utils' extend Puppet::Acceptance::EnvironmentUtils require 'puppet/acceptance/classifier_utils' extend Puppet::Acceptance::ClassifierUtils classify_nodes_as_agent_specified_if_classifer_present step "setup environments" stub_forge_on(master) testdir = create_tmpdir_for_user master, "confdir" puppet_conf_backup_dir = create_tmpdir_for_user(master, "puppet-conf-backup-dir") apply_manifest_on(master, environment_manifest(testdir), :catch_failures => true) step "Test" master_opts = { 'main' => { 'environmentpath' => '$confdir/environments', - 'config_version' => '$confdir/static-version.sh', } } general = [ master_opts, testdir, puppet_conf_backup_dir, { :directory_environments => true } ] env = 'doesnotexist' path = master['puppetpath'] results = use_an_environment(env, "non existent environment", *general) expectations = { :puppet_config => { :exit_code => 1, :matches => [%r{Could not find a directory environment named '#{env}' anywhere in the path.*#{path}}], }, :puppet_module_install => { :exit_code => 1, :matches => [%r{Could not find a directory environment named '#{env}' anywhere in the path.*#{path}}], }, :puppet_module_uninstall => { :exit_code => 1, :matches => [%r{Could not find a directory environment named '#{env}' anywhere in the path.*#{path}}], }, :puppet_apply => { :exit_code => 1, :matches => [%r{Could not find a directory environment named '#{env}' anywhere in the path.*#{path}}], }, :puppet_agent => { :exit_code => 1, :matches => [%r{Warning.*404.*Could not find environment '#{env}'}, %r{Could not retrieve catalog; skipping run}], } } review_results(results,expectations) diff --git a/acceptance/tests/environment/environment_scenario-with_explicit_environment_conf.rb b/acceptance/tests/environment/environment_scenario-with_explicit_environment_conf.rb index 79038939b..86ab591bf 100644 --- a/acceptance/tests/environment/environment_scenario-with_explicit_environment_conf.rb +++ b/acceptance/tests/environment/environment_scenario-with_explicit_environment_conf.rb @@ -1,57 +1,56 @@ test_name "Test a specific, existing directory environment with an explicit environment.conf file" require 'puppet/acceptance/environment_utils' extend Puppet::Acceptance::EnvironmentUtils require 'puppet/acceptance/classifier_utils' extend Puppet::Acceptance::ClassifierUtils classify_nodes_as_agent_specified_if_classifer_present step "setup environments" stub_forge_on(master) testdir = create_tmpdir_for_user master, "confdir" puppet_conf_backup_dir = create_tmpdir_for_user(master, "puppet-conf-backup-dir") apply_manifest_on(master, environment_manifest(testdir), :catch_failures => true) step "Test" master_opts = { 'main' => { 'environmentpath' => '$confdir/environments', - 'config_version' => '$confdir/static-version.sh', } } general = [ master_opts, testdir, puppet_conf_backup_dir, { :directory_environments => true } ] results = use_an_environment("testing_environment_conf", "directory with environment.conf testing", *general) expectations = { :puppet_config => { :exit_code => 0, :matches => [%r{manifest.*#{master['puppetpath']}/environments/testing_environment_conf/nonstandard-manifests$}, %r{modulepath.*#{master['puppetpath']}/environments/testing_environment_conf/nonstandard-modules:.+}, %r{config_version = #{master['puppetpath']}/environments/testing_environment_conf/local-version.sh$}] }, :puppet_module_install => { :exit_code => 0, :matches => [%r{Preparing to install into #{master['puppetpath']}/environments/testing_environment_conf/nonstandard-modules}, %r{pmtacceptance-nginx}], }, :puppet_module_uninstall => { :exit_code => 0, :matches => [%r{Removed.*pmtacceptance-nginx.*from #{master['puppetpath']}/environments/testing_environment_conf/nonstandard-modules}], }, :puppet_apply => { :exit_code => 0, :matches => [%r{include directory testing with environment\.conf testing_mod}], }, :puppet_agent => { :exit_code => 2, :matches => [%r{Applying configuration version 'local testing_environment_conf'}, %r{in directory testing with environment\.conf site.pp}, %r{include directory testing with environment\.conf testing_mod}], }, } review_results(results,expectations) diff --git a/acceptance/tests/environment/static.rb b/acceptance/tests/environment/static.rb deleted file mode 100644 index 288b97602..000000000 --- a/acceptance/tests/environment/static.rb +++ /dev/null @@ -1,130 +0,0 @@ -test_name "legacy environments" -require 'puppet/acceptance/environment_utils' -extend Puppet::Acceptance::EnvironmentUtils -require 'puppet/acceptance/classifier_utils' -extend Puppet::Acceptance::ClassifierUtils - -hosts.each do |host| - skip_test "skip tests requiring forge certs on solaris and aix" if host['platform'] =~ /solaris/ -end - -classify_nodes_as_agent_specified_if_classifer_present - -step "setup environments" - -stub_forge_on(master) - -testdir = create_tmpdir_for_user master, "confdir" -puppet_conf_backup_dir = create_tmpdir_for_user(master, "puppet-conf-backup-dir") - -apply_manifest_on(master, environment_manifest(testdir), :catch_failures => true) - -results = {} -review = {} - -#################### -step "[ Run Tests ]" - -existing_legacy_scenario = "Test a specific, existing legacy environment configuration" -step existing_legacy_scenario -master_opts = { - 'main' => { - 'environmentpath' => '', - }, - 'testing' => { - 'manifest' => "$confdir/testing-manifests", - 'modulepath' => "$confdir/testing-modules", - 'config_version' => "$confdir/static-version.sh", - }, -} -if master.is_pe? - master_opts['testing']['modulepath'] << ":#{master['sitemoduledir']}" -end - -results[existing_legacy_scenario] = use_an_environment("testing", "legacy testing", master_opts, testdir, puppet_conf_backup_dir) - -default_environment_scenario = "Test behavior of default environment" -step default_environment_scenario -results[default_environment_scenario] = use_an_environment(nil, "default environment", master_opts, testdir, puppet_conf_backup_dir) - -non_existent_environment_scenario = "Test for an environment that does not exist" -step non_existent_environment_scenario -results[non_existent_environment_scenario] = use_an_environment("doesnotexist", "non existent environment", master_opts, testdir, puppet_conf_backup_dir) - -######################################## -step "[ Report on Environment Results ]" - -confdir = master.puppet['confdir'] - -step "Reviewing: #{existing_legacy_scenario}" -review[existing_legacy_scenario] = review_results(results[existing_legacy_scenario], - :puppet_config => { - :exit_code => 0, - :matches => [%r{manifest.*#{confdir}/testing-manifests$}, - %r{modulepath.*#{confdir}/testing-modules(?::#{master['sitemoduledir']})?$}, - %r{config_version.*#{confdir}/static-version.sh$}] - }, - :puppet_module_install => { - :exit_code => 0, - :matches => [%r{Preparing to install into #{confdir}/testing-modules}, - %r{pmtacceptance-nginx}], - }, - :puppet_module_uninstall => { - :exit_code => 0, - :matches => [%r{Removed.*pmtacceptance-nginx.*from #{confdir}/testing-modules}], - }, - :puppet_apply => { - :exit_code => 0, - :matches => [%r{include legacy testing environment testing_mod}], - }, - :puppet_agent => { - :exit_code => 2, - :matches => [%r{Applying configuration version 'static'}, - %r{in legacy testing environment site.pp}, - %r{include legacy testing environment testing_mod}], - } -) - -step "Reviewing: #{default_environment_scenario}" -default_expectations = { - :puppet_config => { - :exit_code => 0, - :matches => [%r{manifest.*#{confdir}/manifests/site.pp$}, - %r{modulepath.*#{confdir}/modules:.*}, - %r{^config_version\s+=\s*$}] - }, - :puppet_module_install => { - :exit_code => 0, - :matches => [%r{Preparing to install into #{confdir}/modules}, - %r{pmtacceptance-nginx}], - }, - :puppet_module_uninstall => { - :exit_code => 0, - :matches => [%r{Removed.*pmtacceptance-nginx.*from #{confdir}/modules}], - }, - :puppet_apply => { - :exit_code => 0, - :matches => [%r{include default environment testing_mod}], - }, - :puppet_agent => { - :exit_code => 2, - :matches => [%r{Applying configuration version '\d+'}, - %r{in default environment site.pp}, - %r{include default environment testing_mod}], - }, -} -review[default_environment_scenario] = review_results( - results[default_environment_scenario], - default_expectations -) - -step "Reviewing: #{non_existent_environment_scenario}" -review[non_existent_environment_scenario] = review_results( - results[non_existent_environment_scenario], - default_expectations -) - -######################### -step "[ Assert Success ]" - -assert_review(review) diff --git a/acceptance/tests/environment/use_enc_environment_for_pluginsync.rb b/acceptance/tests/environment/use_enc_environment_for_pluginsync.rb index 22089df89..be1c26f03 100644 --- a/acceptance/tests/environment/use_enc_environment_for_pluginsync.rb +++ b/acceptance/tests/environment/use_enc_environment_for_pluginsync.rb @@ -1,55 +1,56 @@ test_name "Agent should use environment given by ENC for pluginsync" testdir = create_tmpdir_for_user master, 'respect_enc_test' create_remote_file master, "#{testdir}/enc.rb", < true) File { ensure => directory, mode => "0770", owner => #{master.puppet['user']}, group => #{master.puppet['group']}, } file { '#{testdir}/environments':; '#{testdir}/environments/production':; '#{testdir}/environments/special/':; '#{testdir}/environments/special/modules':; '#{testdir}/environments/special/modules/amod':; '#{testdir}/environments/special/modules/amod/lib':; '#{testdir}/environments/special/modules/amod/lib/puppet':; } file { '#{testdir}/environments/special/modules/amod/lib/puppet/foo.rb': ensure => file, mode => "0640", content => "#special_version", } MANIFEST master_opts = { 'main' => { 'environmentpath' => "#{testdir}/environments", }, 'master' => { 'node_terminus' => 'exec', 'external_nodes' => "#{testdir}/enc.rb" }, } with_puppet_running_on master, master_opts, testdir do agents.each do |agent| + agent_vardir = agent.puppet['vardir'] run_agent_on(agent, "--no-daemonize --onetime --server #{master}") - on agent, "cat \"#{agent.puppet['vardir']}/lib/puppet/foo.rb\"" + on agent, "cat \"#{agent_vardir}/lib/puppet/foo.rb\"" assert_match(/#special_version/, stdout, "The plugin from environment 'special' was not synced") - on agent, "rm -rf \"#{agent.puppet['vardir']}/lib\"" + on agent, "rm -rf \"#{agent_vardir}/lib\"" end end diff --git a/acceptance/tests/external_ca_support/fixtures/certchain.sh b/acceptance/tests/external_ca_support/fixtures/certchain.sh index b104db175..ac73e7a06 100755 --- a/acceptance/tests/external_ca_support/fixtures/certchain.sh +++ b/acceptance/tests/external_ca_support/fixtures/certchain.sh @@ -1,553 +1,554 @@ #! /bin/bash ## NOTE: ## This script requires the following in /etc/hosts: ## 127.0.0.2 puppet master1.example.org # This will fail with a stock puppet 3.1.1, but will succeed if all of the # certificate subjects contain only the "CN" portion, and no O, OU, or # emailAddress. # basic config to describe the environment # B="/tmp/certchain" B="$(mktemp -d -t certchain.XXXXXXXX)" HTTPS_PORT=8443 OPENSSL=$(which openssl) # utility method to dedent a heredoc dedent() { python -c 'import sys, textwrap; print textwrap.dedent(sys.stdin.read())' } # invoke openssl openssl() { echo "----" echo "running" ${OPENSSL} ${@} echo " in $PWD" ${OPENSSL} "${@}" } show_cert() { local cert="$1" # openssl x509 -in "${cert}" -noout -text -nameopt RFC2253 openssl x509 -in "${cert}" -noout -text } hash_cert() { local cert="$1" local certdir="${B}/certdir" local h=$(${OPENSSL} x509 -hash -noout -in ${cert}) mkdir -p "${certdir}" ln -s "$cert" "${certdir}/${h}.0" } show_crl() { local crl="$1" openssl crl -in "${crl}" -noout -text } hash_crl() { local crl="$1" local certdir="${B}/certdir" local h=$(${OPENSSL} crl -hash -noout -in ${crl}) mkdir -p "${certdir}" ln -s "$crl" "${certdir}/${h}.r0" } # clean out any messes this script has made clean_up() { stop_apache rm -rf "$B" } stop_apache() { local pid pidfile="${B}/apache/httpd.pid" while true; do pid=$(cat "${pidfile}" 2>/dev/null || true) [ -z "$pid" ] && break # break if the pid is gone kill "$pid" || break # break if the kill fails (process is gone) sleep 0.1 done } # perform basic setup: make directories, etc. set_up() { mkdir -p "$B" } # create CA certificates: # # * $B/root_ca # * $B/master{1..2}_ca # # with each containing: # # * openssl.conf -- suitable for signing certificates # * ca-$name.key -- PEM format certificate key, with no password # * ca-$name.crt -- PEM format certificate create_ca_certs() { local name cn dir subj ca_config for name in root agent-ca master-ca; do dir="${B}/${name}" mkdir -p "${dir}" ( cd "${dir}" # if this is the root cert, make a self-signed cert if [ "$name" = "root" ]; then subj="/CN=Root CA/OU=Server Operations/O=Example Org, LLC" openssl req -new -newkey rsa:2048 -days 7300 -nodes -x509 \ -subj "${subj}" -keyout "ca-${name}.key" -out "ca-${name}.crt" else # make a new key for the CA openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "ca-${name}.key" # build a CSR out of it dedent > openssl.tmp << OPENSSL_TMP [req] prompt = no distinguished_name = dn_config [dn_config] commonName = Intermediate CA (${name}) emailAddress = test@example.org organizationalUnitName = Server Operations organizationName = Example Org, LLC OPENSSL_TMP openssl req -config openssl.tmp -new -key "ca-${name}.key" -out "ca-${name}.csr" rm openssl.tmp # sign it with the root CA openssl ca -config ../root/openssl.conf -in "ca-${name}.csr" -notext -out "ca-${name}.crt" -batch # clean up the now-redundant csr rm "ca-${name}.csr" fi # set up the CA config; this uses the same file for all, but with different options # for the root and master CAs [ "$name" = "root" ] && ca_config=root_ca_config || ca_config=master_ca_config dedent > openssl.conf << OPENSSL_CONF SAN = DNS:puppet [ca] default_ca = ${ca_config} # Root CA [root_ca_config] certificate = ${dir}/ca-${name}.crt private_key = ${dir}/ca-${name}.key database = ${dir}/inventory.txt new_certs_dir = ${dir}/certs serial = ${dir}/serial default_crl_days = 7300 default_days = 7300 default_md = sha1 policy = root_ca_policy x509_extensions = root_ca_exts [root_ca_policy] commonName = supplied emailAddress = supplied organizationName = supplied organizationalUnitName = supplied [root_ca_exts] authorityKeyIdentifier = keyid,issuer:always basicConstraints = critical,CA:true keyUsage = keyCertSign, cRLSign # Master CA [master_ca_config] certificate = ${dir}/ca-${name}.crt private_key = ${dir}/ca-${name}.key database = ${dir}/inventory.txt new_certs_dir = ${dir}/certs serial = ${dir}/serial default_crl_days = 7300 default_days = 7300 default_md = sha1 policy = master_ca_policy x509_extensions = master_ca_exts # Master CA (Email) [master_ca_email_config] certificate = ${dir}/ca-${name}.crt private_key = ${dir}/ca-${name}.key database = ${dir}/inventory.txt new_certs_dir = ${dir}/certs serial = ${dir}/serial default_crl_days = 7300 default_days = 7300 default_md = sha1 email_in_dn = yes policy = master_ca_email_policy x509_extensions = master_ca_exts [master_ca_policy] commonName = supplied [master_ca_email_policy] commonName = supplied emailAddress = supplied # default extensions for clients [master_ca_exts] authorityKeyIdentifier = keyid,issuer:always basicConstraints = critical,CA:false keyUsage = keyEncipherment, digitalSignature extendedKeyUsage = serverAuth, clientAuth [master_ssl_exts] authorityKeyIdentifier = keyid,issuer:always basicConstraints = critical,CA:false keyUsage = keyEncipherment, digitalSignature extendedKeyUsage = serverAuth, clientAuth subjectAltName = \$ENV::SAN # extensions for the master certificate (specifically adding subjectAltName) [master_self_ca_exts] authorityKeyIdentifier = keyid,issuer:always basicConstraints = critical,CA:false keyUsage = keyEncipherment, digitalSignature extendedKeyUsage = serverAuth, clientAuth # include the master's fqdn here, as well as in the CN, to work # around https://bugs.ruby-lang.org/issues/6493 # NOTE: Alt Names should be set in the request, so they know # their FQDN # subjectAltName = DNS:puppet,DNS:${name}.example.org OPENSSL_CONF touch inventory.txt mkdir certs echo 01 > serial show_cert "${dir}/ca-${name}.crt" hash_cert "${dir}/ca-${name}.crt" # generate an empty CRL for this CA openssl ca -config "${dir}/openssl.conf" -gencrl -out "${dir}/ca-${name}.crl" show_crl "${dir}/ca-${name}.crl" hash_crl "${dir}/ca-${name}.crl" ) done } # revoke leaf cert for $1 issued by master CA $2 revoke_leaf_cert() { local fqdn="$1" local ca="${2:-agent-ca}" local dir="${B}/${ca}" # revoke the cert and regenerate the crl openssl ca -config "${dir}/openssl.conf" -revoke "${B}/leaves/${fqdn}.issued_by.${ca}.crt" openssl ca -config "${dir}/openssl.conf" -gencrl -out "${dir}/ca-${ca}.crl" show_crl "${dir}/ca-${ca}.crl" # kill -HUP $(< "${B}/apache/httpd.pid") } # revoke CA cert for $1 revoke_ca_cert() { local master="$1" local dir="${B}/root" # revoke the cert and regenerate the crl openssl ca -config "${dir}/openssl.conf" -revoke "${B}/${master}/ca-${master}.crt" openssl ca -config "${dir}/openssl.conf" -gencrl -out "${dir}/ca-root.crl" show_crl "${dir}/ca-root.crl" kill -HUP $(< "${B}/apache/httpd.pid") } # create a "leaf" certificate for the given fqdn, signed by the given ca name. # $fqdn.issued_by.${ca}.{key,crt} will be placed in "${B}/leaves" create_leaf_cert() { local fqdn="$1" ca="$2" exts="$3" local masterdir="${B}/${ca}" local dir="${B}/leaves" local fname="${fqdn}.issued_by.${ca}" [ -n "$exts" ] && exts="-extensions $exts" mkdir -p "${dir}" ( cd "${dir}" openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${fname}.key" openssl req -subj "/CN=${fqdn}" -new -key "${fname}.key" -out "${fname}.csr" CN="${fqdn}" SAN="DNS:${fqdn}, DNS:${fqdn%%.*}, DNS:puppet, DNS:puppetmaster" \ openssl ca -config "${B}/${ca}/openssl.conf" -in "${fname}.csr" -notext \ -out "${fname}.crt" -batch $exts ) show_cert "${dir}/${fname}.crt" } # Note, we can parameterize SubjectAltNames using environment variables. create_leaf_certs() { create_leaf_cert master1.example.org master-ca master_ssl_exts create_leaf_cert master2.example.org master-ca master_ssl_exts create_leaf_cert agent1.example.org agent-ca create_leaf_cert agent2.example.org agent-ca create_leaf_cert agent3.example.org agent-ca create_leaf_cert master1.example.org agent-ca master_ssl_exts # rogue # create_leaf_cert master1.example.org root master_ssl_exts # rogue create_leaf_cert agent1.example.org master-ca # rogue # create_leaf_cert agent1.example.org root # rogue } # create a "leaf" certificate for the given fqdn, signed by the given ca name, # with an email address in the subject. # $fqdn.issued_by.${ca}.{key,crt} will be placed in "${B}/leaves" create_leaf_email_cert() { local fqdn="$1" ca="$2" exts="$3" local masterdir="${B}/${ca}" local dir="${B}/leaves" local fname="${fqdn}.issued_by.${ca}" mkdir -p "${dir}" ( cd "${dir}" openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${fname}.key" openssl req -subj "/CN=${fqdn}/emailAddress=test@example.com" -new -key "${fname}.key" -out "${fname}.csr" openssl ca -config "${B}/${ca}/openssl.conf" -name master_ca_email_config \ -in "${fname}.csr" -notext -out "${fname}.crt" -batch $exts_arg ) show_cert "${dir}/${fname}.crt" } create_leaf_email_certs() { create_leaf_email_cert master-email1.example.org master-ca master_self_ca_exts create_leaf_email_cert master-email2.example.org master-ca master_self_ca_exts create_leaf_email_cert agent-email1.example.org agent-ca create_leaf_email_cert agent-email2.example.org agent-ca create_leaf_email_cert agent-email3.example.org agent-ca } set_up_apache() { local apachedir="${B}/apache" mkdir -p "${apachedir}/puppetmaster/public" echo 'passed'> "${apachedir}/puppetmaster/public/test.txt" dedent > "${apachedir}/httpd.conf" < Require all granted RackAutoDetect On RackBaseURI / HTTPD_CONF } set_up_puppetmaster() { local apachedir="${B}/apache" local masterdir="${B}/puppetmaster" - mkdir -p "${masterdir}/conf" "${masterdir}/var" "${masterdir}/manifests" + local confdir="${masterdir}/conf" + local environmentdir="${confdir}/environments/production" + mkdir -p "${confdir}" "${masterdir}/var" "${environmentdir}/manifests" dedent > "${apachedir}/puppetmaster/config.ru" < "${masterdir}/conf/puppet.conf" < "${masterdir}/manifests/site.pp" < "${environmentdir}/manifests/site.pp" < "yes I was" } } SITE_PP } start_apache() { local apachedir="${B}/apache" if ! httpd -f "${apachedir}/httpd.conf"; then [ -f "${apachedir}/error_log" ] && tail "${apachedir}/error_log" false fi } check_apache() { # verify the SSL config with openssl. Note that s_client exits with 0 # no matter what, so this greps the output for an OK status. Also note # that this only checks that the validation of the server certs is OK, since # client validation is optional in the httpd config. echo $'GET /test.txt HTTP/1.0\n' | \ openssl s_client -connect "127.0.0.1:${HTTPS_PORT}" -verify 2 \ -cert "${B}/leaves/client2a.example.org.crt" \ -key "${B}/leaves/client2a.example.org.key" \ -CAfile "${B}/root/ca-root.crt" \ | tee "${B}/verify.out" cat "${B}/apache/error_log" grep -q "Verify return code: 0 (ok)" "${B}/verify.out" } check_puppetmaster() { # this is insecure, because otherwise curl will check that 127.0.0.1 == # master1.example.org and fail; validation of the server certs is done # above in check_apache, so this is fine. curl -vks --fail \ --header 'Accept: yaml' \ --cert "${B}/leaves/client2a.example.org.crt" \ --key "${B}/leaves/client2a.example.org.key" \ "https://127.0.0.1:${HTTPS_PORT}/production/catalog/client2a.example.org" >/dev/null echo } # set up the agent with the given fqdn set_up_agent() { local fqdn="$1" local agentdir="${B}/agent" mkdir -p "${agentdir}/conf" "${agentdir}/var" mkdir -p "${agentdir}/conf/ssl/private_keys" "${agentdir}/conf/ssl/certs" dedent > "${agentdir}/conf/puppet.conf" < 'windows' require 'puppet/acceptance/temp_file_utils' extend Puppet::Acceptance::TempFileUtils initialize_temp_dirs agents.each do |agent| - dev_modulepath = get_test_file_path(agent, 'dev/modules') - user_modulepath = get_test_file_path(agent, 'user/modules') + environmentpath = get_test_file_path(agent, 'environments') + dev_modulepath = "#{environmentpath}/dev/modules" # make sure that we use the modulepath from the dev environment puppetconf = get_test_file_path(agent, 'puppet.conf') + on agent, puppet("config", "set", "environmentpath", environmentpath, "--section", "main", "--config", puppetconf) on agent, puppet("config", "set", "environment", "dev", "--section", "user", "--config", puppetconf) - on agent, puppet("config", "set", "modulepath", user_modulepath, "--section", "user", "--config", puppetconf) - on agent, puppet("config", "set", "modulepath", dev_modulepath, "--section", "user", "--config", puppetconf) on agent, 'rm -rf helloworld' on agent, puppet("module", "generate", "puppetlabs-helloworld", "--skip-interview") mkdirs agent, 'helloworld/lib/puppet/application' mkdirs agent, 'helloworld/lib/puppet/face' # copy application, face, and utility module create_remote_file(agent, "helloworld/lib/puppet/application/helloworld.rb", <<'EOM') require 'puppet/face' require 'puppet/application/face_base' class Puppet::Application::Helloworld < Puppet::Application::FaceBase end EOM create_remote_file(agent, "helloworld/lib/puppet/face/helloworld.rb", <<'EOM') Puppet::Face.define(:helloworld, '0.1.0') do summary "Hello world face" description "This is the hello world face" action 'actionprint' do summary "Prints hello world from an action" when_invoked do |options| puts "Hello world from an action" end end action 'moduleprint' do summary "Prints hello world from a required module" when_invoked do |options| require 'puppet/helloworld.rb' Puppet::Helloworld.print end end end EOM create_remote_file(agent, "helloworld/lib/puppet/helloworld.rb", <<'EOM') module Puppet::Helloworld def print puts "Hello world from a required module" end module_function :print end EOM on agent, puppet('module', 'build', 'helloworld') on agent, puppet('module', 'install', '--ignore-dependencies', '--target-dir', dev_modulepath, 'helloworld/pkg/puppetlabs-helloworld-0.1.0.tar.gz') on(agent, puppet('help', '--config', puppetconf)) do assert_match(/helloworld\s*Hello world face/, stdout, "Face missing from list of available subcommands") end on(agent, puppet('help', 'helloworld', '--config', puppetconf)) do assert_match(/This is the hello world face/, stdout, "Descripion help missing") assert_match(/moduleprint\s*Prints hello world from a required module/, stdout, "help for moduleprint action missing") assert_match(/actionprint\s*Prints hello world from an action/, stdout, "help for actionprint action missing") end on(agent, puppet('helloworld', 'actionprint', '--config', puppetconf)) do assert_match(/^Hello world from an action$/, stdout, "face did not print hello world") end on(agent, puppet('helloworld', 'moduleprint', '--config', puppetconf)) do assert_match(/^Hello world from a required module$/, stdout, "face did not load module to print hello world") end end diff --git a/acceptance/tests/loader/func4x_loadable_from_modules.rb b/acceptance/tests/loader/func4x_loadable_from_modules.rb index c59f92764..33b3e49b3 100644 --- a/acceptance/tests/loader/func4x_loadable_from_modules.rb +++ b/acceptance/tests/loader/func4x_loadable_from_modules.rb @@ -1,83 +1,83 @@ test_name "Exercise a module with 4x function and 4x system function" # Purpose: # Test that a packed puppet can call a 4x system function, and that a 4x function in # a module can be called. # # Method: # * Manually construct a very simple module with a manifest that creates a file. # * The file has content that depends on logic that calls both a system function (reduce), and # a function supplied in the module (helloworld::mul10). # * The module is manually constructed to allow the test to also run on Windows where the module tool # is not supported. # * The module is included by calling 'include' from 'puppet apply'. # * Puppet apply is executed to generate the file with the content. # * The generated contents is asserted. # TODO: The test can be improved by adding yet another module that calls the function in helloworld. # TODO: The test can be improved to also test loading of a non namespaced function require 'puppet/acceptance/temp_file_utils' extend Puppet::Acceptance::TempFileUtils initialize_temp_dirs agents.each do |agent| # The modulepath to use in environment 'dev' envs_path = get_test_file_path(agent, 'environments') dev_modulepath = get_test_file_path(agent, 'environments/dev/modules') target_path = get_test_file_path(agent, 'output') mkdirs agent, target_path # make sure that we use the modulepath from the dev environment puppetconf = get_test_file_path(agent, 'puppet.conf') user = agent.puppet['user'] group = agent.puppet['group'] # Setting user/group ensures that when puppet apply's Configurer use()'s the # settings, that it doesn't default to owning them as root, which can have an # impact on following tests. + on agent, puppet("config", "set", "environmentpath", envs_path, "--section", "main", "--config", puppetconf) on agent, puppet("config", "set", "user", user, "--section", "main", "--config", puppetconf) on agent, puppet("config", "set", "group", group, "--section", "main", "--config", puppetconf) on agent, puppet("config", "set", "environment", "dev", "--section", "user", "--config", puppetconf) - on agent, puppet("config", "set", "environmentpath", envs_path, "--section", "main", "--config", puppetconf) # Where the functions in the written modules should go helloworld_functions = 'helloworld/lib/puppet/functions/helloworld' # Clean out the module that will be written to ensure no interference from a previous run on agent, "rm -rf #{File.join(dev_modulepath, 'helloworld')}" mkdirs agent, File.join(dev_modulepath, helloworld_functions) # Write a module # Write the function helloworld::mul10, that multiplies its argument by 10 create_remote_file(agent, File.join(dev_modulepath, helloworld_functions, "mul10.rb"), <<'SOURCE') Puppet::Functions.create_function(:'helloworld::mul10') do def mul10(x) x * 10 end end SOURCE # Write a manifest that calls a 4x function (reduce), and calls a function defined in the module # (helloworld::mul10). # mkdirs agent, File.join(dev_modulepath, "helloworld", "manifests") create_remote_file(agent, File.join(dev_modulepath, "helloworld", "manifests", "init.pp"), < 'file', mode => '0666', content => [1,2,3].reduce("Generated") |$memo, $n| { "${memo}, ${n} => ${helloworld::mul10($n)}" } } } SOURCE # Run apply to generate the file with the output on agent, puppet('apply', '-e', "'include helloworld'", '--config', puppetconf) # Assert that the file was written with the generated content on(agent, "cat #{File.join(target_path, 'result.txt')}") do assert_match(/^Generated, 1 => 10, 2 => 20, 3 => 30$/, stdout, "Generated the wrong content") end end diff --git a/acceptance/tests/modules/install/with_environment.rb b/acceptance/tests/modules/install/with_environment.rb index b7fc76dfe..1eeccce5c 100644 --- a/acceptance/tests/modules/install/with_environment.rb +++ b/acceptance/tests/modules/install/with_environment.rb @@ -1,76 +1,64 @@ test_name 'puppet module install (with environment)' require 'puppet/acceptance/module_utils' extend Puppet::Acceptance::ModuleUtils hosts.each do |host| skip_test "skip tests requiring forge certs on solaris and aix" if host['platform'] =~ /solaris/ end -tmpdir = master.tmpdir('environmentpath') +tmpdir = master.tmpdir('module-install-with-environment') module_author = "pmtacceptance" module_name = "nginx" orig_installed_modules = get_installed_modules_for_hosts hosts teardown do rm_installed_modules_from_hosts orig_installed_modules, (get_installed_modules_for_hosts hosts) end step 'Setup' stub_forge_on(master) -puppet_conf = generate_base_legacy_and_directory_environments(tmpdir) +puppet_conf = generate_base_directory_environments(tmpdir) check_module_install_in = lambda do |environment_path, module_install_args| on master, "puppet module install #{module_author}-#{module_name} --config=#{puppet_conf} #{module_install_args}" do assert_module_installed_ui(stdout, module_author, module_name) assert_match(/#{environment_path}/, stdout, "Notice of non default install path was not displayed") end assert_module_installed_on_disk(master, module_name, environment_path) end -step 'Install a module into a non default legacy environment' do - check_module_install_in.call("#{tmpdir}/legacyenv/modules", - "--environment=legacyenv") -end - -step 'Enable directory environments' do - on master, puppet("config", "set", - "environmentpath", "#{tmpdir}/environments", - "--section", "main", - "--config", puppet_conf) -end - step 'Install a module into a non default directory environment' do check_module_install_in.call("#{tmpdir}/environments/direnv/modules", "--environment=direnv") end step 'Prepare a separate modulepath' modulepath_dir = master.tmpdir("modulepath") apply_manifest_on(master, <<-MANIFEST , :catch_failures => true) file { [ '#{tmpdir}/environments/production', '#{modulepath_dir}', ]: ensure => directory, owner => #{master['user']}, } MANIFEST step "Install a module into --modulepath #{modulepath_dir} despite the implicit production directory env existing" do check_module_install_in.call(modulepath_dir, "--modulepath=#{modulepath_dir}") end step "Uninstall so we can try a different scenario" do on master, "puppet module uninstall #{module_author}-#{module_name} --config=#{puppet_conf} --modulepath=#{modulepath_dir}" end step "Install a module into --modulepath #{modulepath_dir} with a directory env specified" do check_module_install_in.call(modulepath_dir, "--modulepath=#{modulepath_dir} --environment=direnv") end diff --git a/acceptance/tests/modules/list/with_environment.rb b/acceptance/tests/modules/list/with_environment.rb index d3ffc8857..db2f4e0fb 100644 --- a/acceptance/tests/modules/list/with_environment.rb +++ b/acceptance/tests/modules/list/with_environment.rb @@ -1,49 +1,30 @@ test_name 'puppet module list (with environment)' require 'puppet/acceptance/module_utils' extend Puppet::Acceptance::ModuleUtils hosts.each do |host| skip_test "skip tests requiring forge certs on solaris and aix" if host['platform'] =~ /solaris/ end -tmpdir = master.tmpdir('environmentpath') +tmpdir = master.tmpdir('module-list-with-environment') step 'Setup' stub_forge_on(master) -puppet_conf = generate_base_legacy_and_directory_environments(tmpdir) +puppet_conf = generate_base_directory_environments(tmpdir) -install_test_module_in = lambda do |environment| +step 'List modules in a non default directory environment' do on master, puppet("module", "install", "pmtacceptance-nginx", "--config", puppet_conf, - "--environment", environment) -end + "--environment=direnv") -check_module_list_in = lambda do |environment, environment_path| on master, puppet("module", "list", "--config", puppet_conf, - "--environment", environment) do + "--environment=direnv") do - assert_match(/#{environment_path}/, stdout) + assert_match(%r{#{tmpdir}/environments/direnv/modules}, stdout) assert_match(/pmtacceptance-nginx/, stdout) end end - -step 'List modules in a non default legacy environment' do - install_test_module_in.call('legacyenv') - check_module_list_in.call('legacyenv', "#{tmpdir}/legacyenv/modules") -end - -step 'Enable directory environments' do - on master, puppet("config", "set", - "environmentpath", "#{tmpdir}/environments", - "--section", "main", - "--config", puppet_conf) -end - -step 'List modules in a non default directory environment' do - install_test_module_in.call('direnv') - check_module_list_in.call('direnv', "#{tmpdir}/environments/direnv/modules") -end diff --git a/acceptance/tests/modules/uninstall/with_environment.rb b/acceptance/tests/modules/uninstall/with_environment.rb index 77a682f14..8bfe51d0f 100644 --- a/acceptance/tests/modules/uninstall/with_environment.rb +++ b/acceptance/tests/modules/uninstall/with_environment.rb @@ -1,67 +1,48 @@ test_name 'puppet module uninstall (with environment)' require 'puppet/acceptance/module_utils' extend Puppet::Acceptance::ModuleUtils -tmpdir = master.tmpdir('environmentpath') +tmpdir = master.tmpdir('module-uninstall-with-environment') step 'Setup' stub_forge_on(master) -puppet_conf = generate_base_legacy_and_directory_environments(tmpdir) +puppet_conf = generate_base_directory_environments(tmpdir) crakorn_metadata = <<-EOS { "name": "jimmy/crakorn", "version": "0.4.0", "source": "", "author": "jimmy", "license": "MIT", "dependencies": [] } EOS # Configure a non-default environment apply_manifest_on master, %Q{ file { [ - '#{tmpdir}/legacyenv/modules/crakorn', '#{tmpdir}/environments/direnv/modules', '#{tmpdir}/environments/direnv/modules/crakorn', ]: ensure => directory, } - file { - '#{tmpdir}/legacyenv/modules/crakorn/metadata.json': - content => '#{crakorn_metadata}', - } file { '#{tmpdir}/environments/direnv/modules/crakorn/metadata.json': content => '#{crakorn_metadata}', } } -check_module_uninstall_in = lambda do |environment, environment_path| - on master, "puppet module uninstall jimmy-crakorn --config=#{puppet_conf} --environment=#{environment}" do +step 'Uninstall a module from a non default directory environment' do + environment_path = "#{tmpdir}/environments/direnv/modules" + on master, "puppet module uninstall jimmy-crakorn --config=#{puppet_conf} --environment=direnv" do assert_equal <<-OUTPUT, stdout \e[mNotice: Preparing to uninstall 'jimmy-crakorn' ...\e[0m Removed 'jimmy-crakorn' (\e[0;36mv0.4.0\e[0m) from #{environment_path} OUTPUT end - on master, "[ ! -d #{environment_path}/crakorn ]" -end - -step 'Uninstall a module from a non default legacy environment' do - check_module_uninstall_in.call('legacyenv', "#{tmpdir}/legacyenv/modules") -end - -step 'Enable directory environments' do - on master, puppet("config", "set", - "environmentpath", "#{tmpdir}/environments", - "--section", "main", - "--config", puppet_conf) -end - -step 'Uninstall a module from a non default directory environment' do - check_module_uninstall_in.call('direnv', "#{tmpdir}/environments/direnv/modules") + on master, "[ ! -d #{environment_path}/crackorn ]" end diff --git a/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb b/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb index 1e43905ff..b44f97d93 100644 --- a/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb +++ b/acceptance/tests/modules/uninstall/with_multiple_modules_installed.rb @@ -1,72 +1,87 @@ test_name "puppet module uninstall (with multiple modules installed)" hosts.each do |host| skip_test "skip tests requiring forge certs on solaris and aix" if host['platform'] =~ /solaris/ end if master.is_pe? skip_test end step 'Setup' testdir = master.tmpdir('unistallmultiple') stub_forge_on(master) teardown do on master, "rm -rf #{master['distmoduledir']}/java" on master, "rm -rf #{master['distmoduledir']}/stdlub" end -on master, "mkdir -p #{testdir}/modules" -on master, "chown -R #{master['user']}:#{master['group']} #{testdir}" +environmentpath = "#{testdir}/environments" + +apply_manifest_on(master, %Q{ + File { + ensure => directory, + owner => #{master.puppet['user']}, + group => #{master.puppet['group']}, + mode => "0750", + } + file { + [ + '#{environmentpath}', + '#{environmentpath}/production', + ]: + } +}) master_opts = { 'main' => { - 'modulepath' => "#{testdir}/modules:#{master['sitemoduledir']}:#{master['distmoduledir']}" + 'environmentpath' => environmentpath, + 'basemodulepath' => "#{master['sitemoduledir']}:#{master['distmoduledir']}", } } with_puppet_running_on master, master_opts, testdir do on master, puppet("module install pmtacceptance-java --version 1.6.0 --modulepath #{master['distmoduledir']}") - on master, puppet("module install pmtacceptance-java --version 1.7.0 --modulepath #{testdir}/modules") + on master, puppet("module install pmtacceptance-java --version 1.7.0 --modulepath #{environmentpath}/production/modules") on master, puppet("module list --modulepath #{master['distmoduledir']}") do pattern = Regexp.new([ "#{master['distmoduledir']}", "├── pmtacceptance-java \\(.*v1.6.0.*\\)", "└── pmtacceptance-stdlub \\(.*v1.0.0.*\\)" ].join("\n")) assert_match(pattern, result.output) end - on master, puppet("module list --modulepath #{testdir}/modules") do + on master, puppet("module list --modulepath #{environmentpath}/production/modules") do pattern = Regexp.new([ - "#{testdir}/modules", + "#{environmentpath}/production/modules", "├── pmtacceptance-java \\(.*v1.7.0.*\\)", "└── pmtacceptance-stdlub \\(.*v1.0.0.*\\)", ].join("\n")) assert_match(pattern, result.output) end - step "Try to uninstall a module that exists multiple locations in the module path" + step "Try to uninstall a module that exists in multiple locations in the module path" on master, puppet("module uninstall pmtacceptance-java"), :acceptable_exit_codes => [1] do pattern = Regexp.new([ ".*Notice: Preparing to uninstall 'pmtacceptance-java' .*", ".*Error: Could not uninstall module 'pmtacceptance-java'", " Module 'pmtacceptance-java' appears multiple places in the module path", - " 'pmtacceptance-java' \\(v1.7.0\\) was found in #{testdir}/modules", + " 'pmtacceptance-java' \\(v1.7.0\\) was found in #{environmentpath}/production/modules", " 'pmtacceptance-java' \\(v1.6.0\\) was found in #{master['distmoduledir']}", " Use the `--modulepath` option to limit the search to specific directories.*" ].join("\n"), Regexp::MULTILINE) assert_match(pattern, result.output) end step "Uninstall a module that exists multiple locations by restricting the --modulepath" on master, puppet("module uninstall pmtacceptance-java --modulepath #{master['distmoduledir']}") do pattern = Regexp.new([ ".*Notice: Preparing to uninstall 'pmtacceptance-java' .*", "Removed 'pmtacceptance-java' \\(.*v1.6.0.*\\) from #{master['distmoduledir']}" ].join("\n"), Regexp::MULTILINE) assert_match(pattern, result.output) end end diff --git a/acceptance/tests/modules/upgrade/with_environment.rb b/acceptance/tests/modules/upgrade/with_environment.rb index bbb76e890..0ea11b9ed 100644 --- a/acceptance/tests/modules/upgrade/with_environment.rb +++ b/acceptance/tests/modules/upgrade/with_environment.rb @@ -1,55 +1,32 @@ test_name "puppet module upgrade (with environment)" require 'puppet/acceptance/module_utils' extend Puppet::Acceptance::ModuleUtils hosts.each do |host| skip_test "skip tests requiring forge certs on solaris and aix" if host['platform'] =~ /solaris/ end -tmpdir = master.tmpdir('environmentpath') +tmpdir = master.tmpdir('module-upgrade-withenv') module_author = "pmtacceptance" module_name = "java" module_dependencies = ["stdlub"] -orig_installed_modules = get_installed_modules_for_hosts hosts -teardown do - rm_installed_modules_from_hosts orig_installed_modules, (get_installed_modules_for_hosts hosts) -end - step 'Setup' stub_forge_on(master) -puppet_conf = generate_base_legacy_and_directory_environments(tmpdir) +puppet_conf = generate_base_directory_environments(tmpdir) -install_test_module_in = lambda do |environment| - on master, puppet("module install #{module_author}-#{module_name} --config=#{puppet_conf} --version 1.6.0 --environment=#{environment}") do +step "Upgrade a module that has a more recent version published in a directory environment" do + on master, puppet("module install #{module_author}-#{module_name} --config=#{puppet_conf} --version 1.6.0 --environment=direnv") do assert_module_installed_ui(stdout, module_author, module_name) end -end -check_module_upgrade_in = lambda do |environment, environment_path| - on master, puppet("module upgrade #{module_author}-#{module_name} --config=#{puppet_conf} --environment=#{environment}") do + environment_path = "#{tmpdir}/environments/direnv/modules" + on master, puppet("module upgrade #{module_author}-#{module_name} --config=#{puppet_conf} --environment=direnv") do assert_module_installed_ui(stdout, module_author, module_name) on master, "[ -f #{environment_path}/#{module_name}/Modulefile ]" on master, "grep 1.7.1 #{environment_path}/#{module_name}/Modulefile" end end - -step "Upgrade a module that has a more recent version published in a legacy environment" do - install_test_module_in.call('legacyenv') - check_module_upgrade_in.call('legacyenv', "#{tmpdir}/legacyenv/modules") -end - -step 'Enable directory environments' do - on master, puppet("config", "set", - "environmentpath", "#{tmpdir}/environments", - "--section", "main", - "--config", puppet_conf) -end - -step "Upgrade a module that has a more recent version published in a directory environment" do - install_test_module_in.call('direnv') - check_module_upgrade_in.call('direnv', "#{tmpdir}/environments/direnv/modules") -end diff --git a/lib/puppet.rb b/lib/puppet.rb index 0e80b7fe1..5cd6ffffa 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,254 +1,257 @@ require 'puppet/version' if RUBY_VERSION < "1.9.3" raise LoadError, "Puppet #{Puppet.version} requires ruby 1.9.3 or greater." end # see the bottom of the file for further inclusions # Also see the new Vendor support - towards the end # require 'facter' require 'puppet/error' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/settings' require 'puppet/util/feature' require 'puppet/util/suidmanager' require 'puppet/util/run_mode' require 'puppet/external/pson/common' require 'puppet/external/pson/version' require 'puppet/external/pson/pure' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' # The main Puppet class. Everything is contained here. # # @api public module Puppet require 'puppet/file_system' require 'puppet/context' require 'puppet/environments' class << self include Puppet::Util attr_reader :features end # the hash that determines how our system behaves @@settings = Puppet::Settings.new # Note: It's important that these accessors (`self.settings`, `self.[]`) are # defined before we try to load any "features" (which happens a few lines below), # because the implementation of the features loading may examine the values of # settings. def self.settings @@settings end # Get the value for a setting # # @param [Symbol] param the setting to retrieve # # @api public def self.[](param) if param == :debug return Puppet::Util::Log.level == :debug else return @@settings[param] end end require 'puppet/util/logging' extend Puppet::Util::Logging # Setup facter's logging Puppet::Util::Logging.setup_facter_logging! # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. def self.define_settings(section, hash) @@settings.define_settings(section, hash) end # setting access and stuff def self.[]=(param,value) @@settings[param] = value end def self.clear @@settings.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.run_mode # This sucks (the existence of this method); there are a lot of places in our code that branch based the value of # "run mode", but there used to be some really confusing code paths that made it almost impossible to determine # when during the lifecycle of a puppet application run the value would be set properly. A lot of the lifecycle # stuff has been cleaned up now, but it still seems frightening that we rely so heavily on this value. # # I'd like to see about getting rid of the concept of "run_mode" entirely, but there are just too many places in # the code that call this method at the moment... so I've settled for isolating it inside of the Settings class # (rather than using a global variable, as we did previously...). Would be good to revisit this at some point. # # --cprice 2012-03-16 Puppet::Util::RunMode[@@settings.preferred_run_mode] end # Load all of the settings. require 'puppet/defaults' # Initialize puppet's settings. This is intended only for use by external tools that are not # built off of the Faces API or the Puppet::Util::Application class. It may also be used # to initialize state so that a Face may be used programatically, rather than as a stand-alone # command-line tool. # # @api public # @param args [Array] the command line arguments to use for initialization # @return [void] def self.initialize_settings(args = []) do_initialize_settings_for_run_mode(:user, args) end # private helper method to provide the implementation details of initializing for a run mode, # but allowing us to control where the deprecation warning is issued def self.do_initialize_settings_for_run_mode(run_mode, args) Puppet.settings.initialize_global_settings(args) run_mode = Puppet::Util::RunMode[run_mode] Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(run_mode)) Puppet.push_context(Puppet.base_context(Puppet.settings), "Initial context after settings initialization") Puppet::Parser::Functions.reset end private_class_method :do_initialize_settings_for_run_mode # Create a new type. Just proxy to the Type class. The mirroring query # code was deprecated in 2008, but this is still in heavy use. I suppose # this can count as a soft deprecation for the next dev. --daniel 2011-04-12 def self.newtype(name, options = {}, &block) Puppet.deprecation_warning("Puppet.newtype is deprecated and will be removed in a future release. Use Puppet::Type.newtype instead.") Puppet::Type.newtype(name, options, &block) end # Load vendored (setup paths, and load what is needed upfront). # See the Vendor class for how to add additional vendored gems/code require "puppet/vendor" Puppet::Vendor.load_vendored # The bindings used for initialization of puppet + # + # @param settings [Puppet::Settings,Hash] either a Puppet::Settings instance + # or a Hash of settings key/value pairs. # @api private def self.base_context(settings) - environments = settings[:environmentpath] - modulepath = Puppet::Node::Environment.split_path(settings[:basemodulepath]) + environmentpath = settings[:environmentpath] + basemodulepath = Puppet::Node::Environment.split_path(settings[:basemodulepath]) - if environments.empty? - loaders = [Puppet::Environments::Legacy.new] + if environmentpath.nil? || environmentpath.empty? + raise(Puppet::Error, "The environmentpath setting cannot be empty or nil.") else - loaders = Puppet::Environments::Directories.from_path(environments, modulepath) + loaders = Puppet::Environments::Directories.from_path(environmentpath, basemodulepath) # in case the configured environment (used for the default sometimes) # doesn't exist default_environment = Puppet[:environment].to_sym if default_environment == :production loaders << Puppet::Environments::StaticPrivate.new( - Puppet::Node::Environment.create(Puppet[:environment].to_sym, - [], + Puppet::Node::Environment.create(default_environment, + basemodulepath, Puppet::Node::Environment::NO_MANIFEST)) end end { :environments => Puppet::Environments::Cached.new(Puppet::Environments::Combined.new(*loaders)), :http_pool => proc { require 'puppet/network/http' Puppet::Network::HTTP::NoCachePool.new } } end # A simple set of bindings that is just enough to limp along to # initialization where the {base_context} bindings are put in place # @api private def self.bootstrap_context root_environment = Puppet::Node::Environment.create(:'*root*', [], Puppet::Node::Environment::NO_MANIFEST) { :current_environment => root_environment, :root_environment => root_environment } end # @param overrides [Hash] A hash of bindings to be merged with the parent context. # @param description [String] A description of the context. # @api private def self.push_context(overrides, description = "") @context.push(overrides, description) end # Return to the previous context. # @raise [StackUnderflow] if the current context is the root # @api private def self.pop_context @context.pop end # Lookup a binding by name or return a default value provided by a passed block (if given). # @api private def self.lookup(name, &block) @context.lookup(name, &block) end # @param bindings [Hash] A hash of bindings to be merged with the parent context. # @param description [String] A description of the context. # @yield [] A block executed in the context of the temporarily pushed bindings. # @api private def self.override(bindings, description = "", &block) @context.override(bindings, description, &block) end # @api private def self.mark_context(name) @context.mark(name) end # @api private def self.rollback_context(name) @context.rollback(name) end require 'puppet/node' # The single instance used for normal operation @context = Puppet::Context.new(bootstrap_context) end # This feels weird to me; I would really like for us to get to a state where there is never a "require" statement # anywhere besides the very top of a file. That would not be possible at the moment without a great deal of # effort, but I think we should strive for it and revisit this at some point. --cprice 2012-03-16 require 'puppet/indirector' require 'puppet/type' require 'puppet/resource' require 'puppet/parser' require 'puppet/network' require 'puppet/ssl' require 'puppet/module' require 'puppet/data_binding' require 'puppet/util/storage' require 'puppet/status' require 'puppet/file_bucket/file' require 'puppet/plugins' diff --git a/lib/puppet/application/doc.rb b/lib/puppet/application/doc.rb index 9ee2216de..6cc4fb280 100644 --- a/lib/puppet/application/doc.rb +++ b/lib/puppet/application/doc.rb @@ -1,260 +1,256 @@ require 'puppet/application' class Puppet::Application::Doc < Puppet::Application run_mode :master attr_accessor :unknown_args, :manifest def preinit {:references => [], :mode => :text, :format => :to_markdown }.each do |name,value| options[name] = value end @unknown_args = [] @manifest = false end option("--all","-a") option("--outputdir OUTPUTDIR","-o") option("--verbose","-v") option("--debug","-d") option("--charset CHARSET") option("--format FORMAT", "-f") do |arg| method = "to_#{arg}" require 'puppet/util/reference' if Puppet::Util::Reference.method_defined?(method) options[:format] = method else raise "Invalid output format #{arg}" end end option("--mode MODE", "-m") do |arg| require 'puppet/util/reference' if Puppet::Util::Reference.modes.include?(arg) or arg.intern==:rdoc options[:mode] = arg.intern else raise "Invalid output mode #{arg}" end end option("--list", "-l") do |arg| require 'puppet/util/reference' puts Puppet::Util::Reference.references.collect { |r| Puppet::Util::Reference.reference(r).doc }.join("\n") exit(0) end option("--reference REFERENCE", "-r") do |arg| options[:references] << arg.intern end def help <<-'HELP' puppet-doc(8) -- Generate Puppet documentation and references ======== SYNOPSIS -------- Generates a reference for all Puppet types. Largely meant for internal Puppet Labs use. USAGE ----- puppet doc [-a|--all] [-h|--help] [-l|--list] [-o|--outputdir ] [-m|--mode text|pdf|rdoc] [-r|--reference ] [--charset ] DESCRIPTION ----------- If mode is not 'rdoc', then this command generates a Markdown document describing all installed Puppet types or all allowable arguments to puppet executables. It is largely meant for internal use and is used to generate the reference document available on the Puppet Labs web site. In 'rdoc' mode, this command generates an html RDoc hierarchy describing -the manifests that are in 'manifestdir' and 'modulepath' configuration +the manifests that are in 'manifest' and 'modulepath' configuration directives. The generated documentation directory is doc by default but can be changed with the 'outputdir' option. OPTIONS ------- * --all: Output the docs for all of the reference types. In 'rdoc' mode, this also outputs documentation for all resources. * --help: Print this help message * --outputdir: Used only in 'rdoc' mode. The directory to which the rdoc output should be written. * --mode: Determine the output mode. Valid modes are 'text', 'pdf' and 'rdoc'. The 'pdf' mode creates PDF formatted files in the /tmp directory. The default mode is 'text'. * --reference: Build a particular reference. Get a list of references by running 'puppet doc --list'. * --charset: Used only in 'rdoc' mode. It sets the charset used in the html files produced. -* --manifestdir: - Used only in 'rdoc' mode. The directory to scan for stand-alone manifests. - If not supplied, puppet doc will use the manifestdir from puppet.conf. - * --modulepath: Used only in 'rdoc' mode. The directory or directories to scan for modules. - If not supplied, puppet doc will use the modulepath from puppet.conf. + May be used in place of --environment. If both settings are provided, + --modulepath will take precedence. * --environment: Used only in 'rdoc' mode. The configuration environment from which - to read the modulepath and manifestdir settings, when reading said settings - from puppet.conf. + to read the modulepath setting. EXAMPLE ------- $ puppet doc -r type > /tmp/type_reference.markdown or $ puppet doc --outputdir /tmp/rdoc --mode rdoc --modulepath /path/to/modules or $ puppet doc -m pdf -r configuration AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def handle_unknown( opt, arg ) @unknown_args << {:opt => opt, :arg => arg } true end def run_command return [:rdoc].include?(options[:mode]) ? send(options[:mode]) : other end def rdoc exit_code = 0 files = [] unless @manifest env = Puppet.lookup(:current_environment) files += env.modulepath files << ::File.dirname(env.manifest) if env.manifest != Puppet::Node::Environment::NO_MANIFEST end files += command_line.args Puppet.info "scanning: #{files.inspect}" Puppet.settings[:document_all] = options[:all] || false begin require 'puppet/util/rdoc' if @manifest Puppet::Util::RDoc.manifestdoc(files) else options[:outputdir] = "doc" unless options[:outputdir] Puppet::Util::RDoc.rdoc(options[:outputdir], files, options[:charset]) end rescue => detail Puppet.log_exception(detail, "Could not generate documentation: #{detail}") exit_code = 1 end exit exit_code end def other text = "" with_contents = options[:references].length <= 1 exit_code = 0 require 'puppet/util/reference' options[:references].sort { |a,b| a.to_s <=> b.to_s }.each do |name| raise "Could not find reference #{name}" unless section = Puppet::Util::Reference.reference(name) begin # Add the per-section text, but with no ToC text += section.send(options[:format], with_contents) rescue => detail Puppet.log_exception(detail, "Could not generate reference #{name}: #{detail}") exit_code = 1 next end end text += Puppet::Util::Reference.footer unless with_contents # We've only got one reference if options[:mode] == :pdf Puppet::Util::Reference.pdf(text) else puts text end exit exit_code end def setup # sole manifest documentation if command_line.args.size > 0 options[:mode] = :rdoc @manifest = true end if options[:mode] == :rdoc setup_rdoc else setup_reference end setup_logging end def setup_reference if options[:all] # Don't add dynamic references to the "all" list. require 'puppet/util/reference' options[:references] = Puppet::Util::Reference.references.reject do |ref| Puppet::Util::Reference.reference(ref).dynamic? end end options[:references] << :type if options[:references].empty? end def setup_rdoc # consume the unknown options # and feed them as settings if @unknown_args.size > 0 @unknown_args.each do |option| # force absolute path for modulepath when passed on commandline - if option[:opt]=="--modulepath" or option[:opt] == "--manifestdir" + if option[:opt]=="--modulepath" option[:arg] = option[:arg].split(::File::PATH_SEPARATOR).collect { |p| ::File.expand_path(p) }.join(::File::PATH_SEPARATOR) end Puppet.settings.handlearg(option[:opt], option[:arg]) end end end def setup_logging Puppet::Util::Log.level = :warning set_log_level Puppet::Util::Log.newdestination(:console) end end diff --git a/lib/puppet/configurer/fact_handler.rb b/lib/puppet/configurer/fact_handler.rb index 5d20a5b91..627317d6c 100644 --- a/lib/puppet/configurer/fact_handler.rb +++ b/lib/puppet/configurer/fact_handler.rb @@ -1,37 +1,37 @@ require 'puppet/indirector/facts/facter' require 'puppet/configurer' require 'puppet/configurer/downloader' # Break out the code related to facts. This module is # just included into the agent, but having it here makes it # easier to test. module Puppet::Configurer::FactHandler def find_facts # This works because puppet agent configures Facts to use 'facter' for # finding facts and the 'rest' terminus for caching them. Thus, we'll # compile them and then "cache" them on the server. begin - facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value], :environment => @environment) + facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value], :environment => Puppet::Node::Environment.remote(@environment)) unless Puppet[:node_name_fact].empty? Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]] facts.name = Puppet[:node_name_value] end facts rescue SystemExit,NoMemoryError raise rescue Exception => detail message = "Could not retrieve local facts: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end end def facts_for_uploading facts = find_facts text = facts.render(:pson) {:facts_format => :pson, :facts => CGI.escape(text)} end end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 55ed97137..2ef557a93 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,1748 +1,1730 @@ module Puppet def self.default_diffargs if (Facter.value(:kernel) == "AIX" && Facter.value(:kernelmajversion) == "5300") "" else "-u" end end ############################################################################################ # NOTE: For information about the available values for the ":type" property of settings, # see the docs for Settings.define_settings ############################################################################################ AS_DURATION = %q{This setting can be a time interval in seconds (30 or 30s), minutes (30m), hours (6h), days (2d), or years (5y).} # This is defined first so that the facter implementation is replaced before other setting defaults are evaluated. define_settings(:main, :cfacter => { :default => false, :type => :boolean, :desc => 'Whether or not to use the native facter (cfacter) implementation instead of the Ruby one (facter). Defaults to false.', :hook => proc do |value| return unless value raise ArgumentError, 'facter has already evaluated facts.' if Facter.instance_variable_get(:@collection) raise ArgumentError, 'cfacter version 0.2.0 or later is not installed.' unless Puppet.features.cfacter? CFacter.initialize # Setup Facter's logging again now that native facter is initialized Puppet::Util::Logging.setup_facter_logging! end } ) define_settings(:main, :confdir => { :default => nil, :type => :directory, :desc => "The main Puppet configuration directory. The default for this setting is calculated based on the user. If the process is running as root or the user that Puppet is supposed to run as, it defaults to a system directory, but if it's running as any other user, it defaults to being in the user's home directory.", }, :vardir => { :default => nil, :type => :directory, :owner => "service", :group => "service", :desc => "Where Puppet stores dynamic and growing data. The default for this setting is calculated specially, like `confdir`_.", }, ### NOTE: this setting is usually being set to a symbol value. We don't officially have a ### setting type for that yet, but we might want to consider creating one. :name => { :default => nil, :desc => "The name of the application, if we are running as one. The default is essentially $0 without the path or `.rb`.", } ) define_settings(:main, :logdir => { :default => nil, :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "The directory in which to store log files", }, :log_level => { :default => 'notice', :type => :enum, :values => ["debug","info","notice","warning","err","alert","emerg","crit"], :desc => "Default logging level for messages from Puppet. Allowed values are: * debug * info * notice * warning * err * alert * emerg * crit ", :hook => proc {|value| Puppet::Util::Log.level = value }, :call_hook => :on_initialize_and_write, }, :disable_warnings => { :default => [], :type => :array, :desc => "A comma-separated list of warning types to suppress. If large numbers of warnings are making Puppet's logs too large or difficult to use, you can temporarily silence them with this setting. If you are preparing to upgrade Puppet to a new major version, you should re-enable all warnings for a while. Valid values for this setting are: * `deprecations` --- disables deprecation warnings.", :hook => proc do |value| values = munge(value) valid = %w[deprecations] invalid = values - (values & valid) if not invalid.empty? raise ArgumentError, "Cannot disable unrecognized warning types #{invalid.inspect}. Valid values are #{valid.inspect}." end end } ) define_settings(:main, :priority => { :default => nil, :type => :priority, :desc => "The scheduling priority of the process. Valid values are 'high', 'normal', 'low', or 'idle', which are mapped to platform-specific values. The priority can also be specified as an integer value and will be passed as is, e.g. -5. Puppet must be running as a privileged user in order to increase scheduling priority.", }, :trace => { :default => false, :type => :boolean, :desc => "Whether to print stack traces on some errors", :hook => proc do |value| # Enable or disable Facter's trace option too Facter.trace(value) if Facter.respond_to? :trace end }, :profile => { :default => false, :type => :boolean, :desc => "Whether to enable experimental performance profiling", }, :autoflush => { :default => true, :type => :boolean, :desc => "Whether log files should always flush to disk.", :hook => proc { |value| Log.autoflush = value } }, :syslogfacility => { :default => "daemon", :desc => "What syslog facility to use when logging to syslog. Syslog has a fixed list of valid facilities, and you must choose one of those; you cannot just make one up." }, :statedir => { :default => "$vardir/state", :type => :directory, :mode => "01755", :desc => "The directory where Puppet state is stored. Generally, this directory can be removed without causing harm (although it might result in spurious service restarts)." }, :rundir => { :default => nil, :type => :directory, :mode => "0755", :owner => "service", :group => "service", :desc => "Where Puppet PID files are kept." }, :genconfig => { :default => false, :type => :boolean, :desc => "When true, causes Puppet applications to print an example config file to stdout and exit. The example will include descriptions of each setting, and the current (or default) value of each setting, incorporating any settings overridden on the CLI (with the exception of `genconfig` itself). This setting only makes sense when specified on the command line as `--genconfig`.", }, :genmanifest => { :default => false, :type => :boolean, :desc => "Whether to just print a manifest to stdout and exit. Only makes sense when specified on the command line as `--genmanifest`. Takes into account arguments specified on the CLI.", }, :configprint => { :default => "", :desc => "Print the value of a specific configuration setting. If the name of a setting is provided for this, then the value is printed and puppet exits. Comma-separate multiple values. For a list of all values, specify 'all'.", }, :color => { :default => "ansi", :type => :string, :desc => "Whether to use colors when logging to the console. Valid values are `ansi` (equivalent to `true`), `html`, and `false`, which produces no color. Defaults to false on Windows, as its console does not support ansi colors.", }, :mkusers => { :default => false, :type => :boolean, :desc => "Whether to create the necessary user and group that puppet agent will run as.", }, :manage_internal_file_permissions => { :default => true, :type => :boolean, :desc => "Whether Puppet should manage the owner, group, and mode of files it uses internally", }, :onetime => { :default => false, :type => :boolean, :desc => "Perform one configuration run and exit, rather than spawning a long-running daemon. This is useful for interactively running puppet agent, or running puppet agent from cron.", :short => 'o', }, :path => { :default => "none", :desc => "The shell search path. Defaults to whatever is inherited from the parent process. This setting can only be set in the `[main]` section of puppet.conf; it cannot be set in `[master]`, `[agent]`, or an environment config section.", :call_hook => :on_define_and_write, :hook => proc do |value| ENV["PATH"] = "" if ENV["PATH"].nil? ENV["PATH"] = value unless value == "none" paths = ENV["PATH"].split(File::PATH_SEPARATOR) Puppet::Util::Platform.default_paths.each do |path| ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path) end value end }, :libdir => { :type => :directory, :default => "$vardir/lib", :desc => "An extra search path for Puppet. This is only useful for those files that Puppet will load on demand, and is only guaranteed to work for those cases. In fact, the autoload mechanism is responsible for making sure this directory is in Ruby's search path\n", :call_hook => :on_initialize_and_write, :hook => proc do |value| $LOAD_PATH.delete(@oldlibdir) if defined?(@oldlibdir) and $LOAD_PATH.include?(@oldlibdir) @oldlibdir = value $LOAD_PATH << value end }, :ignoreimport => { :default => false, :type => :boolean, :desc => "If true, allows the parser to continue without requiring all files referenced with `import` statements to exist. This setting was primarily designed for use with commit hooks for parse-checking.", }, :environment => { :default => "production", :desc => "The environment Puppet is running in. For clients (e.g., `puppet agent`) this determines the environment itself, which is used to find modules and much more. For servers (i.e., `puppet master`) this provides the default environment for nodes we know nothing about." }, :environmentpath => { - :default => "", + :default => "$confdir/environments", :desc => "A search path for directory environments, as a list of directories separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.) This setting must have a value set to enable **directory environments.** The recommended value is `$confdir/environments`. For more details, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", :type => :path, }, :always_cache_features => { :type => :boolean, :default => false, :desc => <<-'EOT' Affects how we cache attempts to load Puppet 'features'. If false, then calls to `Puppet.features.?` will always attempt to load the feature (which can be an expensive operation) unless it has already been loaded successfully. This makes it possible for a single agent run to, e.g., install a package that provides the underlying capabilities for a feature, and then later load that feature during the same run (even if the feature had been tested earlier and had not been available). If this setting is set to true, then features will only be checked once, and if they are not available, the negative result is cached and returned for all subsequent attempts to load the feature. This behavior is almost always appropriate for the server, and can result in a significant performance improvement for features that are checked frequently. EOT }, :diff_args => { :default => lambda { default_diffargs }, :desc => "Which arguments to pass to the diff command when printing differences between files. The command to use can be chosen with the `diff` setting.", }, :diff => { :default => (Puppet.features.microsoft_windows? ? "" : "diff"), :desc => "Which diff command to use when printing differences between files. This setting has no default value on Windows, as standard `diff` is not available, but Puppet can use many third-party diff tools.", }, :show_diff => { :type => :boolean, :default => false, :desc => "Whether to log and report a contextual diff when files are being replaced. This causes partial file contents to pass through Puppet's normal logging and reporting system, so this setting should be used with caution if you are sending Puppet's reports to an insecure destination. This feature currently requires the `diff/lcs` Ruby library.", }, :daemonize => { :type => :boolean, :default => (Puppet.features.microsoft_windows? ? false : true), :desc => "Whether to send the process into the background. This defaults to true on POSIX systems, and to false on Windows (where Puppet currently cannot daemonize).", :short => "D", :hook => proc do |value| if value and Puppet.features.microsoft_windows? raise "Cannot daemonize on Windows" end end }, :maximum_uid => { :default => 4294967290, :desc => "The maximum allowed UID. Some platforms use negative UIDs but then ship with tools that do not know how to handle signed ints, so the UIDs show up as huge numbers that can then not be fed back into the system. This is a hackish way to fail in a slightly more useful way when that happens.", }, :route_file => { :default => "$confdir/routes.yaml", :desc => "The YAML file containing indirector route configuration.", }, :node_terminus => { :type => :terminus, :default => "plain", :desc => "Where to find information about nodes.", }, :node_cache_terminus => { :type => :terminus, :default => nil, :desc => "How to store cached nodes. Valid values are (none), 'json', 'msgpack', 'yaml' or write only yaml ('write_only_yaml'). The master application defaults to 'write_only_yaml', all others to none.", }, :data_binding_terminus => { :type => :terminus, :default => "hiera", :desc => "Where to retrive information about data.", }, :hiera_config => { :default => "$confdir/hiera.yaml", :desc => "The hiera configuration file. Puppet only reads this file on startup, so you must restart the puppet master every time you edit it.", :type => :file, }, :binder_config => { :default => nil, :desc => "The binder configuration file. Puppet reads this file on each request to configure the bindings system. If set to nil (the default), a $confdir/binder_config.yaml is optionally loaded. If it does not exists, a default configuration is used. If the setting :binding_config is specified, it must reference a valid and existing yaml file.", :type => :file, }, :catalog_terminus => { :type => :terminus, :default => "compiler", :desc => "Where to get node catalogs. This is useful to change if, for instance, you'd like to pre-compile catalogs and store them in memcached or some other easily-accessed store.", }, :catalog_cache_terminus => { :type => :terminus, :default => nil, :desc => "How to store cached catalogs. Valid values are 'json', 'msgpack' and 'yaml'. The agent application defaults to 'json'." }, :facts_terminus => { :default => 'facter', :desc => "The node facts terminus.", }, :default_file_terminus => { :type => :terminus, :default => "rest", :desc => "The default source for files if no server is given in a uri, e.g. puppet:///file. The default of `rest` causes the file to be retrieved using the `server` setting. When running `apply` the default is `file_server`, causing requests to be filled locally." }, :httplog => { :default => "$logdir/http.log", :type => :file, :owner => "root", :mode => "0640", :desc => "Where the puppet agent web server logs.", }, :http_proxy_host => { :default => "none", :desc => "The HTTP proxy host to use for outgoing connections. Note: You may need to use a FQDN for the server hostname when using a proxy. Environment variable http_proxy or HTTP_PROXY will override this value", }, :http_proxy_port => { :default => 3128, :desc => "The HTTP proxy port to use for outgoing connections", }, :http_proxy_user => { :default => "none", :desc => "The user name for an authenticated HTTP proxy. Requires the `http_proxy_host` setting.", }, :http_proxy_password =>{ :default => "none", :hook => proc do |value| if Puppet.settings[:http_proxy_password] =~ /[@!# \/]/ raise "Passwords set in the http_proxy_password setting must be valid as part of a URL, and any reserved characters must be URL-encoded. We received: #{value}" end end, :desc => "The password for the user of an authenticated HTTP proxy. Requires the `http_proxy_user` setting. Note that passwords must be valid when used as part of a URL. If a password contains any characters with special meanings in URLs (as specified by RFC 3986 section 2.2), they must be URL-encoded. (For example, `#` would become `%23`.)", }, :http_keepalive_timeout => { :default => "4s", :type => :duration, :desc => "The maximum amount of time a persistent HTTP connection can remain idle in the connection pool, before it is closed. This timeout should be shorter than the keepalive timeout used on the HTTP server, e.g. Apache KeepAliveTimeout directive. #{AS_DURATION}" }, :http_debug => { :default => false, :type => :boolean, :desc => "Whether to write HTTP request and responses to stderr. This should never be used in a production environment." }, :http_connect_timeout => { :default => "2m", :type => :duration, :desc => "The maximum amount of time to wait when establishing an HTTP connection. The default value is 2 minutes. #{AS_DURATION}", }, :http_read_timeout => { :type => :duration, :desc => "The time to wait for one block to be read from an HTTP connection. If nothing is read after the elapsed interval then the connection will be closed. The default value is unlimited. #{AS_DURATION}", }, :filetimeout => { :default => "15s", :type => :duration, :desc => "The minimum time to wait between checking for updates in configuration files. This timeout determines how quickly Puppet checks whether a file (such as manifests or templates) has changed on disk. #{AS_DURATION}", }, :environment_timeout => { :default => "unlimited", :type => :ttl, :desc => "The time to live for a cached environment. #{AS_DURATION} This setting can also be set to `unlimited`, which causes the environment to be cached until the master is restarted." }, :environment_data_provider => { :default => "none", :desc => "The name of a registered environment data provider. The two built in and registered providers are 'none' (no environment specific data), and 'function' (environment specific data obtained by calling the function 'environment::data()'). Other environment data providers may be registered in modules on the module path. For such custom data providers see the respective module documentation." }, - :config_version => { - :default => "", - :desc => "How to determine the configuration version. By default, it will be the - time that the configuration is parsed, but you can provide a shell script to override how the - version is determined. The output of this script will be added to every log message in the - reports, allowing you to correlate changes on your hosts to the source version on the server. - - Setting a global value for config_version in puppet.conf is deprecated. Please set a - per-environment value in environment.conf instead. For more info, see - http://docs.puppetlabs.com/puppet/latest/reference/environments.html", - :deprecated => :allowed_on_commandline, - }, :prerun_command => { :default => "", :desc => "A command to run before every agent run. If this command returns a non-zero return code, the entire Puppet run will fail.", }, :postrun_command => { :default => "", :desc => "A command to run after every agent run. If this command returns a non-zero return code, the entire Puppet run will be considered to have failed, even though it might have performed work during the normal run.", }, :freeze_main => { :default => false, :type => :boolean, :desc => "Freezes the 'main' class, disallowing any code to be added to it. This essentially means that you can't have any code outside of a node, class, or definition other than in the site manifest.", } ) Puppet.define_settings(:module_tool, :module_repository => { :default => 'https://forgeapi.puppetlabs.com', :desc => "The module repository", }, :module_working_dir => { :default => '$vardir/puppet-module', :desc => "The directory into which module tool data is stored", }, :module_skeleton_dir => { :default => '$module_working_dir/skeleton', :desc => "The directory which the skeleton for module tool generate is stored.", }, :forge_authorization => { :default => nil, :desc => "The authorization key to connect to the Puppet Forge. Leave blank for unauthorized or license based connections", }, :module_groups => { :default => nil, :desc => "Extra module groups to request from the Puppet Forge", } ) Puppet.define_settings( :main, # We have to downcase the fqdn, because the current ssl stuff (as oppsed to in master) doesn't have good facilities for # manipulating naming. :certname => { :default => lambda { Puppet::Settings.default_certname.downcase }, :desc => "The name to use when handling certificates. When a node requests a certificate from the CA puppet master, it uses the value of the `certname` setting as its requested Subject CN. This is the name used when managing a node's permissions in [auth.conf](http://docs.puppetlabs.com/puppet/latest/reference/config_file_auth.html). In most cases, it is also used as the node's name when matching [node definitions](http://docs.puppetlabs.com/puppet/latest/reference/lang_node_definitions.html) and requesting data from an ENC. (This can be changed with the `node_name_value` and `node_name_fact` settings, although you should only do so if you have a compelling reason.) A node's certname is available in Puppet manifests as `$trusted['certname']`. (See [Facts and Built-In Variables](http://docs.puppetlabs.com/puppet/latest/reference/lang_facts_and_builtin_vars.html) for more details.) * For best compatibility, you should limit the value of `certname` to only use letters, numbers, periods, underscores, and dashes. (That is, it should match `/\A[a-z0-9._-]+\Z/`.) * The special value `ca` is reserved, and can't be used as the certname for a normal node. Defaults to the node's fully qualified domain name.", :hook => proc { |value| raise(ArgumentError, "Certificate names must be lower case; see #1168") unless value == value.downcase }}, :dns_alt_names => { :default => '', :desc => < { :default => "$confdir/csr_attributes.yaml", :type => :file, :desc => < { :default => "$ssldir/certs", :type => :directory, :mode => "0755", :owner => "service", :group => "service", :desc => "The certificate directory." }, :ssldir => { :default => "$confdir/ssl", :type => :directory, :mode => "0771", :owner => "service", :group => "service", :desc => "Where SSL certificates are kept." }, :publickeydir => { :default => "$ssldir/public_keys", :type => :directory, :mode => "0755", :owner => "service", :group => "service", :desc => "The public key directory." }, :requestdir => { :default => "$ssldir/certificate_requests", :type => :directory, :mode => "0755", :owner => "service", :group => "service", :desc => "Where host certificate requests are stored." }, :privatekeydir => { :default => "$ssldir/private_keys", :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "The private key directory." }, :privatedir => { :default => "$ssldir/private", :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "Where the client stores private certificate information." }, :passfile => { :default => "$privatedir/password", :type => :file, :mode => "0640", :owner => "service", :group => "service", :desc => "Where puppet agent stores the password for its private key. Generally unused." }, :hostcsr => { :default => "$ssldir/csr_$certname.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their certificate requests." }, :hostcert => { :default => "$certdir/$certname.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their certificates." }, :hostprivkey => { :default => "$privatekeydir/$certname.pem", :type => :file, :mode => "0640", :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their private key." }, :hostpubkey => { :default => "$publickeydir/$certname.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where individual hosts store and look for their public key." }, :localcacert => { :default => "$certdir/ca.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where each client stores the CA certificate." }, :ssl_client_ca_auth => { :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Certificate authorities who issue server certificates. SSL servers will not be considered authentic unless they possess a certificate issued by an authority listed in this file. If this setting has no value then the Puppet master's CA certificate (localcacert) will be used." }, :ssl_server_ca_auth => { :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Certificate authorities who issue client certificates. SSL clients will not be considered authentic unless they possess a certificate issued by an authority listed in this file. If this setting has no value then the Puppet master's CA certificate (localcacert) will be used." }, :hostcrl => { :default => "$ssldir/crl.pem", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "Where the host's certificate revocation list can be found. This is distinct from the certificate authority's CRL." }, :certificate_revocation => { :default => true, :type => :boolean, :desc => "Whether certificate revocation should be supported by downloading a Certificate Revocation List (CRL) to all clients. If enabled, CA chaining will almost definitely not work.", }, :digest_algorithm => { :default => 'md5', :type => :enum, :values => ["md5", "sha256"], :desc => 'Which digest algorithm to use for file resources and the filebucket. Valid values are md5, sha256. Default is md5.', } ) define_settings( :ca, :ca_name => { :default => "Puppet CA: $certname", :desc => "The name to use the Certificate Authority certificate.", }, :cadir => { :default => "$ssldir/ca", :type => :directory, :owner => "service", :group => "service", :mode => "0755", :desc => "The root directory for the certificate authority." }, :cacert => { :default => "$cadir/ca_crt.pem", :type => :file, :owner => "service", :group => "service", :mode => "0644", :desc => "The CA certificate." }, :cakey => { :default => "$cadir/ca_key.pem", :type => :file, :owner => "service", :group => "service", :mode => "0640", :desc => "The CA private key." }, :capub => { :default => "$cadir/ca_pub.pem", :type => :file, :owner => "service", :group => "service", :mode => "0644", :desc => "The CA public key." }, :cacrl => { :default => "$cadir/ca_crl.pem", :type => :file, :owner => "service", :group => "service", :mode => "0644", :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", }, :caprivatedir => { :default => "$cadir/private", :type => :directory, :owner => "service", :group => "service", :mode => "0750", :desc => "Where the CA stores private certificate information." }, :csrdir => { :default => "$cadir/requests", :type => :directory, :owner => "service", :group => "service", :mode => "0755", :desc => "Where the CA stores certificate requests" }, :signeddir => { :default => "$cadir/signed", :type => :directory, :owner => "service", :group => "service", :mode => "0755", :desc => "Where the CA stores signed certificates." }, :capass => { :default => "$caprivatedir/ca.pass", :type => :file, :owner => "service", :group => "service", :mode => "0640", :desc => "Where the CA stores the password for the private key." }, :serial => { :default => "$cadir/serial", :type => :file, :owner => "service", :group => "service", :mode => "0644", :desc => "Where the serial number for certificates is stored." }, :autosign => { :default => "$confdir/autosign.conf", :type => :autosign, :desc => "Whether (and how) to autosign certificate requests. This setting is only relevant on a puppet master acting as a certificate authority (CA). Valid values are true (autosigns all certificate requests; not recommended), false (disables autosigning certificates), or the absolute path to a file. The file specified in this setting may be either a **configuration file** or a **custom policy executable.** Puppet will automatically determine what it is: If the Puppet user (see the `user` setting) can execute the file, it will be treated as a policy executable; otherwise, it will be treated as a config file. If a custom policy executable is configured, the CA puppet master will run it every time it receives a CSR. The executable will be passed the subject CN of the request _as a command line argument,_ and the contents of the CSR in PEM format _on stdin._ It should exit with a status of 0 if the cert should be autosigned and non-zero if the cert should not be autosigned. If a certificate request is not autosigned, it will persist for review. An admin user can use the `puppet cert sign` command to manually sign it, or can delete the request. For info on autosign configuration files, see [the guide to Puppet's config files](http://docs.puppetlabs.com/guides/configuring.html).", }, :allow_duplicate_certs => { :default => false, :type => :boolean, :desc => "Whether to allow a new certificate request to overwrite an existing certificate.", }, :ca_ttl => { :default => "5y", :type => :duration, :desc => "The default TTL for new certificates. #{AS_DURATION}" }, :req_bits => { :default => 4096, :desc => "The bit length of the certificates.", }, :keylength => { :default => 4096, :desc => "The bit length of keys.", }, :cert_inventory => { :default => "$cadir/inventory.txt", :type => :file, :mode => "0644", :owner => "service", :group => "service", :desc => "The inventory file. This is a text file to which the CA writes a complete listing of all certificates." } ) # Define the config default. define_settings(:application, :config_file_name => { :type => :string, :default => Puppet::Settings.default_config_file_name, :desc => "The name of the puppet config file.", }, :config => { :type => :file, :default => "$confdir/${config_file_name}", :desc => "The configuration file for the current puppet application.", }, :pidfile => { :type => :file, :default => "$rundir/${run_mode}.pid", :desc => "The file containing the PID of a running process. This file is intended to be used by service management frameworks and monitoring systems to determine if a puppet process is still in the process table.", }, :bindaddress => { :default => "0.0.0.0", :desc => "The address a listening server should bind to.", } ) - define_settings(:master, - :user => { - :default => "puppet", - :desc => "The user puppet master should run as.", - }, - :group => { - :default => "puppet", - :desc => "The group puppet master should run as.", - }, - :manifestdir => { - :default => "$confdir/manifests", - :type => :directory, - :desc => "Used to build the default value of the `manifest` setting. Has no other purpose. - - This setting is deprecated.", - :deprecated => :completely, - }, + define_settings(:environment, :manifest => { - :default => "$manifestdir/site.pp", + :default => nil, :type => :file_or_directory, :desc => "The entry-point manifest for puppet master. This can be one file or a directory of manifests to be evaluated in alphabetical order. Puppet manages this path as a directory if one exists or if the path ends with a / or \\. - Setting a global value for `manifest` in puppet.conf is deprecated. Please use + Setting a global value for `manifest` in puppet.conf is not allowed + (but it can be overridden from them commandline). Please use directory environments instead. If you need to use something other than the environment's `manifests` directory as the main manifest, you can set `manifest` in environment.conf. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", - :deprecated => :allowed_on_commandline, + }, + :modulepath => { + :default => "", + :type => :path, + :desc => "The search path for modules, as a list of directories separated by the system + path separator character. (The POSIX path separator is ':', and the + Windows path separator is ';'.) + + Setting a global value for `modulepath` in puppet.conf is not allowed + (but it can be overridden from the commandline). Please use + directory environments instead. If you need to use something other than the + default modulepath of `:$basemodulepath`, + you can set `modulepath` in environment.conf. For more info, see + http://docs.puppetlabs.com/puppet/latest/reference/environments.html", + }, + :config_version => { + :default => "", + :desc => "How to determine the configuration version. By default, it will be the + time that the configuration is parsed, but you can provide a shell script to override how the + version is determined. The output of this script will be added to every log message in the + reports, allowing you to correlate changes on your hosts to the source version on the server. + + Setting a global value for config_version in puppet.conf is not allowed + (but it can be overridden from the commandline). Please set a + per-environment value in environment.conf instead. For more info, see + http://docs.puppetlabs.com/puppet/latest/reference/environments.html", + }, + ) + + define_settings(:master, + :user => { + :default => "puppet", + :desc => "The user puppet master should run as.", + }, + :group => { + :default => "puppet", + :desc => "The group puppet master should run as.", }, :default_manifest => { :default => "./manifests", :type => :string, :desc => "The default main manifest for directory environments. Any environment that doesn't set the `manifest` setting in its `environment.conf` file will use this manifest. This setting's value can be an absolute or relative path. An absolute path will make all environments default to the same main manifest; a relative path will allow each environment to use its own manifest, and Puppet will resolve the path relative to each environment's main directory. In either case, the path can point to a single file or to a directory of manifests to be evaluated in alphabetical order.", }, :disable_per_environment_manifest => { :default => false, :type => :boolean, :desc => "Whether to disallow an environment-specific main manifest. When set to `true`, Puppet will use the manifest specified in the `default_manifest` setting for all environments. If an environment specifies a different main manifest in its `environment.conf` file, catalog requests for that environment will fail with an error. This setting requires `default_manifest` to be set to an absolute path.", :hook => proc do |value| if value && !Pathname.new(Puppet[:default_manifest]).absolute? raise(Puppet::Settings::ValidationError, "The 'default_manifest' setting must be set to an absolute path when 'disable_per_environment_manifest' is true") end end, }, :code => { :default => "", :desc => "Code to parse directly. This is essentially only used by `puppet`, and should only be set if you're writing your own Puppet executable.", }, :masterhttplog => { :default => "$logdir/masterhttp.log", :type => :file, :owner => "service", :group => "service", :mode => "0660", :create => true, :desc => "Where the puppet master web server saves its access log. This is only used when running a WEBrick puppet master. When puppet master is running under a Rack server like Passenger, that web server will have its own logging behavior." }, :masterport => { :default => 8140, :desc => "The port for puppet master traffic. For puppet master, this is the port to listen on; for puppet agent, this is the port to make requests on. Both applications use this setting to get the port.", }, :node_name => { :default => "cert", :desc => "How the puppet master determines the client's identity and sets the 'hostname', 'fqdn' and 'domain' facts for use in the manifest, in particular for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's certificate) and 'facter' (use the hostname that the client reported in its facts)", }, :bucketdir => { :default => "$vardir/bucket", :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "Where FileBucket files are stored." }, :rest_authconfig => { :default => "$confdir/auth.conf", :type => :file, :desc => "The configuration file that defines the rights to the different rest indirections. This can be used as a fine-grained authorization system for `puppet master`.", }, :ca => { :default => true, :type => :boolean, :desc => "Whether the master should function as a certificate authority.", }, :trusted_oid_mapping_file => { :default => "$confdir/custom_trusted_oid_mapping.yaml", :type => :file, :desc => "File that provides mapping between custom SSL oids and user-friendly names" }, :basemodulepath => { :default => "$confdir/modules#{File::PATH_SEPARATOR}/usr/share/puppet/modules", :type => :path, :desc => "The search path for **global** modules. Should be specified as a list of directories separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.) - If you are using directory environments, these are the modules that will - be used by _all_ environments. Note that the `modules` directory of the active - environment will have priority over any global directories. For more info, see - http://docs.puppetlabs.com/puppet/latest/reference/environments.html - - This setting also provides the default value for the deprecated `modulepath` - setting, which is used when directory environments are disabled.", - }, - :modulepath => { - :default => "$basemodulepath", - :type => :path, - :desc => "The search path for modules, as a list of directories separated by the system - path separator character. (The POSIX path separator is ':', and the - Windows path separator is ';'.) - - Setting a global value for `modulepath` in puppet.conf is deprecated. Please use - directory environments instead. If you need to use something other than the - default modulepath of `:$basemodulepath`, - you can set `modulepath` in environment.conf. For more info, see + These are the modules that will be used by _all_ environments. Note that + the `modules` directory of the active environment will have priority over + any global directories. For more info, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", - :deprecated => :allowed_on_commandline, }, :ssl_client_header => { :default => "HTTP_X_CLIENT_DN", :desc => "The header containing an authenticated client's SSL DN. This header must be set by the proxy to the authenticated client's SSL DN (e.g., `/CN=puppet.puppetlabs.com`). Puppet will parse out the Common Name (CN) from the Distinguished Name (DN) and use the value of the CN field for authorization. Note that the name of the HTTP header gets munged by the web server common gateway inteface: an `HTTP_` prefix is added, dashes are converted to underscores, and all letters are uppercased. Thus, to use the `X-Client-DN` header, this setting should be `HTTP_X_CLIENT_DN`.", }, :ssl_client_verify_header => { :default => "HTTP_X_CLIENT_VERIFY", :desc => "The header containing the status message of the client verification. This header must be set by the proxy to 'SUCCESS' if the client successfully authenticated, and anything else otherwise. Note that the name of the HTTP header gets munged by the web server common gateway inteface: an `HTTP_` prefix is added, dashes are converted to underscores, and all letters are uppercased. Thus, to use the `X-Client-Verify` header, this setting should be `HTTP_X_CLIENT_VERIFY`.", }, # To make sure this directory is created before we try to use it on the server, we need # it to be in the server section (#1138). :yamldir => { :default => "$vardir/yaml", :type => :directory, :owner => "service", :group => "service", :mode => "0750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, :server_datadir => { :default => "$vardir/server_data", :type => :directory, :owner => "service", :group => "service", :mode => "0750", :desc => "The directory in which serialized data is stored, usually in a subdirectory."}, :reports => { :default => "store", :desc => "The list of report handlers to use. When using multiple report handlers, their names should be comma-separated, with whitespace allowed. (For example, `reports = http, store`.) This setting is relevant to puppet master and puppet apply. The puppet master will call these report handlers with the reports it receives from agent nodes, and puppet apply will call them with its own report. (In all cases, the node applying the catalog must have `report = true`.) See the report reference for information on the built-in report handlers; custom report handlers can also be loaded from modules. (Report handlers are loaded from the lib directory, at `puppet/reports/NAME.rb`.)", }, :reportdir => { :default => "$vardir/reports", :type => :directory, :mode => "0750", :owner => "service", :group => "service", :desc => "The directory in which to store reports. Each node gets a separate subdirectory in this directory. This setting is only used when the `store` report processor is enabled (see the `reports` setting)."}, :reporturl => { :default => "http://localhost:3000/reports/upload", :desc => "The URL that reports should be forwarded to. This setting is only used when the `http` report processor is enabled (see the `reports` setting).", }, :fileserverconfig => { :default => "$confdir/fileserver.conf", :type => :file, :desc => "Where the fileserver configuration is stored.", }, :strict_hostname_checking => { :default => false, :desc => "Whether to only search for the complete hostname as it is in the certificate when searching for node information in the catalogs.", } ) define_settings(:device, :devicedir => { :default => "$vardir/devices", :type => :directory, :mode => "0750", :desc => "The root directory of devices' $vardir.", }, :deviceconfig => { :default => "$confdir/device.conf", :desc => "Path to the device config file for puppet device.", } ) define_settings(:agent, :node_name_value => { :default => "$certname", :desc => "The explicit value used for the node name for all requests the agent makes to the master. WARNING: This setting is mutually exclusive with node_name_fact. Changing this setting also requires changes to the default auth.conf configuration on the Puppet Master. Please see http://links.puppetlabs.com/node_name_value for more information." }, :node_name_fact => { :default => "", :desc => "The fact name used to determine the node name used for all requests the agent makes to the master. WARNING: This setting is mutually exclusive with node_name_value. Changing this setting also requires changes to the default auth.conf configuration on the Puppet Master. Please see http://links.puppetlabs.com/node_name_fact for more information.", :hook => proc do |value| if !value.empty? and Puppet[:node_name_value] != Puppet[:certname] raise "Cannot specify both the node_name_value and node_name_fact settings" end end }, :statefile => { :default => "$statedir/state.yaml", :type => :file, :mode => "0660", :desc => "Where puppet agent and puppet master store state associated with the running configuration. In the case of puppet master, this file reflects the state discovered through interacting with clients." }, :clientyamldir => { :default => "$vardir/client_yaml", :type => :directory, :mode => "0750", :desc => "The directory in which client-side YAML data is stored." }, :client_datadir => { :default => "$vardir/client_data", :type => :directory, :mode => "0750", :desc => "The directory in which serialized data is stored on the client." }, :classfile => { :default => "$statedir/classes.txt", :type => :file, :owner => "root", :mode => "0640", :desc => "The file in which puppet agent stores a list of the classes associated with the retrieved configuration. Can be loaded in the separate `puppet` executable using the `--loadclasses` option."}, :resourcefile => { :default => "$statedir/resources.txt", :type => :file, :owner => "root", :mode => "0640", :desc => "The file in which puppet agent stores a list of the resources associated with the retrieved configuration." }, :puppetdlog => { :default => "$logdir/puppetd.log", :type => :file, :owner => "root", :mode => "0640", :desc => "The fallback log file. This is only used when the `--logdest` option is not specified AND Puppet is running on an operating system where both the POSIX syslog service and the Windows Event Log are unavailable. (Currently, no supported operating systems match that description.) Despite the name, both puppet agent and puppet master will use this file as the fallback logging destination. For control over logging destinations, see the `--logdest` command line option in the manual pages for puppet master, puppet agent, and puppet apply. You can see man pages by running `puppet --help`, or read them online at http://docs.puppetlabs.com/references/latest/man/." }, :server => { :default => "puppet", :desc => "The puppet master server to which the puppet agent should connect." }, :use_srv_records => { :default => false, :type => :boolean, :desc => "Whether the server will search for SRV records in DNS for the current domain.", }, :srv_domain => { :default => lambda { Puppet::Settings.domain_fact }, :desc => "The domain which will be queried to find the SRV records of servers to use.", }, :ignoreschedules => { :default => false, :type => :boolean, :desc => "Boolean; whether puppet agent should ignore schedules. This is useful for initial puppet agent runs.", }, :default_schedules => { :default => true, :type => :boolean, :desc => "Boolean; whether to generate the default schedule resources. Setting this to false is useful for keeping external report processors clean of skipped schedule resources.", }, :noop => { :default => false, :type => :boolean, :desc => "Whether to apply catalogs in noop mode, which allows Puppet to partially simulate a normal run. This setting affects puppet agent and puppet apply. When running in noop mode, Puppet will check whether each resource is in sync, like it does when running normally. However, if a resource attribute is not in the desired state (as declared in the catalog), Puppet will take no action, and will instead report the changes it _would_ have made. These simulated changes will appear in the report sent to the puppet master, or be shown on the console if running puppet agent or puppet apply in the foreground. The simulated changes will not send refresh events to any subscribing or notified resources, although Puppet will log that a refresh event _would_ have been sent. **Important note:** [The `noop` metaparameter](http://docs.puppetlabs.com/references/latest/metaparameter.html#noop) allows you to apply individual resources in noop mode, and will override the global value of the `noop` setting. This means a resource with `noop => false` _will_ be changed if necessary, even when running puppet agent with `noop = true` or `--noop`. (Conversely, a resource with `noop => true` will only be simulated, even when noop mode is globally disabled.)", }, :runinterval => { :default => "30m", :type => :duration, :desc => "How often puppet agent applies the catalog. Note that a runinterval of 0 means \"run continuously\" rather than \"never run.\" If you want puppet agent to never run, you should start it with the `--no-client` option. #{AS_DURATION}", }, :ca_server => { :default => "$server", :desc => "The server to use for certificate authority requests. It's a separate server because it cannot and does not need to horizontally scale.", }, :ca_port => { :default => "$masterport", :desc => "The port to use for the certificate authority.", }, :preferred_serialization_format => { :default => "pson", :desc => "The preferred means of serializing ruby instances for passing over the wire. This won't guarantee that all instances will be serialized using this method, since not all classes can be guaranteed to support this format, but it will be used for all classes that support it.", }, :agent_catalog_run_lockfile => { :default => "$statedir/agent_catalog_run.lock", :type => :string, # (#2888) Ensure this file is not added to the settings catalog. :desc => "A lock file to indicate that a puppet agent catalog run is currently in progress. The file contains the pid of the process that holds the lock on the catalog run.", }, :agent_disabled_lockfile => { :default => "$statedir/agent_disabled.lock", :type => :file, :desc => "A lock file to indicate that puppet agent runs have been administratively disabled. File contains a JSON object with state information.", }, :usecacheonfailure => { :default => true, :type => :boolean, :desc => "Whether to use the cached configuration when the remote configuration will not compile. This option is useful for testing new configurations, where you want to fix the broken configuration rather than reverting to a known-good one.", }, :use_cached_catalog => { :default => false, :type => :boolean, :desc => "Whether to only use the cached catalog rather than compiling a new catalog on every run. Puppet can be run with this enabled by default and then selectively disabled when a recompile is desired.", }, :ignoremissingtypes => { :default => false, :type => :boolean, :desc => "Skip searching for classes and definitions that were missing during a prior compilation. The list of missing objects is maintained per-environment and persists until the environment is cleared or the master is restarted.", }, :ignorecache => { :default => false, :type => :boolean, :desc => "Ignore cache and always recompile the configuration. This is useful for testing new configurations, where the local cache may in fact be stale even if the timestamps are up to date - if the facts change or if the server changes.", }, :splaylimit => { :default => "$runinterval", :type => :duration, :desc => "The maximum time to delay before runs. Defaults to being the same as the run interval. #{AS_DURATION}", }, :splay => { :default => false, :type => :boolean, :desc => "Whether to sleep for a pseudo-random (but consistent) amount of time before a run.", }, :clientbucketdir => { :default => "$vardir/clientbucket", :type => :directory, :mode => "0750", :desc => "Where FileBucket files are stored locally." }, :configtimeout => { :default => "2m", :type => :duration, :desc => "How long the client should wait for the configuration to be retrieved before considering it a failure. This setting is deprecated and has been replaced by http_connect_timeout and http_read_timeout. #{AS_DURATION}", :deprecated => :completely, :hook => proc do |value| Puppet[:http_connect_timeout] = value Puppet[:http_read_timeout] = value end }, :report_server => { :default => "$server", :desc => "The server to send transaction reports to.", }, :report_port => { :default => "$masterport", :desc => "The port to communicate with the report_server.", }, :report => { :default => true, :type => :boolean, :desc => "Whether to send reports after every transaction.", }, :lastrunfile => { :default => "$statedir/last_run_summary.yaml", :type => :file, :mode => "0644", :desc => "Where puppet agent stores the last run report summary in yaml format." }, :lastrunreport => { :default => "$statedir/last_run_report.yaml", :type => :file, :mode => "0640", :desc => "Where puppet agent stores the last run report in yaml format." }, :graph => { :default => false, :type => :boolean, :desc => "Whether to create dot graph files for the different configuration graphs. These dot files can be interpreted by tools like OmniGraffle or dot (which is part of ImageMagick).", }, :graphdir => { :default => "$statedir/graphs", :type => :directory, :desc => "Where to store dot-outputted graphs.", }, :waitforcert => { :default => "2m", :type => :duration, :desc => "How frequently puppet agent should ask for a signed certificate. When starting for the first time, puppet agent will submit a certificate signing request (CSR) to the server named in the `ca_server` setting (usually the puppet master); this may be autosigned, or may need to be approved by a human, depending on the CA server's configuration. Puppet agent cannot apply configurations until its approved certificate is available. Since the certificate may or may not be available immediately, puppet agent will repeatedly try to fetch it at this interval. You can turn off waiting for certificates by specifying a time of 0, in which case puppet agent will exit if it cannot get a cert. #{AS_DURATION}", }, :ordering => { :type => :enum, :values => ["manifest", "title-hash", "random"], :default => "manifest", :desc => "How unrelated resources should be ordered when applying a catalog. Allowed values are `title-hash`, `manifest`, and `random`. This setting affects puppet agent and puppet apply, but not puppet master. * `manifest` (the default) will use the order in which the resources were declared in their manifest files. * `title-hash` (the default in 3.x) will order resources randomly, but will use the same order across runs and across nodes. It is only of value if you're migrating from 3.x and have errors running with `manifest`. * `random` will order resources randomly and change their order with each run. This can work like a fuzzer for shaking out undeclared dependencies. Regardless of this setting's value, Puppet will always obey explicit dependencies set with the before/require/notify/subscribe metaparameters and the `->`/`~>` chaining arrows; this setting only affects the relative ordering of _unrelated_ resources." } ) define_settings(:inspect, :archive_files => { :type => :boolean, :default => false, :desc => "During an inspect run, whether to archive files whose contents are audited to a file bucket.", }, :archive_file_server => { :default => "$server", :desc => "During an inspect run, the file bucket server to archive files to if archive_files is set.", } ) # Plugin information. define_settings( :main, :plugindest => { :type => :directory, :default => "$libdir", :desc => "Where Puppet should store plugins that it pulls down from the central server.", }, :pluginsource => { :default => "puppet:///plugins", :desc => "From where to retrieve plugins. The standard Puppet `file` type is used for retrieval, so anything that is a valid file source can be used here.", }, :pluginfactdest => { :type => :directory, :default => "$vardir/facts.d", :desc => "Where Puppet should store external facts that are being handled by pluginsync", }, :pluginfactsource => { :default => "puppet:///pluginfacts", :desc => "Where to retrieve external facts for pluginsync", }, :pluginsync => { :default => true, :type => :boolean, :desc => "Whether plugins should be synced with the central server.", }, :pluginsignore => { :default => ".svn CVS .git", :desc => "What files to ignore when pulling down plugins.", } ) # Central fact information. define_settings( :main, :factpath => { :type => :path, :default => "$vardir/lib/facter#{File::PATH_SEPARATOR}$vardir/facts", :desc => "Where Puppet should look for facts. Multiple directories should be separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.)", :call_hook => :on_initialize_and_write, # Call our hook with the default value, so we always get the value added to facter. :hook => proc do |value| paths = value.split(File::PATH_SEPARATOR) Facter.search(*paths) end } ) define_settings( :transaction, :tags => { :default => "", :desc => "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. Values must be comma-separated.", }, :evaltrace => { :default => false, :type => :boolean, :desc => "Whether each resource should log when it is being evaluated. This allows you to interactively see exactly what is being done.", }, :summarize => { :default => false, :type => :boolean, :desc => "Whether to print a transaction summary.", } ) define_settings( :main, :external_nodes => { :default => "none", :desc => "An external command that can produce node information. The command's output must be a YAML dump of a hash, and that hash must have a `classes` key and/or a `parameters` key, where `classes` is an array or hash and `parameters` is a hash. For unknown nodes, the command should exit with a non-zero exit code. This command makes it straightforward to store your node mapping information in other data sources like databases.", } ) define_settings( :ldap, :ldapssl => { :default => false, :type => :boolean, :desc => "Whether SSL should be used when searching for nodes. Defaults to false because SSL usually requires certificates to be set up on the client side.", }, :ldaptls => { :default => false, :type => :boolean, :desc => "Whether TLS should be used when searching for nodes. Defaults to false because TLS usually requires certificates to be set up on the client side.", }, :ldapserver => { :default => "ldap", :desc => "The LDAP server. Only used if `node_terminus` is set to `ldap`.", }, :ldapport => { :default => 389, :desc => "The LDAP port. Only used if `node_terminus` is set to `ldap`.", }, :ldapstring => { :default => "(&(objectclass=puppetClient)(cn=%s))", :desc => "The search string used to find an LDAP node.", }, :ldapclassattrs => { :default => "puppetclass", :desc => "The LDAP attributes to use to define Puppet classes. Values should be comma-separated.", }, :ldapstackedattrs => { :default => "puppetvar", :desc => "The LDAP attributes that should be stacked to arrays by adding the values in all hierarchy elements of the tree. Values should be comma-separated.", }, :ldapattrs => { :default => "all", :desc => "The LDAP attributes to include when querying LDAP for nodes. All returned attributes are set as variables in the top-level scope. Multiple values should be comma-separated. The value 'all' returns all attributes.", }, :ldapparentattr => { :default => "parentnode", :desc => "The attribute to use to define the parent node.", }, :ldapuser => { :default => "", :desc => "The user to use to connect to LDAP. Must be specified as a full DN.", }, :ldappassword => { :default => "", :desc => "The password to use to connect to LDAP.", }, :ldapbase => { :default => "", :desc => "The search base for LDAP searches. It's impossible to provide a meaningful default here, although the LDAP libraries might have one already set. Generally, it should be the 'ou=Hosts' branch under your main directory.", } ) define_settings(:master, :storeconfigs => { :default => false, :type => :boolean, :desc => "Whether to store each client's configuration, including catalogs, facts, and related data. This also enables the import and export of resources in the Puppet language - a mechanism for exchange resources between nodes. By default this uses the 'puppetdb' backend. You can adjust the backend using the storeconfigs_backend setting.", # Call our hook with the default value, so we always get the libdir set. :call_hook => :on_initialize_and_write, :hook => proc do |value| require 'puppet/node' require 'puppet/node/facts' if value Puppet::Resource::Catalog.indirection.cache_class = :store_configs Puppet.settings.override_default(:catalog_cache_terminus, :store_configs) Puppet::Node::Facts.indirection.cache_class = :store_configs Puppet::Resource.indirection.terminus_class = :store_configs end end }, :storeconfigs_backend => { :type => :terminus, :default => "puppetdb", :desc => "Configure the backend terminus used for StoreConfigs. By default, this uses the PuppetDB store, which must be installed and configured before turning on StoreConfigs." } ) define_settings(:parser, - :templatedir => { - :default => "$vardir/templates", - :type => :directory, - :desc => "Where Puppet looks for template files. Can be a list of colon-separated - directories. - - This setting is deprecated. Please put your templates in modules instead.", - :deprecated => :completely, - }, - :allow_variables_with_dashes => { :default => false, :desc => <<-'EOT' Permit hyphens (`-`) in variable names and issue deprecation warnings about them. This setting **should always be `false`;** setting it to `true` will cause subtle and wide-ranging bugs. It will be removed in a future version. Hyphenated variables caused major problems in the language, but were allowed between Puppet 2.7.3 and 2.7.14. If you used them during this window, we apologize for the inconvenience --- you can temporarily set this to `true` in order to upgrade, and can rename your variables at your leisure. Please revert it to `false` after you have renamed all affected variables. EOT }, :max_errors => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation errors in case multiple errors have been detected. A value of 0 is the same as a value of 1; a minimum of one error is always raised. The count is per manifest. EOT }, :max_warnings => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation warnings in case multiple warnings have been detected. A value of 0 blocks logging of warnings. The count is per manifest. EOT }, :max_deprecations => { :default => 10, :desc => <<-'EOT' Sets the max number of logged/displayed parser validation deprecation warnings in case multiple deprecation warnings have been detected. A value of 0 blocks the logging of deprecation warnings. The count is per manifest. EOT }, :strict_variables => { :default => false, :type => :boolean, :desc => <<-'EOT' Makes the parser raise errors when referencing unknown variables. (This does not affect referencing variables that are explicitly set to undef). EOT } ) define_settings(:puppetdoc, :document_all => { :default => false, :type => :boolean, :desc => "Whether to document all resources when using `puppet doc` to generate manifest documentation.", } ) end diff --git a/lib/puppet/environments.rb b/lib/puppet/environments.rb index 22017325c..8c804a092 100644 --- a/lib/puppet/environments.rb +++ b/lib/puppet/environments.rb @@ -1,445 +1,389 @@ # @api private module Puppet::Environments class EnvironmentNotFound < Puppet::Error def initialize(environment_name, original = nil) environmentpath = Puppet[:environmentpath] super("Could not find a directory environment named '#{environment_name}' anywhere in the path: #{environmentpath}. Does the directory exist?", original) end end # @api private module EnvironmentCreator # Create an anonymous environment. # # @param module_path [String] A list of module directories separated by the # PATH_SEPARATOR # @param manifest [String] The path to the manifest # @return A new environment with the `name` `:anonymous` # # @api private def for(module_path, manifest) Puppet::Node::Environment.create(:anonymous, module_path.split(File::PATH_SEPARATOR), manifest) end end # Provide any common methods that loaders should have. It requires that any # classes that include this module implement get # @api private module EnvironmentLoader # @!macro loader_get_or_fail def get!(name) environment = get(name) if environment environment else raise EnvironmentNotFound, name end end end # @!macro [new] loader_search_paths # A list of indicators of where the loader is getting its environments from. # @return [Array] The URIs of the load locations # # @!macro [new] loader_list # @return [Array] All of the environments known # to the loader # # @!macro [new] loader_get # Find a named environment # # @param name [String,Symbol] The name of environment to find # @return [Puppet::Node::Environment, nil] the requested environment or nil # if it wasn't found # # @!macro [new] loader_get_conf # Attempt to obtain the initial configuration for the environment. Not all # loaders can provide this. # # @param name [String,Symbol] The name of the environment whose configuration # we are looking up # @return [Puppet::Setting::EnvironmentConf, nil] the configuration for the # requested environment, or nil if not found or no configuration is available # # @!macro [new] loader_get_or_fail # Find a named environment or raise # Puppet::Environments::EnvironmentNotFound when the named environment is # does not exist. # # @param name [String,Symbol] The name of environment to find # @return [Puppet::Node::Environment] the requested environment # A source of pre-defined environments. # # @api private class Static include EnvironmentCreator include EnvironmentLoader def initialize(*environments) @environments = environments end # @!macro loader_search_paths def search_paths ["data:text/plain,internal"] end # @!macro loader_list def list @environments end # @!macro loader_get def get(name) @environments.find do |env| env.name == name.intern end end # Returns a basic environment configuration object tied to the environment's # implementation values. Will not interpolate. # # @!macro loader_get_conf def get_conf(name) env = get(name) if env Puppet::Settings::EnvironmentConf.static_for(env) else nil end end end # A source of unlisted pre-defined environments. # # Used only for internal bootstrapping environments which are not relevant # to an end user (such as the fall back 'configured' environment). # # @api private class StaticPrivate < Static # Unlisted # # @!macro loader_list def list [] end end - # Old-style environments that come either from explicit stanzas in - # puppet.conf or from dynamic environments created from use of `$environment` - # in puppet.conf. - # - # @example Explicit Stanza - # [environment_name] - # modulepath=/var/my_env/modules - # - # @example Dynamic Environments - # [master] - # modulepath=/var/$environment/modules - # - # @api private - class Legacy - include EnvironmentCreator - - # @!macro loader_search_paths - def search_paths - ["file://#{Puppet[:config]}"] - end - - # @note The list of environments for the Legacy environments is always - # empty. - # - # @!macro loader_list - def list - [] - end - - # @note Because the Legacy system cannot list out all of its environments, - # get is able to return environments that are not returned by a call to - # {#list}. - # - # @!macro loader_get - def get(name) - Puppet::Node::Environment.new(name) - end - - # @note Because the Legacy system cannot list out all of its environments, - # this method will never fail and is only calling get directly. - # - # @!macro loader_get_or_fail - def get!(name) - get(name) - end - - # @note we could return something here, but since legacy environments - # are deprecated, there is no point. - # - # @!macro loader_get_conf - def get_conf(name) - nil - end - end - # Reads environments from a directory on disk. Each environment is # represented as a sub-directory. The environment's manifest setting is the # `manifest` directory of the environment directory. The environment's # modulepath setting is the global modulepath (from the `[master]` section # for the master) prepended with the `modules` directory of the environment # directory. # # @api private class Directories include EnvironmentLoader def initialize(environment_dir, global_module_path) @environment_dir = environment_dir @global_module_path = global_module_path end # Generate an array of directory loaders from a path string. # @param path [String] path to environment directories # @param global_module_path [Array] the global modulepath setting # @return [Array] An array # of configured directory loaders. def self.from_path(path, global_module_path) environments = path.split(File::PATH_SEPARATOR) environments.map do |dir| Puppet::Environments::Directories.new(dir, global_module_path) end end # @!macro loader_search_paths def search_paths ["file://#{@environment_dir}"] end # @!macro loader_list def list valid_directories.collect do |envdir| name = Puppet::FileSystem.basename_string(envdir).intern setting_values = Puppet.settings.values(name, Puppet.settings.preferred_run_mode) env = Puppet::Node::Environment.create( name, Puppet::Node::Environment.split_path(setting_values.interpolate(:modulepath)), setting_values.interpolate(:manifest), setting_values.interpolate(:config_version) ) - env.watching = false env end end # @!macro loader_get def get(name) list.find { |env| env.name == name.intern } end # @!macro loader_get_conf def get_conf(name) valid_directories.each do |envdir| envname = Puppet::FileSystem.basename_string(envdir) if envname == name.to_s return Puppet::Settings::EnvironmentConf.load_from(envdir, @global_module_path) end end nil end private def valid_directories if Puppet::FileSystem.directory?(@environment_dir) Puppet::FileSystem.children(@environment_dir).select do |child| name = Puppet::FileSystem.basename_string(child) Puppet::FileSystem.directory?(child) && Puppet::Node::Environment.valid_name?(name) end else [] end end end # Combine together multiple loaders to act as one. # @api private class Combined include EnvironmentLoader def initialize(*loaders) @loaders = loaders end # @!macro loader_search_paths def search_paths @loaders.collect(&:search_paths).flatten end # @!macro loader_list def list @loaders.collect(&:list).flatten end # @!macro loader_get def get(name) @loaders.each do |loader| if env = loader.get(name) return env end end nil end # @!macro loader_get_conf def get_conf(name) @loaders.each do |loader| if conf = loader.get_conf(name) return conf end end nil end end class Cached include EnvironmentLoader class DefaultCacheExpirationService def created(env) end def expired?(env_name) false end def evicted(env_name) end end def self.cache_expiration_service=(service) @cache_expiration_service = service end def self.cache_expiration_service @cache_expiration_service || DefaultCacheExpirationService.new end def initialize(loader) @loader = loader @cache = {} @cache_expiration_service = Puppet::Environments::Cached.cache_expiration_service end # @!macro loader_list def list @loader.list end # @!macro loader_search_paths def search_paths @loader.search_paths end # @!macro loader_get def get(name) evict_if_expired(name) if result = @cache[name] return result.value elsif (result = @loader.get(name)) @cache[name] = entry(result) result end end # Clears the cache of the environment with the given name. # (The intention is that this could be used from a MANUAL cache eviction command (TBD) def clear(name) @cache.delete(name) end # Clears all cached environments. # (The intention is that this could be used from a MANUAL cache eviction command (TBD) def clear_all() @cache = {} end # This implementation evicts the cache, and always gets the current # configuration of the environment # # TODO: While this is wasteful since it # needs to go on a search for the conf, it is too disruptive to optimize # this. # # @!macro loader_get_conf def get_conf(name) evict_if_expired(name) @loader.get_conf(name) end # Creates a suitable cache entry given the time to live for one environment # def entry(env) @cache_expiration_service.created(env) ttl = (conf = get_conf(env.name)) ? conf.environment_timeout : Puppet.settings.value(:environment_timeout) Puppet.debug("Caching environment '#{env.name}' (cache ttl: #{ttl})") case ttl when 0 NotCachedEntry.new(env) # Entry that is always expired (avoids syscall to get time) when Float::INFINITY Entry.new(env) # Entry that never expires (avoids syscall to get time) else TTLEntry.new(env, ttl) end end # Evicts the entry if it has expired # Also clears caches in Settings that may prevent the entry from being updated def evict_if_expired(name) if (result = @cache[name]) && (result.expired? || @cache_expiration_service.expired?(name)) Puppet.debug("Evicting cache entry for environment '#{name}'") @cache.delete(name) @cache_expiration_service.evicted(name) Puppet.settings.clear_environment_settings(name) end end # Never evicting entry class Entry attr_reader :value def initialize(value) @value = value end def expired? false end end # Always evicting entry class NotCachedEntry < Entry def expired? true end end # Time to Live eviction policy entry class TTLEntry < Entry def initialize(value, ttl_seconds) super value @ttl = Time.now + ttl_seconds end def expired? Time.now > @ttl end end end end diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index deecfd45b..a2a249d1c 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -1,339 +1,342 @@ require 'puppet/util/logging' require 'semver' require 'json' # Support for modules class Puppet::Module class Error < Puppet::Error; end class MissingModule < Error; end class IncompatibleModule < Error; end class UnsupportedPlatform < Error; end class IncompatiblePlatform < Error; end class MissingMetadata < Error; end class InvalidName < Error; end class InvalidFilePattern < Error; end include Puppet::Util::Logging FILETYPES = { "manifests" => "manifests", "files" => "files", "templates" => "templates", "plugins" => "lib", "pluginfacts" => "facts.d", } # Find and return the +module+ that +path+ belongs to. If +path+ is # absolute, or if there is no module whose name is the first component # of +path+, return +nil+ def self.find(modname, environment = nil) return nil unless modname # Unless a specific environment is given, use the current environment env = environment ? Puppet.lookup(:environments).get!(environment) : Puppet.lookup(:current_environment) env.module(modname) end attr_reader :name, :environment, :path, :metadata attr_writer :environment attr_accessor :dependencies, :forge_name attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page def initialize(name, path, environment) @name = name @path = path @environment = environment assert_validity load_metadata if has_metadata? validate_puppet_version @absolute_path_to_manifests = Puppet::FileSystem::PathPattern.absolute(manifests) end def has_metadata? return false unless metadata_file return false unless Puppet::FileSystem.exist?(metadata_file) begin metadata = JSON.parse(File.read(metadata_file)) rescue JSON::JSONError => e Puppet.debug("#{name} has an invalid and unparsable metadata.json file. The parse error: #{e.message}") return false end return metadata.is_a?(Hash) && !metadata.keys.empty? end FILETYPES.each do |type, location| # A boolean method to let external callers determine if # we have files of a given type. define_method(type +'?') do type_subpath = subpath(location) unless Puppet::FileSystem.exist?(type_subpath) Puppet.debug("No #{type} found in subpath '#{type_subpath}' " + "(file / directory does not exist)") return false end return true end # A method for returning a given file of a given type. # e.g., file = mod.manifest("my/manifest.pp") # # If the file name is nil, then the base directory for the # file type is passed; this is used for fileserving. define_method(type.sub(/s$/, '')) do |file| # If 'file' is nil then they're asking for the base path. # This is used for things like fileserving. if file full_path = File.join(subpath(location), file) else full_path = subpath(location) end return nil unless Puppet::FileSystem.exist?(full_path) return full_path end # Return the base directory for the given type define_method(type) do subpath(location) end end def license_file return @license_file if defined?(@license_file) return @license_file = nil unless path @license_file = File.join(path, "License") end def load_metadata @metadata = data = JSON.parse(File.read(metadata_file)) @forge_name = data['name'].gsub('-', '/') if data['name'] [:source, :author, :version, :license, :puppetversion, :dependencies].each do |attr| unless value = data[attr.to_s] unless attr == :puppetversion raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}" end end # NOTICE: The fallback to `versionRequirement` is something we'd like to # not have to support, but we have a reasonable number of releases that # don't use `version_requirement`. When we can deprecate this, we should. if attr == :dependencies value.tap do |dependencies| dependencies.each do |dep| dep['version_requirement'] ||= dep['versionRequirement'] || '>= 0.0.0' end end end send(attr.to_s + "=", value) end end # Return the list of manifests matching the given glob pattern, - # defaulting to 'init.{pp,rb}' for empty modules. + # defaulting to 'init.pp' for empty modules. def match_manifests(rest) if rest wanted_manifests = wanted_manifests_from(rest) searched_manifests = wanted_manifests.glob.reject { |f| FileTest.directory?(f) } else searched_manifests = [] end # (#4220) Always ensure init.pp in case class is defined there. - init_manifests = [manifest("init.pp"), manifest("init.rb")].compact - init_manifests + searched_manifests + init_manifest = manifest("init.pp") + if !init_manifest.nil? && !searched_manifests.include?(init_manifest) + searched_manifests.unshift(init_manifest) + end + searched_manifests end def all_manifests return [] unless Puppet::FileSystem.exist?(manifests) - Dir.glob(File.join(manifests, '**', '*.{rb,pp}')) + Dir.glob(File.join(manifests, '**', '*.pp')) end def metadata_file return @metadata_file if defined?(@metadata_file) return @metadata_file = nil unless path @metadata_file = File.join(path, "metadata.json") end def modulepath File.dirname(path) if path end # Find all plugin directories. This is used by the Plugins fileserving mount. def plugin_directory subpath("lib") end def plugin_fact_directory subpath("facts.d") end def has_external_facts? File.directory?(plugin_fact_directory) end def supports(name, version = nil) @supports ||= [] @supports << [name, version] end def to_s result = "Module #{name}" result += "(#{path})" if path result end def dependencies_as_modules dependent_modules = [] dependencies and dependencies.each do |dep| author, dep_name = dep["name"].split('/') found_module = environment.module(dep_name) dependent_modules << found_module if found_module end dependent_modules end def required_by environment.module_requirements[self.forge_name] || {} end def has_local_changes? Puppet.deprecation_warning("This method is being removed.") require 'puppet/module_tool/applications' changes = Puppet::ModuleTool::Applications::Checksummer.run(path) !changes.empty? end def local_changes Puppet.deprecation_warning("This method is being removed.") require 'puppet/module_tool/applications' Puppet::ModuleTool::Applications::Checksummer.run(path) end # Identify and mark unmet dependencies. A dependency will be marked unmet # for the following reasons: # # * not installed and is thus considered missing # * installed and does not meet the version requirements for this module # * installed and doesn't use semantic versioning # # Returns a list of hashes representing the details of an unmet dependency. # # Example: # # [ # { # :reason => :missing, # :name => 'puppetlabs-mysql', # :version_constraint => 'v0.0.1', # :mod_details => { # :installed_version => '0.0.1' # } # :parent => { # :name => 'puppetlabs-bacula', # :version => 'v1.0.0' # } # } # ] # def unmet_dependencies unmet_dependencies = [] return unmet_dependencies unless dependencies dependencies.each do |dependency| forge_name = dependency['name'] version_string = dependency['version_requirement'] || '>= 0.0.0' dep_mod = begin environment.module_by_forge_name(forge_name) rescue nil end error_details = { :name => forge_name, :version_constraint => version_string.gsub(/^(?=\d)/, "v"), :parent => { :name => self.forge_name, :version => self.version.gsub(/^(?=\d)/, "v") }, :mod_details => { :installed_version => dep_mod.nil? ? nil : dep_mod.version } } unless dep_mod error_details[:reason] = :missing unmet_dependencies << error_details next end if version_string begin required_version_semver_range = SemVer[version_string] actual_version_semver = SemVer.new(dep_mod.version) rescue ArgumentError error_details[:reason] = :non_semantic_version unmet_dependencies << error_details next end unless required_version_semver_range.include? actual_version_semver error_details[:reason] = :version_mismatch unmet_dependencies << error_details next end end end unmet_dependencies end def validate_puppet_version return unless puppetversion and puppetversion != Puppet.version raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}" end + def ==(other) + self.name == other.name && + self.version == other.version && + self.path == other.path && + self.environment == other.environment + end + private def wanted_manifests_from(pattern) begin - extended = File.extname(pattern).empty? ? "#{pattern}.{pp,rb}" : pattern + extended = File.extname(pattern).empty? ? "#{pattern}.pp" : pattern relative_pattern = Puppet::FileSystem::PathPattern.relative(extended) rescue Puppet::FileSystem::PathPattern::InvalidPattern => error raise Puppet::Module::InvalidFilePattern.new( "The pattern \"#{pattern}\" to find manifests in the module \"#{name}\" " + "is invalid and potentially unsafe.", error) end relative_pattern.prefix_with(@absolute_path_to_manifests) end def subpath(type) File.join(path, type) end def assert_validity raise InvalidName, "Invalid module name #{name}; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/ end - - def ==(other) - self.name == other.name && - self.version == other.version && - self.path == other.path && - self.environment == other.environment - end end diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index 9bcc54000..8baaf6a90 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,595 +1,523 @@ require 'puppet/util' -require 'puppet/util/cacher' require 'monitor' require 'puppet/parser/parser_factory' # Just define it, so this class has fewer load dependencies. class Puppet::Node end # Puppet::Node::Environment acts as a container for all configuration # that is expected to vary between environments. # # ## The root environment # # In addition to normal environments that are defined by the user,there is a # special 'root' environment. It is defined as an instance variable on the # Puppet::Node::Environment metaclass. The environment name is `*root*` and can # be accessed by looking up the `:root_environment` using {Puppet.lookup}. # # The primary purpose of the root environment is to contain parser functions # that are not bound to a specific environment. The main case for this is for # logging functions. Logging functions are attached to the 'root' environment # when {Puppet::Parser::Functions.reset} is called. class Puppet::Node::Environment - include Puppet::Util::Cacher NO_MANIFEST = :no_manifest - # @api private - def self.seen - @seen ||= {} - end - - # Create a new environment with the given name, or return an existing one - # - # The environment class memoizes instances so that attempts to instantiate an - # environment with the same name with an existing environment will return the - # existing environment. - # - # @overload self.new(environment) - # @param environment [Puppet::Node::Environment] - # @return [Puppet::Node::Environment] the environment passed as the param, - # this is implemented so that a calling class can use strings or - # environments interchangeably. - # - # @overload self.new(string) - # @param string [String, Symbol] - # @return [Puppet::Node::Environment] An existing environment if it exists, - # else a new environment with that name + # The create() factory method should be used instead. # - # @overload self.new() - # @return [Puppet::Node::Environment] The environment as set by - # Puppet.settings[:environment] - # - # @api public - def self.new(name = nil) - return name if name.is_a?(self) - name ||= Puppet.settings.value(:environment) - - raise ArgumentError, "Environment name must be specified" unless name - - symbol = name.to_sym - - return seen[symbol] if seen[symbol] - - obj = self.create(symbol, - split_path(Puppet.settings.value(:modulepath, symbol)), - Puppet.settings.value(:manifest, symbol), - Puppet.settings.value(:config_version, symbol)) - seen[symbol] = obj + # @api private + def self.new(*args) + create(*args) end + private_class_method :new # Create a new environment with the given name # - # @param name [Symbol] the name of the + # @param name [Symbol] the name of the environment # @param modulepath [Array] the list of paths from which to load modules # @param manifest [String] the path to the manifest for the environment or - # the constant Puppet::Node::Environment::NO_MANIFEST if there is none. + # the constant Puppet::Node::Environment::NO_MANIFEST if there is none. # @param config_version [String] path to a script whose output will be added # to report logs (optional) # @return [Puppet::Node::Environment] # # @api public def self.create(name, modulepath, manifest = NO_MANIFEST, config_version = nil) obj = self.allocate obj.send(:initialize, - name, + name.intern, expand_dirs(extralibs() + modulepath), manifest == NO_MANIFEST ? manifest : File.expand_path(manifest), config_version) obj end + # A remote subclass to make it easier to trace instances when debugging. + # @api private + class Remote < Puppet::Node::Environment; end + # A "reference" to a remote environment. The created environment instance # isn't expected to exist on the local system, but is instead a reference to # environment information on a remote system. For instance when a catalog is # being applied, this will be used on the agent. # # @note This does not provide access to the information of the remote # environment's modules, manifest, or anything else. It is simply a value # object to pass around and use as an environment. # # @param name [Symbol] The name of the remote environment # def self.remote(name) - create(name, [], NO_MANIFEST) + Remote.create(name, [], NO_MANIFEST) end # Instantiate a new environment # - # @note {Puppet::Node::Environment.new} is overridden to return memoized - # objects, so this will not be invoked with the normal Ruby initialization - # semantics. + # @note {Puppet::Node::Environment.new} is private for historical reasons, as + # previously it had been overridden to return memoized objects and was + # replaced with {Puppet::Node::Environment.create}, so this will not be + # invoked with the normal Ruby initialization semantics. # # @param name [Symbol] The environment name def initialize(name, modulepath, manifest, config_version) @name = name @modulepath = modulepath @manifest = manifest @config_version = config_version - # set watching to true for legacy environments - the directory based environment loaders will set this to - # false for directory based environments after the environment has been created. - @watching = true - end - - # Returns if files are being watched or not. - # @api private - # - def watching? - @watching - end - - # Turns watching of files on or off - # @param flag [TrueClass, FalseClass] if files should be watched or not - # @ api private - def watching=(flag) - @watching = flag end # Creates a new Puppet::Node::Environment instance, overriding any of the passed # parameters. # # @param env_params [Hash<{Symbol => String,Array}>] new environment # parameters (:modulepath, :manifest, :config_version) # @return [Puppet::Node::Environment] def override_with(env_params) return self.class.create(name, env_params[:modulepath] || modulepath, env_params[:manifest] || manifest, env_params[:config_version] || config_version) end - # Creates a new Puppet::Node::Environment instance, overriding manfiest - # modulepath, or :config_version from the passed settings if they were + # Creates a new Puppet::Node::Environment instance, overriding :manifest, + # :modulepath, or :config_version from the passed settings if they were # originally set from the commandline, or returns self if there is nothing to # override. # # @param settings [Puppet::Settings] an initialized puppet settings instance # @return [Puppet::Node::Environment] new overridden environment or self if # there are no commandline changes from settings. def override_from_commandline(settings) overrides = {} if settings.set_by_cli?(:modulepath) overrides[:modulepath] = self.class.split_path(settings.value(:modulepath)) end if settings.set_by_cli?(:config_version) overrides[:config_version] = settings.value(:config_version) end - if settings.set_by_cli?(:manifest) || - (settings.set_by_cli?(:manifestdir) && settings.value(:manifest).start_with?(settings.value(:manifestdir))) + if settings.set_by_cli?(:manifest) overrides[:manifest] = settings.value(:manifest) end overrides.empty? ? self : self.override_with(overrides) end - # Retrieve the environment for the current process. - # - # @note This should only used when a catalog is being compiled. - # - # @api private - # - # @return [Puppet::Node::Environment] the currently set environment if one - # has been explicitly set, else it will return the '*root*' environment - def self.current - Puppet.deprecation_warning("Puppet::Node::Environment.current has been replaced by Puppet.lookup(:current_environment), see http://links.puppetlabs.com/current-env-deprecation") - Puppet.lookup(:current_environment) - end - # @param [String] name Environment name to check for valid syntax. # @return [Boolean] true if name is valid # @api public def self.valid_name?(name) !!name.match(/\A\w+\Z/) end - # Clear all memoized environments and the 'current' environment - # - # @api private - def self.clear - seen.clear - end - # @!attribute [r] name # @api public # @return [Symbol] the human readable environment name that serves as the # environment identifier attr_reader :name # @api public # @return [Array] All directories present on disk in the modulepath def modulepath @modulepath.find_all do |p| Puppet::FileSystem.directory?(p) end end # @api public # @return [Array] All directories in the modulepath (even if they are not present on disk) def full_modulepath @modulepath end # @!attribute [r] manifest # @api public # @return [String] path to the manifest file or directory. attr_reader :manifest # @!attribute [r] config_version # @api public # @return [String] path to a script whose output will be added to report logs # (optional) attr_reader :config_version # Checks to make sure that this environment did not have a manifest set in # its original environment.conf if Puppet is configured with # +disable_per_environment_manifest+ set true. If it did, the environment's # modules may not function as intended by the original authors, and we may # seek to halt a puppet compilation for a node in this environment. # # The only exception to this would be if the environment.conf manifest is an exact, # uninterpolated match for the current +default_manifest+ setting. # # @return [Boolean] true if using directory environments, and # Puppet[:disable_per_environment_manifest] is true, and this environment's # original environment.conf had a manifest setting that is not the # Puppet[:default_manifest]. # @api private def conflicting_manifest_settings? - return false if Puppet[:environmentpath].empty? || !Puppet[:disable_per_environment_manifest] + return false if !Puppet[:disable_per_environment_manifest] environment_conf = Puppet.lookup(:environments).get_conf(name) original_manifest = environment_conf.raw_setting(:manifest) !original_manifest.nil? && !original_manifest.empty? && original_manifest != Puppet[:default_manifest] end # Checks the environment and settings for any conflicts # @return [Array] an array of validation errors # @api public def validation_errors errors = [] if conflicting_manifest_settings? errors << "The 'disable_per_environment_manifest' setting is true, and the '#{name}' environment has an environment.conf manifest that conflicts with the 'default_manifest' setting." end errors end # Return an environment-specific Puppet setting. # # @api public # # @param param [String, Symbol] The environment setting to look up # @return [Object] The resolved setting value def [](param) Puppet.settings.value(param, self.name) end # @api public # @return [Puppet::Resource::TypeCollection] The current global TypeCollection def known_resource_types if @known_resource_types.nil? @known_resource_types = Puppet::Resource::TypeCollection.new(self) @known_resource_types.import_ast(perform_initial_import(), '') end @known_resource_types end # Yields each modules' plugin directory if the plugin directory (modulename/lib) # is present on the filesystem. # # @yield [String] Yields the plugin directory from each module to the block. # @api public def each_plugin_directory(&block) modules.map(&:plugin_directory).each do |lib| lib = Puppet::Util::Autoload.cleanpath(lib) yield lib if File.directory?(lib) end end # Locate a module instance by the module name alone. # # @api public # # @param name [String] The module name # @return [Puppet::Module, nil] The module if found, else nil def module(name) modules.find {|mod| mod.name == name} end # Locate a module instance by the full forge name (EG authorname/module) # # @api public # # @param forge_name [String] The module name # @return [Puppet::Module, nil] The module if found, else nil def module_by_forge_name(forge_name) author, modname = forge_name.split('/') found_mod = self.module(modname) found_mod and found_mod.forge_name == forge_name ? found_mod : nil end - # @!attribute [r] modules - # Return all modules for this environment in the order they appear in the - # modulepath. - # @note If multiple modules with the same name are present they will - # both be added, but methods like {#module} and {#module_by_forge_name} - # will return the first matching entry in this list. - # @note This value is cached so that the filesystem doesn't have to be - # re-enumerated every time this method is invoked, since that - # enumeration could be a costly operation and this method is called - # frequently. The cache expiry is determined by `Puppet[:filetimeout]`. - # @see Puppet::Util::Cacher.cached_attr - # @api public - # @return [Array] All modules for this environment - cached_attr(:modules, Puppet[:filetimeout]) do - module_references = [] - seen_modules = {} - modulepath.each do |path| - Dir.entries(path).each do |name| - next if name == "." || name == ".." - warn_about_mistaken_path(path, name) - if not seen_modules[name] - module_references << {:name => name, :path => File.join(path, name)} - seen_modules[name] = true + # Return all modules for this environment in the order they appear in the + # modulepath. + # @note If multiple modules with the same name are present they will + # both be added, but methods like {#module} and {#module_by_forge_name} + # will return the first matching entry in this list. + # @note This value is cached so that the filesystem doesn't have to be + # re-enumerated every time this method is invoked, since that + # enumeration could be a costly operation and this method is called + # frequently. The cache expiry is determined by `Puppet[:filetimeout]`. + # @api public + # @return [Array] All modules for this environment + def modules + if @modules.nil? + module_references = [] + seen_modules = {} + modulepath.each do |path| + Dir.entries(path).each do |name| + next if name == "." || name == ".." + warn_about_mistaken_path(path, name) + if not seen_modules[name] + module_references << {:name => name, :path => File.join(path, name)} + seen_modules[name] = true + end end end - end - module_references.collect do |reference| - begin - Puppet::Module.new(reference[:name], reference[:path], self) - rescue Puppet::Module::Error => e - Puppet.log_exception(e) - nil - end - end.compact + @modules = module_references.collect do |reference| + begin + Puppet::Module.new(reference[:name], reference[:path], self) + rescue Puppet::Module::Error => e + Puppet.log_exception(e) + nil + end + end.compact + end + @modules end # Generate a warning if the given directory in a module path entry is named `lib`. # # @api private # # @param path [String] The module directory containing the given directory # @param name [String] The directory name def warn_about_mistaken_path(path, name) if name == "lib" Puppet.debug("Warning: Found directory named 'lib' in module path ('#{path}/lib'); unless " + "you are expecting to load a module named 'lib', your module path may be set " + "incorrectly.") end end # Modules broken out by directory in the modulepath # # @note This method _changes_ the current working directory while enumerating # the modules. This seems rather dangerous. # # @api public # # @return [Hash>] A hash whose keys are file # paths, and whose values is an array of Puppet Modules for that path def modules_by_path modules_by_path = {} modulepath.each do |path| Dir.chdir(path) do module_names = Dir.glob('*').select do |d| FileTest.directory?(d) && (File.basename(d) =~ /\A\w+(-\w+)*\Z/) end modules_by_path[path] = module_names.sort.map do |name| Puppet::Module.new(name, File.join(path, name), self) end end end modules_by_path end # All module requirements for all modules in the environment modulepath # # @api public # # @comment This has nothing to do with an environment. It seems like it was # stuffed into the first convenient class that vaguely involved modules. # # @example # environment.module_requirements # # => { # # 'username/amodule' => [ # # { # # 'name' => 'username/moduledep', # # 'version' => '1.2.3', # # 'version_requirement' => '>= 1.0.0', # # }, # # { # # 'name' => 'username/anotherdep', # # 'version' => '4.5.6', # # 'version_requirement' => '>= 3.0.0', # # } # # ] # # } # # # # @return [Hash>>] See the method example # for an explanation of the return value. def module_requirements deps = {} modules.each do |mod| next unless mod.forge_name deps[mod.forge_name] ||= [] mod.dependencies and mod.dependencies.each do |mod_dep| dep_name = mod_dep['name'].tr('-', '/') (deps[dep_name] ||= []) << { 'name' => mod.forge_name, 'version' => mod.version, 'version_requirement' => mod_dep['version_requirement'] } end end deps.each do |mod, mod_deps| deps[mod] = mod_deps.sort_by { |d| d['name'] } end deps end - # Set a periodic watcher on the file, so we can tell if it has changed. - # If watching has been turned off, this call has no effect. - # @param file[File,String] File instance or filename - # @api private - def watch_file(file) - if watching? - known_resource_types.watch_file(file.to_s) - end - end - # Checks if a reparse is required (cache of files is stale). - # This call does nothing unless files are being watched. # def check_for_reparse - if (Puppet[:code] != @parsed_code) || (watching? && @known_resource_types && @known_resource_types.require_reparse?) + if (Puppet[:code] != @parsed_code || @known_resource_types.parse_failed?) @parsed_code = nil @known_resource_types = nil end end # @return [String] The YAML interpretation of the object # Return the name of the environment as a string interpretation of the object def to_yaml to_s.to_yaml end # @return [String] The stringified value of the `name` instance variable # @api public def to_s name.to_s end + # @api public + def inspect + %Q{<#{self.class}:#{self.object_id} @name="#{name}" @manifest="#{manifest}" @modulepath="#{full_modulepath.join(":")}" >} + end + # @return [Symbol] The `name` value, cast to a string, then cast to a symbol. # # @api public # # @note the `name` instance variable is a Symbol, but this casts the value # to a String and then converts it back into a Symbol which will needlessly # create an object that needs to be garbage collected def to_sym to_s.to_sym end def self.split_path(path_string) path_string.split(File::PATH_SEPARATOR) end def ==(other) return true if other.kind_of?(Puppet::Node::Environment) && self.name == other.name && self.full_modulepath == other.full_modulepath && self.manifest == other.manifest end alias eql? == def hash [self.class, name, full_modulepath, manifest].hash end private def self.extralibs() if ENV["PUPPETLIB"] split_path(ENV["PUPPETLIB"]) else [] end end def self.expand_dirs(dirs) dirs.collect do |dir| File.expand_path(dir) end end # Reparse the manifests for the given environment # # There are two sources that can be used for the initial parse: # - # 1. The value of `Puppet.settings[:code]`: Puppet can take a string from + # 1. The value of `Puppet[:code]`: Puppet can take a string from # its settings and parse that as a manifest. This is used by various # Puppet applications to read in a manifest and pass it to the # environment as a side effect. This is attempted first. - # 2. The contents of `Puppet.settings[:manifest]`: Puppet will try to load - # the environment manifest. By default this is `$manifestdir/site.pp` + # 2. The contents of this environment's +manifest+ attribute: Puppet will + # try to load the environment manifest. # # @note This method will return an empty hostclass if - # `Puppet.settings[:ignoreimport]` is set to true. + # `Puppet[:ignoreimport]` is set to true. # # @return [Puppet::Parser::AST::Hostclass] The AST hostclass object # representing the 'main' hostclass def perform_initial_import return empty_parse_result if Puppet[:ignoreimport] - parser = Puppet::Parser::ParserFactory.parser(self) + parser = Puppet::Parser::ParserFactory.parser @parsed_code = Puppet[:code] if @parsed_code != "" parser.string = @parsed_code parser.parse else file = self.manifest - # if the manifest file is a reference to a directory, parse and combine all .pp files in that - # directory + # if the manifest file is a reference to a directory, parse and combine + # all .pp files in that directory if file == NO_MANIFEST - Puppet::Parser::AST::Hostclass.new('') + empty_parse_result elsif File.directory?(file) parse_results = Puppet::FileSystem::PathPattern.absolute(File.join(file, '**/*.pp')).glob.sort.map do | file_to_parse | parser.file = file_to_parse parser.parse end # Use a parser type specific merger to concatenate the results Puppet::Parser::AST::Hostclass.new('', :code => Puppet::Parser::ParserFactory.code_merger.concatenate(parse_results)) else parser.file = file parser.parse end end rescue => detail @known_resource_types.parse_failed = true msg = "Could not parse for environment #{self}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end # Return an empty toplevel hostclass to indicate that no file was loaded # # This is used as the return value of {#perform_initial_import} when # `Puppet.settings[:ignoreimport]` is true. # # @return [Puppet::Parser::AST::Hostclass] def empty_parse_result return Puppet::Parser::AST::Hostclass.new('') end + # A None subclass to make it easier to trace the NONE environment when debugging. + # @api private + class None < Puppet::Node::Environment; end + # A special "null" environment # # This environment should be used when there is no specific environment in # effect. - NONE = create(:none, []) + NONE = None.create(:none, []) end diff --git a/lib/puppet/parser/e4_parser_adapter.rb b/lib/puppet/parser/e4_parser_adapter.rb index e74903751..373741df9 100644 --- a/lib/puppet/parser/e4_parser_adapter.rb +++ b/lib/puppet/parser/e4_parser_adapter.rb @@ -1,72 +1,60 @@ require 'puppet/pops' module Puppet; module Parser; end; end; # Adapts an egrammar/eparser to respond to the public API of the classic parser # and makes use of the new evaluator. # class Puppet::Parser::E4ParserAdapter - # Empty adapter fulfills watch_file contract without doing anything. - # @api private - class NullFileWatcher - def watch_file(file) - #nop - end - end - - # @param file_watcher [#watch_file] something that can watch a file - def initialize(file_watcher = nil) - @file_watcher = file_watcher || NullFileWatcher.new + def initialize @file = '' @string = '' @use = :unspecified @@evaluating_parser ||= Puppet::Pops::Parser::EvaluatingParser.new() end def file=(file) @file = file @use = :file - # watch if possible, but only if the file is something worth watching - @file_watcher.watch_file(file) if !file.nil? && file != '' end def parse(string = nil) self.string= string if string parse_result = if @use == :string # Parse with a source_file to set in created AST objects (it was either given, or it may be unknown # if caller did not set a file and the present a string. # @@evaluating_parser.parse_string(@string, @file || "unknown-source-location") else @@evaluating_parser.parse_file(@file) end # the parse_result may be # * empty / nil (no input) # * a Model::Program # * a Model::Expression # model = parse_result.nil? ? nil : parse_result.current args = {} Puppet::Pops::Model::AstTransformer.new(@file).merge_location(args, model) ast_code = if model.is_a? Puppet::Pops::Model::Program Puppet::Parser::AST::PopsBridge::Program.new(model, args) else args[:value] = model Puppet::Parser::AST::PopsBridge::Expression.new(args) end # Create the "main" class for the content - this content will get merged with all other "main" content Puppet::Parser::AST::Hostclass.new('', :code => ast_code) end def string=(string) @string = string @use = :string end end diff --git a/lib/puppet/parser/files.rb b/lib/puppet/parser/files.rb index 81c523ffd..1c99767ad 100644 --- a/lib/puppet/parser/files.rb +++ b/lib/puppet/parser/files.rb @@ -1,137 +1,92 @@ -require 'puppet/module' - module Puppet::Parser::Files module_function # Return a list of manifests as absolute filenames matching the given # pattern. # - # @param pattern [String] A reference for a file in a module. It is the format "/" + # @param pattern [String] A reference for a file in a module. It is the + # format "/" # @param environment [Puppet::Node::Environment] the environment of modules # # @return [Array(String, Array)] the module name and the list of files found # @api private def find_manifests_in_modules(pattern, environment) module_name, file_pattern = split_file_path(pattern) - begin - if mod = environment.module(module_name) - return [mod.name, mod.match_manifests(file_pattern)] - end - rescue Puppet::Module::InvalidName - # one of the modules being loaded might have an invalid name and so - # looking for one might blow up since we load them lazily. + + if mod = environment.module(module_name) + [mod.name, mod.match_manifests(file_pattern)] + else + [nil, []] end - [nil, []] end # Find the path to the given file selector. Files can be selected in # one of two ways: # * absolute path: the path is simply returned # * modulename/filename selector: a file is found in the file directory # of the named module. # # In the second case a nil is returned if there isn't a file found. In the # first case (absolute path), there is no existence check done and so the # path will be returned even if there isn't a file available. # # @param template [String] the file selector # @param environment [Puppet::Node::Environment] the environment in which to search # @return [String, nil] the absolute path to the file or nil if there is no file found # # @api private def find_file(file, environment) - if Puppet::Util.absolute_path?(file) - file - else - path, module_file = split_file_path(file) - mod = environment.module(path) - - if module_file && mod - mod.file(module_file) - else - nil - end + find_in_module(file, environment) do |mod,module_file| + mod.file(module_file) end end # Find the path to the given template selector. Templates can be selected in - # a number of ways: + # a couple of ways: # * absolute path: the path is simply returned - # * path relative to the templatepath setting: a file is found and the path - # is returned # * modulename/filename selector: a file is found in the template directory # of the named module. # # In the last two cases a nil is returned if there isn't a file found. In the # first case (absolute path), there is no existence check done and so the # path will be returned even if there isn't a file available. # # @param template [String] the template selector # @param environment [Puppet::Node::Environment] the environment in which to search # @return [String, nil] the absolute path to the template file or nil if there is no file found # # @api private def find_template(template, environment) - if Puppet::Util.absolute_path?(template) - template - else - in_templatepath = find_template_in_templatepath(template, environment) - if in_templatepath - in_templatepath - else - find_template_in_module(template, environment) - end - end - end - - # Templatepaths are deprecated functionality, this will be going away in - # Puppet 4. - # - # @api private - def find_template_in_templatepath(template, environment) - template_paths = templatepath(environment) - if template_paths - template_paths.collect do |path| - File::join(path, template) - end.find do |f| - Puppet::FileSystem.exist?(f) - end - else - nil + find_in_module(template, environment) do |mod,template_file| + mod.template(template_file) end end # @api private - def find_template_in_module(template, environment) - path, file = split_file_path(template) - mod = environment.module(path) - - if file && mod - mod.template(file) + def find_in_module(reference, environment) + if Puppet::Util.absolute_path?(reference) + reference else - nil - end - end + path, file = split_file_path(reference) + mod = environment.module(path) - # Return an array of paths by splitting the +templatedir+ config - # parameter. - # @api private - def templatepath(environment) - dirs = Puppet.settings.value(:templatedir, environment.to_s).split(File::PATH_SEPARATOR) - dirs.select do |p| - File::directory?(p) + if file && mod + yield(mod, file) + else + nil + end end end # Split the path into the module and the rest of the path, or return # nil if the path is empty or absolute (starts with a /). # @api private def split_file_path(path) if path == "" || Puppet::Util.absolute_path?(path) nil else path.split(File::SEPARATOR, 2) end end end diff --git a/lib/puppet/parser/parser_factory.rb b/lib/puppet/parser/parser_factory.rb index 9f4adce08..1cb819757 100644 --- a/lib/puppet/parser/parser_factory.rb +++ b/lib/puppet/parser/parser_factory.rb @@ -1,60 +1,60 @@ module Puppet; end module Puppet::Parser # The ParserFactory makes selection of parser possible. # Currently, it is possible to switch between two different parsers: # * classic_parser, the parser in 3.1 # * eparser, the Expression Based Parser # class ParserFactory # Produces a parser instance for the given environment - def self.parser(environment) - evaluating_parser(environment) + def self.parser + evaluating_parser end # Creates an instance of an E4ParserAdapter that adapts an # EvaluatingParser to the 3x way of parsing. # - def self.evaluating_parser(file_watcher) + def self.evaluating_parser # Since RGen is optional, test that it is installed assert_rgen_installed() unless defined?(Puppet::Pops::Parser::E4ParserAdapter) require 'puppet/parser/e4_parser_adapter' require 'puppet/pops/parser/code_merger' end - E4ParserAdapter.new(file_watcher) + E4ParserAdapter.new end # Asserts that RGen >= 0.6.6 is installed by checking that certain behavior is available. # Note that this assert is expensive as it also requires puppet/pops (if not already loaded). # def self.assert_rgen_installed @@asserted ||= false return if @@asserted @@asserted = true begin require 'rgen/metamodel_builder' rescue LoadError raise Puppet::DevError.new("The gem 'rgen' version >= 0.7.0 is required when using the setting '--parser future'. Please install 'rgen'.") end # Since RGen is optional, there is nothing specifying its version. # It is not installed in any controlled way, so not possible to use gems to check (it may be installed some other way). # Instead check that "eContainer, and eContainingFeature" has been installed. require 'puppet/pops' begin litstring = Puppet::Pops::Model::LiteralString.new(); container = Puppet::Pops::Model::ArithmeticExpression.new(); container.left_expr = litstring raise "no eContainer" if litstring.eContainer() != container raise "no eContainingFeature" if litstring.eContainingFeature() != :left_expr rescue => e # TODO: RGen can raise exceptions for other reasons! raise Puppet::DevError.new("The gem 'rgen' version >= 0.7.0 is required when using '--parser future'. An older version is installed, please update.") end end def self.code_merger Puppet::Pops::Parser::CodeMerger.new end end end diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb index ae36e43ec..00576c861 100644 --- a/lib/puppet/parser/templatewrapper.rb +++ b/lib/puppet/parser/templatewrapper.rb @@ -1,103 +1,100 @@ require 'puppet/parser/files' require 'erb' # A simple wrapper for templates, so they don't have full access to # the scope objects. # # @api private class Puppet::Parser::TemplateWrapper include Puppet::Util Puppet::Util.logmethods(self) def initialize(scope) @__scope__ = scope end # @return [String] The full path name of the template that is being executed # @api public def file @__file__ end # @return [Puppet::Parser::Scope] The scope in which the template is evaluated # @api public def scope @__scope__ end # Find which line in the template (if any) we were called from. # @return [String] the line number # @api private def script_line identifier = Regexp.escape(@__file__ || "(erb)") (caller.find { |l| l =~ /#{identifier}:/ }||"")[/:(\d+):/,1] end private :script_line # Should return true if a variable is defined, false if it is not # @api public def has_variable?(name) scope.include?(name.to_s) end # @return [Array] The list of defined classes # @api public def classes scope.catalog.classes end # @return [Array] The tags defined in the current scope # @api public def tags scope.tags end # @return [Array] All the defined tags # @api public def all_tags scope.catalog.tags end # @api private def file=(filename) unless @__file__ = Puppet::Parser::Files.find_template(filename, scope.compiler.environment) raise Puppet::ParseError, "Could not find template '#{filename}'" end - - # We'll only ever not have a parser in testing, but, eh. - scope.known_resource_types.watch_file(@__file__) end # @api private def result(string = nil) if string template_source = "inline template" else string = File.read(@__file__) template_source = @__file__ end # Expose all the variables in our scope as instance variables of the # current object, making it possible to access them without conflict # to the regular methods. benchmark(:debug, "Bound template variables for #{template_source}") do scope.to_hash.each do |name, value| realname = name.gsub(/[^\w]/, "_") instance_variable_set("@#{realname}", value) end end result = nil benchmark(:debug, "Interpolated template #{template_source}") do template = ERB.new(string, 0, "-") template.filename = @__file__ result = template.result(binding) end result end def to_s "template[#{(@__file__ ? @__file__ : "inline")}]" end end diff --git a/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb index 9ed1806a4..01dde7494 100644 --- a/lib/puppet/parser/type_loader.rb +++ b/lib/puppet/parser/type_loader.rb @@ -1,152 +1,151 @@ require 'find' require 'forwardable' -require 'puppet/node/environment' require 'puppet/parser/parser_factory' class Puppet::Parser::TypeLoader extend Forwardable # Import manifest files that match a given file glob pattern. # # @param pattern [String] the file glob to apply when determining which files # to load # @param dir [String] base directory to use when the file is not # found in a module # @api private def import(pattern, dir) return if Puppet[:ignoreimport] modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment) if files.empty? abspat = File.expand_path(pattern, dir) file_pattern = abspat + (File.extname(abspat).empty? ? '.pp' : '' ) files = Dir.glob(file_pattern).uniq.reject { |f| FileTest.directory?(f) } modname = nil if files.empty? raise_no_files_found(pattern) end end load_files(modname, files) end # Load all of the manifest files in all known modules. # @api private def import_all # And then load all files from each module, but (relying on system # behavior) only load files from the first module of a given name. E.g., # given first/foo and second/foo, only files from first/foo will be loaded. environment.modules.each do |mod| load_files(mod.name, mod.all_manifests) end end def_delegator :environment, :known_resource_types def initialize(env) self.environment = env end def environment @environment end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = Puppet.lookup(:environments).get!(env) else @environment = env end end # Try to load the object with the given fully qualified name. def try_load_fqname(type, fqname) return nil if fqname == "" # special-case main. files_to_try_for(fqname).each do |filename| begin imported_types = import_from_modules(filename) if result = imported_types.find { |t| t.type == type and t.name == fqname } Puppet.debug "Automatically imported #{fqname} from #{filename} into #{environment}" return result end rescue Puppet::ImportError => detail # I'm not convienced we should just drop these errors, but this # preserves existing behaviours. end end # Nothing found. return nil end def parse_file(file) Puppet.debug("importing '#{file}' in environment #{environment}") - parser = Puppet::Parser::ParserFactory.parser(environment) + parser = Puppet::Parser::ParserFactory.parser parser.file = file return parser.parse end private def import_from_modules(pattern) modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment) if files.empty? raise_no_files_found(pattern) end load_files(modname, files) end def raise_no_files_found(pattern) raise Puppet::ImportError, "No file(s) found for import of '#{pattern}'" end def load_files(modname, files) @loaded ||= {} loaded_asts = [] files.reject { |file| @loaded[file] }.each do |file| # NOTE: This ugly implementation will be replaced in Puppet 3.5. # The implementation now makes use of a global variable because the context support is # not available until Puppet 3.5. # The use case is that parsing for the purpose of searching for information # should not abort. There is currently one such use case in indirector/resourcetype/parser # if Puppet.lookup(:squelch_parse_errors) {|| false } begin loaded_asts << parse_file(file) rescue => e # Resume from errors so that all parseable files would # still be parsed. Mark this file as loaded so that # it would not be parsed next time (handle it as if # it was successfully parsed). Puppet.debug("Unable to parse '#{file}': #{e.message}") end else loaded_asts << parse_file(file) end @loaded[file] = true end loaded_asts.collect do |ast| known_resource_types.import_ast(ast, modname) end.flatten end # Return a list of all file basenames that should be tried in order # to load the object with the given fully qualified name. def files_to_try_for(qualified_name) qualified_name.split('::').inject([]) do |paths, name| add_path_for_name(paths, name) end end def add_path_for_name(paths, name) if paths.empty? [name] else paths.unshift(File.join(paths.first, name)) end end end diff --git a/lib/puppet/pops/loaders.rb b/lib/puppet/pops/loaders.rb index 5fa1d3c61..6109fcb13 100644 --- a/lib/puppet/pops/loaders.rb +++ b/lib/puppet/pops/loaders.rb @@ -1,238 +1,238 @@ class Puppet::Pops::Loaders class LoaderError < Puppet::Error; end attr_reader :static_loader attr_reader :puppet_system_loader attr_reader :public_environment_loader attr_reader :private_environment_loader def initialize(environment) # The static loader can only be changed after a reboot @@static_loader ||= Puppet::Pops::Loader::StaticLoader.new() # Create the set of loaders # 1. Puppet, loads from the "running" puppet - i.e. bundled functions, types, extension points and extensions # Does not change without rebooting the service running puppet. # @@puppet_system_loader ||= create_puppet_system_loader() # 2. Environment loader - i.e. what is bound across the environment, may change for each setup # TODO: loaders need to work when also running in an agent doing catalog application. There is no # concept of environment the same way as when running as a master (except when doing apply). # The creation mechanisms should probably differ between the two. # @private_environment_loader = create_environment_loader(environment) # 3. module loaders are set up from the create_environment_loader, they register themselves end # Clears the cached static and puppet_system loaders (to enable testing) # def self.clear @@static_loader = nil @@puppet_system_loader = nil end def static_loader @@static_loader end def puppet_system_loader @@puppet_system_loader end def public_loader_for_module(module_name) md = @module_resolver[module_name] || (return nil) # Note, this loader is not resolved until there is interest in the visibility of entities from the # perspective of something contained in the module. (Many request may pass through a module loader # without it loading anything. # See {#private_loader_for_module}, and not in {#configure_loaders_for_modules} md.public_loader end def private_loader_for_module(module_name) md = @module_resolver[module_name] || (return nil) # Since there is interest in the visibility from the perspective of entities contained in the # module, it must be resolved (to provide this visibility). # See {#configure_loaders_for_modules} unless md.resolved? @module_resolver.resolve(md) end md.private_loader end private def create_puppet_system_loader() Puppet::Pops::Loader::ModuleLoaders.system_loader_from(static_loader, self) end def create_environment_loader(environment) # This defines where to start parsing/evaluating - the "initial import" (to use 3x terminology) # Is either a reference to a single .pp file, or a directory of manifests. If the environment becomes # a module and can hold functions, types etc. then these are available across all other modules without # them declaring this dependency - it is however valuable to be able to treat it the same way # bindings and other such system related configuration. # This is further complicated by the many options available: # - The environment may not have a directory, the code comes from one appointed 'manifest' (site.pp) # - The environment may have a directory and also point to a 'manifest' # - The code to run may be set in settings (code) # Further complication is that there is nothing specifying what the visibility is into # available modules. (3x is everyone sees everything). # Puppet binder currently reads confdir/bindings - that is bad, it should be using the new environment support. # The environment is not a namespace, so give it a nil "module_name" module_name = nil loader_name = "environment:#{environment.name}" - env_conf = Puppet.lookup(:environments).get_conf('production') + env_conf = Puppet.lookup(:environments).get_conf(environment.name) if env_conf.nil? || !env_conf.is_a?(Puppet::Settings::EnvironmentConf) # Not a real directory environment, cannot work as a module TODO: Drop when legacy env are dropped? loader = Puppet::Pops::Loader::SimpleEnvironmentLoader.new(puppet_system_loader, loader_name) else # View the environment as a module to allow loading from it - this module is always called 'environment' loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(puppet_system_loader, self, 'environment', env_conf.path_to_env) end # An environment has a module path even if it has a null loader configure_loaders_for_modules(loader, environment) # modules should see this loader @public_environment_loader = loader # Code in the environment gets to see all modules (since there is no metadata for the environment) # but since this is not given to the module loaders, they can not load global code (since they can not # have prior knowledge about this loader = Puppet::Pops::Loader::DependencyLoader.new(loader, "environment", @module_resolver.all_module_loaders()) # The module loader gets the private loader via a lazy operation to look up the module's private loader. # This does not work for an environment since it is not resolved the same way. # TODO: The EnvironmentLoader could be a specialized loader instead of using a ModuleLoader to do the work. # This is subject to future design - an Environment may move more in the direction of a Module. @public_environment_loader.private_loader = loader loader end def configure_loaders_for_modules(parent_loader, environment) @module_resolver = mr = ModuleResolver.new() environment.modules.each do |puppet_module| # Create data about this module md = LoaderModuleData.new(puppet_module) mr[puppet_module.name] = md md.public_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(parent_loader, self, md.name, md.path) end # NOTE: Do not resolve all modules here - this is wasteful if only a subset of modules / functions are used # The resolution is triggered by asking for a module's private loader, since this means there is interest # in the visibility from that perspective. # If later, it is wanted that all resolutions should be made up-front (to capture errors eagerly, this # can be introduced (better for production), but may be irritating in development mode. end # =LoaderModuleData # Information about a Module and its loaders. # TODO: should have reference to real model element containing all module data; this is faking it # TODO: Should use Puppet::Module to get the metadata (as a hash) - a somewhat blunt instrument, but that is # what is available with a reasonable API. # class LoaderModuleData attr_accessor :state attr_accessor :public_loader attr_accessor :private_loader attr_accessor :resolutions # The Puppet::Module this LoaderModuleData represents in the loader configuration attr_reader :puppet_module # @param puppet_module [Puppet::Module] the module instance for the module being represented # def initialize(puppet_module) @state = :initial @puppet_module = puppet_module @resolutions = [] @public_loader = nil @private_loader = nil end def name @puppet_module.name end def version @puppet_module.version end def path @puppet_module.path end def resolved? @state == :resolved end def restrict_to_dependencies? @puppet_module.has_metadata? end def unmet_dependencies? @puppet_module.unmet_dependencies.any? end def dependency_names @puppet_module.dependencies_as_modules.collect(&:name) end end # Resolves module loaders - resolution of model dependencies is done by Puppet::Module # class ModuleResolver def initialize() @index = {} @all_module_loaders = nil end def [](name) @index[name] end def []=(name, module_data) @index[name] = module_data end def all_module_loaders @all_module_loaders ||= @index.values.map {|md| md.public_loader } end def resolve(module_data) if module_data.resolved? return else module_data.private_loader = if module_data.restrict_to_dependencies? create_loader_with_only_dependencies_visible(module_data) else create_loader_with_all_modules_visible(module_data) end end end private def create_loader_with_all_modules_visible(from_module_data) Puppet.debug("ModuleLoader: module '#{from_module_data.name}' has unknown dependencies - it will have all other modules visible") Puppet::Pops::Loader::DependencyLoader.new(from_module_data.public_loader, from_module_data.name, all_module_loaders()) end def create_loader_with_only_dependencies_visible(from_module_data) if from_module_data.unmet_dependencies? Puppet.warning("ModuleLoader: module '#{from_module_data.name}' has unresolved dependencies"+ " - it will only see those that are resolved."+ " Use 'puppet module list --tree' to see information about modules") end dependency_loaders = from_module_data.dependency_names.collect { |name| @index[name].public_loader } Puppet::Pops::Loader::DependencyLoader.new(from_module_data.public_loader, from_module_data.name, dependency_loaders) end end end diff --git a/lib/puppet/resource/type_collection.rb b/lib/puppet/resource/type_collection.rb index f5b0bda31..1e1be3430 100644 --- a/lib/puppet/resource/type_collection.rb +++ b/lib/puppet/resource/type_collection.rb @@ -1,230 +1,215 @@ require 'puppet/parser/type_loader' require 'puppet/util/file_watcher' require 'puppet/util/warnings' class Puppet::Resource::TypeCollection attr_reader :environment attr_accessor :parse_failed include Puppet::Util::Warnings def clear @hostclasses.clear @definitions.clear @nodes.clear - @watched_files.clear @notfound.clear end def initialize(env) @environment = env @hostclasses = {} @definitions = {} @nodes = {} @notfound = {} # So we can keep a list and match the first-defined regex @node_list = [] - - @watched_files = Puppet::Util::FileWatcher.new end def import_ast(ast, modname) ast.instantiate(modname).each do |instance| add(instance) end end def inspect "TypeCollection" + { :hostclasses => @hostclasses.keys, :definitions => @definitions.keys, :nodes => @nodes.keys }.inspect end def <<(thing) add(thing) self end def add(instance) if instance.type == :hostclass and other = @hostclasses[instance.name] and other.type == :hostclass other.merge(instance) return other end method = "add_#{instance.type}" send(method, instance) instance.resource_type_collection = self instance end def add_hostclass(instance) dupe_check(instance, @hostclasses) { |dupe| "Class '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined as a class" } @hostclasses[instance.name] = instance instance end def hostclass(name) @hostclasses[munge_name(name)] end def add_node(instance) dupe_check(instance, @nodes) { |dupe| "Node '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } @node_list << instance @nodes[instance.name] = instance instance end def loader @loader ||= Puppet::Parser::TypeLoader.new(environment) end def node(name) name = munge_name(name) if node = @nodes[name] return node end @node_list.each do |node| next unless node.name_is_regex? return node if node.match(name) end nil end def node_exists?(name) @nodes[munge_name(name)] end def nodes? @nodes.length > 0 end def add_definition(instance) dupe_check(instance, @hostclasses) { |dupe| "'#{instance.name}' is already defined#{dupe.error_context} as a class; cannot redefine as a definition" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined" } @definitions[instance.name] = instance end def definition(name) @definitions[munge_name(name)] end def find_node(namespaces, name) @nodes[munge_name(name)] end def find_hostclass(namespaces, name, options = {}) find_or_load(namespaces, name, :hostclass, options) end def find_definition(namespaces, name) find_or_load(namespaces, name, :definition) end [:hostclasses, :nodes, :definitions].each do |m| define_method(m) do instance_variable_get("@#{m}").dup end end - def require_reparse? - @parse_failed || stale? - end - - def stale? - @watched_files.changed? + def parse_failed? + @parse_failed end def version if !defined?(@version) if environment.config_version.nil? || environment.config_version == "" @version = Time.now.to_i else @version = Puppet::Util::Execution.execute([environment.config_version]).strip end end @version rescue Puppet::ExecutionFailure => e raise Puppet::ParseError, "Execution of config_version command `#{environment.config_version}` failed: #{e.message}", e.backtrace end - def watch_file(filename) - @watched_files.watch(filename) - end - - def watching_file?(filename) - @watched_files.watching?(filename) - end - private # Return a list of all possible fully-qualified names that might be # meant by the given name, in the context of namespaces. def resolve_namespaces(namespaces, name) name = name.downcase if name =~ /^::/ # name is explicitly fully qualified, so just return it, sans # initial "::". return [name.sub(/^::/, '')] end if name == "" # The name "" has special meaning--it always refers to a "main" # hostclass which contains all toplevel resources. return [""] end namespaces = [namespaces] unless namespaces.is_a?(Array) namespaces = namespaces.collect { |ns| ns.downcase } result = [] namespaces.each do |namespace| ary = namespace.split("::") # Search each namespace nesting in innermost-to-outermost order. while ary.length > 0 result << "#{ary.join("::")}::#{name}" ary.pop end # Finally, search the toplevel namespace. result << name end return result.uniq end # Resolve namespaces and find the given object. Autoload it if # necessary. def find_or_load(namespaces, name, type, options = {}) searchspace = options[:assume_fqname] ? [name].flatten : resolve_namespaces(namespaces, name) searchspace.each do |fqname| result = send(type, fqname) unless result if @notfound[fqname] and Puppet[:ignoremissingtypes] # do not try to autoload if we already tried and it wasn't conclusive # as this is a time consuming operation. Warn the user. debug_once "Not attempting to load #{type} #{fqname} as this object was missing during a prior compilation" else result = loader.try_load_fqname(type, fqname) @notfound[fqname] = result.nil? end end return result if result end return nil end def munge_name(name) name.to_s.downcase end def dupe_check(instance, hash) return unless dupe = hash[instance.name] message = yield dupe instance.fail Puppet::ParseError, message end end diff --git a/lib/puppet/settings.rb b/lib/puppet/settings.rb index 8be617233..895abd495 100644 --- a/lib/puppet/settings.rb +++ b/lib/puppet/settings.rb @@ -1,1442 +1,1461 @@ require 'puppet' require 'getoptlong' require 'puppet/util/watched_file' require 'puppet/util/command_line/puppet_option_parser' require 'forwardable' # The class for handling configuration files. class Puppet::Settings extend Forwardable include Enumerable require 'puppet/settings/errors' require 'puppet/settings/base_setting' require 'puppet/settings/string_setting' require 'puppet/settings/enum_setting' require 'puppet/settings/array_setting' require 'puppet/settings/file_setting' require 'puppet/settings/directory_setting' require 'puppet/settings/file_or_directory_setting' require 'puppet/settings/path_setting' require 'puppet/settings/boolean_setting' require 'puppet/settings/terminus_setting' require 'puppet/settings/duration_setting' require 'puppet/settings/ttl_setting' require 'puppet/settings/priority_setting' require 'puppet/settings/autosign_setting' require 'puppet/settings/config_file' require 'puppet/settings/value_translator' require 'puppet/settings/environment_conf' # local reference for convenience PuppetOptionParser = Puppet::Util::CommandLine::PuppetOptionParser attr_accessor :files attr_reader :timer - # These are the settings that every app is required to specify; there are reasonable defaults defined in application.rb. + # These are the settings that every app is required to specify; there are + # reasonable defaults defined in application.rb. REQUIRED_APP_SETTINGS = [:logdir, :confdir, :vardir] + # The acceptable sections of the puppet.conf configuration file. + ALLOWED_SECTION_NAMES = ['main', 'master', 'agent', 'user'].freeze + # This method is intended for puppet internal use only; it is a convenience method that # returns reasonable application default settings values for a given run_mode. def self.app_defaults_for_run_mode(run_mode) { :name => run_mode.to_s, :run_mode => run_mode.name, :confdir => run_mode.conf_dir, :vardir => run_mode.var_dir, :rundir => run_mode.run_dir, :logdir => run_mode.log_dir, } end def self.default_certname() hostname = hostname_fact domain = domain_fact if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end fqdn.to_s.gsub(/\.$/, '') end def self.hostname_fact() Facter.value :hostname end def self.domain_fact() Facter.value :domain end def self.default_config_file_name "puppet.conf" end # Create a new collection of config settings. def initialize @config = {} @shortnames = {} @created = [] # Keep track of set values. @value_sets = { :cli => Values.new(:cli, @config), :memory => Values.new(:memory, @config), :application_defaults => Values.new(:application_defaults, @config), :overridden_defaults => Values.new(:overridden_defaults, @config), } @configuration_file = nil # And keep a per-environment cache @cache = Hash.new { |hash, key| hash[key] = {} } @values = Hash.new { |hash, key| hash[key] = {} } # The list of sections we've used. @used = [] @hooks_to_call_on_application_initialization = [] @deprecated_setting_names = [] @deprecated_settings_that_have_been_configured = [] @translate = Puppet::Settings::ValueTranslator.new @config_file_parser = Puppet::Settings::ConfigFile.new(@translate) end # @param name [Symbol] The name of the setting to fetch # @return [Puppet::Settings::BaseSetting] The setting object def setting(name) @config[name] end # Retrieve a config value # @param param [Symbol] the name of the setting # @return [Object] the value of the setting # @api private def [](param) if @deprecated_setting_names.include?(param) issue_deprecation_warning(setting(param), "Accessing '#{param}' as a setting is deprecated.") end value(param) end # Set a config value. This doesn't set the defaults, it sets the value itself. # @param param [Symbol] the name of the setting # @param value [Object] the new value of the setting # @api private def []=(param, value) if @deprecated_setting_names.include?(param) issue_deprecation_warning(setting(param), "Modifying '#{param}' as a setting is deprecated.") end @value_sets[:memory].set(param, value) unsafe_flush_cache end # Create a new default value for the given setting. The default overrides are # higher precedence than the defaults given in defaults.rb, but lower # precedence than any other values for the setting. This allows one setting # `a` to change the default of setting `b`, but still allow a user to provide # a value for setting `b`. # # @param param [Symbol] the name of the setting # @param value [Object] the new default value for the setting # @api private def override_default(param, value) @value_sets[:overridden_defaults].set(param, value) unsafe_flush_cache end # Generate the list of valid arguments, in a format that GetoptLong can # understand, and add them to the passed option list. def addargs(options) # Add all of the settings as valid options. self.each { |name, setting| setting.getopt_args.each { |args| options << args } } options end # Generate the list of valid arguments, in a format that OptionParser can # understand, and add them to the passed option list. def optparse_addargs(options) # Add all of the settings as valid options. self.each { |name, setting| options << setting.optparse_args } options end # Is our setting a boolean setting? def boolean?(param) param = param.to_sym @config.include?(param) and @config[param].kind_of?(BooleanSetting) end # Remove all set values, potentially skipping cli values. def clear unsafe_clear end # Remove all set values, potentially skipping cli values. def unsafe_clear(clear_cli = true, clear_application_defaults = false) if clear_application_defaults @value_sets[:application_defaults] = Values.new(:application_defaults, @config) @app_defaults_initialized = false end if clear_cli @value_sets[:cli] = Values.new(:cli, @config) # Only clear the 'used' values if we were explicitly asked to clear out # :cli values; otherwise, it may be just a config file reparse, # and we want to retain this cli values. @used = [] end @value_sets[:memory] = Values.new(:memory, @config) @value_sets[:overridden_defaults] = Values.new(:overridden_defaults, @config) @deprecated_settings_that_have_been_configured.clear @values.clear @cache.clear end private :unsafe_clear # Clears all cached settings for a particular environment to ensure # that changes to environment.conf are reflected in the settings if # the environment timeout has expired. # # param [String, Symbol] environment the name of environment to clear settings for # # @api private def clear_environment_settings(environment) if environment.nil? return end @cache[environment.to_sym].clear @values[environment.to_sym] = {} end # Clear @cache, @used and the Environment. # # Whenever an object is returned by Settings, a copy is stored in @cache. # As long as Setting attributes that determine the content of returned # objects remain unchanged, Settings can keep returning objects from @cache # without re-fetching or re-generating them. # # Whenever a Settings attribute changes, such as @values or @preferred_run_mode, # this method must be called to clear out the caches so that updated # objects will be returned. def flush_cache unsafe_flush_cache end def unsafe_flush_cache clearused - - # Clear the list of environments, because they cache, at least, the module path. - # We *could* preferentially just clear them if the modulepath is changed, - # but we don't really know if, say, the vardir is changed and the modulepath - # is defined relative to it. We need the defined?(stuff) because of loading - # order issues. - Puppet::Node::Environment.clear if defined?(Puppet::Node) and defined?(Puppet::Node::Environment) end private :unsafe_flush_cache def clearused @cache.clear @used = [] end def global_defaults_initialized?() @global_defaults_initialized end def initialize_global_settings(args = []) raise Puppet::DevError, "Attempting to initialize global default settings more than once!" if global_defaults_initialized? # The first two phases of the lifecycle of a puppet application are: # 1) Parse the command line options and handle any of them that are # registered, defined "global" puppet settings (mostly from defaults.rb). # 2) Parse the puppet config file(s). parse_global_options(args) parse_config_files @global_defaults_initialized = true end # This method is called during application bootstrapping. It is responsible for parsing all of the # command line options and initializing the settings accordingly. # # It will ignore options that are not defined in the global puppet settings list, because they may # be valid options for the specific application that we are about to launch... however, at this point # in the bootstrapping lifecycle, we don't yet know what that application is. def parse_global_options(args) # Create an option parser option_parser = PuppetOptionParser.new option_parser.ignore_invalid_options = true # Add all global options to it. self.optparse_addargs([]).each do |option| option_parser.on(*option) do |arg| opt, val = Puppet::Settings.clean_opt(option[0], arg) handlearg(opt, val) end end option_parser.on('--run_mode', "The effective 'run mode' of the application: master, agent, or user.", :REQUIRED) do |arg| Puppet.settings.preferred_run_mode = arg end option_parser.parse(args) # remove run_mode options from the arguments so that later parses don't think # it is an unknown option. while option_index = args.index('--run_mode') do args.delete_at option_index args.delete_at option_index end args.reject! { |arg| arg.start_with? '--run_mode=' } end private :parse_global_options # A utility method (public, is used by application.rb and perhaps elsewhere) that munges a command-line # option string into the format that Puppet.settings expects. (This mostly has to deal with handling the # "no-" prefix on flag/boolean options). # # @param [String] opt the command line option that we are munging # @param [String, TrueClass, FalseClass] val the value for the setting (as determined by the OptionParser) def self.clean_opt(opt, val) # rewrite --[no-]option to --no-option if that's what was given if opt =~ /\[no-\]/ and !val opt = opt.gsub(/\[no-\]/,'no-') end # otherwise remove the [no-] prefix to not confuse everybody opt = opt.gsub(/\[no-\]/, '') [opt, val] end def app_defaults_initialized? @app_defaults_initialized end def initialize_app_defaults(app_defaults) REQUIRED_APP_SETTINGS.each do |key| raise SettingsError, "missing required app default setting '#{key}'" unless app_defaults.has_key?(key) end app_defaults.each do |key, value| if key == :run_mode self.preferred_run_mode = value else @value_sets[:application_defaults].set(key, value) unsafe_flush_cache end end apply_metadata call_hooks_deferred_to_application_initialization issue_deprecations @app_defaults_initialized = true end def call_hooks_deferred_to_application_initialization(options = {}) @hooks_to_call_on_application_initialization.each do |setting| begin setting.handle(self.value(setting.name)) rescue InterpolationError => err raise InterpolationError, err, err.backtrace unless options[:ignore_interpolation_dependency_errors] #swallow. We're not concerned if we can't call hooks because dependencies don't exist yet #we'll get another chance after application defaults are initialized end end end private :call_hooks_deferred_to_application_initialization # Return a value's description. def description(name) if obj = @config[name.to_sym] obj.desc else nil end end def_delegator :@config, :each # Iterate over each section name. def eachsection yielded = [] @config.each do |name, object| section = object.section unless yielded.include? section yield section yielded << section end end end # Return an object by name. def setting(param) param = param.to_sym @config[param] end # Handle a command-line argument. def handlearg(opt, value = nil) @cache.clear if value.is_a?(FalseClass) value = "false" elsif value.is_a?(TrueClass) value = "true" end value &&= @translate[value] str = opt.sub(/^--/,'') bool = true newstr = str.sub(/^no-/, '') if newstr != str str = newstr bool = false end str = str.intern if @config[str].is_a?(Puppet::Settings::BooleanSetting) if value == "" or value.nil? value = bool end end if s = @config[str] @deprecated_settings_that_have_been_configured << s if s.completely_deprecated? end @value_sets[:cli].set(str, value) unsafe_flush_cache end def include?(name) name = name.intern if name.is_a? String @config.include?(name) end # check to see if a short name is already defined def shortinclude?(short) short = short.intern if name.is_a? String @shortnames.include?(short) end # Prints the contents of a config file with the available config settings, or it # prints a single value of a config setting. def print_config_options env = value(:environment) val = value(:configprint) if val == "all" hash = {} each do |name, obj| val = value(name,env) val = val.inspect if val == "" hash[name] = val end hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val| puts "#{name} = #{val}" end else val.split(/\s*,\s*/).sort.each do |v| if include?(v) #if there is only one value, just print it for back compatibility if v == val puts value(val,env) break end puts "#{v} = #{value(v,env)}" else puts "invalid setting: #{v}" return false end end end true end def generate_config puts to_config true end def generate_manifest puts to_manifest true end def print_configs return print_config_options if value(:configprint) != "" return generate_config if value(:genconfig) generate_manifest if value(:genmanifest) end def print_configs? (value(:configprint) != "" || value(:genconfig) || value(:genmanifest)) && true end # Return a given object's file metadata. def metadata(param) if obj = @config[param.to_sym] and obj.is_a?(FileSetting) { :owner => obj.owner, :group => obj.group, :mode => obj.mode }.delete_if { |key, value| value.nil? } else nil end end # Make a directory with the appropriate user, group, and mode def mkdir(default) obj = get_config_file_default(default) Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do mode = obj.mode || 0750 Dir.mkdir(obj.value, mode) end end # The currently configured run mode that is preferred for constructing the application configuration. def preferred_run_mode @preferred_run_mode_name || :user end # PRIVATE! This only exists because we need a hook to validate the run mode when it's being set, and # it should never, ever, ever, ever be called from outside of this file. # This method is also called when --run_mode MODE is used on the command line to set the default # # @param mode [String|Symbol] the name of the mode to have in effect # @api private def preferred_run_mode=(mode) mode = mode.to_s.downcase.intern raise ValidationError, "Invalid run mode '#{mode}'" unless [:master, :agent, :user].include?(mode) @preferred_run_mode_name = mode # Changing the run mode has far-reaching consequences. Flush any cached # settings so they will be re-generated. flush_cache mode end # Return all of the settings associated with a given section. def params(section = nil) if section section = section.intern if section.is_a? String @config.find_all { |name, obj| obj.section == section }.collect { |name, obj| name } else @config.keys end end def parse_config(text, file = "text") begin - data = @config_file_parser.parse_file(file, text) + data = @config_file_parser.parse_file(file, text, ALLOWED_SECTION_NAMES) rescue => detail Puppet.log_exception(detail, "Could not parse #{file}: #{detail}") return end # If we get here and don't have any data, we just return and don't muck with the current state of the world. return if data.nil? # If we get here then we have some data, so we need to clear out any # previous settings that may have come from config files. unsafe_clear(false, false) + # Screen settings which have been deprecated and removed from puppet.conf + # but are still valid on the command line and/or in environment.conf + screen_non_puppet_conf_settings(data) + + # Make note of deprecated settings we will warn about later in initialization record_deprecations_from_puppet_conf(data) # And now we can repopulate with the values from our last parsing of the config files. @configuration_file = data # Determine our environment, if we have one. if @config[:environment] env = self.value(:environment).to_sym else env = "none" end # Call any hooks we should be calling. value_sets = value_sets_for(env, preferred_run_mode) @config.values.select(&:has_hook?).each do |setting| value_sets.each do |source| if source.include?(setting.name) # We still have to use value to retrieve the value, since # we want the fully interpolated value, not $vardir/lib or whatever. # This results in extra work, but so few of the settings # will have associated hooks that it ends up being less work this # way overall. if setting.call_hook_on_initialize? @hooks_to_call_on_application_initialization << setting else setting.handle(ChainedValues.new( preferred_run_mode, env, value_sets, @config).interpolate(setting.name)) end break end end end call_hooks_deferred_to_application_initialization :ignore_interpolation_dependency_errors => true apply_metadata end # Parse the configuration file. Just provides thread safety. def parse_config_files file = which_configuration_file if Puppet::FileSystem.exist?(file) begin text = read_file(file) rescue => detail Puppet.log_exception(detail, "Could not load #{file}: #{detail}") return end else return end parse_config(text, file) end private :parse_config_files def main_config_file if explicit_config_file? return self[:config] else return File.join(Puppet::Util::RunMode[:master].conf_dir, config_file_name) end end private :main_config_file def user_config_file return File.join(Puppet::Util::RunMode[:user].conf_dir, config_file_name) end private :user_config_file # This method is here to get around some life-cycle issues. We need to be # able to determine the config file name before the settings / defaults are # fully loaded. However, we also need to respect any overrides of this value # that the user may have specified on the command line. # # The easiest way to do this is to attempt to read the setting, and if we # catch an error (meaning that it hasn't been set yet), we'll fall back to # the default value. def config_file_name begin return self[:config_file_name] if self[:config_file_name] rescue SettingsError # This just means that the setting wasn't explicitly set on the command line, so we will ignore it and # fall through to the default name. end return self.class.default_config_file_name end private :config_file_name def apply_metadata # We have to do it in the reverse of the search path, # because multiple sections could set the same value # and I'm too lazy to only set the metadata once. if @configuration_file - searchpath.reverse.each do |source| - source = preferred_run_mode if source == :run_mode - if section = @configuration_file.sections[source] + searchpath(nil, preferred_run_mode).reverse.each do |source| + if source.type == :section && section = @configuration_file.sections[source.name] apply_metadata_from_section(section) end end end end private :apply_metadata def apply_metadata_from_section(section) section.settings.each do |setting| if setting.has_metadata? && type = @config[setting.name] type.set_meta(setting.meta) end end end SETTING_TYPES = { :string => StringSetting, :file => FileSetting, :directory => DirectorySetting, :file_or_directory => FileOrDirectorySetting, :path => PathSetting, :boolean => BooleanSetting, :terminus => TerminusSetting, :duration => DurationSetting, :ttl => TTLSetting, :array => ArraySetting, :enum => EnumSetting, :priority => PrioritySetting, :autosign => AutosignSetting, } # Create a new setting. The value is passed in because it's used to determine # what kind of setting we're creating, but the value itself might be either # a default or a value, so we can't actually assign it. # # See #define_settings for documentation on the legal values for the ":type" option. def newsetting(hash) klass = nil hash[:section] = hash[:section].to_sym if hash[:section] if type = hash[:type] unless klass = SETTING_TYPES[type] raise ArgumentError, "Invalid setting type '#{type}'" end hash.delete(:type) else # The only implicit typing we still do for settings is to fall back to "String" type if they didn't explicitly # specify a type. Personally I'd like to get rid of this too, and make the "type" option mandatory... but # there was a little resistance to taking things quite that far for now. --cprice 2012-03-19 klass = StringSetting end hash[:settings] = self setting = klass.new(hash) setting end # This has to be private, because it doesn't add the settings to @config private :newsetting # Iterate across all of the objects in a given section. def persection(section) section = section.to_sym self.each { |name, obj| if obj.section == section yield obj end } end # Reparse our config file, if necessary. def reparse_config_files if files if filename = any_files_changed? Puppet.notice "Config file #{filename} changed; triggering re-parse of all config files." parse_config_files reuse end end end def files return @files if @files @files = [] [main_config_file, user_config_file].each do |path| if Puppet::FileSystem.exist?(path) @files << Puppet::Util::WatchedFile.new(path) end end @files end private :files # Checks to see if any of the config files have been modified # @return the filename of the first file that is found to have changed, or # nil if no files have changed def any_files_changed? files.each do |file| return file.to_str if file.changed? end nil end private :any_files_changed? def reuse return unless defined?(@used) new = @used @used = [] self.use(*new) end + class SearchPathElement < Struct.new(:name, :type); end + # The order in which to search for values. - def searchpath(environment = nil) - [:memory, :cli, environment, :run_mode, :main, :application_defaults, :overridden_defaults].compact + # + # @param environment [String,Symbol] symbolic reference to an environment name + # @param run_mode [Symbol] symbolic reference to a Puppet run mode + # @return [Array] + # @api private + def searchpath(environment = nil, run_mode = preferred_run_mode) + searchpath = [ + SearchPathElement.new(:memory, :values), + SearchPathElement.new(:cli, :values), + ] + searchpath << SearchPathElement.new(environment.intern, :environment) if environment + searchpath << SearchPathElement.new(run_mode, :section) if run_mode + searchpath << SearchPathElement.new(:main, :section) + searchpath << SearchPathElement.new(:application_defaults, :values) + searchpath << SearchPathElement.new(:overridden_defaults, :values) end # Get a list of objects per section def sectionlist sectionlist = [] self.each { |name, obj| section = obj.section || "puppet" sections[section] ||= [] sectionlist << section unless sectionlist.include?(section) sections[section] << obj } return sectionlist, sections end def service_user_available? return @service_user_available if defined?(@service_user_available) if self[:user] user = Puppet::Type.type(:user).new :name => self[:user], :audit => :ensure @service_user_available = user.exists? else @service_user_available = false end end def service_group_available? return @service_group_available if defined?(@service_group_available) if self[:group] group = Puppet::Type.type(:group).new :name => self[:group], :audit => :ensure @service_group_available = group.exists? else @service_group_available = false end end # Allow later inspection to determine if the setting was set on the # command line, or through some other code path. Used for the # `dns_alt_names` option during cert generate. --daniel 2011-10-18 def set_by_cli?(param) param = param.to_sym !@value_sets[:cli].lookup(param).nil? end def set_value(param, value, type, options = {}) Puppet.deprecation_warning("Puppet.settings.set_value is deprecated. Use Puppet[]= instead.") if @value_sets[type] @value_sets[type].set(param, value) unsafe_flush_cache end end # Deprecated; use #define_settings instead def setdefaults(section, defs) Puppet.deprecation_warning("'setdefaults' is deprecated and will be removed; please call 'define_settings' instead") define_settings(section, defs) end # Define a group of settings. # # @param [Symbol] section a symbol to use for grouping multiple settings together into a conceptual unit. This value # (and the conceptual separation) is not used very often; the main place where it will have a potential impact # is when code calls Settings#use method. See docs on that method for further details, but basically that method # just attempts to do any preparation that may be necessary before code attempts to leverage the value of a particular # setting. This has the most impact for file/directory settings, where #use will attempt to "ensure" those # files / directories. # @param [Hash[Hash]] defs the settings to be defined. This argument is a hash of hashes; each key should be a symbol, # which is basically the name of the setting that you are defining. The value should be another hash that specifies # the parameters for the particular setting. Legal values include: # [:default] => not required; this is the value for the setting if no other value is specified (via cli, config file, etc.) # For string settings this may include "variables", demarcated with $ or ${} which will be interpolated with values of other settings. # The default value may also be a Proc that will be called only once to evaluate the default when the setting's value is retrieved. # [:desc] => required; a description of the setting, used in documentation / help generation # [:type] => not required, but highly encouraged! This specifies the data type that the setting represents. If # you do not specify it, it will default to "string". Legal values include: # :string - A generic string setting # :boolean - A boolean setting; values are expected to be "true" or "false" # :file - A (single) file path; puppet may attempt to create this file depending on how the settings are used. This type # also supports additional options such as "mode", "owner", "group" # :directory - A (single) directory path; puppet may attempt to create this file depending on how the settings are used. This type # also supports additional options such as "mode", "owner", "group" # :path - This is intended to be used for settings whose value can contain multiple directory paths, respresented # as strings separated by the system path separator (e.g. system path, module path, etc.). # [:mode] => an (optional) octal value to be used as the permissions/mode for :file and :directory settings # [:owner] => optional owner username/uid for :file and :directory settings # [:group] => optional group name/gid for :file and :directory settings # def define_settings(section, defs) section = section.to_sym call = [] defs.each do |name, hash| raise ArgumentError, "setting definition for '#{name}' is not a hash!" unless hash.is_a? Hash name = name.to_sym hash[:name] = name hash[:section] = section raise ArgumentError, "Setting #{name} is already defined" if @config.include?(name) tryconfig = newsetting(hash) if short = tryconfig.short if other = @shortnames[short] raise ArgumentError, "Setting #{other.name} is already using short name '#{short}'" end @shortnames[short] = tryconfig end @config[name] = tryconfig # Collect the settings that need to have their hooks called immediately. # We have to collect them so that we can be sure we're fully initialized before # the hook is called. if tryconfig.has_hook? if tryconfig.call_hook_on_define? call << tryconfig elsif tryconfig.call_hook_on_initialize? @hooks_to_call_on_application_initialization << tryconfig end end @deprecated_setting_names << name if tryconfig.deprecated? end call.each do |setting| setting.handle(self.value(setting.name)) end end # Convert the settings we manage into a catalog full of resources that model those settings. def to_catalog(*sections) sections = nil if sections.empty? catalog = Puppet::Resource::Catalog.new("Settings", Puppet::Node::Environment::NONE) @config.keys.find_all { |key| @config[key].is_a?(FileSetting) }.each do |key| - next if (key == :manifestdir && should_skip_manifestdir?()) file = @config[key] + next if file.value.nil? next unless (sections.nil? or sections.include?(file.section)) next unless resource = file.to_resource next if catalog.resource(resource.ref) Puppet.debug("Using settings: adding file resource '#{key}': '#{resource.inspect}'") catalog.add_resource(resource) end add_user_resources(catalog, sections) add_environment_resources(catalog, sections) catalog end - def should_skip_manifestdir?() - setting = @config[:environmentpath] - !(setting.nil? || setting.value.nil? || setting.value.empty?) - end - - private :should_skip_manifestdir? - # Convert our list of config settings into a configuration file. def to_config str = %{The configuration file for #{Puppet.run_mode.name}. Note that this file is likely to have unused settings in it; any setting that's valid anywhere in Puppet can be in any config file, even if it's not used. Every section can specify three special parameters: owner, group, and mode. These parameters affect the required permissions of any files specified after their specification. Puppet will sometimes use these parameters to check its own configured state, so they can be used to make Puppet a bit more self-managing. The file format supports octothorpe-commented lines, but not partial-line comments. Generated on #{Time.now}. }.gsub(/^/, "# ") # Add a section heading that matches our name. str += "[#{preferred_run_mode}]\n" eachsection do |section| persection(section) do |obj| str += obj.to_config + "\n" unless obj.name == :genconfig end end return str end # Convert to a parseable manifest def to_manifest catalog = to_catalog catalog.resource_refs.collect do |ref| catalog.resource(ref).to_manifest end.join("\n\n") end # Create the necessary objects to use a section. This is idempotent; # you can 'use' a section as many times as you want. def use(*sections) sections = sections.collect { |s| s.to_sym } sections = sections.reject { |s| @used.include?(s) } return if sections.empty? begin catalog = to_catalog(*sections).to_ral rescue => detail Puppet.log_and_raise(detail, "Could not create resources for managing Puppet's files and directories in sections #{sections.inspect}: #{detail}") end catalog.host_config = false catalog.apply do |transaction| if transaction.any_failed? report = transaction.report status_failures = report.resource_statuses.values.select { |r| r.failed? } status_fail_msg = status_failures. collect(&:events). flatten. select { |event| event.status == 'failure' }. collect { |event| "#{event.resource}: #{event.message}" }.join("; ") raise "Got #{status_failures.length} failure(s) while initializing: #{status_fail_msg}" end end sections.each { |s| @used << s } @used.uniq! end def valid?(param) param = param.to_sym @config.has_key?(param) end def uninterpolated_value(param, environment = nil) Puppet.deprecation_warning("Puppet.settings.uninterpolated_value is deprecated. Use Puppet.settings.value instead") param = param.to_sym environment &&= environment.to_sym values(environment, self.preferred_run_mode).lookup(param) end # Retrieve an object that can be used for looking up values of configuration # settings. # # @param environment [Symbol] The name of the environment in which to lookup # @param section [Symbol] The name of the configuration section in which to lookup # @return [Puppet::Settings::ChainedValues] An object to perform lookups # @api public def values(environment, section) @values[environment][section] ||= ChainedValues.new( section, environment, value_sets_for(environment, section), @config) end # Find the correct value using our search path. # # @param param [String, Symbol] The value to look up # @param environment [String, Symbol] The environment to check for the value # @param bypass_interpolation [true, false] Whether to skip interpolation # # @return [Object] The looked up value # # @raise [InterpolationError] def value(param, environment = nil, bypass_interpolation = false) param = param.to_sym environment &&= environment.to_sym setting = @config[param] # Short circuit to nil for undefined settings. return nil if setting.nil? # Check the cache first. It needs to be a per-environment # cache so that we don't spread values from one env # to another. if @cache[environment||"none"].has_key?(param) return @cache[environment||"none"][param] elsif bypass_interpolation val = values(environment, self.preferred_run_mode).lookup(param) else val = values(environment, self.preferred_run_mode).interpolate(param) end @cache[environment||"none"][param] = val val end ## # (#15337) All of the logic to determine the configuration file to use # should be centralized into this method. The simplified approach is: # # 1. If there is an explicit configuration file, use that. (--confdir or # --config) # 2. If we're running as a root process, use the system puppet.conf # (usually /etc/puppet/puppet.conf) # 3. Otherwise, use the user puppet.conf (usually ~/.puppet/puppet.conf) # # @api private # @todo this code duplicates {Puppet::Util::RunMode#which_dir} as described # in {http://projects.puppetlabs.com/issues/16637 #16637} def which_configuration_file if explicit_config_file? or Puppet.features.root? then return main_config_file else return user_config_file end end # This method just turns a file into a new ConfigFile::Conf instance # @param file [String] absolute path to the configuration file # @return [Puppet::Settings::ConfigFile::Conf] # @api private - def parse_file(file) - @config_file_parser.parse_file(file, read_file(file)) + def parse_file(file, allowed_sections = []) + @config_file_parser.parse_file(file, read_file(file), allowed_sections) end private DEPRECATION_REFS = { - [:manifest, :modulepath, :config_version, :templatedir, :manifestdir] => - "See http://links.puppetlabs.com/env-settings-deprecations" + # intentionally empty. This could be repopulated if we deprecate more settings + # and have reference links to associate with them }.freeze + def screen_non_puppet_conf_settings(puppet_conf) + puppet_conf.sections.values.each do |section| + forbidden = section.settings.select { |setting| Puppet::Settings::EnvironmentConf::ENVIRONMENT_CONF_ONLY_SETTINGS.include?(setting.name) } + raise(SettingsError, "Cannot set #{forbidden.map { |s| s.name }.join(", ")} settings in puppet.conf") if !forbidden.empty? + end + end + # Record that we want to issue a deprecation warning later in the application # initialization cycle when we have settings bootstrapped to the point where # we can read the Puppet[:disable_warnings] setting. # # We are only recording warnings applicable to settings set in puppet.conf # itself. def record_deprecations_from_puppet_conf(puppet_conf) - conf_sections = puppet_conf.sections.inject([]) do |accum,entry| - accum << entry[1] if [:main, :master, :agent, :user].include?(entry[0]) - accum - end - - conf_sections.each do |section| + puppet_conf.sections.values.each do |section| section.settings.each do |conf_setting| if setting = self.setting(conf_setting.name) @deprecated_settings_that_have_been_configured << setting if setting.deprecated? end end end end def issue_deprecations @deprecated_settings_that_have_been_configured.each do |setting| issue_deprecation_warning(setting) end end def issue_deprecation_warning(setting, msg = nil) name = setting.name ref = DEPRECATION_REFS.find { |params,reference| params.include?(name) } ref = ref[1] if ref case when msg msg << " #{ref}" if ref Puppet.deprecation_warning(msg) when setting.completely_deprecated? Puppet.deprecation_warning("Setting #{name} is deprecated. #{ref}", "setting-#{name}") when setting.allowed_on_commandline? Puppet.deprecation_warning("Setting #{name} is deprecated in puppet.conf. #{ref}", "puppet-conf-setting-#{name}") end end def get_config_file_default(default) obj = nil unless obj = @config[default] raise ArgumentError, "Unknown default #{default}" end raise ArgumentError, "Default #{default} is not a file" unless obj.is_a? FileSetting obj end def add_environment_resources(catalog, sections) path = self[:environmentpath] envdir = path.split(File::PATH_SEPARATOR).first if path configured_environment = self[:environment] if configured_environment == "production" && envdir && Puppet::FileSystem.exist?(envdir) configured_environment_path = File.join(envdir, configured_environment) if !Puppet::FileSystem.symlink?(configured_environment_path) catalog.add_resource( Puppet::Resource.new(:file, configured_environment_path, :parameters => { :ensure => 'directory' }) ) end end end def add_user_resources(catalog, sections) return unless Puppet.features.root? return if Puppet.features.microsoft_windows? return unless self[:mkusers] @config.each do |name, setting| next unless setting.respond_to?(:owner) next unless sections.nil? or sections.include?(setting.section) if user = setting.owner and user != "root" and catalog.resource(:user, user).nil? resource = Puppet::Resource.new(:user, user, :parameters => {:ensure => :present}) resource[:gid] = self[:group] if self[:group] catalog.add_resource resource end if group = setting.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil? catalog.add_resource Puppet::Resource.new(:group, group, :parameters => {:ensure => :present}) end end end # Yield each search source in turn. def value_sets_for(environment, mode) - searchpath(environment).collect do |name| - case name - when :cli, :memory, :application_defaults, :overridden_defaults - @value_sets[name] - when :run_mode - if @configuration_file - section = @configuration_file.sections[mode] - if section - ValuesFromSection.new(mode, section) - end + searchpath(environment, mode).collect do |source| + case source.type + when :values + @value_sets[source.name] + when :section + if @configuration_file && section = @configuration_file.sections[source.name] + ValuesFromSection.new(source.name, section) end + when :environment + ValuesFromEnvironmentConf.new(source.name) else - values_from_section = nil - if @configuration_file - if section = @configuration_file.sections[name] - values_from_section = ValuesFromSection.new(name, section) - end - end - if values_from_section.nil? && global_defaults_initialized? - values_from_section = ValuesFromEnvironmentConf.new(name) - end - values_from_section + raise(Puppet::DevError, "Unknown searchpath case: #{source.type} for the #{source} settings path element.") end end.compact end # Read the file in. # @api private def read_file(file) return Puppet::FileSystem.read(file) end # Private method for internal test use only; allows to do a comprehensive clear of all settings between tests. # # @return nil def clear_everything_for_tests() unsafe_clear(true, true) @configuration_file = nil @global_defaults_initialized = false @app_defaults_initialized = false end private :clear_everything_for_tests def explicit_config_file? # Figure out if the user has provided an explicit configuration file. If # so, return the path to the file, if not return nil. # # The easiest way to determine whether an explicit one has been specified # is to simply attempt to evaluate the value of ":config". This will # obviously be successful if they've passed an explicit value for :config, # but it will also result in successful interpolation if they've only # passed an explicit value for :confdir. # # If they've specified neither, then the interpolation will fail and we'll # get an exception. # begin return true if self[:config] rescue InterpolationError # This means we failed to interpolate, which means that they didn't # explicitly specify either :config or :confdir... so we'll fall out to # the default value. return false end end private :explicit_config_file? # Lookup configuration setting value through a chain of different value sources. # # @api public class ChainedValues ENVIRONMENT_SETTING = "environment".freeze ENVIRONMENT_INTERPOLATION_ALLOWED = ['config_version'].freeze # @see Puppet::Settings.values # @api private def initialize(mode, environment, value_sets, defaults) @mode = mode @environment = environment @value_sets = value_sets @defaults = defaults end # Lookup the uninterpolated value. # # @param name [Symbol] The configuration setting name to look up # @return [Object] The configuration setting value or nil if the setting is not known # @api public def lookup(name) set = @value_sets.find do |set| set.include?(name) end if set value = set.lookup(name) if !value.nil? return value end end @defaults[name].default end # Lookup the interpolated value. All instances of `$name` in the value will # be replaced by performing a lookup of `name` and substituting the text # for `$name` in the original value. This interpolation is only performed # if the looked up value is a String. # # @param name [Symbol] The configuration setting name to look up # @return [Object] The configuration setting value or nil if the setting is not known # @api public def interpolate(name) setting = @defaults[name] if setting val = lookup(name) # if we interpolate code, all hell breaks loose. if name == :code val else # Convert it if necessary begin val = convert(val, name) rescue InterpolationError => err # This happens because we don't have access to the param name when the # exception is originally raised, but we want it in the message raise InterpolationError, "Error converting value for param '#{name}': #{err}", err.backtrace end setting.munge(val) end else nil end end private def convert(value, setting_name) case value when nil nil when String failed_environment_interpolation = false interpolated_value = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |expression| varname = $2 || $1 interpolated_expression = if varname != ENVIRONMENT_SETTING || ok_to_interpolate_environment(setting_name) if varname == ENVIRONMENT_SETTING && @environment @environment elsif varname == "run_mode" @mode elsif !(pval = interpolate(varname.to_sym)).nil? pval else raise InterpolationError, "Could not find value for #{expression}" end else failed_environment_interpolation = true expression end interpolated_expression end if failed_environment_interpolation Puppet.warning("You cannot interpolate $environment within '#{setting_name}' when using directory environments. Its value will remain #{interpolated_value}.") end interpolated_value else value end end def ok_to_interpolate_environment(setting_name) - return true if Puppet.settings.value(:environmentpath, nil, true).empty? - ENVIRONMENT_INTERPOLATION_ALLOWED.include?(setting_name.to_s) end end class Values extend Forwardable + attr_reader :name + def initialize(name, defaults) @name = name @values = {} @defaults = defaults end def_delegator :@values, :include? def_delegator :@values, :[], :lookup def set(name, value) default = @defaults[name] if !default raise ArgumentError, "Attempt to assign a value to unknown setting #{name.inspect}" end if default.has_hook? default.handle(value) end @values[name] = value end + + def inspect + %Q{<#{self.class}:#{self.object_id} @name="#{@name}" @values="#{@values}">} + end end class ValuesFromSection + attr_reader :name + def initialize(name, section) @name = name @section = section end def include?(name) !@section.setting(name).nil? end def lookup(name) setting = @section.setting(name) if setting setting.value end end + + def inspect + %Q{<#{self.class}:#{self.object_id} @name="#{@name}" @section="#{@section}">} + end end # @api private class ValuesFromEnvironmentConf def initialize(environment_name) @environment_name = environment_name end + def name + @environment_name + end + def include?(name) if Puppet::Settings::EnvironmentConf::VALID_SETTINGS.include?(name) && conf return true end false end def lookup(name) return nil unless Puppet::Settings::EnvironmentConf::VALID_SETTINGS.include?(name) conf.send(name) if conf end def conf @conf ||= if environments = Puppet.lookup(:environments) environments.get_conf(@environment_name) end end + + def inspect + %Q{<#{self.class}:#{self.object_id} @environment_name="#{@environment_name}" @conf="#{@conf}">} + end end end diff --git a/lib/puppet/settings/base_setting.rb b/lib/puppet/settings/base_setting.rb index 232ec3a9a..6fcb47f78 100644 --- a/lib/puppet/settings/base_setting.rb +++ b/lib/puppet/settings/base_setting.rb @@ -1,167 +1,171 @@ require 'puppet/settings/errors' # The base setting type class Puppet::Settings::BaseSetting attr_accessor :name, :desc, :section, :default, :call_hook attr_reader :short, :deprecated def self.available_call_hook_values [:on_define_and_write, :on_initialize_and_write, :on_write_only] end def call_hook=(value) if value.nil? Puppet.warning "Setting :#{name} :call_hook is nil, defaulting to :on_write_only" value = :on_write_only end raise ArgumentError, "Invalid option #{value} for call_hook" unless self.class.available_call_hook_values.include? value @call_hook = value end def call_hook_on_define? call_hook == :on_define_and_write end def call_hook_on_initialize? call_hook == :on_initialize_and_write end # get the arguments in getopt format def getopt_args if short [["--#{name}", "-#{short}", GetoptLong::REQUIRED_ARGUMENT]] else [["--#{name}", GetoptLong::REQUIRED_ARGUMENT]] end end # get the arguments in OptionParser format def optparse_args if short ["--#{name}", "-#{short}", desc, :REQUIRED] else ["--#{name}", desc, :REQUIRED] end end def hook=(block) @has_hook = true meta_def :handle, &block end def has_hook? @has_hook end # Create the new element. Pretty much just sets the name. def initialize(args = {}) unless @settings = args.delete(:settings) raise ArgumentError.new("You must refer to a settings object") end # explicitly set name prior to calling other param= methods to provide meaningful feedback during # other warnings @name = args[:name] if args.include? :name #set the default value for call_hook @call_hook = :on_write_only if args[:hook] and not args[:call_hook] @has_hook = false raise ArgumentError, "Cannot reference :call_hook for :#{@name} if no :hook is defined" if args[:call_hook] and not args[:hook] args.each do |param, value| method = param.to_s + "=" raise ArgumentError, "#{self.class} (setting '#{args[:name]}') does not accept #{param}" unless self.respond_to? method self.send(method, value) end raise ArgumentError, "You must provide a description for the #{self.name} config option" unless self.desc end def iscreated @iscreated = true end def iscreated? @iscreated end # short name for the celement def short=(value) raise ArgumentError, "Short names can only be one character." if value.to_s.length != 1 @short = value.to_s end def default(check_application_defaults_first = false) if @default.is_a? Proc @default = @default.call end return @default unless check_application_defaults_first return @settings.value(name, :application_defaults, true) || @default end # Convert the object to a config statement. def to_config require 'puppet/util/docs' # Scrub any funky indentation; comment out description. str = Puppet::Util::Docs.scrub(@desc).gsub(/^/, "# ") + "\n" # Add in a statement about the default. str << "# The default value is '#{default(true)}'.\n" if default(true) # If the value has not been overridden, then print it out commented # and unconverted, so it's clear that that's the default and how it # works. value = @settings.value(self.name) if value != @default line = "#{@name} = #{value}" else line = "# #{@name} = #{@default}" end str << (line + "\n") # Indent str.gsub(/^/, " ") end # @param bypass_interpolation [Boolean] Set this true to skip the # interpolation step, returning the raw setting value. Defaults to false. # @return [String] Retrieves the value, or if it's not set, retrieves the default. # @api public def value(bypass_interpolation = false) @settings.value(self.name, nil, bypass_interpolation) end # Modify the value when it is first evaluated def munge(value) value end def set_meta(meta) Puppet.notice("#{name} does not support meta data. Ignoring.") end def deprecated=(deprecation) raise(ArgumentError, "'#{deprecation}' is an unknown setting deprecation state. Must be either :completely or :allowed_on_commandline") unless [:completely, :allowed_on_commandline].include?(deprecation) @deprecated = deprecation end def deprecated? !!@deprecated end # True if we should raise a deprecation_warning if the setting is submitted # on the commandline or is set in puppet.conf. def completely_deprecated? @deprecated == :completely end # True if we should raise a deprecation_warning if the setting is found in # puppet.conf, but not if the user sets it on the commandline def allowed_on_commandline? @deprecated == :allowed_on_commandline end + + def inspect + %Q{<#{self.class}:#{self.object_id} @name="#{@name}" @section="#{@section}" @default="#{@default}" @call_hook="#{@call_hook}">} + end end diff --git a/lib/puppet/settings/config_file.rb b/lib/puppet/settings/config_file.rb index e53e90018..36543f030 100644 --- a/lib/puppet/settings/config_file.rb +++ b/lib/puppet/settings/config_file.rb @@ -1,139 +1,142 @@ require 'puppet/settings/ini_file' ## # @api private # # Parses puppet configuration files # class Puppet::Settings::ConfigFile - ALLOWED_SECTION_NAMES = ['main', 'master', 'agent', 'user'].freeze - ## # @param value_converter [Proc] a function that will convert strings into ruby types - # def initialize(value_converter) @value_converter = value_converter end - def parse_file(file, text) + # @param file [String, File] pointer to the file whose text we are parsing + # @param text [String] the actual text of the inifile to be parsed + # @param allowed_section_names [Array] an optional array of accepted section + # names; if this list is non-empty, sections outside of it will raise an + # error. + # @return A Struct with a +sections+ array representing each configuration section + def parse_file(file, text, allowed_section_names = []) result = Conf.new + if !allowed_section_names.empty? + allowed_section_names << 'main' unless allowed_section_names.include?('main') + end ini = Puppet::Settings::IniFile.parse(StringIO.new(text)) - unique_sections_in(ini, file).each do |section_name| + unique_sections_in(ini, file, allowed_section_names).each do |section_name| section = Section.new(section_name.to_sym) result.with_section(section) ini.lines_in(section_name).each do |line| if line.is_a?(Puppet::Settings::IniFile::SettingLine) parse_setting(line, section) elsif line.text !~ /^\s*#|^\s*$/ raise Puppet::Settings::ParseError.new("Could not match line #{line.text}", file, line.line_number) end end end result end Conf = Struct.new(:sections) do def initialize super({}) end def with_section(section) sections[section.name] = section self end end Section = Struct.new(:name, :settings) do def initialize(name) super(name, []) end def with_setting(name, value, meta) settings << Setting.new(name, value, meta) self end def setting(name) settings.find { |setting| setting.name == name } end end Setting = Struct.new(:name, :value, :meta) do def has_metadata? meta != NO_META end end Meta = Struct.new(:owner, :group, :mode) NO_META = Meta.new(nil, nil, nil) private - def unique_sections_in(ini, file) + def unique_sections_in(ini, file, allowed_section_names) ini.section_lines.collect do |section| - if section.name == "application_defaults" || section.name == "global_defaults" - raise Puppet::Error, "Illegal section '#{section.name}' in config file #{file} at line #{section.line_number}" - end - if !ALLOWED_SECTION_NAMES.include?(section.name) - Puppet.deprecation_warning("Sections other than #{ALLOWED_SECTION_NAMES.join(', ')} are deprecated in puppet.conf. Please use the directory environments feature to specify environments. (See http://docs.puppetlabs.com/puppet/latest/reference/environments.html)") + if !allowed_section_names.empty? && !allowed_section_names.include?(section.name) + raise(Puppet::Error, "Illegal section '#{section.name}' in config file #{file} at line #{section.line_number}. The only valid puppet.conf sections are: [#{allowed_section_names.join(", ")}]. Please use the directory environments feature to specify environments. (See http://docs.puppetlabs.com/puppet/latest/reference/environments.html)") end section.name end.uniq end def parse_setting(setting, section) var = setting.name.intern # We don't want to munge modes, because they're specified in octal, so we'll # just leave them as a String, since Puppet handles that case correctly. if var == :mode value = setting.value else value = @value_converter[setting.value] end # Check to see if this is a file argument and it has extra options begin if value.is_a?(String) and options = extract_fileinfo(value) section.with_setting(var, options[:value], Meta.new(options[:owner], options[:group], options[:mode])) else section.with_setting(var, value, NO_META) end rescue Puppet::Error => detail raise Puppet::Settings::ParseError.new(detail.message, file, setting.line_number, detail) end end def empty_section { :_meta => {} } end def extract_fileinfo(string) result = {} value = string.sub(/\{\s*([^}]+)\s*\}/) do params = $1 params.split(/\s*,\s*/).each do |str| if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/ param, value = $1.intern, $2 result[param] = value raise ArgumentError, "Invalid file option '#{param}'" unless [:owner, :mode, :group].include?(param) if param == :mode and value !~ /^\d+$/ raise ArgumentError, "File modes must be numbers" end else raise ArgumentError, "Could not parse '#{string}'" end end '' end result[:value] = value.sub(/\s*$/, '') result end end diff --git a/lib/puppet/settings/environment_conf.rb b/lib/puppet/settings/environment_conf.rb index 49d45878c..ec28cc809 100644 --- a/lib/puppet/settings/environment_conf.rb +++ b/lib/puppet/settings/environment_conf.rb @@ -1,180 +1,182 @@ # Configuration settings for a single directory Environment. # @api private class Puppet::Settings::EnvironmentConf - VALID_SETTINGS = [:modulepath, :manifest, :config_version, :environment_timeout, :environment_data_provider].freeze + ENVIRONMENT_CONF_ONLY_SETTINGS = [:modulepath, :manifest, :config_version].freeze + VALID_SETTINGS = (ENVIRONMENT_CONF_ONLY_SETTINGS + [:environment_timeout, :environment_data_provider]).freeze # Given a path to a directory environment, attempts to load and parse an # environment.conf in ini format, and return an EnvironmentConf instance. # # An environment.conf is optional, so if the file itself is missing, or # empty, an EnvironmentConf with default values will be returned. # # @note logs warnings if the environment.conf contains any ini sections, # or has settings other than the three handled for directory environments # (:manifest, :modulepath, :config_version) # # @param path_to_env [String] path to the directory environment # @param global_module_path [Array] the installation's base modulepath # setting, appended to default environment modulepaths # @return [EnvironmentConf] the parsed EnvironmentConf object def self.load_from(path_to_env, global_module_path) path_to_env = File.expand_path(path_to_env) conf_file = File.join(path_to_env, 'environment.conf') config = nil begin config = Puppet.settings.parse_file(conf_file) validate(conf_file, config) section = config.sections[:main] rescue Errno::ENOENT # environment.conf is an optional file end new(path_to_env, section, global_module_path) end # Provides a configuration object tied directly to the passed environment. # Configuration values are exactly those returned by the environment object, # without interpolation. This is a special case for the default configured # environment returned by the Puppet::Environments::StaticPrivate loader. def self.static_for(environment, environment_timeout = 0) Static.new(environment, environment_timeout) end attr_reader :section, :path_to_env, :global_modulepath # Create through EnvironmentConf.load_from() def initialize(path_to_env, section, global_module_path) @path_to_env = path_to_env @section = section @global_module_path = global_module_path end def manifest puppet_conf_manifest = Pathname.new(Puppet.settings.value(:default_manifest)) disable_per_environment_manifest = Puppet.settings.value(:disable_per_environment_manifest) fallback_manifest_directory = if puppet_conf_manifest.absolute? puppet_conf_manifest.to_s else File.join(@path_to_env, puppet_conf_manifest.to_s) end if disable_per_environment_manifest environment_conf_manifest = absolute(raw_setting(:manifest)) if environment_conf_manifest && fallback_manifest_directory != environment_conf_manifest errmsg = ["The 'disable_per_environment_manifest' setting is true, but the", "environment located at #{@path_to_env} has a manifest setting in its", "environment.conf of '#{environment_conf_manifest}' which does not match", "the default_manifest setting '#{puppet_conf_manifest}'. If this", "environment is expecting to find modules in", "'#{environment_conf_manifest}', they will not be available!"] Puppet.err(errmsg.join(' ')) end fallback_manifest_directory.to_s else get_setting(:manifest, fallback_manifest_directory) do |manifest| absolute(manifest) end end end def environment_timeout # gen env specific config or use the default value get_setting(:environment_timeout, Puppet.settings.value(:environment_timeout)) do |ttl| # munges the string form statically without really needed the settings system, only # its ability to munge "4s, 3m, 5d, and 'unlimited' into seconds - if already munged into # numeric form, the TTLSetting handles that. Puppet::Settings::TTLSetting.munge(ttl, 'environment_timeout') end end def environment_data_provider get_setting(:environment_data_provider, Puppet.settings.value(:environment_data_provider)) do |value| value end end def modulepath default_modulepath = [File.join(@path_to_env, "modules")] + @global_module_path get_setting(:modulepath, default_modulepath) do |modulepath| path = modulepath.kind_of?(String) ? modulepath.split(File::PATH_SEPARATOR) : modulepath path.map { |p| absolute(p) }.join(File::PATH_SEPARATOR) end end def config_version get_setting(:config_version) do |config_version| absolute(config_version) end end def raw_setting(setting_name) setting = section.setting(setting_name) if section setting.value if setting end private def self.validate(path_to_conf_file, config) valid = true section_keys = config.sections.keys main = config.sections[:main] if section_keys.size > 1 Puppet.warning("Invalid sections in environment.conf at '#{path_to_conf_file}'. Environment conf may not have sections. The following sections are being ignored: '#{(section_keys - [:main]).join(',')}'") valid = false end extraneous_settings = main.settings.map(&:name) - VALID_SETTINGS if !extraneous_settings.empty? Puppet.warning("Invalid settings in environment.conf at '#{path_to_conf_file}'. The following unknown setting(s) are being ignored: #{extraneous_settings.join(', ')}") valid = false end return valid end def get_setting(setting_name, default = nil) value = raw_setting(setting_name) value ||= default yield value end def absolute(path) return nil if path.nil? if path =~ /^\$/ # Path begins with $something interpolatable path else File.expand_path(path, @path_to_env) end end # Models configuration for an environment that is not loaded from a directory. # # @api private class Static - attr_reader :environment_timeout + attr_reader :environment_timeout, :environment_data_provider - def initialize(environment, environment_timeout) + def initialize(environment, environment_timeout, environment_data_provider = 'none') @environment = environment @environment_timeout = environment_timeout + @environment_data_provider = environment_data_provider end def manifest @environment.manifest end def modulepath @environment.modulepath.join(File::PATH_SEPARATOR) end def config_version @environment.config_version end end end diff --git a/lib/puppet/test/test_helper.rb b/lib/puppet/test/test_helper.rb index 960cbf128..417cbbe6d 100644 --- a/lib/puppet/test/test_helper.rb +++ b/lib/puppet/test/test_helper.rb @@ -1,234 +1,240 @@ require 'puppet/indirector/data_binding/hiera' require 'tmpdir' require 'fileutils' module Puppet::Test # This class is intended to provide an API to be used by external projects # when they are running tests that depend on puppet core. This should # allow us to vary the implementation details of managing puppet's state # for testing, from one version of puppet to the next--without forcing # the external projects to do any of that state management or be aware of # the implementation details. # # For now, this consists of a few very simple signatures. The plan is # that it should be the responsibility of the puppetlabs_spec_helper # to broker between external projects and this API; thus, if any # hacks are required (e.g. to determine whether or not a particular) # version of puppet supports this API, those hacks will be consolidated in # one place and won't need to be duplicated in every external project. # # This should also alleviate the anti-pattern that we've been following, # wherein each external project starts off with a copy of puppet core's # test_helper.rb and is exposed to risk of that code getting out of # sync with core. # # Since this class will be "library code" that ships with puppet, it does # not use API from any existing test framework such as rspec. This should # theoretically allow it to be used with other unit test frameworks in the # future, if desired. # # Note that in the future this API could potentially be expanded to handle # other features such as "around_test", but we didn't see a compelling # reason to deal with that right now. class TestHelper # Call this method once, as early as possible, such as before loading tests # that call Puppet. # @return nil def self.initialize() # This meta class instance variable is used as a guard to ensure that # before_each, and after_each are only called once. This problem occurs # when there are more than one puppet test infrastructure "orchestrator in us. # The use of both puppetabs-spec_helper, and rodjek-rspec_puppet will cause # two resets of the puppet environment, and will cause problem rolling back to # a known point as there is no way to differentiate where the calls are coming # from. See more information in #before_each_test, and #after_each_test # Note that the variable is only initialized to 0 if nil. This is important # as more than one orchestrator will call initialize. A second call can not # simply set it to 0 since that would potentially destroy an active guard. # @@reentry_count ||= 0 + @environmentpath = Dir.mktmpdir('environments') + Dir.mkdir("#{@environmentpath}/production") owner = Process.pid Puppet.push_context(Puppet.base_context({ - :environmentpath => "", + :environmentpath => @environmentpath, :basemodulepath => "", - :manifest => "/dev/null" }), "Initial for specs") Puppet::Parser::Functions.reset + + ObjectSpace.define_finalizer(Puppet.lookup(:environments), proc { + if Process.pid == owner + FileUtils.rm_rf(@environmentpath) + end + }) end # Call this method once, when beginning a test run--prior to running # any individual tests. # @return nil def self.before_all_tests() # Make sure that all of the setup is also done for any before(:all) blocks end # Call this method once, at the end of a test run, when no more tests # will be run. # @return nil def self.after_all_tests() end # The name of the rollback mark used in the Puppet.context. This is what # the test infrastructure returns to for each test. # ROLLBACK_MARK = "initial testing state" # Call this method once per test, prior to execution of each invididual test. # @return nil def self.before_each_test() # When using both rspec-puppet and puppet-rspec-helper, there are two packages trying # to be helpful and orchestrate the callback sequence. We let only the first win, the # second callback results in a no-op. # Likewise when entering after_each_test(), a check is made to make tear down happen # only once. # return unless @@reentry_count == 0 @@reentry_count = 1 Puppet.mark_context(ROLLBACK_MARK) # We need to preserve the current state of all our indirection cache and # terminus classes. This is pretty important, because changes to these # are global and lead to order dependencies in our testing. # # We go direct to the implementation because there is no safe, sane public # API to manage restoration of these to their default values. This # should, once the value is proved, be moved to a standard API on the # indirector. # # To make things worse, a number of the tests stub parts of the # indirector. These stubs have very specific expectations that what # little of the public API we could use is, well, likely to explode # randomly in some tests. So, direct access. --daniel 2011-08-30 $saved_indirection_state = {} indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections) indirections.each do |indirector| $saved_indirection_state[indirector.name] = { :@terminus_class => indirector.instance_variable_get(:@terminus_class), :@cache_class => indirector.instance_variable_get(:@cache_class) } end # The process environment is a shared, persistent resource. $old_env = ENV.to_hash # So is the load_path $old_load_path = $LOAD_PATH.dup initialize_settings_before_each() Puppet.push_context( { :trusted_information => Puppet::Context::TrustedInformation.new('local', 'testing', {}), }, "Context for specs") Puppet::Parser::Functions.reset - Puppet::Node::Environment.clear Puppet::Application.clear! Puppet::Util::Profiler.clear Puppet.clear_deprecation_warnings Puppet::DataBinding::Hiera.instance_variable_set("@hiera", nil) end # Call this method once per test, after execution of each individual test. # @return nil def self.after_each_test() # Ensure that a matching tear down only happens once per completed setup # (see #before_each_test). return unless @@reentry_count == 1 @@reentry_count = 0 Puppet.settings.send(:clear_everything_for_tests) Puppet::Util::Storage.clear Puppet::Util::ExecutionStub.reset Puppet.clear_deprecation_warnings # uncommenting and manipulating this can be useful when tracking down calls to deprecated code #Puppet.log_deprecations_to_file("deprecations.txt", /^Puppet::Util.exec/) # Restore the indirector configuration. See before hook. indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections) indirections.each do |indirector| $saved_indirection_state.fetch(indirector.name, {}).each do |variable, value| indirector.instance_variable_set(variable, value) end end $saved_indirection_state = nil # Restore the global process environment. Can't just assign because this # is a magic variable, sadly, and doesn't do that™. It is sufficiently # faster to use the compare-then-set model to avoid excessive work that it # justifies the complexity. --daniel 2012-03-15 unless ENV.to_hash == $old_env ENV.clear $old_env.each {|k, v| ENV[k] = v } end # Restore the load_path late, to avoid messing with stubs from the test. $LOAD_PATH.clear $old_load_path.each {|x| $LOAD_PATH << x } Puppet.rollback_context(ROLLBACK_MARK) end ######################################################################################### # PRIVATE METHODS (not part of the public TestHelper API--do not call these from outside # of this class!) ######################################################################################### def self.app_defaults_for_tests() { :logdir => "/dev/null", :confdir => "/dev/null", :vardir => "/dev/null", :rundir => "/dev/null", :hiera_config => "/dev/null", } end private_class_method :app_defaults_for_tests def self.initialize_settings_before_each() Puppet.settings.preferred_run_mode = "user" # Initialize "app defaults" settings to a good set of test values Puppet.settings.initialize_app_defaults(app_defaults_for_tests) # Avoid opening ports to the outside world Puppet.settings[:bindaddress] = "127.0.0.1" # We don't want to depend upon the reported domain name of the # machine running the tests, nor upon the DNS setup of that # domain. Puppet.settings[:use_srv_records] = false # Longer keys are secure, but they sure make for some slow testing - both # in terms of generating keys, and in terms of anything the next step down # the line doing validation or whatever. Most tests don't care how long # or secure it is, just that it exists, so these are better and faster # defaults, in testing only. # # I would make these even shorter, but OpenSSL doesn't support anything # below 512 bits. Sad, really, because a 0 bit key would be just fine. Puppet[:req_bits] = 512 Puppet[:keylength] = 512 # Although we setup a testing context during initialization, some tests # will end up creating their own context using the real context objects # and use the setting for the environments. In order to avoid those tests # having to deal with a missing environmentpath we can just set it right # here. Puppet[:environmentpath] = @environmentpath Puppet[:environment_timeout] = 0 end private_class_method :initialize_settings_before_each end end diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index ab2874684..c68723d8c 100644 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,241 +1,241 @@ require 'net/http' require 'uri' require 'tempfile' require 'puppet/util/checksums' require 'puppet/network/http' require 'puppet/network/http/compression' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums include Puppet::Network::HTTP::Compression.module attr_reader :actual_content desc <<-'EOT' The desired contents of a file, as a string. This attribute is mutually exclusive with `source` and `target`. Newlines and tabs can be specified in double-quoted strings using standard escaped syntax --- \n for a newline, and \t for a tab. With very small files, you can construct content strings directly in the manifest... define resolve(nameserver1, nameserver2, domain, search) { $str = "search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 " file { "/etc/resolv.conf": content => "$str", } } ...but for larger files, this attribute is more useful when combined with the [template](http://docs.puppetlabs.com/references/latest/function.html#template) or [file](http://docs.puppetlabs.com/references/latest/function.html#file) function. EOT # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '#{newvalue}'" elsif newvalue == :absent return "undefined content from '#{currentvalue}'" else return "content changed '#{currentvalue}' to '#{newvalue}'" end end def checksum_type if source = resource.parameter(:source) result = source.checksum else result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end def length (actual_content and actual_content.length) || 0 end def content self.should end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else if resource[:ensure] == :present and resource[:content] and s = resource.stat resource.warning "Ensure set to :present but file type is #{s.ftype} so no content will be synced" end return true end return true if ! @resource.replace? result = super if ! result and Puppet[:show_diff] and resource.show_diff? write_temporarily do |path| send @resource[:loglevel], "\n" + diff(@resource[:path], path) end end result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}", detail.backtrace end end # Make sure we're also managing the checksum property. def should=(value) # treat the value as a bytestring, in Ruby versions that support it, regardless of the encoding # in which it has been supplied value = value.dup.force_encoding(Encoding::ASCII_8BIT) if value.respond_to?(:force_encoding) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. @resource.write(:content) return_event end def write_temporarily tempfile = Tempfile.new("puppet-file") tempfile.open write(tempfile) tempfile.close yield tempfile.path tempfile.delete end def write(file) resource.parameter(:checksum).sum_stream { |sum| each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| sum << chunk file.print chunk } } end # the content is munged so if it's a checksum source_or_content is nil # unless the checksum indirectly comes from source def each_chunk_from(source_or_content) if source_or_content.is_a?(String) yield source_or_content elsif content_is_really_a_checksum? && source_or_content.nil? yield read_file_from_filebucket elsif source_or_content.nil? yield '' elsif Puppet[:default_file_terminus] == :file_server yield source_or_content.content elsif source_or_content.local? chunk_file_from_disk(source_or_content) { |chunk| yield chunk } else chunk_file_from_source(source_or_content) { |chunk| yield chunk } end end private def content_is_really_a_checksum? checksum?(should) end def chunk_file_from_disk(source_or_content) File.open(source_or_content.full_path, "rb") do |src| while chunk = src.read(8192) yield chunk end end end def get_from_source(source_or_content, &block) source = source_or_content.metadata.source - request = Puppet::Indirector::Request.new(:file_content, :find, source, nil, :environment => resource.catalog.environment) + request = Puppet::Indirector::Request.new(:file_content, :find, source, nil, :environment => resource.catalog.environment_instance) request.do_request(:fileserver) do |req| connection = Puppet::Network::HttpPool.http_instance(req.server, req.port) connection.request_get(Puppet::Network::HTTP::API::V1.indirection2uri(req), add_accept_encoding({"Accept" => "raw"}), &block) end end def chunk_file_from_source(source_or_content) get_from_source(source_or_content) do |response| case response.code when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } } else # Raise the http error if we didn't get a 'success' of some kind. message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" raise Net::HTTPError.new(message, response) end end end def read_file_from_filebucket raise "Could not get filebucket from file" unless dipper = resource.bucket sum = should.sub(/\{\w+\}/, '') dipper.getfile(sum) rescue => detail self.fail Puppet::Error, "Could not retrieve content for #{should} from filebucket: #{detail}", detail end end end diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index 8860f0e69..bb4ffcec6 100644 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,258 +1,258 @@ require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet::Type.type(:file).newparam(:source) do include Puppet::Util::Diff attr_accessor :source, :local desc <<-'EOT' A source file, which will be copied into place on the local system. Values can be URIs pointing to remote files, or fully qualified paths to files available on the local system (including files on NFS shares or Windows mapped drives). This attribute is mutually exclusive with `content` and `target`. The available URI schemes are *puppet* and *file*. *Puppet* URIs will retrieve files from Puppet's built-in file server, and are usually formatted as: `puppet:///modules/name_of_module/filename` This will fetch a file from a module on the puppet master (or from a local module when using puppet apply). Given a `modulepath` of `/etc/puppetlabs/puppet/modules`, the example above would resolve to `/etc/puppetlabs/puppet/modules/name_of_module/files/filename`. Unlike `content`, the `source` attribute can be used to recursively copy directories if the `recurse` attribute is set to `true` or `remote`. If a source directory contains symlinks, use the `links` attribute to specify whether to recreate links or follow them. Multiple `source` values can be specified as an array, and Puppet will use the first source that exists. This can be used to serve different files to different system types: file { "/etc/nfs.conf": source => [ "puppet:///modules/nfs/conf.$host", "puppet:///modules/nfs/conf.$operatingsystem", "puppet:///modules/nfs/conf" ] } Alternately, when serving directories recursively, multiple sources can be combined by setting the `sourceselect` attribute to `all`. EOT validate do |sources| sources = [sources] unless sources.is_a?(Array) sources.each do |source| next if Puppet::Util.absolute_path?(source) begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail Puppet::Error, "Could not understand source #{source}: #{detail}", detail end self.fail "Cannot use relative URLs '#{source}'" unless uri.absolute? self.fail "Cannot use opaque URLs '#{source}'" unless uri.hierarchical? self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless %w{file puppet}.include?(uri.scheme) end end SEPARATOR_REGEX = [Regexp.escape(File::SEPARATOR.to_s), Regexp.escape(File::ALT_SEPARATOR.to_s)].join munge do |sources| sources = [sources] unless sources.is_a?(Array) sources.map do |source| source = source.sub(/[#{SEPARATOR_REGEX}]+$/, '') if Puppet::Util.absolute_path?(source) URI.unescape(Puppet::Util.path_to_uri(source).to_s) else source end end end def change_to_s(currentvalue, newvalue) # newvalue = "{md5}#{@metadata.checksum}" if resource.property(:ensure).retrieve == :absent return "creating from source #{metadata.source} with contents #{metadata.checksum}" else return "replacing from source #{metadata.source} with contents #{metadata.checksum}" end end def checksum metadata && metadata.checksum end # Look up (if necessary) and return local content. def content return @content if @content raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source - unless tmp = Puppet::FileServing::Content.indirection.find(metadata.source, :environment => resource.catalog.environment, :links => resource[:links]) + unless tmp = Puppet::FileServing::Content.indirection.find(metadata.source, :environment => resource.catalog.environment_instance, :links => resource[:links]) self.fail "Could not find any content at %s" % metadata.source end @content = tmp.content end # Copy the values from the source to the resource. Yay. def copy_source_values devfail "Somehow got asked to copy source values without any metadata" unless metadata # conditionally copy :checksum if metadata.ftype != "directory" && !(metadata.ftype == "link" && metadata.links == :manage) copy_source_value(:checksum) end # Take each of the stats and set them as states on the local file # if a value has not already been provided. [:owner, :mode, :group].each do |metadata_method| next if metadata_method == :owner and !Puppet.features.root? next if metadata_method == :group and !Puppet.features.root? if Puppet.features.microsoft_windows? # Warn on Windows if source permissions are being used and the file resource # does not have mode owner and group all set (which would take precedence). if [:use, :use_when_creating].include?(resource[:source_permissions]) && (resource[:owner] == nil || resource[:group] == nil || resource[:mode] == nil) err_msg = "Copying %s from the source" << " file on Windows is not supported;" << " use source_permissions => ignore." self.fail Puppet::Error, err_msg % 'owner/mode/group' end # But never try to copy remote owner/group on Windows next if [:owner, :group].include?(metadata_method) && !local? end case resource[:source_permissions] when :ignore next when :use_when_creating next if Puppet::FileSystem.exist?(resource[:path]) end copy_source_value(metadata_method) end if resource[:ensure] == :absent # We know all we need to elsif metadata.ftype != "link" resource[:ensure] = metadata.ftype elsif resource[:links] == :follow resource[:ensure] = :present else resource[:ensure] = "link" resource[:target] = metadata.destination end end attr_writer :metadata # Provide, and retrieve if necessary, the metadata for this file. Fail # if we can't find data about this host, and fail if there are any # problems in our query. def metadata return @metadata if @metadata return nil unless value value.each do |source| begin options = { :environment => resource.catalog.environment_instance, :links => resource[:links], :source_permissions => resource[:source_permissions] } if data = Puppet::FileServing::Metadata.indirection.find(source, options) @metadata = data @metadata.source = source break end rescue => detail self.fail Puppet::Error, "Could not retrieve file metadata for #{source}: #{detail}", detail end end self.fail "Could not retrieve information from environment #{resource.catalog.environment} source(s) #{value.join(", ")}" unless @metadata @metadata end def local? found? and scheme == "file" end def full_path Puppet::Util.uri_to_path(uri) if found? end def server? uri and uri.host end def server (uri and uri.host) or Puppet.settings[:server] end def port (uri and uri.port) or Puppet.settings[:masterport] end def uri @uri ||= URI.parse(URI.escape(metadata.source)) end private def scheme (uri and uri.scheme) end def found? ! (metadata.nil? or metadata.ftype.nil?) end def copy_source_value(metadata_method) param_name = (metadata_method == :checksum) ? :content : metadata_method if resource[param_name].nil? or resource[param_name] == :absent value = metadata.send(metadata_method) # Force the mode value in file resources to be a string containing octal. value = value.to_s(8) if param_name == :mode && value.is_a?(Numeric) resource[param_name] = value end end end Puppet::Type.type(:file).newparam(:source_permissions) do desc <<-'EOT' Whether (and how) Puppet should copy owner, group, and mode permissions from the `source` to `file` resources when the permissions are not explicitly specified. (In all cases, explicit permissions will take precedence.) Valid values are `use`, `use_when_creating`, and `ignore`: * `ignore` (the default) will never apply the owner, group, or mode from the `source` when managing a file. When creating new files without explicit permissions, the permissions they receive will depend on platform-specific behavior. On POSIX, Puppet will use the umask of the user it is running as. On Windows, Puppet will use the default DACL associated with the user it is running as. * `use` will cause Puppet to apply the owner, group, and mode from the `source` to any files it is managing. * `use_when_creating` will only apply the owner, group, and mode from the `source` when creating a file; existing files will not have their permissions overwritten. EOT defaultto :ignore newvalues(:use, :use_when_creating, :ignore) end end diff --git a/lib/puppet/util/cacher.rb b/lib/puppet/util/cacher.rb deleted file mode 100644 index 24017de32..000000000 --- a/lib/puppet/util/cacher.rb +++ /dev/null @@ -1,74 +0,0 @@ -module Puppet::Util::Cacher - # Our module has been extended in a class; we can only add the Instance methods, - # which become *class* methods in the class. - def self.extended(other) - class << other - extend ClassMethods - include InstanceMethods - end - end - - # Our module has been included in a class, which means the class gets the class methods - # and all of its instances get the instance methods. - def self.included(other) - other.extend(ClassMethods) - other.send(:include, InstanceMethods) - end - - # Methods that can get added to a class. - module ClassMethods - # Provide a means of defining an attribute whose value will be cached. - # Must provide a block capable of defining the value if it's flushed.. - def cached_attr(name, ttl, &block) - init_method = "init_#{name}" - define_method(init_method, &block) - - set_attr_ttl(name, ttl) - - define_method(name) do - cached_value(name) - end - - define_method(name.to_s + "=") do |value| - # Make sure the cache timestamp is set - value_cache[name] = value - set_expiration(name) - end - end - - def attr_ttl(name) - @attr_ttls[name] - end - - def set_attr_ttl(name, value) - @attr_ttls ||= {} - @attr_ttls[name] = Integer(value) - end - end - - # Methods that get added to instances. - module InstanceMethods - private - - def cached_value(name) - if value_cache[name].nil? or expired_by_ttl?(name) - value_cache[name] = send("init_#{name}") - set_expiration(name) - end - value_cache[name] - end - - def expired_by_ttl?(name) - @attr_expirations[name] < Time.now - end - - def set_expiration(name) - @attr_expirations ||= {} - @attr_expirations[name] = Time.now + self.class.attr_ttl(name) - end - - def value_cache - @value_cache ||= {} - end - end -end diff --git a/spec/integration/directory_environments_spec.rb b/spec/integration/directory_environments_spec.rb index c0de08776..c4b923300 100644 --- a/spec/integration/directory_environments_spec.rb +++ b/spec/integration/directory_environments_spec.rb @@ -1,51 +1,51 @@ require 'spec_helper' describe "directory environments" do let(:args) { ['--configprint', 'modulepath', '--environment', 'direnv'] } let(:puppet) do app = Puppet::Application[:apply] app.stubs(:command_line).returns(stub('command_line', :args => [])) app end context "with a single directory environmentpath" do before(:each) do environmentdir = PuppetSpec::Files.tmpdir('envpath') Puppet[:environmentpath] = environmentdir FileUtils.mkdir_p(environmentdir + "/direnv/modules") end it "config prints the environments modulepath" do Puppet.settings.initialize_global_settings(args) expect do expect { puppet.run }.to exit_with(0) - end.to have_printed('direnv/modules') + end.to have_printed('/direnv/modules') end it "config prints the cli --modulepath despite environment" do - args << '--modulepath' << 'completely/different' + args << '--modulepath' << '/completely/different' Puppet.settings.initialize_global_settings(args) expect do expect { puppet.run }.to exit_with(0) - end.to have_printed('completely/different') + end.to have_printed('/completely/different') end end context "with an environmentpath having multiple directories" do let(:args) { ['--configprint', 'modulepath', '--environment', 'otherdirenv'] } before(:each) do envdir1 = File.join(Puppet[:confdir], 'env1') envdir2 = File.join(Puppet[:confdir], 'env2') Puppet[:environmentpath] = [envdir1, envdir2].join(File::PATH_SEPARATOR) FileUtils.mkdir_p(envdir2 + "/otherdirenv/modules") end it "config prints a directory environment modulepath" do Puppet.settings.initialize_global_settings(args) expect do expect { puppet.run }.to exit_with(0) end.to have_printed('otherdirenv/modules') end end end diff --git a/spec/integration/environments/default_manifest_spec.rb b/spec/integration/environments/default_manifest_spec.rb index 7a4c68bd0..c1eb38ad6 100644 --- a/spec/integration/environments/default_manifest_spec.rb +++ b/spec/integration/environments/default_manifest_spec.rb @@ -1,250 +1,215 @@ require 'spec_helper' module EnvironmentsDefaultManifestsSpec describe "default manifests" do context "puppet with default_manifest settings" do let(:confdir) { Puppet[:confdir] } let(:environmentpath) { File.expand_path("envdir", confdir) } context "relative default" do let(:testingdir) { File.join(environmentpath, "testing") } before(:each) do FileUtils.mkdir_p(testingdir) end it "reads manifest from ./manifest of a basic directory environment" do manifestsdir = File.join(testingdir, "manifests") FileUtils.mkdir_p(manifestsdir) File.open(File.join(manifestsdir, "site.pp"), "w") do |f| f.puts("notify { 'ManifestFromRelativeDefault': }") end File.open(File.join(confdir, "puppet.conf"), "w") do |f| f.puts("environmentpath=#{environmentpath}") end expect(a_catalog_compiled_for_environment('testing')).to( include_resource('Notify[ManifestFromRelativeDefault]') ) end end context "set absolute" do let(:testingdir) { File.join(environmentpath, "testing") } before(:each) do FileUtils.mkdir_p(testingdir) end it "reads manifest from an absolute default_manifest" do manifestsdir = File.expand_path("manifests", confdir) FileUtils.mkdir_p(manifestsdir) File.open(File.join(confdir, "puppet.conf"), "w") do |f| f.puts(<<-EOF) environmentpath=#{environmentpath} default_manifest=#{manifestsdir} EOF end File.open(File.join(manifestsdir, "site.pp"), "w") do |f| f.puts("notify { 'ManifestFromAbsoluteDefaultManifest': }") end expect(a_catalog_compiled_for_environment('testing')).to( include_resource('Notify[ManifestFromAbsoluteDefaultManifest]') ) end it "reads manifest from directory environment manifest when environment.conf manifest set" do default_manifestsdir = File.expand_path("manifests", confdir) File.open(File.join(confdir, "puppet.conf"), "w") do |f| f.puts(<<-EOF) environmentpath=#{environmentpath} default_manifest=#{default_manifestsdir} EOF end manifestsdir = File.join(testingdir, "special_manifests") FileUtils.mkdir_p(manifestsdir) File.open(File.join(manifestsdir, "site.pp"), "w") do |f| f.puts("notify { 'ManifestFromEnvironmentConfManifest': }") end File.open(File.join(testingdir, "environment.conf"), "w") do |f| f.puts("manifest=./special_manifests") end expect(a_catalog_compiled_for_environment('testing')).to( include_resource('Notify[ManifestFromEnvironmentConfManifest]') ) expect(Puppet[:default_manifest]).to eq(default_manifestsdir) end it "ignores manifests in the local ./manifests if default_manifest specifies another directory" do default_manifestsdir = File.expand_path("manifests", confdir) FileUtils.mkdir_p(default_manifestsdir) File.open(File.join(confdir, "puppet.conf"), "w") do |f| f.puts(<<-EOF) environmentpath=#{environmentpath} default_manifest=#{default_manifestsdir} EOF end File.open(File.join(default_manifestsdir, "site.pp"), "w") do |f| f.puts("notify { 'ManifestFromAbsoluteDefaultManifest': }") end implicit_manifestsdir = File.join(testingdir, "manifests") FileUtils.mkdir_p(implicit_manifestsdir) File.open(File.join(implicit_manifestsdir, "site.pp"), "w") do |f| f.puts("notify { 'ManifestFromImplicitRelativeEnvironmentManifestDirectory': }") end expect(a_catalog_compiled_for_environment('testing')).to( include_resource('Notify[ManifestFromAbsoluteDefaultManifest]') ) end end context "with disable_per_environment_manifest true" do let(:manifestsdir) { File.expand_path("manifests", confdir) } let(:testingdir) { File.join(environmentpath, "testing") } before(:each) do FileUtils.mkdir_p(testingdir) end before(:each) do FileUtils.mkdir_p(manifestsdir) File.open(File.join(confdir, "puppet.conf"), "w") do |f| f.puts(<<-EOF) environmentpath=#{environmentpath} default_manifest=#{manifestsdir} disable_per_environment_manifest=true EOF end File.open(File.join(manifestsdir, "site.pp"), "w") do |f| f.puts("notify { 'ManifestFromAbsoluteDefaultManifest': }") end end it "reads manifest from the default manifest setting" do expect(a_catalog_compiled_for_environment('testing')).to( include_resource('Notify[ManifestFromAbsoluteDefaultManifest]') ) end it "refuses to compile if environment.conf specifies a different manifest" do File.open(File.join(testingdir, "environment.conf"), "w") do |f| f.puts("manifest=./special_manifests") end expect { a_catalog_compiled_for_environment('testing') }.to( raise_error(Puppet::Error, /disable_per_environment_manifest.*environment.conf.*manifest.*conflict/) ) end it "reads manifest from default_manifest setting when environment.conf has manifest set if setting equals default_manifest setting" do File.open(File.join(testingdir, "environment.conf"), "w") do |f| f.puts("manifest=#{manifestsdir}") end expect(a_catalog_compiled_for_environment('testing')).to( include_resource('Notify[ManifestFromAbsoluteDefaultManifest]') ) end it "logs errors if environment.conf specifies a different manifest" do File.open(File.join(testingdir, "environment.conf"), "w") do |f| f.puts("manifest=./special_manifests") end Puppet.initialize_settings expect(Puppet[:environmentpath]).to eq(environmentpath) environment = Puppet.lookup(:environments).get('testing') expect(environment.manifest).to eq(manifestsdir) expect(@logs.first.to_s).to match(%r{disable_per_environment_manifest.*is true, but.*environment.*at #{testingdir}.*has.*environment.conf.*manifest.*#{testingdir}/special_manifests}) end it "raises an error if default_manifest is not absolute" do File.open(File.join(confdir, "puppet.conf"), "w") do |f| f.puts(<<-EOF) environmentpath=#{environmentpath} default_manifest=./relative disable_per_environment_manifest=true EOF end expect { Puppet.initialize_settings }.to raise_error(Puppet::Settings::ValidationError, /default_manifest.*must be.*absolute.*when.*disable_per_environment_manifest.*true/) end end - - context "in legacy environments" do - let(:environmentpath) { '' } - let(:manifestsdir) { File.expand_path("default_manifests", confdir) } - let(:legacy_manifestsdir) { File.expand_path('manifests', confdir) } - - before(:each) do - FileUtils.mkdir_p(manifestsdir) - - File.open(File.join(confdir, "puppet.conf"), "w") do |f| - f.puts(<<-EOF) - default_manifest=#{manifestsdir} - disable_per_environment_manifest=true - manifest=#{legacy_manifestsdir} - EOF - end - - File.open(File.join(manifestsdir, "site.pp"), "w") do |f| - f.puts("notify { 'ManifestFromAbsoluteDefaultManifest': }") - end - end - - it "has no effect on compilation" do - FileUtils.mkdir_p(legacy_manifestsdir) - - File.open(File.join(legacy_manifestsdir, "site.pp"), "w") do |f| - f.puts("notify { 'ManifestFromLegacy': }") - end - - expect(a_catalog_compiled_for_environment('testing')).to( - include_resource('Notify[ManifestFromLegacy]') - ) - end - end end - RSpec::Matchers.define :include_resource do |expected| match do |actual| actual.resources.map(&:ref).include?(expected) end def failure_message_for_should "expected #{@actual.resources.map(&:ref)} to include #{expected}" end def failure_message_for_should_not "expected #{@actual.resources.map(&:ref)} not to include #{expected}" end end def a_catalog_compiled_for_environment(envname) Puppet.initialize_settings expect(Puppet[:environmentpath]).to eq(environmentpath) node = Puppet::Node.new('testnode', :environment => 'testing') expect(node.environment).to eq(Puppet.lookup(:environments).get('testing')) Puppet::Parser::Compiler.compile(node) end end end diff --git a/spec/integration/environments/settings_interpolation_spec.rb b/spec/integration/environments/settings_interpolation_spec.rb index 5d97fe76e..a1d3ef216 100644 --- a/spec/integration/environments/settings_interpolation_spec.rb +++ b/spec/integration/environments/settings_interpolation_spec.rb @@ -1,165 +1,110 @@ require 'pp' require 'spec_helper' +require 'puppet_spec/settings' module SettingsInterpolationSpec describe "interpolating $environment" do + include PuppetSpec::Settings + let(:confdir) { Puppet[:confdir] } let(:cmdline_args) { ['--confdir', confdir, '--vardir', Puppet[:vardir], '--hiera_config', Puppet[:hiera_config]] } before(:each) do FileUtils.mkdir_p(confdir) end shared_examples_for "a setting that does not interpolate $environment" do before(:each) do set_puppet_conf(confdir, <<-EOF) environmentpath=$confdir/environments #{setting}=#{value} EOF end it "does not interpolate $environment" do Puppet.initialize_settings(cmdline_args) expect(Puppet[:environmentpath]).to eq("#{confdir}/environments") expect(Puppet[setting.intern]).to eq(expected) end it "displays the interpolated value in the warning" do Puppet.initialize_settings(cmdline_args) Puppet[setting.intern] expect(@logs).to have_matching_log(/cannot interpolate \$environment within '#{setting}'.*Its value will remain #{Regexp.escape(expected)}/) end end - context "when environmentpath is set" do - - describe "config_version" do - it "interpolates $environment" do - envname = 'testing' - setting = 'config_version' - value = '/some/script $environment' - expected = "#{File.expand_path('/some/script')} testing" - - set_puppet_conf(confdir, <<-EOF) - environmentpath=$confdir/environments - environment=#{envname} - EOF - - set_environment_conf("#{confdir}/environments", envname, <<-EOF) - #{setting}=#{value} - EOF - - Puppet.initialize_settings(cmdline_args) - expect(Puppet[:environmentpath]).to eq("#{confdir}/environments") - environment = Puppet.lookup(:environments).get(envname) - expect(environment.config_version).to eq(expected) - expect(@logs).to be_empty - end - end - - describe "basemodulepath" do - let(:setting) { "basemodulepath" } - let(:value) { "$confdir/environments/$environment/modules:$confdir/environments/$environment/other_modules" } - let(:expected) { "#{confdir}/environments/$environment/modules:#{confdir}/environments/$environment/other_modules" } - - it_behaves_like "a setting that does not interpolate $environment" - - it "logs a single warning for multiple instances of $environment in the setting" do - set_puppet_conf(confdir, <<-EOF) - environmentpath=$confdir/environments - #{setting}=#{value} - EOF + describe "config_version" do + it "interpolates $environment" do + envname = 'testing' + setting = 'config_version' + value = '/some/script $environment' + expected = "#{File.expand_path('/some/script')} testing" - Puppet.initialize_settings(cmdline_args) - expect(@logs.map(&:to_s).grep(/cannot interpolate \$environment within '#{setting}'/).count).to eq(1) - end - end + set_puppet_conf(confdir, <<-EOF) + environmentpath=$confdir/environments + environment=#{envname} + EOF - describe "environment" do - let(:setting) { "environment" } - let(:value) { "whatareyouthinking$environment" } - let(:expected) { value } + set_environment_conf("#{confdir}/environments", envname, <<-EOF) + #{setting}=#{value} + EOF - it_behaves_like "a setting that does not interpolate $environment" + Puppet.initialize_settings(cmdline_args) + expect(Puppet[:environmentpath]).to eq("#{confdir}/environments") + environment = Puppet.lookup(:environments).get(envname) + expect(environment.config_version).to eq(expected) + expect(@logs).to be_empty end + end - describe "the default_manifest" do - let(:setting) { "default_manifest" } - let(:value) { "$confdir/manifests/$environment" } - let(:expected) { "#{confdir}/manifests/$environment" } - - it_behaves_like "a setting that does not interpolate $environment" - end + describe "basemodulepath" do + let(:setting) { "basemodulepath" } + let(:value) { "$confdir/environments/$environment/modules:$confdir/environments/$environment/other_modules" } + let(:expected) { "#{confdir}/environments/$environment/modules:#{confdir}/environments/$environment/other_modules" } - it "does not interpolate $environment and logs a warning when interpolating environmentpath" do - setting = 'environmentpath' - value = "$confdir/environments/$environment" - expected = "#{confdir}/environments/$environment" + it_behaves_like "a setting that does not interpolate $environment" + it "logs a single warning for multiple instaces of $environment in the setting" do set_puppet_conf(confdir, <<-EOF) + environmentpath=$confdir/environments #{setting}=#{value} EOF Puppet.initialize_settings(cmdline_args) - expect(Puppet[setting.intern]).to eq(expected) - expect(@logs).to have_matching_log(/cannot interpolate \$environment within '#{setting}'/) + expect(@logs.map(&:to_s).grep(/cannot interpolate \$environment within '#{setting}'/).count).to eq(1) end end - def assert_does_interpolate_environment(setting, value, expected_interpolation) - set_puppet_conf(confdir, <<-EOF) - #{setting}=#{value} - EOF + describe "environment" do + let(:setting) { "environment" } + let(:value) { "whatareyouthinking$environment" } + let(:expected) { value } - Puppet.initialize_settings(cmdline_args) - expect(Puppet[:environmentpath]).to be_empty - expect(Puppet[setting.intern]).to eq(expected_interpolation) - expect(@logs).to_not have_matching_log(/cannot interpolate \$environment within '#{setting}'/) + it_behaves_like "a setting that does not interpolate $environment" end - context "when environmentpath is not set" do - it "does interpolate $environment in config_version" do - value = "/some/script $environment" - expect = "/some/script production" - assert_does_interpolate_environment("config_version", value, expect) - end - - it "does interpolate $environment in basemodulepath" do - value = "$confdir/environments/$environment/modules:$confdir/environments/$environment/other_modules" - expected = "#{confdir}/environments/production/modules:#{confdir}/environments/production/other_modules" - assert_does_interpolate_environment("basemodulepath", value, expected) - end - - it "does interpolate $environment in default_manifest, which is fine, because this setting isn't used" do - value = "$confdir/manifests/$environment" - expected = "#{confdir}/manifests/production" - - assert_does_interpolate_environment("default_manifest", value, expected) - end + describe "the default_manifest" do + let(:setting) { "default_manifest" } + let(:value) { "$confdir/manifests/$environment" } + let(:expected) { "#{confdir}/manifests/$environment" } - it "raises something" do - value = expected = "whatareyouthinking$environment" - expect { - assert_does_interpolate_environment("environment", value, expected) - }.to raise_error(SystemStackError, /stack level too deep/) - end + it_behaves_like "a setting that does not interpolate $environment" end - def set_puppet_conf(confdir, settings) - write_file(File.join(confdir, "puppet.conf"), settings) - end + it "does not interpolate $environment and logs a warning when interpolating environmentpath" do + setting = 'environmentpath' + value = "$confdir/environments/$environment" + expected = "#{confdir}/environments/$environment" - def set_environment_conf(environmentpath, environment, settings) - envdir = File.join(environmentpath, environment) - FileUtils.mkdir_p(envdir) - write_file(File.join(envdir, 'environment.conf'), settings) - end + set_puppet_conf(confdir, <<-EOF) + #{setting}=#{value} + EOF - def write_file(file, contents) - File.open(file, "w") do |f| - f.puts(contents) - end + Puppet.initialize_settings(cmdline_args) + expect(Puppet[setting.intern]).to eq(expected) + expect(@logs).to have_matching_log(/cannot interpolate \$environment within '#{setting}'/) end end end diff --git a/spec/integration/environments/settings_spec.rb b/spec/integration/environments/settings_spec.rb new file mode 100644 index 000000000..f06f06789 --- /dev/null +++ b/spec/integration/environments/settings_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' +require 'puppet_spec/settings' + +describe "environment settings" do + include PuppetSpec::Settings + + let(:confdir) { Puppet[:confdir] } + let(:cmdline_args) { ['--confdir', confdir, '--vardir', Puppet[:vardir], '--hiera_config', Puppet[:hiera_config]] } + let(:environmentpath) { File.expand_path("envdir", confdir) } + let(:testingdir) { File.join(environmentpath, "testing") } + + before(:each) do + FileUtils.mkdir_p(testingdir) + end + + def init_puppet_conf(settings = {}) + set_puppet_conf(confdir, <<-EOF) + environmentpath=#{environmentpath} + #{settings.map { |k,v| "#{k}=#{v}" }.join("\n")} + EOF + Puppet.initialize_settings + end + + it "raises an error if you set manifest in puppet.conf" do + expect { init_puppet_conf("manifest" => "/something") }.to raise_error(Puppet::Settings::SettingsError, /Cannot set manifest.*in puppet.conf/) + end + + it "raises an error if you set modulepath in puppet.conf" do + expect { init_puppet_conf("modulepath" => "/something") }.to raise_error(Puppet::Settings::SettingsError, /Cannot set modulepath.*in puppet.conf/) + end + + it "raises an error if you set config_version in puppet.conf" do + expect { init_puppet_conf("config_version" => "/something") }.to raise_error(Puppet::Settings::SettingsError, /Cannot set config_version.*in puppet.conf/) + end + + context "when given an environment" do + before(:each) do + init_puppet_conf + end + + context "without an environment.conf" do + it "reads manifest from environment.conf defaults" do + expect(Puppet.settings.value(:manifest, :testing)).to eq(File.join(testingdir, "manifests")) + end + + it "reads modulepath from environment.conf defaults" do + expect(Puppet.settings.value(:modulepath, :testing)).to match(/#{File.join(testingdir, "modules")}/) + end + + it "reads config_version from environment.conf defaults" do + expect(Puppet.settings.value(:config_version, :testing)).to eq('') + end + end + + context "with an environment.conf" do + before(:each) do + set_environment_conf(environmentpath, 'testing', <<-EOF) + manifest=/special/manifest + modulepath=/special/modulepath + config_version=/special/config_version + EOF + end + + it "reads the configured manifest" do + expect(Puppet.settings.value(:manifest, :testing)).to eq('/special/manifest') + end + + it "reads the configured modulepath" do + expect(Puppet.settings.value(:modulepath, :testing)).to eq('/special/modulepath') + end + + it "reads the configured config_version" do + expect(Puppet.settings.value(:config_version, :testing)).to eq('/special/config_version') + end + end + + context "when environment name collides with a puppet.conf section" do + let(:testingdir) { File.join(environmentpath, "main") } + + it "reads manifest from environment.conf defaults" do + expect(Puppet.settings.value(:environmentpath)).to eq(environmentpath) + expect(Puppet.settings.value(:manifest, :main)).to eq(File.join(testingdir, "manifests")) + end + + context "and an environment.conf" do + before(:each) do + set_environment_conf(environmentpath, 'main', <<-EOF) + manifest=/special/manifest + EOF + end + + it "reads manifest from environment.conf settings" do + expect(Puppet.settings.value(:environmentpath)).to eq(environmentpath) + expect(Puppet.settings.value(:manifest, :main)).to eq("/special/manifest") + end + end + end + end + +end diff --git a/spec/integration/indirector/file_content/file_server_spec.rb b/spec/integration/indirector/file_content/file_server_spec.rb index ee0db17a9..f2b72b697 100755 --- a/spec/integration/indirector/file_content/file_server_spec.rb +++ b/spec/integration/indirector/file_content/file_server_spec.rb @@ -1,89 +1,91 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/indirector/file_content/file_server' require 'shared_behaviours/file_server_terminus' require 'puppet_spec/files' describe Puppet::Indirector::FileContent::FileServer, " when finding files" do it_should_behave_like "Puppet::Indirector::FileServerTerminus" include PuppetSpec::Files before do @terminus = Puppet::Indirector::FileContent::FileServer.new @test_class = Puppet::FileServing::Content Puppet::FileServing::Configuration.instance_variable_set(:@configuration, nil) end it "should find plugin file content in the environment specified in the request" do path = tmpfile("file_content_with_env") Dir.mkdir(path) modpath = File.join(path, "mod") FileUtils.mkdir_p(File.join(modpath, "lib")) file = File.join(modpath, "lib", "file.rb") File.open(file, "wb") { |f| f.write "1\r\n" } Puppet.settings[:modulepath] = "/no/such/file" env = Puppet::Node::Environment.create(:foo, [path]) result = Puppet::FileServing::Content.indirection.search("plugins", :environment => env, :recurse => true) result.should_not be_nil result.length.should == 2 result.map {|x| x.should be_instance_of(Puppet::FileServing::Content) } result.find {|x| x.relative_path == 'file.rb' }.content.should == "1\r\n" end it "should find file content in modules" do path = tmpfile("file_content") Dir.mkdir(path) modpath = File.join(path, "mymod") FileUtils.mkdir_p(File.join(modpath, "files")) file = File.join(modpath, "files", "myfile") File.open(file, "wb") { |f| f.write "1\r\n" } env = Puppet::Node::Environment.create(:foo, [path]) result = Puppet::FileServing::Content.indirection.find("modules/mymod/myfile", :environment => env) result.should_not be_nil result.should be_instance_of(Puppet::FileServing::Content) result.content.should == "1\r\n" end it "should find file content in files when node name expansions are used" do Puppet::FileSystem.stubs(:exist?).returns true Puppet::FileSystem.stubs(:exist?).with(Puppet[:fileserverconfig]).returns(true) - @path = tmpfile("file_server_testing") + path = tmpfile("file_server_testing") - Dir.mkdir(@path) - subdir = File.join(@path, "mynode") + Dir.mkdir(path) + subdir = File.join(path, "mynode") Dir.mkdir(subdir) File.open(File.join(subdir, "myfile"), "wb") { |f| f.write "1\r\n" } # Use a real mount, so the integration is a bit deeper. - @mount1 = Puppet::FileServing::Configuration::Mount::File.new("one") - @mount1.stubs(:allowed?).returns true - @mount1.path = File.join(@path, "%h") + mount1 = Puppet::FileServing::Configuration::Mount::File.new("one") + mount1.stubs(:allowed?).returns true + mount1.path = File.join(path, "%h") + + parser = stub 'parser', :changed? => false + parser.stubs(:parse).returns("one" => mount1) - @parser = stub 'parser', :changed? => false - @parser.stubs(:parse).returns("one" => @mount1) + Puppet::FileServing::Configuration::Parser.stubs(:new).returns(parser) - Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser) + path = File.join(path, "myfile") - path = File.join(@path, "myfile") + env = Puppet::Node::Environment.create(:foo, []) - result = Puppet::FileServing::Content.indirection.find("one/myfile", :environment => "foo", :node => "mynode") + result = Puppet::FileServing::Content.indirection.find("one/myfile", :environment => env, :node => "mynode") result.should_not be_nil result.should be_instance_of(Puppet::FileServing::Content) result.content.should == "1\r\n" end end diff --git a/spec/integration/parser/compiler_spec.rb b/spec/integration/parser/compiler_spec.rb index dc7b4e537..1f42c2cbf 100755 --- a/spec/integration/parser/compiler_spec.rb +++ b/spec/integration/parser/compiler_spec.rb @@ -1,928 +1,938 @@ require 'spec_helper' require 'puppet_spec/compiler' require 'matchers/resource' class CompilerTestResource attr_accessor :builtin, :virtual, :evaluated, :type, :title def initialize(type, title) @type = type @title = title end def [](attr) return nil if attr == :stage :main end def ref "#{type.to_s.capitalize}[#{title}]" end def evaluated? @evaluated end def builtin_type? @builtin end def virtual? @virtual end def class? false end def stage? false end def evaluate end def file "/fake/file/goes/here" end def line "42" end end describe Puppet::Parser::Compiler do include PuppetSpec::Files include Matchers::Resource def resource(type, title) Puppet::Parser::Resource.new(type, title, :scope => @scope) end + let(:environment) { Puppet::Node::Environment.create(:testing, []) } + before :each do # Push me faster, I wanna go back in time! (Specifically, freeze time # across the test since we have a bunch of version == timestamp code # hidden away in the implementation and we keep losing the race.) # --daniel 2011-04-21 now = Time.now Time.stubs(:now).returns(now) - environment = Puppet::Node::Environment.create(:testing, []) @node = Puppet::Node.new("testnode", :facts => Puppet::Node::Facts.new("facts", {}), :environment => environment) @known_resource_types = environment.known_resource_types @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(@compiler, :source => stub('source')) @scope_resource = Puppet::Parser::Resource.new(:file, "/my/file", :scope => @scope) @scope.resource = @scope_resource end it "should have a class method that compiles, converts, and returns a catalog" do compiler = stub 'compiler' Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler catalog = stub 'catalog' compiler.expects(:compile).returns catalog converted_catalog = stub 'converted_catalog' catalog.expects(:to_resource).returns converted_catalog Puppet::Parser::Compiler.compile(@node).should equal(converted_catalog) end it "should fail intelligently when a class-level compile fails" do Puppet::Parser::Compiler.expects(:new).raises ArgumentError lambda { Puppet::Parser::Compiler.compile(@node) }.should raise_error(Puppet::Error) end it "should use the node's environment as its environment" do @compiler.environment.should equal(@node.environment) end it "fails if the node's environment has validation errors" do conflicted_environment = Puppet::Node::Environment.create(:testing, [], '/some/environment.conf/manifest.pp') conflicted_environment.stubs(:validation_errors).returns(['bad environment']) @node.environment = conflicted_environment expect { Puppet::Parser::Compiler.compile(@node) }.to raise_error(Puppet::Error, /Compilation has been halted because.*bad environment/) end it "should include the resource type collection helper" do Puppet::Parser::Compiler.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper) end it "should be able to return a class list containing all added classes" do @compiler.add_class "" @compiler.add_class "one" @compiler.add_class "two" @compiler.classlist.sort.should == %w{one two}.sort end it "should clear the global caches before compile" do compiler = stub 'compiler' Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler catalog = stub 'catalog' compiler.expects(:compile).returns catalog catalog.expects(:to_resource) $known_resource_types = "rspec" $env_module_directories = "rspec" Puppet::Parser::Compiler.compile(@node) $known_resource_types = nil $env_module_directories = nil end describe "when initializing" do it "should set its node attribute" do @compiler.node.should equal(@node) end it "should detect when ast nodes are absent" do @compiler.ast_nodes?.should be_false end it "should detect when ast nodes are present" do @known_resource_types.expects(:nodes?).returns true @compiler.ast_nodes?.should be_true end it "should copy the known_resource_types version to the catalog" do @compiler.catalog.version.should == @known_resource_types.version end it "should copy any node classes into the class list" do node = Puppet::Node.new("mynode") node.classes = %w{foo bar} compiler = Puppet::Parser::Compiler.new(node) compiler.classlist.should =~ ['foo', 'bar'] end it "should transform node class hashes into a class list" do node = Puppet::Node.new("mynode") node.classes = {'foo'=>{'one'=>'p1'}, 'bar'=>{'two'=>'p2'}} compiler = Puppet::Parser::Compiler.new(node) compiler.classlist.should =~ ['foo', 'bar'] end it "should add a 'main' stage to the catalog" do @compiler.catalog.resource(:stage, :main).should be_instance_of(Puppet::Parser::Resource) end end describe "when managing scopes" do it "should create a top scope" do @compiler.topscope.should be_instance_of(Puppet::Parser::Scope) end it "should be able to create new scopes" do @compiler.newscope(@compiler.topscope).should be_instance_of(Puppet::Parser::Scope) end it "should set the parent scope of the new scope to be the passed-in parent" do scope = mock 'scope' newscope = @compiler.newscope(scope) newscope.parent.should equal(scope) end it "should set the parent scope of the new scope to its topscope if the parent passed in is nil" do scope = mock 'scope' newscope = @compiler.newscope(nil) newscope.parent.should equal(@compiler.topscope) end end describe "when compiling" do def compile_methods [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, :finish, :store, :extract, :evaluate_relationships] end # Stub all of the main compile methods except the ones we're specifically interested in. def compile_stub(*except) (compile_methods - except).each { |m| @compiler.stubs(m) } end it "should set node parameters as variables in the top scope" do params = {"a" => "b", "c" => "d"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compiler.compile @compiler.topscope['a'].should == "b" @compiler.topscope['c'].should == "d" end it "should set the client and server versions on the catalog" do params = {"clientversion" => "2", "serverversion" => "3"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compiler.compile @compiler.catalog.client_version.should == "2" @compiler.catalog.server_version.should == "3" end it "should evaluate the main class if it exists" do compile_stub(:evaluate_main) main_class = @known_resource_types.add Puppet::Resource::Type.new(:hostclass, "") main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) } @compiler.topscope.expects(:source=).with(main_class) @compiler.compile end it "should create a new, empty 'main' if no main class exists" do compile_stub(:evaluate_main) @compiler.compile @known_resource_types.find_hostclass([""], "").should be_instance_of(Puppet::Resource::Type) end it "should add an edge between the main stage and main class" do @compiler.compile (stage = @compiler.catalog.resource(:stage, "main")).should be_instance_of(Puppet::Parser::Resource) (klass = @compiler.catalog.resource(:class, "")).should be_instance_of(Puppet::Parser::Resource) @compiler.catalog.edge?(stage, klass).should be_true end it "should evaluate all added collections" do colls = [] # And when the collections fail to evaluate. colls << mock("coll1-false") colls << mock("coll2-false") colls.each { |c| c.expects(:evaluate).returns(false) } @compiler.add_collection(colls[0]) @compiler.add_collection(colls[1]) compile_stub(:evaluate_generators) @compiler.compile end it "should ignore builtin resources" do resource = resource(:file, "testing") @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end it "should evaluate unevaluated resources" do resource = CompilerTestResource.new(:file, "testing") @compiler.add_resource(@scope, resource) # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.evaluated = true } @compiler.compile end it "should not evaluate already-evaluated resources" do resource = resource(:file, "testing") resource.stubs(:evaluated?).returns true @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end it "should evaluate unevaluated resources created by evaluating other resources" do resource = CompilerTestResource.new(:file, "testing") @compiler.add_resource(@scope, resource) resource2 = CompilerTestResource.new(:file, "other") # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.evaluated = true; @compiler.add_resource(@scope, resource2) } resource2.expects(:evaluate).with { |*whatever| resource2.evaluated = true } @compiler.compile end describe "when finishing" do before do @compiler.send(:evaluate_main) @catalog = @compiler.catalog end def add_resource(name, parent = nil) resource = Puppet::Parser::Resource.new "file", name, :scope => @scope @compiler.add_resource(@scope, resource) @catalog.add_edge(parent, resource) if parent resource end it "should call finish() on all resources" do # Add a resource that does respond to :finish resource = Puppet::Parser::Resource.new "file", "finish", :scope => @scope resource.expects(:finish) @compiler.add_resource(@scope, resource) # And one that does not dnf_resource = stub_everything "dnf", :ref => "File[dnf]", :type => "file" @compiler.add_resource(@scope, dnf_resource) @compiler.send(:finish) end it "should call finish() in add_resource order" do resources = sequence('resources') resource1 = add_resource("finish1") resource1.expects(:finish).in_sequence(resources) resource2 = add_resource("finish2") resource2.expects(:finish).in_sequence(resources) @compiler.send(:finish) end it "should add each container's metaparams to its contained resources" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) @compiler.send(:finish) resource1[:noop].should be_true end it "should add metaparams recursively" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2[:noop].should be_true end it "should prefer metaparams from immediate parents" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) resource2 = add_resource("foo", resource1) resource1[:noop] = false @compiler.send(:finish) resource2[:noop].should be_false end it "should merge tags downward" do main = @catalog.resource(:class, :main) main.tag("one") resource1 = add_resource("meh", main) resource1.tag "two" resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2.tags.should be_include("one") resource2.tags.should be_include("two") end it "should work if only middle resources have metaparams set" do main = @catalog.resource(:class, :main) resource1 = add_resource("meh", main) resource1[:noop] = true resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2[:noop].should be_true end end it "should return added resources in add order" do resource1 = resource(:file, "yay") @compiler.add_resource(@scope, resource1) resource2 = resource(:file, "youpi") @compiler.add_resource(@scope, resource2) @compiler.resources.should == [resource1, resource2] end it "should add resources that do not conflict with existing resources" do resource = resource(:file, "yay") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_vertex(resource) end it "should fail to add resources that conflict with existing resources" do path = make_absolute("/foo") file1 = resource(:file, path) file2 = resource(:file, path) @compiler.add_resource(@scope, file1) lambda { @compiler.add_resource(@scope, file2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) end it "should add an edge from the scope resource to the added resource" do resource = resource(:file, "yay") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_edge(@scope.resource, resource) end it "should not add non-class resources that don't specify a stage to the 'main' stage" do main = @compiler.catalog.resource(:stage, :main) resource = resource(:file, "foo") @compiler.add_resource(@scope, resource) @compiler.catalog.should_not be_edge(main, resource) end it "should not add any parent-edges to stages" do stage = resource(:stage, "other") @compiler.add_resource(@scope, stage) @scope.resource = resource(:class, "foo") @compiler.catalog.edge?(@scope.resource, stage).should be_false end it "should not attempt to add stages to other stages" do other_stage = resource(:stage, "other") second_stage = resource(:stage, "second") @compiler.add_resource(@scope, other_stage) @compiler.add_resource(@scope, second_stage) second_stage[:stage] = "other" @compiler.catalog.edge?(other_stage, second_stage).should be_false end it "should have a method for looking up resources" do resource = resource(:yay, "foo") @compiler.add_resource(@scope, resource) @compiler.findresource("Yay[foo]").should equal(resource) end it "should be able to look resources up by type and title" do resource = resource(:yay, "foo") @compiler.add_resource(@scope, resource) @compiler.findresource("Yay", "foo").should equal(resource) end it "should not evaluate virtual defined resources" do resource = resource(:file, "testing") resource.virtual = true @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end end describe "when evaluating collections" do it "should evaluate each collection" do 2.times { |i| coll = mock 'coll%s' % i @compiler.add_collection(coll) # This is the hard part -- we have to emulate the fact that # collections delete themselves if they are done evaluating. coll.expects(:evaluate).with do @compiler.delete_collection(coll) end } @compiler.compile end it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:unresolved_resources).returns(nil) @compiler.add_collection(coll) lambda { @compiler.compile }.should_not raise_error end it "should fail when there are unevaluated resource collections that refer to a specific resource" do coll = stub 'coll', :evaluate => false coll.expects(:unresolved_resources).returns(:something) @compiler.add_collection(coll) lambda { @compiler.compile }.should raise_error(Puppet::ParseError, 'Failed to realize virtual resources something') end it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:unresolved_resources).returns([:one, :two]) @compiler.add_collection(coll) lambda { @compiler.compile }.should raise_error(Puppet::ParseError, 'Failed to realize virtual resources one, two') end end describe "when evaluating relationships" do it "should evaluate each relationship with its catalog" do dep = stub 'dep' dep.expects(:evaluate).with(@compiler.catalog) @compiler.add_relationship dep @compiler.evaluate_relationships end end describe "when told to evaluate missing classes" do it "should fail if there's no source listed for the scope" do scope = stub 'scope', :source => nil proc { @compiler.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError) end it "should raise an error if a class is not found" do @scope.expects(:find_hostclass).with("notfound", {:assume_fqname => false}).returns(nil) lambda{ @compiler.evaluate_classes(%w{notfound}, @scope) }.should raise_error(Puppet::Error, /Could not find class/) end it "should raise an error when it can't find class" do klasses = {'foo'=>nil} @node.classes = klasses @compiler.topscope.expects(:find_hostclass).with('foo', {:assume_fqname => false}).returns(nil) lambda{ @compiler.compile }.should raise_error(Puppet::Error, /Could not find class foo for testnode/) end end describe "when evaluating found classes" do before do Puppet.settings[:data_binding_terminus] = "none" @class = stub 'class', :name => "my::class" @scope.stubs(:find_hostclass).with("myclass", {:assume_fqname => false}).returns(@class) @resource = stub 'resource', :ref => "Class[myclass]", :type => "file" end + around do |example| + Puppet.override( + :environments => Puppet::Environments::Static.new(environment), + :description => "Static loader for specs" + ) do + example.run + end + end + it "should evaluate each class" do @compiler.catalog.stubs(:tag) @class.expects(:ensure_in_catalog).with(@scope) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope) end describe "and the classes are specified as a hash with parameters" do before do @node.classes = {} @ast_obj = Puppet::Parser::AST::Leaf.new(:value => 'foo') end # Define the given class with default parameters def define_class(name, parameters) @node.classes[name] = parameters klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'p1' => @ast_obj, 'p2' => @ast_obj}) @compiler.topscope.known_resource_types.add klass end def compile @catalog = @compiler.compile end it "should record which classes are evaluated" do classes = {'foo'=>{}, 'bar::foo'=>{}, 'bar'=>{}} classes.each { |c, params| define_class(c, params) } compile() classes.each { |name, p| @catalog.classes.should include(name) } end it "should provide default values for parameters that have no values specified" do define_class('foo', {}) compile() @catalog.resource(:class, 'foo')['p1'].should == "foo" end it "should use any provided values" do define_class('foo', {'p1' => 'real_value'}) compile() @catalog.resource(:class, 'foo')['p1'].should == "real_value" end it "should support providing some but not all values" do define_class('foo', {'p1' => 'real_value'}) compile() @catalog.resource(:class, 'Foo')['p1'].should == "real_value" @catalog.resource(:class, 'Foo')['p2'].should == "foo" end it "should ensure each node class is in catalog and has appropriate tags" do klasses = ['bar::foo'] @node.classes = klasses ast_obj = Puppet::Parser::AST::Leaf.new(:value => 'foo') klasses.each do |name| klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'p1' => ast_obj, 'p2' => ast_obj}) @compiler.topscope.known_resource_types.add klass end catalog = @compiler.compile r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' } r2.tags.should == Puppet::Util::TagSet.new(['bar::foo', 'class', 'bar', 'foo']) end end it "should fail if required parameters are missing" do klass = {'foo'=>{'a'=>'one'}} @node.classes = klass klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {'a' => nil, 'b' => nil}) @compiler.topscope.known_resource_types.add klass lambda { @compiler.compile }.should raise_error(Puppet::ParseError, "Must pass b to Class[Foo]") end it "should fail if invalid parameters are passed" do klass = {'foo'=>{'3'=>'one'}} @node.classes = klass klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {}) @compiler.topscope.known_resource_types.add klass lambda { @compiler.compile }.should raise_error(Puppet::ParseError, "Invalid parameter 3 on Class[Foo]") end it "should ensure class is in catalog without params" do @node.classes = klasses = {'foo'=>nil} foo = Puppet::Resource::Type.new(:hostclass, 'foo') @compiler.topscope.known_resource_types.add foo catalog = @compiler.compile catalog.classes.should include 'foo' end it "should not evaluate the resources created for found classes unless asked" do @compiler.catalog.stubs(:tag) @resource.expects(:evaluate).never @class.expects(:ensure_in_catalog).returns(@resource) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope) end it "should immediately evaluate the resources created for found classes when asked" do @compiler.catalog.stubs(:tag) @resource.expects(:evaluate) @class.expects(:ensure_in_catalog).returns(@resource) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes that have already been evaluated" do @compiler.catalog.stubs(:tag) @scope.stubs(:class_scope).with(@class).returns(@scope) @compiler.expects(:add_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compiler.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes previously evaluated with different capitalization" do @compiler.catalog.stubs(:tag) @scope.stubs(:find_hostclass).with("MyClass",{:assume_fqname => false}).returns(@class) @scope.stubs(:class_scope).with(@class).returns(@scope) @compiler.expects(:add_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compiler.evaluate_classes(%w{MyClass}, @scope, false) end end describe "when evaluating AST nodes with no AST nodes present" do it "should do nothing" do @compiler.expects(:ast_nodes?).returns(false) @compiler.known_resource_types.expects(:nodes).never Puppet::Parser::Resource.expects(:new).never @compiler.send(:evaluate_ast_node) end end describe "when evaluating AST nodes with AST nodes present" do before do @compiler.known_resource_types.stubs(:nodes?).returns true # Set some names for our test @node.stubs(:names).returns(%w{a b c}) @compiler.known_resource_types.stubs(:node).with("a").returns(nil) @compiler.known_resource_types.stubs(:node).with("b").returns(nil) @compiler.known_resource_types.stubs(:node).with("c").returns(nil) # It should check this last, of course. @compiler.known_resource_types.stubs(:node).with("default").returns(nil) end it "should fail if the named node cannot be found" do proc { @compiler.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError) end it "should evaluate the first node class matching the node name" do node_class = stub 'node', :name => "c", :evaluate_code => nil @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) @compiler.compile end it "should match the default node if no matching node can be found" do node_class = stub 'node', :name => "default", :evaluate_code => nil @compiler.known_resource_types.stubs(:node).with("default").returns(node_class) node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil, :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) @compiler.compile end it "should evaluate the node resource immediately rather than using lazy evaluation" do node_class = stub 'node', :name => "c" @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) node_resource.expects(:evaluate) @compiler.send(:evaluate_ast_node) end end describe "when evaluating node classes" do include PuppetSpec::Compiler describe "when provided classes in array format" do let(:node) { Puppet::Node.new('someone', :classes => ['something']) } describe "when the class exists" do it "should succeed if the class is already included" do manifest = <<-MANIFEST class something {} include something MANIFEST catalog = compile_to_catalog(manifest, node) catalog.resource('Class', 'Something').should_not be_nil end it "should evaluate the class without parameters if it's not already included" do manifest = "class something {}" catalog = compile_to_catalog(manifest, node) catalog.resource('Class', 'Something').should_not be_nil end end it "should fail if the class doesn't exist" do expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) end end describe "when provided classes in hash format" do describe "for classes without parameters" do let(:node) { Puppet::Node.new('someone', :classes => {'something' => {}}) } describe "when the class exists" do it "should succeed if the class is already included" do manifest = <<-MANIFEST class something {} include something MANIFEST catalog = compile_to_catalog(manifest, node) catalog.resource('Class', 'Something').should_not be_nil end it "should evaluate the class if it's not already included" do manifest = <<-MANIFEST class something {} MANIFEST catalog = compile_to_catalog(manifest, node) catalog.resource('Class', 'Something').should_not be_nil end end it "should fail if the class doesn't exist" do expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) end end describe "for classes with parameters" do let(:node) { Puppet::Node.new('someone', :classes => {'something' => {'configuron' => 'defrabulated'}}) } describe "when the class exists" do it "should fail if the class is already included" do manifest = <<-MANIFEST class something($configuron=frabulated) {} include something MANIFEST expect { compile_to_catalog(manifest, node) }.to raise_error(Puppet::Error, /Class\[Something\] is already declared/) end it "should evaluate the class if it's not already included" do manifest = <<-MANIFEST class something($configuron=frabulated) {} MANIFEST catalog = compile_to_catalog(manifest, node) resource = catalog.resource('Class', 'Something') resource['configuron'].should == 'defrabulated' end end it "should fail if the class doesn't exist" do expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) end it 'evaluates classes declared with parameters before unparameterized classes' do node = Puppet::Node.new('someone', :classes => { 'app::web' => {}, 'app' => { 'port' => 8080 } }) manifest = <<-MANIFEST class app($port = 80) { } class app::web($port = $app::port) inherits app { notify { expected: message => "$port" } } MANIFEST catalog = compile_to_catalog(manifest, node) expect(catalog).to have_resource("Class[App]").with_parameter(:port, 8080) expect(catalog).to have_resource("Class[App::Web]") expect(catalog).to have_resource("Notify[expected]").with_parameter(:message, "8080") end end end end describe "when managing resource overrides" do before do @override = stub 'override', :ref => "File[/foo]", :type => "my" @resource = resource(:file, "/foo") end it "should be able to store overrides" do lambda { @compiler.add_override(@override) }.should_not raise_error end it "should apply overrides to the appropriate resources" do @compiler.add_resource(@scope, @resource) @resource.expects(:merge).with(@override) @compiler.add_override(@override) @compiler.compile end it "should accept overrides before the related resource has been created" do @resource.expects(:merge).with(@override) # First store the override @compiler.add_override(@override) # Then the resource @compiler.add_resource(@scope, @resource) # And compile, so they get resolved @compiler.compile end it "should fail if the compile is finished and resource overrides have not been applied" do @compiler.add_override(@override) lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Could not find resource(s) File[/foo] for overriding' end end end diff --git a/spec/lib/puppet_spec/settings.rb b/spec/lib/puppet_spec/settings.rb index f3dbc4223..a3d7fcdd8 100644 --- a/spec/lib/puppet_spec/settings.rb +++ b/spec/lib/puppet_spec/settings.rb @@ -1,15 +1,31 @@ module PuppetSpec::Settings # It would probably be preferable to refactor defaults.rb such that the real definitions of # these settings were available as a variable, which was then accessible for use during tests. # However, I'm not doing that yet because I don't want to introduce any additional moving parts # to this already very large changeset. # Would be nice to clean this up later. --cprice 2012-03-20 TEST_APP_DEFAULT_DEFINITIONS = { :name => { :default => "test", :desc => "name" }, :logdir => { :type => :directory, :default => "test", :desc => "logdir" }, :confdir => { :type => :directory, :default => "test", :desc => "confdir" }, :vardir => { :type => :directory, :default => "test", :desc => "vardir" }, :rundir => { :type => :directory, :default => "test", :desc => "rundir" }, } + + def set_puppet_conf(confdir, settings) + write_file(File.join(confdir, "puppet.conf"), settings) + end + + def set_environment_conf(environmentpath, environment, settings) + envdir = File.join(environmentpath, environment) + FileUtils.mkdir_p(envdir) + write_file(File.join(envdir, 'environment.conf'), settings) + end + + def write_file(file, contents) + File.open(file, "w") do |f| + f.puts(contents) + end + end end diff --git a/spec/unit/application/doc_spec.rb b/spec/unit/application/doc_spec.rb index 2f8624abd..80dfd6e7f 100755 --- a/spec/unit/application/doc_spec.rb +++ b/spec/unit/application/doc_spec.rb @@ -1,344 +1,335 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/application/doc' require 'puppet/util/reference' require 'puppet/util/rdoc' describe Puppet::Application::Doc do before :each do @doc = Puppet::Application[:doc] @doc.stubs(:puts) @doc.preinit Puppet::Util::Log.stubs(:newdestination) end it "should declare an other command" do @doc.should respond_to(:other) end it "should declare a rdoc command" do @doc.should respond_to(:rdoc) end it "should declare a fallback for unknown options" do @doc.should respond_to(:handle_unknown) end it "should declare a preinit block" do @doc.should respond_to(:preinit) end describe "in preinit" do it "should set references to []" do @doc.preinit @doc.options[:references].should == [] end it "should init mode to text" do @doc.preinit @doc.options[:mode].should == :text end it "should init format to to_markdown" do @doc.preinit @doc.options[:format].should == :to_markdown end end describe "when handling options" do [:all, :outputdir, :verbose, :debug, :charset].each do |option| it "should declare handle_#{option} method" do @doc.should respond_to("handle_#{option}".to_sym) end it "should store argument value when calling handle_#{option}" do @doc.options.expects(:[]=).with(option, 'arg') @doc.send("handle_#{option}".to_sym, 'arg') end end it "should store the format if valid" do Puppet::Util::Reference.stubs(:method_defined?).with('to_format').returns(true) @doc.handle_format('format') @doc.options[:format].should == 'to_format' end it "should raise an error if the format is not valid" do Puppet::Util::Reference.stubs(:method_defined?).with('to_format').returns(false) expect { @doc.handle_format('format') }.to raise_error(RuntimeError, /Invalid output format/) end it "should store the mode if valid" do Puppet::Util::Reference.stubs(:modes).returns(stub('mode', :include? => true)) @doc.handle_mode('mode') @doc.options[:mode].should == :mode end it "should store the mode if :rdoc" do Puppet::Util::Reference.modes.stubs(:include?).with('rdoc').returns(false) @doc.handle_mode('rdoc') @doc.options[:mode].should == :rdoc end it "should raise an error if the mode is not valid" do Puppet::Util::Reference.modes.stubs(:include?).with('unknown').returns(false) expect { @doc.handle_mode('unknown') }.to raise_error(RuntimeError, /Invalid output mode/) end it "should list all references on list and exit" do reference = stubs 'reference' ref = stubs 'ref' Puppet::Util::Reference.stubs(:references).returns([reference]) Puppet::Util::Reference.expects(:reference).with(reference).returns(ref) ref.expects(:doc) expect { @doc.handle_list(nil) }.to exit_with 0 end it "should add reference to references list with --reference" do @doc.options[:references] = [:ref1] @doc.handle_reference('ref2') @doc.options[:references].should == [:ref1,:ref2] end end describe "during setup" do before :each do Puppet::Log.stubs(:newdestination) @doc.command_line.stubs(:args).returns([]) end it "should default to rdoc mode if there are command line arguments" do @doc.command_line.stubs(:args).returns(["1"]) @doc.stubs(:setup_rdoc) @doc.setup @doc.options[:mode].should == :rdoc end it "should call setup_rdoc in rdoc mode" do @doc.options[:mode] = :rdoc @doc.expects(:setup_rdoc) @doc.setup end it "should call setup_reference if not rdoc" do @doc.options[:mode] = :test @doc.expects(:setup_reference) @doc.setup end describe "configuring logging" do before :each do Puppet::Util::Log.stubs(:newdestination) end describe "with --debug" do before do @doc.options[:debug] = true end it "should set log level to debug" do @doc.setup Puppet::Util::Log.level.should == :debug end it "should set log destination to console" do Puppet::Util::Log.expects(:newdestination).with(:console) @doc.setup end end describe "with --verbose" do before do @doc.options[:verbose] = true end it "should set log level to info" do @doc.setup Puppet::Util::Log.level.should == :info end it "should set log destination to console" do Puppet::Util::Log.expects(:newdestination).with(:console) @doc.setup end end describe "without --debug or --verbose" do before do @doc.options[:debug] = false @doc.options[:verbose] = false end it "should set log level to warning" do @doc.setup Puppet::Util::Log.level.should == :warning end it "should set log destination to console" do Puppet::Util::Log.expects(:newdestination).with(:console) @doc.setup end end end describe "in non-rdoc mode" do it "should get all non-dynamic reference if --all" do @doc.options[:all] = true static = stub 'static', :dynamic? => false dynamic = stub 'dynamic', :dynamic? => true Puppet::Util::Reference.stubs(:reference).with(:static).returns(static) Puppet::Util::Reference.stubs(:reference).with(:dynamic).returns(dynamic) Puppet::Util::Reference.stubs(:references).returns([:static,:dynamic]) @doc.setup_reference @doc.options[:references].should == [:static] end it "should default to :type if no references" do @doc.setup_reference @doc.options[:references].should == [:type] end end describe "in rdoc mode" do describe "when there are unknown args" do it "should expand --modulepath if any" do @doc.unknown_args = [ { :opt => "--modulepath", :arg => "path" } ] Puppet.settings.stubs(:handlearg) @doc.setup_rdoc @doc.unknown_args[0][:arg].should == File.expand_path('path') end - it "should expand --manifestdir if any" do - @doc.unknown_args = [ { :opt => "--manifestdir", :arg => "path" } ] - Puppet.settings.stubs(:handlearg) - - @doc.setup_rdoc - - @doc.unknown_args[0][:arg].should == File.expand_path('path') - end - it "should give them to Puppet.settings" do @doc.unknown_args = [ { :opt => :option, :arg => :argument } ] Puppet.settings.expects(:handlearg).with(:option,:argument) @doc.setup_rdoc end end it "should operate in master run_mode" do @doc.class.run_mode.name.should == :master @doc.setup_rdoc end end end describe "when running" do describe "in rdoc mode" do include PuppetSpec::Files let(:envdir) { tmpdir('env') } let(:modules) { File.join(envdir, "modules") } let(:modules2) { File.join(envdir, "modules2") } let(:manifests) { File.join(envdir, "manifests") } before :each do @doc.manifest = false Puppet.stubs(:info) Puppet[:trace] = false Puppet[:modulepath] = modules - Puppet[:manifestdir] = manifests + Puppet[:manifest] = manifests @doc.options[:all] = false @doc.options[:outputdir] = 'doc' @doc.options[:charset] = nil Puppet.settings.stubs(:define_settings) Puppet::Util::RDoc.stubs(:rdoc) @doc.command_line.stubs(:args).returns([]) end around(:each) do |example| FileUtils.mkdir_p(modules) env = Puppet::Node::Environment.create(Puppet[:environment].to_sym, [modules], "#{manifests}/site.pp") Puppet.override({:environments => Puppet::Environments::Static.new(env), :current_environment => env}) do example.run end end it "should set document_all on --all" do @doc.options[:all] = true Puppet.settings.expects(:[]=).with(:document_all, true) expect { @doc.rdoc }.to exit_with(0) end it "should call Puppet::Util::RDoc.rdoc in full mode" do Puppet::Util::RDoc.expects(:rdoc).with('doc', [modules, manifests], nil) expect { @doc.rdoc }.to exit_with(0) end it "should call Puppet::Util::RDoc.rdoc with a charset if --charset has been provided" do @doc.options[:charset] = 'utf-8' Puppet::Util::RDoc.expects(:rdoc).with('doc', [modules, manifests], "utf-8") expect { @doc.rdoc }.to exit_with(0) end it "should call Puppet::Util::RDoc.rdoc in full mode with outputdir set to doc if no --outputdir" do @doc.options[:outputdir] = false Puppet::Util::RDoc.expects(:rdoc).with('doc', [modules, manifests], nil) expect { @doc.rdoc }.to exit_with(0) end it "should call Puppet::Util::RDoc.manifestdoc in manifest mode" do @doc.manifest = true Puppet::Util::RDoc.expects(:manifestdoc) expect { @doc.rdoc }.to exit_with(0) end - it "should get modulepath and manifestdir values from the environment" do + it "should get modulepath and manifest values from the environment" do FileUtils.mkdir_p(modules) FileUtils.mkdir_p(modules2) env = Puppet::Node::Environment.create(Puppet[:environment].to_sym, [modules, modules2], "envmanifests/site.pp") Puppet.override({:environments => Puppet::Environments::Static.new(env), :current_environment => env}) do Puppet::Util::RDoc.stubs(:rdoc).with('doc', [modules.to_s, modules2.to_s, env.manifest.to_s], nil) expect { @doc.rdoc }.to exit_with(0) end end end describe "in the other modes" do it "should get reference in given format" do reference = stub 'reference' @doc.options[:mode] = :none @doc.options[:references] = [:ref] Puppet::Util::Reference.expects(:reference).with(:ref).returns(reference) @doc.options[:format] = :format @doc.stubs(:exit) reference.expects(:send).with { |format,contents| format == :format }.returns('doc') @doc.other end end end end diff --git a/spec/unit/configurer/fact_handler_spec.rb b/spec/unit/configurer/fact_handler_spec.rb index 3e3c033ae..8d933655a 100755 --- a/spec/unit/configurer/fact_handler_spec.rb +++ b/spec/unit/configurer/fact_handler_spec.rb @@ -1,89 +1,96 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/configurer' require 'puppet/configurer/fact_handler' require 'matchers/json' class FactHandlerTester include Puppet::Configurer::FactHandler + attr_accessor :environment + + def initialize(environment) + self.environment = environment + end + def reload_facter # don't want to do this in tests end end describe Puppet::Configurer::FactHandler do include JSONMatchers + let(:facthandler) { FactHandlerTester.new('production') } + before :each do - @facthandler = FactHandlerTester.new Puppet::Node::Facts.indirection.terminus_class = :memory end describe "when finding facts" do it "should use the node name value to retrieve the facts" do foo_facts = Puppet::Node::Facts.new('foo') bar_facts = Puppet::Node::Facts.new('bar') Puppet::Node::Facts.indirection.save(foo_facts) Puppet::Node::Facts.indirection.save(bar_facts) Puppet[:certname] = 'foo' Puppet[:node_name_value] = 'bar' - @facthandler.find_facts.should == bar_facts + facthandler.find_facts.should == bar_facts end it "should set the facts name based on the node_name_fact" do facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name') Puppet::Node::Facts.indirection.save(facts) Puppet[:node_name_fact] = 'my_name_fact' - @facthandler.find_facts.name.should == 'other_node_name' + facthandler.find_facts.name.should == 'other_node_name' end it "should set the node_name_value based on the node_name_fact" do facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name') Puppet::Node::Facts.indirection.save(facts) Puppet[:node_name_fact] = 'my_name_fact' - @facthandler.find_facts + facthandler.find_facts Puppet[:node_name_value].should == 'other_node_name' end it "should fail if finding facts fails" do Puppet::Node::Facts.indirection.expects(:find).raises RuntimeError - expect { @facthandler.find_facts }.to raise_error(Puppet::Error, /Could not retrieve local facts/) + expect { facthandler.find_facts }.to raise_error(Puppet::Error, /Could not retrieve local facts/) end it "should only load fact plugins once" do Puppet::Node::Facts.indirection.expects(:find).once - @facthandler.find_facts + facthandler.find_facts end end it "should serialize and CGI escape the fact values for uploading" do facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name') Puppet::Node::Facts.indirection.save(facts) - text = CGI.escape(@facthandler.find_facts.render(:pson)) + text = CGI.escape(facthandler.find_facts.render(:pson)) - @facthandler.facts_for_uploading.should == {:facts_format => :pson, :facts => text} + facthandler.facts_for_uploading.should == {:facts_format => :pson, :facts => text} end it "should properly accept facts containing a '+'" do facts = Puppet::Node::Facts.new('foo', 'afact' => 'a+b') Puppet::Node::Facts.indirection.save(facts) - text = CGI.escape(@facthandler.find_facts.render(:pson)) + text = CGI.escape(facthandler.find_facts.render(:pson)) - @facthandler.facts_for_uploading.should == {:facts_format => :pson, :facts => text} + facthandler.facts_for_uploading.should == {:facts_format => :pson, :facts => text} end it "should generate valid facts data against the facts schema" do facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name') Puppet::Node::Facts.indirection.save(facts) - expect(CGI.unescape(@facthandler.facts_for_uploading[:facts])).to validate_against('api/schemas/facts.json') + expect(CGI.unescape(facthandler.facts_for_uploading[:facts])).to validate_against('api/schemas/facts.json') end end diff --git a/spec/unit/configurer_spec.rb b/spec/unit/configurer_spec.rb index 7e532b888..fdcb6acb6 100755 --- a/spec/unit/configurer_spec.rb +++ b/spec/unit/configurer_spec.rb @@ -1,680 +1,680 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/configurer' describe Puppet::Configurer do before do Puppet.settings.stubs(:use).returns(true) @agent = Puppet::Configurer.new @agent.stubs(:init_storage) Puppet::Util::Storage.stubs(:store) Puppet[:server] = "puppetmaster" Puppet[:report] = true end it "should include the Fact Handler module" do Puppet::Configurer.ancestors.should be_include(Puppet::Configurer::FactHandler) end describe "when executing a pre-run hook" do it "should do nothing if the hook is set to an empty string" do Puppet.settings[:prerun_command] = "" Puppet::Util.expects(:exec).never @agent.execute_prerun_command end it "should execute any pre-run command provided via the 'prerun_command' setting" do Puppet.settings[:prerun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.execute_prerun_command end it "should fail if the command fails" do Puppet.settings[:prerun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.execute_prerun_command.should be_false end end describe "when executing a post-run hook" do it "should do nothing if the hook is set to an empty string" do Puppet.settings[:postrun_command] = "" Puppet::Util.expects(:exec).never @agent.execute_postrun_command end it "should execute any post-run command provided via the 'postrun_command' setting" do Puppet.settings[:postrun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.execute_postrun_command end it "should fail if the command fails" do Puppet.settings[:postrun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.execute_postrun_command.should be_false end end describe "when executing a catalog run" do before do Puppet.settings.stubs(:use).returns(true) @agent.stubs(:download_plugins) Puppet::Node::Facts.indirection.terminus_class = :memory @facts = Puppet::Node::Facts.new(Puppet[:node_name_value]) Puppet::Node::Facts.indirection.save(@facts) @catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote(Puppet[:environment].to_sym)) @catalog.stubs(:to_ral).returns(@catalog) Puppet::Resource::Catalog.indirection.terminus_class = :rest Puppet::Resource::Catalog.indirection.stubs(:find).returns(@catalog) @agent.stubs(:send_report) @agent.stubs(:save_last_run_summary) Puppet::Util::Log.stubs(:close_all) end after :all do Puppet::Node::Facts.indirection.reset_terminus_class Puppet::Resource::Catalog.indirection.reset_terminus_class end it "should initialize storage" do Puppet::Util::Storage.expects(:load) @agent.run end it "downloads plugins when told" do @agent.expects(:download_plugins) @agent.run(:pluginsync => true) end it "does not download plugins when told" do @agent.expects(:download_plugins).never @agent.run(:pluginsync => false) end it "should carry on when it can't fetch its node definition" do error = Net::HTTPError.new(400, 'dummy server communication error') Puppet::Node.indirection.expects(:find).raises(error) @agent.run.should == 0 end it "applies a cached catalog when it can't connect to the master" do error = Errno::ECONNREFUSED.new('Connection refused - connect(2)') Puppet::Node.indirection.expects(:find).raises(error) Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entry(:ignore_cache => true)).raises(error) Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entry(:ignore_terminus => true)).returns(@catalog) @agent.run.should == 0 end it "should initialize a transaction report if one is not provided" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns report @agent.run end it "should respect node_name_fact when setting the host on a report" do Puppet[:node_name_fact] = 'my_name_fact' @facts.values = {'my_name_fact' => 'node_name_from_fact'} report = Puppet::Transaction::Report.new("apply") @agent.run(:report => report) report.host.should == 'node_name_from_fact' end it "should pass the new report to the catalog" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.stubs(:new).returns report @catalog.expects(:apply).with{|options| options[:report] == report} @agent.run end it "should use the provided report if it was passed one" do report = Puppet::Transaction::Report.new("apply") @catalog.expects(:apply).with {|options| options[:report] == report} @agent.run(:report => report) end it "should set the report as a log destination" do report = Puppet::Transaction::Report.new("apply") report.expects(:<<).with(instance_of(Puppet::Util::Log)).at_least_once @agent.run(:report => report) end it "should retrieve the catalog" do @agent.expects(:retrieve_catalog) @agent.run end it "should log a failure and do nothing if no catalog can be retrieved" do @agent.expects(:retrieve_catalog).returns nil Puppet.expects(:err).with "Could not retrieve catalog; skipping run" @agent.run end it "should apply the catalog with all options to :run" do @agent.expects(:retrieve_catalog).returns @catalog @catalog.expects(:apply).with { |args| args[:one] == true } @agent.run :one => true end it "should accept a catalog and use it instead of retrieving a different one" do @agent.expects(:retrieve_catalog).never @catalog.expects(:apply) @agent.run :one => true, :catalog => @catalog end it "should benchmark how long it takes to apply the catalog" do @agent.expects(:benchmark).with(:notice, instance_of(String)) @agent.expects(:retrieve_catalog).returns @catalog @catalog.expects(:apply).never # because we're not yielding @agent.run end it "should execute post-run hooks after the run" do @agent.expects(:execute_postrun_command) @agent.run end it "should send the report" do report = Puppet::Transaction::Report.new("apply", nil, "test", "aaaa") Puppet::Transaction::Report.expects(:new).returns(report) @agent.expects(:send_report).with(report) report.environment.should == "test" report.transaction_uuid.should == "aaaa" @agent.run end it "should send the transaction report even if the catalog could not be retrieved" do @agent.expects(:retrieve_catalog).returns nil report = Puppet::Transaction::Report.new("apply", nil, "test", "aaaa") Puppet::Transaction::Report.expects(:new).returns(report) @agent.expects(:send_report).with(report) report.environment.should == "test" report.transaction_uuid.should == "aaaa" @agent.run end it "should send the transaction report even if there is a failure" do @agent.expects(:retrieve_catalog).raises "whatever" report = Puppet::Transaction::Report.new("apply", nil, "test", "aaaa") Puppet::Transaction::Report.expects(:new).returns(report) @agent.expects(:send_report).with(report) report.environment.should == "test" report.transaction_uuid.should == "aaaa" @agent.run.should be_nil end it "should remove the report as a log destination when the run is finished" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) @agent.run Puppet::Util::Log.destinations.should_not include(report) end it "should return the report exit_status as the result of the run" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) report.expects(:exit_status).returns(1234) @agent.run.should == 1234 end it "should send the transaction report even if the pre-run command fails" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:prerun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.expects(:send_report).with(report) @agent.run.should be_nil end it "should include the pre-run command failure in the report" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:prerun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.run.should be_nil report.logs.find { |x| x.message =~ /Could not run command from prerun_command/ }.should be end it "should send the transaction report even if the post-run command fails" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:postrun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @agent.expects(:send_report).with(report) @agent.run.should be_nil end it "should include the post-run command failure in the report" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:postrun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") report.expects(:<<).with { |log| log.message.include?("Could not run command from postrun_command") } @agent.run.should be_nil end it "should execute post-run command even if the pre-run command fails" do Puppet.settings[:prerun_command] = "/my/precommand" Puppet.settings[:postrun_command] = "/my/postcommand" Puppet::Util::Execution.expects(:execute).with(["/my/precommand"]).raises(Puppet::ExecutionFailure, "Failed") Puppet::Util::Execution.expects(:execute).with(["/my/postcommand"]) @agent.run.should be_nil end it "should finalize the report" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) report.expects(:finalize_report) @agent.run end it "should not apply the catalog if the pre-run command fails" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:prerun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @catalog.expects(:apply).never() @agent.expects(:send_report) @agent.run.should be_nil end it "should apply the catalog, send the report, and return nil if the post-run command fails" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns(report) Puppet.settings[:postrun_command] = "/my/command" Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed") @catalog.expects(:apply) @agent.expects(:send_report) @agent.run.should be_nil end it "should refetch the catalog if the server specifies a new environment in the catalog" do - @catalog.stubs(:environment).returns("second_env") - @agent.expects(:retrieve_catalog).returns(@catalog).twice + catalog = Puppet::Resource::Catalog.new("tester", Puppet::Node::Environment.remote('second_env')) + @agent.expects(:retrieve_catalog).returns(catalog).twice @agent.run end it "should change the environment setting if the server specifies a new environment in the catalog" do @catalog.stubs(:environment).returns("second_env") @agent.run @agent.environment.should == "second_env" end it "should fix the report if the server specifies a new environment in the catalog" do report = Puppet::Transaction::Report.new("apply", nil, "test", "aaaa") Puppet::Transaction::Report.expects(:new).returns(report) @agent.expects(:send_report).with(report) @catalog.stubs(:environment).returns("second_env") @agent.stubs(:retrieve_catalog).returns(@catalog) @agent.run report.environment.should == "second_env" end it "should clear the global caches" do $env_module_directories = false @agent.run $env_module_directories.should == nil end describe "when not using a REST terminus for catalogs" do it "should not pass any facts when retrieving the catalog" do Puppet::Resource::Catalog.indirection.terminus_class = :compiler @agent.expects(:facts_for_uploading).never Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:facts].nil? }.returns @catalog @agent.run end end describe "when using a REST terminus for catalogs" do it "should pass the prepared facts and the facts format as arguments when retrieving the catalog" do Puppet::Resource::Catalog.indirection.terminus_class = :rest @agent.expects(:facts_for_uploading).returns(:facts => "myfacts", :facts_format => :foo) Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:facts] == "myfacts" and options[:facts_format] == :foo }.returns @catalog @agent.run end end end describe "when sending a report" do include PuppetSpec::Files before do Puppet.settings.stubs(:use).returns(true) @configurer = Puppet::Configurer.new Puppet[:lastrunfile] = tmpfile('last_run_file') @report = Puppet::Transaction::Report.new("apply") Puppet[:reports] = "none" end it "should print a report summary if configured to do so" do Puppet.settings[:summarize] = true @report.expects(:summary).returns "stuff" @configurer.expects(:puts).with("stuff") @configurer.send_report(@report) end it "should not print a report summary if not configured to do so" do Puppet.settings[:summarize] = false @configurer.expects(:puts).never @configurer.send_report(@report) end it "should save the report if reporting is enabled" do Puppet.settings[:report] = true Puppet::Transaction::Report.indirection.expects(:save).with(@report, nil, instance_of(Hash)) @configurer.send_report(@report) end it "should not save the report if reporting is disabled" do Puppet.settings[:report] = false Puppet::Transaction::Report.indirection.expects(:save).with(@report, nil, instance_of(Hash)).never @configurer.send_report(@report) end it "should save the last run summary if reporting is enabled" do Puppet.settings[:report] = true @configurer.expects(:save_last_run_summary).with(@report) @configurer.send_report(@report) end it "should save the last run summary if reporting is disabled" do Puppet.settings[:report] = false @configurer.expects(:save_last_run_summary).with(@report) @configurer.send_report(@report) end it "should log but not fail if saving the report fails" do Puppet.settings[:report] = true Puppet::Transaction::Report.indirection.expects(:save).raises("whatever") Puppet.expects(:err) lambda { @configurer.send_report(@report) }.should_not raise_error end end describe "when saving the summary report file" do include PuppetSpec::Files before do Puppet.settings.stubs(:use).returns(true) @configurer = Puppet::Configurer.new @report = stub 'report', :raw_summary => {} Puppet[:lastrunfile] = tmpfile('last_run_file') end it "should write the last run file" do @configurer.save_last_run_summary(@report) Puppet::FileSystem.exist?(Puppet[:lastrunfile]).should be_true end it "should write the raw summary as yaml" do @report.expects(:raw_summary).returns("summary") @configurer.save_last_run_summary(@report) File.read(Puppet[:lastrunfile]).should == YAML.dump("summary") end it "should log but not fail if saving the last run summary fails" do # The mock will raise an exception on any method used. This should # simulate a nice hard failure from the underlying OS for us. fh = Class.new(Object) do def method_missing(*args) raise "failed to do #{args[0]}" end end.new Puppet::Util.expects(:replace_file).yields(fh) Puppet.expects(:err) expect { @configurer.save_last_run_summary(@report) }.to_not raise_error end it "should create the last run file with the correct mode" do Puppet.settings.setting(:lastrunfile).expects(:mode).returns('664') @configurer.save_last_run_summary(@report) if Puppet::Util::Platform.windows? require 'puppet/util/windows/security' mode = Puppet::Util::Windows::Security.get_mode(Puppet[:lastrunfile]) else mode = Puppet::FileSystem.stat(Puppet[:lastrunfile]).mode end (mode & 0777).should == 0664 end it "should report invalid last run file permissions" do Puppet.settings.setting(:lastrunfile).expects(:mode).returns('892') Puppet.expects(:err).with(regexp_matches(/Could not save last run local report.*892 is invalid/)) @configurer.save_last_run_summary(@report) end end describe "when requesting a node" do it "uses the transaction uuid in the request" do Puppet::Node.indirection.expects(:find).with(anything, has_entries(:transaction_uuid => anything)).twice @agent.run end end describe "when retrieving a catalog" do before do Puppet.settings.stubs(:use).returns(true) @agent.stubs(:facts_for_uploading).returns({}) @catalog = Puppet::Resource::Catalog.new # this is the default when using a Configurer instance Puppet::Resource::Catalog.indirection.stubs(:terminus_class).returns :rest @agent.stubs(:convert_catalog).returns @catalog end describe "and configured to only retrieve a catalog from the cache" do before do Puppet.settings[:use_cached_catalog] = true end it "should first look in the cache for a catalog" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.never @agent.retrieve_catalog({}).should == @catalog end it "should compile a new catalog if none is found in the cache" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog @agent.retrieve_catalog({}).should == @catalog end end it "should use the Catalog class to get its catalog" do Puppet::Resource::Catalog.indirection.expects(:find).returns @catalog @agent.retrieve_catalog({}) end it "should use its node_name_value to retrieve the catalog" do Facter.stubs(:value).returns "eh" Puppet.settings[:node_name_value] = "myhost.domain.com" Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| name == "myhost.domain.com" }.returns @catalog @agent.retrieve_catalog({}) end it "should default to returning a catalog retrieved directly from the server, skipping the cache" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog @agent.retrieve_catalog({}).should == @catalog end it "should log and return the cached catalog when no catalog can be retrieved from the server" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog Puppet.expects(:notice) @agent.retrieve_catalog({}).should == @catalog end it "should not look in the cache for a catalog if one is returned from the server" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.never @agent.retrieve_catalog({}).should == @catalog end it "should return the cached catalog when retrieving the remote catalog throws an exception" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.raises "eh" Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog @agent.retrieve_catalog({}).should == @catalog end it "should log and return nil if no catalog can be retrieved from the server and :usecacheonfailure is disabled" do Puppet[:usecacheonfailure] = false Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet.expects(:warning) @agent.retrieve_catalog({}).should be_nil end it "should return nil if no cached catalog is available and no catalog can be retrieved from the server" do Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil @agent.retrieve_catalog({}).should be_nil end it "should convert the catalog before returning" do Puppet::Resource::Catalog.indirection.stubs(:find).returns @catalog @agent.expects(:convert_catalog).with { |cat, dur| cat == @catalog }.returns "converted catalog" @agent.retrieve_catalog({}).should == "converted catalog" end it "should return nil if there is an error while retrieving the catalog" do Puppet::Resource::Catalog.indirection.expects(:find).at_least_once.raises "eh" @agent.retrieve_catalog({}).should be_nil end end describe "when converting the catalog" do before do Puppet.settings.stubs(:use).returns(true) @catalog = Puppet::Resource::Catalog.new @oldcatalog = stub 'old_catalog', :to_ral => @catalog end it "should convert the catalog to a RAL-formed catalog" do @oldcatalog.expects(:to_ral).returns @catalog @agent.convert_catalog(@oldcatalog, 10).should equal(@catalog) end it "should finalize the catalog" do @catalog.expects(:finalize) @agent.convert_catalog(@oldcatalog, 10) end it "should record the passed retrieval time with the RAL catalog" do @catalog.expects(:retrieval_duration=).with 10 @agent.convert_catalog(@oldcatalog, 10) end it "should write the RAL catalog's class file" do @catalog.expects(:write_class_file) @agent.convert_catalog(@oldcatalog, 10) end it "should write the RAL catalog's resource file" do @catalog.expects(:write_resource_file) @agent.convert_catalog(@oldcatalog, 10) end end end diff --git a/spec/unit/face/config_spec.rb b/spec/unit/face/config_spec.rb index 2c43b33b7..f065cfab9 100755 --- a/spec/unit/face/config_spec.rb +++ b/spec/unit/face/config_spec.rb @@ -1,147 +1,142 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/face' module PuppetFaceSpecs describe Puppet::Face[:config, '0.0.1'] do FS = Puppet::FileSystem it "prints a single setting without the name" do Puppet[:trace] = true expect { subject.print("trace") }.to have_printed('true') end it "prints multiple settings with the names" do Puppet[:trace] = true Puppet[:syslogfacility] = "file" expect { subject.print("trace", "syslogfacility") }.to have_printed(<<-OUTPUT) trace = true syslogfacility = file OUTPUT end it "prints the setting from the selected section" do Puppet.settings.parse_config(<<-CONF) - [other] + [user] syslogfacility = file CONF - expect { subject.print("syslogfacility", :section => "other") }.to have_printed('file') + expect { subject.print("syslogfacility", :section => "user") }.to have_printed('file') end it "defaults to all when no arguments are given" do subject.expects(:puts).times(Puppet.settings.to_a.length) subject.print end it "prints out all of the settings when asked for 'all'" do subject.expects(:puts).times(Puppet.settings.to_a.length) subject.print('all') end shared_examples_for :config_printing_a_section do |section| def add_section_option(args, section) args << { :section => section } if section args end it "prints directory env settings for an env that exists" do FS.overlay( FS::MemoryFile.a_directory(File.expand_path("/dev/null/environments"), [ FS::MemoryFile.a_directory("production", [ FS::MemoryFile.a_missing_file("environment.conf"), ]), ]) ) do args = "environmentpath","manifest","modulepath","environment","basemodulepath" expect { subject.print(*add_section_option(args, section)) }.to have_printed(<<-OUTPUT) environmentpath = #{File.expand_path("/dev/null/environments")} manifest = #{File.expand_path("/dev/null/environments/production/manifests")} modulepath = #{File.expand_path("/dev/null/environments/production/modules")}#{File::PATH_SEPARATOR}#{File.expand_path("/some/base")} environment = production basemodulepath = #{File.expand_path("/some/base")} OUTPUT end end it "interpolates settings in environment.conf" do FS.overlay( FS::MemoryFile.a_directory(File.expand_path("/dev/null/environments"), [ FS::MemoryFile.a_directory("production", [ FS::MemoryFile.a_regular_file_containing("environment.conf", <<-CONTENT), modulepath=/custom/modules#{File::PATH_SEPARATOR}$basemodulepath CONTENT ]), ]) ) do args = "environmentpath","manifest","modulepath","environment","basemodulepath" expect { subject.print(*add_section_option(args, section)) }.to have_printed(<<-OUTPUT) environmentpath = #{File.expand_path("/dev/null/environments")} manifest = #{File.expand_path("/dev/null/environments/production/manifests")} modulepath = #{File.expand_path("/custom/modules")}#{File::PATH_SEPARATOR}#{File.expand_path("/some/base")} environment = production basemodulepath = #{File.expand_path("/some/base")} OUTPUT end end it "prints the default configured env settings for an env that does not exist" do pending "This case no longer exists because Application will through an error before we even get here because of the non-existent environment" Puppet[:environment] = 'doesnotexist' FS.overlay( FS::MemoryFile.a_directory(File.expand_path("/dev/null/environments"), [ FS::MemoryFile.a_missing_file("doesnotexist") ]) ) do args = "environmentpath","manifest","modulepath","environment","basemodulepath" expect { subject.print(*add_section_option(args, section)) }.to have_printed(<<-OUTPUT) environmentpath = #{File.expand_path("/dev/null/environments")} manifest = no_manifest modulepath = environment = doesnotexist basemodulepath = #{File.expand_path("/some/base")} OUTPUT end end end context "when printing environment settings" do - before(:each) do - Puppet.settings.stubs(:global_defaults_initialized?).returns(:true) - end - context "from main section" do before(:each) do Puppet.settings.parse_config(<<-CONF) [main] environmentpath=$confdir/environments basemodulepath=/some/base CONF end it_behaves_like :config_printing_a_section, nil end context "from master section" do before(:each) do Puppet.settings.parse_config(<<-CONF) [master] environmentpath=$confdir/environments basemodulepath=/some/base CONF - Puppet.settings.stubs(:global_defaults_initialized?).returns(:true) end it_behaves_like :config_printing_a_section, :master end end end end diff --git a/spec/unit/indirector/node/exec_spec.rb b/spec/unit/indirector/node/exec_spec.rb index 092d430bb..b39483f92 100755 --- a/spec/unit/indirector/node/exec_spec.rb +++ b/spec/unit/indirector/node/exec_spec.rb @@ -1,76 +1,87 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/indirector/node/exec' require 'puppet/indirector/request' describe Puppet::Node::Exec do before do @indirection = mock 'indirection' Puppet.settings[:external_nodes] = File.expand_path("/echo") @searcher = Puppet::Node::Exec.new end describe "when constructing the command to run" do it "should use the external_node script as the command" do Puppet[:external_nodes] = "/bin/echo" @searcher.command.should == %w{/bin/echo} end it "should throw an exception if no external node command is set" do Puppet[:external_nodes] = "none" proc { @searcher.find(stub('request', :key => "foo")) }.should raise_error(ArgumentError) end end describe "when handling the results of the command" do + let(:testing_env) { Puppet::Node::Environment.create(:testing, []) } + let(:other_env) { Puppet::Node::Environment.create(:other, []) } + before do @name = "yay" @node = Puppet::Node.new(@name) @node.stubs(:fact_merge) Puppet::Node.expects(:new).with(@name).returns(@node) @result = {} # Use a local variable so the reference is usable in the execute definition. result = @result @searcher.meta_def(:execute) do |command, arguments| return YAML.dump(result) end @request = Puppet::Indirector::Request.new(:node, :find, @name, nil) end + around do |example| + envs = Puppet::Environments::Static.new(testing_env, other_env) + + Puppet.override(:environments => envs) do + example.run + end + end + it "should translate the YAML into a Node instance" do # Use an empty hash @searcher.find(@request).should equal(@node) end it "should set the resulting parameters as the node parameters" do @result[:parameters] = {"a" => "b", "c" => "d"} @searcher.find(@request) @node.parameters.should == {"a" => "b", "c" => "d"} end it "should set the resulting classes as the node classes" do @result[:classes] = %w{one two} @searcher.find(@request) @node.classes.should == [ 'one', 'two' ] end it "should merge the node's facts with its parameters" do @node.expects(:fact_merge) @searcher.find(@request) end it "should set the node's environment if one is provided" do - @result[:environment] = "yay" + @result[:environment] = "testing" @searcher.find(@request) - @node.environment.to_s.should == 'yay' + expect(@node.environment.name).to eq(:testing) end it "should set the node's environment based on the request if not otherwise provided" do - @request.environment = "boo" + @request.environment = "other" @searcher.find(@request) - @node.environment.to_s.should == 'boo' + expect(@node.environment.name).to eq(:other) end end end diff --git a/spec/unit/indirector/request_spec.rb b/spec/unit/indirector/request_spec.rb index 03d79843f..3f0819ac4 100755 --- a/spec/unit/indirector/request_spec.rb +++ b/spec/unit/indirector/request_spec.rb @@ -1,492 +1,507 @@ #! /usr/bin/env ruby require 'spec_helper' require 'matchers/json' require 'puppet/indirector/request' describe Puppet::Indirector::Request do include JSONMatchers describe "when initializing" do it "should always convert the indirection name to a symbol" do Puppet::Indirector::Request.new("ind", :method, "mykey", nil).indirection_name.should == :ind end it "should use provided value as the key if it is a string" do Puppet::Indirector::Request.new(:ind, :method, "mykey", nil).key.should == "mykey" end it "should use provided value as the key if it is a symbol" do Puppet::Indirector::Request.new(:ind, :method, :mykey, nil).key.should == :mykey end it "should use the name of the provided instance as its key if an instance is provided as the key instead of a string" do instance = mock 'instance', :name => "mykey" request = Puppet::Indirector::Request.new(:ind, :method, nil, instance) request.key.should == "mykey" request.instance.should equal(instance) end it "should support options specified as a hash" do expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil, :one => :two) }.to_not raise_error end it "should support nil options" do expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil, nil) }.to_not raise_error end it "should support unspecified options" do expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil) }.to_not raise_error end it "should use an empty options hash if nil was provided" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, nil).options.should == {} end it "should default to a nil node" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).node.should be_nil end it "should set its node attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :node => "foo.com").node.should == "foo.com" end it "should default to a nil ip" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).ip.should be_nil end it "should set its ip attribute if provided in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ip => "192.168.0.1").ip.should == "192.168.0.1" end it "should default to being unauthenticated" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_authenticated end it "should set be marked authenticated if configured in the options" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :authenticated => "eh").should be_authenticated end it "should keep its options as a hash even if a node is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :node => "eh").options.should be_instance_of(Hash) end it "should keep its options as a hash even if another option is specified" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :foo => "bar").options.should be_instance_of(Hash) end it "should treat options other than :ip, :node, and :authenticated as options rather than attributes" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :server => "bar").options[:server].should == "bar" end it "should normalize options to use symbols as keys" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, "foo" => "bar").options[:foo].should == "bar" end describe "and the request key is a URI" do let(:file) { File.expand_path("/my/file with spaces") } + let(:an_environment) { Puppet::Node::Environment.create(:an_environment, []) } + let(:env_loaders) { Puppet::Environments::Static.new(an_environment) } + + around(:each) do |example| + Puppet.override({ :environments => env_loaders }, "Static environment loader for specs") do + example.run + end + end describe "and the URI is a 'file' URI" do before do @request = Puppet::Indirector::Request.new(:ind, :method, "#{URI.unescape(Puppet::Util.path_to_uri(file).to_s)}", nil) end it "should set the request key to the unescaped full file path" do @request.key.should == file end it "should not set the protocol" do @request.protocol.should be_nil end it "should not set the port" do @request.port.should be_nil end it "should not set the server" do @request.server.should be_nil end end it "should set the protocol to the URI scheme" do - Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).protocol.should == "http" + Puppet::Indirector::Request.new(:ind, :method, "http://host/an_environment", nil).protocol.should == "http" end it "should set the server if a server is provided" do - Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).server.should == "host" + Puppet::Indirector::Request.new(:ind, :method, "http://host/an_environment", nil).server.should == "host" end it "should set the server and port if both are provided" do - Puppet::Indirector::Request.new(:ind, :method, "http://host:543/stuff", nil).port.should == 543 + Puppet::Indirector::Request.new(:ind, :method, "http://host:543/an_environment", nil).port.should == 543 end it "should default to the masterport if the URI scheme is 'puppet'" do Puppet[:masterport] = "321" - Puppet::Indirector::Request.new(:ind, :method, "puppet://host/stuff", nil).port.should == 321 + Puppet::Indirector::Request.new(:ind, :method, "puppet://host/an_environment", nil).port.should == 321 end it "should use the provided port if the URI scheme is not 'puppet'" do - Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).port.should == 80 + Puppet::Indirector::Request.new(:ind, :method, "http://host/an_environment", nil).port.should == 80 end it "should set the request key to the unescaped key part path from the URI" do - Puppet::Indirector::Request.new(:ind, :method, "http://host/environment/terminus/stuff with spaces", nil).key.should == "stuff with spaces" + Puppet::Indirector::Request.new(:ind, :method, "http://host/an_environment/terminus/stuff with spaces", nil).key.should == "stuff with spaces" end it "should set the :uri attribute to the full URI" do - Puppet::Indirector::Request.new(:ind, :method, "http:///stu ff", nil).uri.should == 'http:///stu ff' + Puppet::Indirector::Request.new(:ind, :method, "http:///an_environment/stu ff", nil).uri.should == 'http:///an_environment/stu ff' end it "should not parse relative URI" do Puppet::Indirector::Request.new(:ind, :method, "foo/bar", nil).uri.should be_nil end it "should not parse opaque URI" do Puppet::Indirector::Request.new(:ind, :method, "mailto:joe", nil).uri.should be_nil end end it "should allow indication that it should not read a cached instance" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ignore_cache => true).should be_ignore_cache end it "should default to not ignoring the cache" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_ignore_cache end it "should allow indication that it should not not read an instance from the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ignore_terminus => true).should be_ignore_terminus end it "should default to not ignoring the terminus" do Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_ignore_terminus end end it "should look use the Indirection class to return the appropriate indirection" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key, nil) request.indirection.should equal(ind) end it "should use its indirection to look up the appropriate model" do ind = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind request = Puppet::Indirector::Request.new(:myind, :method, :key, nil) ind.expects(:model).returns "mymodel" request.model.should == "mymodel" end it "should fail intelligently when asked to find a model but the indirection cannot be found" do Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns nil request = Puppet::Indirector::Request.new(:myind, :method, :key, nil) expect { request.model }.to raise_error(ArgumentError) end it "should have a method for determining if the request is plural or singular" do Puppet::Indirector::Request.new(:myind, :method, :key, nil).should respond_to(:plural?) end it "should be considered plural if the method is 'search'" do Puppet::Indirector::Request.new(:myind, :search, :key, nil).should be_plural end it "should not be considered plural if the method is not 'search'" do Puppet::Indirector::Request.new(:myind, :find, :key, nil).should_not be_plural end it "should use its uri, if it has one, as its string representation" do - Puppet::Indirector::Request.new(:myind, :find, "foo://bar/baz", nil).to_s.should == "foo://bar/baz" + Puppet.override({ + :environments => Puppet::Environments::Static.new( + Puppet::Node::Environment.create(:baz, []) + )}, + "Static loader for spec") do + + Puppet::Indirector::Request.new(:myind, :find, "foo://bar/baz", nil).to_s.should == "foo://bar/baz" + end end it "should use its indirection name and key, if it has no uri, as its string representation" do Puppet::Indirector::Request.new(:myind, :find, "key", nil) == "/myind/key" end it "should be able to return the URI-escaped key" do Puppet::Indirector::Request.new(:myind, :find, "my key", nil).escaped_key.should == URI.escape("my key") end it "should set its environment to an environment instance when a string is specified as its environment" do env = Puppet::Node::Environment.create(:foo, []) Puppet.override(:environments => Puppet::Environments::Static.new(env)) do Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :environment => "foo").environment.should == env end end it "should use any passed in environment instances as its environment" do env = Puppet::Node::Environment.create(:foo, []) Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :environment => env).environment.should equal(env) end it "should use the current environment when none is provided" do configured = Puppet::Node::Environment.create(:foo, []) Puppet[:environment] = "foo" expect(Puppet::Indirector::Request.new(:myind, :find, "my key", nil).environment).to eq(Puppet.lookup(:current_environment)) end it "should support converting its options to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key", nil ).should respond_to(:to_hash) end it "should include all of its attributes when its options are converted to a hash" do Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :node => 'foo').to_hash[:node].should == 'foo' end describe "when building a query string from its options" do def a_request_with_options(options) Puppet::Indirector::Request.new(:myind, :find, "my key", nil, options) end def the_parsed_query_string_from(request) CGI.parse(request.query_string.sub(/^\?/, '')) end it "should return an empty query string if there are no options" do request = a_request_with_options(nil) request.query_string.should == "" end it "should return an empty query string if the options are empty" do request = a_request_with_options({}) request.query_string.should == "" end it "should prefix the query string with '?'" do request = a_request_with_options(:one => "two") request.query_string.should =~ /^\?/ end it "should include all options in the query string, separated by '&'" do request = a_request_with_options(:one => "two", :three => "four") the_parsed_query_string_from(request).should == { "one" => ["two"], "three" => ["four"] } end it "should ignore nil options" do request = a_request_with_options(:one => "two", :three => nil) the_parsed_query_string_from(request).should == { "one" => ["two"] } end it "should convert 'true' option values into strings" do request = a_request_with_options(:one => true) the_parsed_query_string_from(request).should == { "one" => ["true"] } end it "should convert 'false' option values into strings" do request = a_request_with_options(:one => false) the_parsed_query_string_from(request).should == { "one" => ["false"] } end it "should convert to a string all option values that are integers" do request = a_request_with_options(:one => 50) the_parsed_query_string_from(request).should == { "one" => ["50"] } end it "should convert to a string all option values that are floating point numbers" do request = a_request_with_options(:one => 1.2) the_parsed_query_string_from(request).should == { "one" => ["1.2"] } end it "should CGI-escape all option values that are strings" do request = a_request_with_options(:one => "one two") the_parsed_query_string_from(request).should == { "one" => ["one two"] } end it "should convert an array of values into multiple entries for the same key" do request = a_request_with_options(:one => %w{one two}) the_parsed_query_string_from(request).should == { "one" => ["one", "two"] } end it "should stringify simple data types inside an array" do request = a_request_with_options(:one => ['one', nil]) the_parsed_query_string_from(request).should == { "one" => ["one"] } end it "should error if an array contains another array" do request = a_request_with_options(:one => ['one', ["not allowed"]]) expect { request.query_string }.to raise_error(ArgumentError) end it "should error if an array contains illegal data" do request = a_request_with_options(:one => ['one', { :not => "allowed" }]) expect { request.query_string }.to raise_error(ArgumentError) end it "should convert to a string and CGI-escape all option values that are symbols" do request = a_request_with_options(:one => :"sym bol") the_parsed_query_string_from(request).should == { "one" => ["sym bol"] } end it "should fail if options other than booleans or strings are provided" do request = a_request_with_options(:one => { :one => :two }) expect { request.query_string }.to raise_error(ArgumentError) end end context '#do_request' do before :each do @request = Puppet::Indirector::Request.new(:myind, :find, "my key", nil) end context 'when not using SRV records' do before :each do Puppet.settings[:use_srv_records] = false end it "yields the request with the default server and port when no server or port were specified on the original request" do count = 0 rval = @request.do_request(:puppet, 'puppet.example.com', '90210') do |got| count += 1 got.server.should == 'puppet.example.com' got.port.should == '90210' 'Block return value' end count.should == 1 rval.should == 'Block return value' end end context 'when using SRV records' do before :each do Puppet.settings[:use_srv_records] = true Puppet.settings[:srv_domain] = 'example.com' end it "yields the request with the original server and port unmodified" do @request.server = 'puppet.example.com' @request.port = '90210' count = 0 rval = @request.do_request do |got| count += 1 got.server.should == 'puppet.example.com' got.port.should == '90210' 'Block return value' end count.should == 1 rval.should == 'Block return value' end context "when SRV returns servers" do before :each do @dns_mock = mock('dns') Resolv::DNS.expects(:new).returns(@dns_mock) @port = 7205 @host = '_x-puppet._tcp.example.com' @srv_records = [Resolv::DNS::Resource::IN::SRV.new(0, 0, @port, @host)] @dns_mock.expects(:getresources). with("_x-puppet._tcp.#{Puppet.settings[:srv_domain]}", Resolv::DNS::Resource::IN::SRV). returns(@srv_records) end it "yields a request using the server and port from the SRV record" do count = 0 rval = @request.do_request do |got| count += 1 got.server.should == '_x-puppet._tcp.example.com' got.port.should == 7205 @block_return end count.should == 1 rval.should == @block_return end it "should fall back to the default server when the block raises a SystemCallError" do count = 0 second_pass = nil rval = @request.do_request(:puppet, 'puppet', 8140) do |got| count += 1 if got.server == '_x-puppet._tcp.example.com' then raise SystemCallError, "example failure" else second_pass = got end @block_return end second_pass.server.should == 'puppet' second_pass.port.should == 8140 count.should == 2 rval.should == @block_return end end end end describe "#remote?" do def request(options = {}) Puppet::Indirector::Request.new('node', 'find', 'localhost', nil, options) end it "should not be unless node or ip is set" do request.should_not be_remote end it "should be remote if node is set" do request(:node => 'example.com').should be_remote end it "should be remote if ip is set" do request(:ip => '127.0.0.1').should be_remote end it "should be remote if node and ip are set" do request(:node => 'example.com', :ip => '127.0.0.1').should be_remote end end end diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index 9a8c8fd1f..ae078afcb 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,722 +1,721 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' require 'puppet_spec/modules' require 'puppet/module_tool/checksums' describe Puppet::Module do include PuppetSpec::Files let(:env) { mock("environment") } let(:path) { "/path" } let(:name) { "mymod" } let(:mod) { Puppet::Module.new(name, path, env) } before do # This is necessary because of the extra checks we have for the deprecated # 'plugins' directory Puppet::FileSystem.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do env = Puppet::Node::Environment.create(:myenv, []) env.expects(:module).with(name).returns "yep" Puppet.override(:environments => Puppet::Environments::Static.new(env)) do Puppet::Module.find(name, "myenv").should == "yep" end end it "should return nil if asked for a named module that doesn't exist" do env = Puppet::Node::Environment.create(:myenv, []) env.expects(:module).with(name).returns nil Puppet.override(:environments => Puppet::Environments::Static.new(env)) do Puppet::Module.find(name, "myenv").should be_nil end end describe "attributes" do it "should support a 'version' attribute" do mod.version = 1.09 mod.version.should == 1.09 end it "should support a 'source' attribute" do mod.source = "http://foo/bar" mod.source.should == "http://foo/bar" end it "should support a 'project_page' attribute" do mod.project_page = "http://foo/bar" mod.project_page.should == "http://foo/bar" end it "should support an 'author' attribute" do mod.author = "Luke Kanies " mod.author.should == "Luke Kanies " end it "should support a 'license' attribute" do mod.license = "GPL2" mod.license.should == "GPL2" end it "should support a 'summary' attribute" do mod.summary = "GPL2" mod.summary.should == "GPL2" end it "should support a 'description' attribute" do mod.description = "GPL2" mod.description.should == "GPL2" end it "should support specifying a compatible puppet version" do mod.puppetversion = "0.25" mod.puppetversion.should == "0.25" end end it "should validate that the puppet version is compatible" do mod.puppetversion = "0.25" Puppet.expects(:version).returns "0.25" mod.validate_puppet_version end it "should fail if the specified puppet version is not compatible" do mod.puppetversion = "0.25" Puppet.stubs(:version).returns "0.24" lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule) end describe "when finding unmet dependencies" do before do Puppet::FileSystem.unstub(:exist?) @modpath = tmpdir('modpath') Puppet.settings[:modulepath] = @modpath end it "should list modules that are missing" do metadata_file = "#{@modpath}/needy/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).returns true mod = PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar" }] } ) mod.unmet_dependencies.should == [{ :reason => :missing, :name => "baz/foobar", :version_constraint => ">= 2.2.0", :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' }, :mod_details => { :installed_version => nil } }] end it "should list modules that are missing and have invalid names" do metadata_file = "#{@modpath}/needy/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).returns true mod = PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar=bar" }] } ) mod.unmet_dependencies.should == [{ :reason => :missing, :name => "baz/foobar=bar", :version_constraint => ">= 2.2.0", :parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' }, :mod_details => { :installed_version => nil } }] end it "should list modules with unmet version requirement" do env = Puppet::Node::Environment.create(:testing, [@modpath]) ['test_gte_req', 'test_specific_req', 'foobar'].each do |mod_name| metadata_file = "#{@modpath}/#{mod_name}/metadata.json" Puppet::FileSystem.stubs(:exist?).with(metadata_file).returns true end mod = PuppetSpec::Modules.create( 'test_gte_req', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar" }] }, :environment => env ) mod2 = PuppetSpec::Modules.create( 'test_specific_req', @modpath, :metadata => { :dependencies => [{ "version_requirement" => "1.0.0", "name" => "baz/foobar" }] }, :environment => env ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '2.0.0', :author => 'baz' }, :environment => env ) mod.unmet_dependencies.should == [{ :reason => :version_mismatch, :name => "baz/foobar", :version_constraint => ">= 2.2.0", :parent => { :version => "v9.9.9", :name => "puppetlabs/test_gte_req" }, :mod_details => { :installed_version => "2.0.0" } }] mod2.unmet_dependencies.should == [{ :reason => :version_mismatch, :name => "baz/foobar", :version_constraint => "v1.0.0", :parent => { :version => "v9.9.9", :name => "puppetlabs/test_specific_req" }, :mod_details => { :installed_version => "2.0.0" } }] end it "should consider a dependency without a version requirement to be satisfied" do env = Puppet::Node::Environment.create(:testing, [@modpath]) mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [{ "name" => "baz/foobar" }] }, :environment => env ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '2.0.0', :author => 'baz' }, :environment => env ) mod.unmet_dependencies.should be_empty end it "should consider a dependency without a semantic version to be unmet" do env = Puppet::Node::Environment.create(:testing, [@modpath]) metadata_file = "#{@modpath}/foobar/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).times(3).returns true mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [{ "name" => "baz/foobar" }] }, :environment => env ) PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :version => '5.1', :author => 'baz' }, :environment => env ) mod.unmet_dependencies.should == [{ :reason => :non_semantic_version, :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" }, :mod_details => { :installed_version => "5.1" }, :name => "baz/foobar", :version_constraint => ">= 0.0.0" }] end it "should have valid dependencies when no dependencies have been specified" do mod = PuppetSpec::Modules.create( 'foobar', @modpath, :metadata => { :dependencies => [] } ) mod.unmet_dependencies.should == [] end it "should only list unmet dependencies" do env = Puppet::Node::Environment.create(:testing, [@modpath]) [name, 'satisfied'].each do |mod_name| metadata_file = "#{@modpath}/#{mod_name}/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).twice.returns true end mod = PuppetSpec::Modules.create( name, @modpath, :metadata => { :dependencies => [ { "version_requirement" => ">= 2.2.0", "name" => "baz/satisfied" }, { "version_requirement" => ">= 2.2.0", "name" => "baz/notsatisfied" } ] }, :environment => env ) PuppetSpec::Modules.create( 'satisfied', @modpath, :metadata => { :version => '3.3.0', :author => 'baz' }, :environment => env ) mod.unmet_dependencies.should == [{ :reason => :missing, :mod_details => { :installed_version => nil }, :parent => { :version => "v9.9.9", :name => "puppetlabs/#{name}" }, :name => "baz/notsatisfied", :version_constraint => ">= 2.2.0" }] end it "should be empty when all dependencies are met" do env = Puppet::Node::Environment.create(:testing, [@modpath]) mod = PuppetSpec::Modules.create( 'mymod2', @modpath, :metadata => { :dependencies => [ { "version_requirement" => ">= 2.2.0", "name" => "baz/satisfied" }, { "version_requirement" => "< 2.2.0", "name" => "baz/alsosatisfied" } ] }, :environment => env ) PuppetSpec::Modules.create( 'satisfied', @modpath, :metadata => { :version => '3.3.0', :author => 'baz' }, :environment => env ) PuppetSpec::Modules.create( 'alsosatisfied', @modpath, :metadata => { :version => '2.1.0', :author => 'baz' }, :environment => env ) mod.unmet_dependencies.should be_empty end end describe "when managing supported platforms" do it "should support specifying a supported platform" do mod.supports "solaris" end it "should support specifying a supported platform and version" do mod.supports "solaris", 1.0 end end it "should return nil if asked for a module whose name is 'nil'" do Puppet::Module.find(nil, "myenv").should be_nil end it "should provide support for logging" do Puppet::Module.ancestors.should be_include(Puppet::Util::Logging) end it "should be able to be converted to a string" do mod.to_s.should == "Module #{name}(#{path})" end it "should fail if its name is not alphanumeric" do lambda { Puppet::Module.new(".something", "/path", env) }.should raise_error(Puppet::Module::InvalidName) end it "should require a name at initialization" do lambda { Puppet::Module.new }.should raise_error(ArgumentError) end it "should accept an environment at initialization" do Puppet::Module.new("foo", "/path", env).environment.should == env end describe '#modulepath' do it "should return the directory the module is installed in, if a path exists" do mod = Puppet::Module.new("foo", "/a/foo", env) mod.modulepath.should == '/a' end end [:plugins, :pluginfacts, :templates, :files, :manifests].each do |filetype| case filetype when :plugins dirname = "lib" when :pluginfacts dirname = "facts.d" else dirname = filetype.to_s end it "should be able to return individual #{filetype}" do module_file = File.join(path, dirname, "my/file") Puppet::FileSystem.expects(:exist?).with(module_file).returns true mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should == module_file end it "should consider #{filetype} to be present if their base directory exists" do module_file = File.join(path, dirname) Puppet::FileSystem.expects(:exist?).with(module_file).returns true mod.send(filetype.to_s + "?").should be_true end it "should consider #{filetype} to be absent if their base directory does not exist" do module_file = File.join(path, dirname) Puppet::FileSystem.expects(:exist?).with(module_file).returns false mod.send(filetype.to_s + "?").should be_false end it "should return nil if asked to return individual #{filetype} that don't exist" do module_file = File.join(path, dirname, "my/file") Puppet::FileSystem.expects(:exist?).with(module_file).returns false mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return the base directory if asked for a nil path" do base = File.join(path, dirname) Puppet::FileSystem.expects(:exist?).with(base).returns true mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base end end it "should return the path to the plugin directory" do mod.plugin_directory.should == File.join(path, "lib") end end describe Puppet::Module, "when finding matching manifests" do before do @mod = Puppet::Module.new("mymod", "/a", mock("environment")) @pq_glob_with_extension = "yay/*.xx" @fq_glob_with_extension = "/a/manifests/#{@pq_glob_with_extension}" end it "should return all manifests matching the glob pattern" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.stubs(:directory?).returns false @mod.match_manifests(@pq_glob_with_extension).should == %w{foo bar} end it "should not return directories" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar}) FileTest.expects(:directory?).with("foo").returns false FileTest.expects(:directory?).with("bar").returns true @mod.match_manifests(@pq_glob_with_extension).should == %w{foo} end it "should default to the 'init' file if no glob pattern is specified" do Puppet::FileSystem.expects(:exist?).with("/a/manifests/init.pp").returns(true) - Puppet::FileSystem.expects(:exist?).with("/a/manifests/init.rb").returns(false) @mod.match_manifests(nil).should == %w{/a/manifests/init.pp} end it "should return all manifests matching the glob pattern in all existing paths" do Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{a b}) @mod.match_manifests(@pq_glob_with_extension).should == %w{a b} end - it "should match the glob pattern plus '.{pp,rb}' if no extension is specified" do - Dir.expects(:glob).with("/a/manifests/yay/foo.{pp,rb}").returns(%w{yay}) + it "should match the glob pattern plus '.pp' if no extension is specified" do + Dir.expects(:glob).with("/a/manifests/yay/foo.pp").returns(%w{yay}) @mod.match_manifests("yay/foo").should == %w{yay} end it "should return an empty array if no manifests matched" do Dir.expects(:glob).with(@fq_glob_with_extension).returns([]) @mod.match_manifests(@pq_glob_with_extension).should == [] end it "should raise an error if the pattern tries to leave the manifest directory" do expect do @mod.match_manifests("something/../../*") end.to raise_error(Puppet::Module::InvalidFilePattern, 'The pattern "something/../../*" to find manifests in the module "mymod" is invalid and potentially unsafe.') end end describe Puppet::Module do include PuppetSpec::Files before do @modpath = tmpdir('modpath') @module = PuppetSpec::Modules.create('mymod', @modpath) end it "should use 'License' in its current path as its metadata file" do @module.license_file.should == "#{@modpath}/mymod/License" end it "should cache the license file" do @module.expects(:path).once.returns nil @module.license_file @module.license_file end it "should use 'metadata.json' in its current path as its metadata file" do @module.metadata_file.should == "#{@modpath}/mymod/metadata.json" end it "should have metadata if it has a metadata file and its data is not empty" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should have metadata if it has a metadata file and its data is not empty" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should not have metadata if has a metadata file and its data is empty" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "/* +-----------------------------------------------------------------------+ | | | ==> DO NOT EDIT THIS FILE! <== | | | | You should edit the `Modulefile` and run `puppet-module build` | | to generate the `metadata.json` file for your releases. | | | +-----------------------------------------------------------------------+ */ {}" @module.should_not be_has_metadata end it "should know if it is missing a metadata file" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns false @module.should_not be_has_metadata end it "should be able to parse its metadata file" do @module.should respond_to(:load_metadata) end it "should parse its metadata file on initialization if it is present" do Puppet::Module.any_instance.expects(:has_metadata?).returns true Puppet::Module.any_instance.expects(:load_metadata) Puppet::Module.new("yay", "/path", mock("env")) end it "should tolerate failure to parse" do Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns(my_fixture('trailing-comma.json')) @module.has_metadata?.should be_false end def a_module_with_metadata(data) text = data.to_pson mod = Puppet::Module.new("foo", "/path", mock("env")) mod.stubs(:metadata_file).returns "/my/file" File.stubs(:read).with("/my/file").returns text mod end describe "when loading the metadata file" do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", :puppetversion => "0.25", :dependencies => [] } @module = a_module_with_metadata(@data) end %w{source author version license}.each do |attr| it "should set #{attr} if present in the metadata file" do @module.load_metadata @module.send(attr).should == @data[attr.to_sym] end it "should fail if #{attr} is not present in the metadata file" do @data.delete(attr.to_sym) @text = @data.to_pson File.stubs(:read).with("/my/file").returns @text lambda { @module.load_metadata }.should raise_error( Puppet::Module::MissingMetadata, "No #{attr} module metadata provided for foo" ) end end it "should set puppetversion if present in the metadata file" do @module.load_metadata @module.puppetversion.should == @data[:puppetversion] end context "when versionRequirement is used for dependency version info" do before do @data = { :license => "GPL2", :author => "luke", :version => "1.0", :source => "http://foo/", :puppetversion => "0.25", :dependencies => [ { "versionRequirement" => "0.0.1", "name" => "pmtacceptance/stdlib" }, { "versionRequirement" => "0.1.0", "name" => "pmtacceptance/apache" } ] } @module = a_module_with_metadata(@data) end it "should set the dependency version_requirement key" do @module.load_metadata @module.dependencies[0]['version_requirement'].should == "0.0.1" end it "should set the version_requirement key for all dependencies" do @module.load_metadata @module.dependencies[0]['version_requirement'].should == "0.0.1" @module.dependencies[1]['version_requirement'].should == "0.1.0" end end end it "should be able to tell if there are local changes" do modpath = tmpdir('modpath') foo_checksum = 'acbd18db4cc2f85cedef654fccc4a4d8' checksummed_module = PuppetSpec::Modules.create( 'changed', modpath, :metadata => { :checksums => { "foo" => foo_checksum, } } ) foo_path = Pathname.new(File.join(checksummed_module.path, 'foo')) IO.binwrite(foo_path, 'notfoo') Puppet::ModuleTool::Checksums.new(foo_path).checksum(foo_path).should_not == foo_checksum checksummed_module.has_local_changes?.should be_true IO.binwrite(foo_path, 'foo') Puppet::ModuleTool::Checksums.new(foo_path).checksum(foo_path).should == foo_checksum checksummed_module.has_local_changes?.should be_false end it "should know what other modules require it" do env = Puppet::Node::Environment.create(:testing, [@modpath]) dependable = PuppetSpec::Modules.create( 'dependable', @modpath, :metadata => {:author => 'puppetlabs'}, :environment => env ) PuppetSpec::Modules.create( 'needy', @modpath, :metadata => { :author => 'beggar', :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "puppetlabs/dependable" }] }, :environment => env ) PuppetSpec::Modules.create( 'wantit', @modpath, :metadata => { :author => 'spoiled', :dependencies => [{ "version_requirement" => "< 5.0.0", "name" => "puppetlabs/dependable" }] }, :environment => env ) dependable.required_by.should =~ [ { "name" => "beggar/needy", "version" => "9.9.9", "version_requirement" => ">= 2.2.0" }, { "name" => "spoiled/wantit", "version" => "9.9.9", "version_requirement" => "< 5.0.0" } ] end end diff --git a/spec/unit/module_tool_spec.rb b/spec/unit/module_tool_spec.rb index dee8e13bf..0eafb2e7d 100755 --- a/spec/unit/module_tool_spec.rb +++ b/spec/unit/module_tool_spec.rb @@ -1,330 +1,327 @@ #! /usr/bin/env ruby # encoding: UTF-8 require 'spec_helper' require 'puppet/module_tool' describe Puppet::ModuleTool do describe '.is_module_root?' do it 'should return true if directory has a Modulefile file' do FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/metadata.json')). returns(false) FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/Modulefile')). returns(true) subject.is_module_root?(Pathname.new('/a/b/c')).should be_true end it 'should return true if directory has a metadata.json file' do FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/metadata.json')). returns(true) subject.is_module_root?(Pathname.new('/a/b/c')).should be_true end it 'should return false if directory does not have a metadata.json or a Modulefile file' do FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/metadata.json')). returns(false) FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/Modulefile')). returns(false) subject.is_module_root?(Pathname.new('/a/b/c')).should be_false end end describe '.find_module_root' do let(:sample_path) { Pathname.new('/a/b/c').expand_path } it 'should return the first path as a pathname when it contains a module file' do Puppet::ModuleTool.expects(:is_module_root?).with(sample_path). returns(true) subject.find_module_root(sample_path).should == sample_path end it 'should return a parent path as a pathname when it contains a module file' do Puppet::ModuleTool.expects(:is_module_root?). with(responds_with(:to_s, File.expand_path('/a/b/c'))).returns(false) Puppet::ModuleTool.expects(:is_module_root?). with(responds_with(:to_s, File.expand_path('/a/b'))).returns(true) subject.find_module_root(sample_path).should == Pathname.new('/a/b').expand_path end it 'should return nil when no module root can be found' do Puppet::ModuleTool.expects(:is_module_root?).at_least_once.returns(false) subject.find_module_root(sample_path).should be_nil end end describe '.format_tree' do it 'should return an empty tree when given an empty list' do subject.format_tree([]).should == '' end it 'should return a shallow when given a list without dependencies' do list = [ { :text => 'first' }, { :text => 'second' }, { :text => 'third' } ] subject.format_tree(list).should == <<-TREE ├── first ├── second └── third TREE end it 'should return a deeply nested tree when given a list with deep dependencies' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] } ] }, ] subject.format_tree(list).should == <<-TREE └─┬ first └─┬ second └── third TREE end it 'should show connectors when deep dependencies are not on the last node of the top level' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] } ] }, { :text => 'fourth' } ] subject.format_tree(list).should == <<-TREE ├─┬ first │ └─┬ second │ └── third └── fourth TREE end it 'should show connectors when deep dependencies are not on the last node of any level' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] }, { :text => 'fourth' } ] } ] subject.format_tree(list).should == <<-TREE └─┬ first ├─┬ second │ └── third └── fourth TREE end it 'should show connectors in every case when deep dependencies are not on the last node' do list = [ { :text => 'first', :dependencies => [ { :text => 'second', :dependencies => [ { :text => 'third' } ] }, { :text => 'fourth' } ] }, { :text => 'fifth' } ] subject.format_tree(list).should == <<-TREE ├─┬ first │ ├─┬ second │ │ └── third │ └── fourth └── fifth TREE end end describe '.set_option_defaults' do let(:options) { {} } let(:modulepath) { ['/env/module/path', '/global/module/path'] } let(:environment_name) { :current_environment } let(:environment) { Puppet::Node::Environment.create(environment_name, modulepath) } subject do described_class.set_option_defaults(options) options end around do |example| - envs = Puppet::Environments::Combined.new( - Puppet::Environments::Static.new(environment), - Puppet::Environments::Legacy.new - ) + envs = Puppet::Environments::Static.new(environment) Puppet.override(:environments => envs) do example.run end end describe ':environment' do context 'as String' do let(:options) { { :environment => "#{environment_name}" } } it 'assigns the environment with the given name to :environment_instance' do expect(subject).to include :environment_instance => environment end end context 'as Symbol' do let(:options) { { :environment => :"#{environment_name}" } } it 'assigns the environment with the given name to :environment_instance' do expect(subject).to include :environment_instance => environment end end context 'as Puppet::Node::Environment' do let(:env) { Puppet::Node::Environment.create('anonymous', []) } let(:options) { { :environment => env } } it 'assigns the given environment to :environment_instance' do expect(subject).to include :environment_instance => env end end end describe ':modulepath' do let(:options) do { :modulepath => %w[bar foo baz].join(File::PATH_SEPARATOR) } end let(:paths) { options[:modulepath].split(File::PATH_SEPARATOR).map { |dir| File.expand_path(dir) } } it 'is expanded to an absolute path' do expect(subject[:environment_instance].full_modulepath).to eql paths end it 'is used to compute :target_dir' do expect(subject).to include :target_dir => paths.first end context 'conflicts with :environment' do let(:options) do { :modulepath => %w[bar foo baz].join(File::PATH_SEPARATOR), :environment => environment_name } end it 'replaces the modulepath of the :environment_instance' do expect(subject[:environment_instance].full_modulepath).to eql paths end it 'is used to compute :target_dir' do expect(subject).to include :target_dir => paths.first end end end describe ':target_dir' do let(:options) do { :target_dir => 'foo' } end let(:target) { File.expand_path(options[:target_dir]) } it 'is expanded to an absolute path' do expect(subject).to include :target_dir => target end it 'is prepended to the modulepath of the :environment_instance' do expect(subject[:environment_instance].full_modulepath.first).to eql target end context 'conflicts with :modulepath' do let(:options) do { :target_dir => 'foo', :modulepath => %w[bar foo baz].join(File::PATH_SEPARATOR) } end it 'is prepended to the modulepath of the :environment_instance' do expect(subject[:environment_instance].full_modulepath.first).to eql target end it 'shares the provided :modulepath via the :environment_instance' do paths = %w[foo] + options[:modulepath].split(File::PATH_SEPARATOR) paths.map! { |dir| File.expand_path(dir) } expect(subject[:environment_instance].full_modulepath).to eql paths end end context 'conflicts with :environment' do let(:options) do { :target_dir => 'foo', :environment => environment_name } end it 'is prepended to the modulepath of the :environment_instance' do expect(subject[:environment_instance].full_modulepath.first).to eql target end it 'shares the provided :modulepath via the :environment_instance' do paths = %w[foo] + environment.full_modulepath paths.map! { |dir| File.expand_path(dir) } expect(subject[:environment_instance].full_modulepath).to eql paths end end context 'when not passed' do it 'is populated with the first component of the modulepath' do expect(subject).to include :target_dir => subject[:environment_instance].full_modulepath.first end end end end describe '.parse_module_dependency' do it 'parses a dependency without a version range expression' do name, range, expr = subject.parse_module_dependency('source', 'name' => 'foo-bar') expect(name).to eql('foo-bar') expect(range).to eql(Semantic::VersionRange.parse('>= 0.0.0')) expect(expr).to eql('>= 0.0.0') end it 'parses a dependency with a version range expression' do name, range, expr = subject.parse_module_dependency('source', 'name' => 'foo-bar', 'version_requirement' => '1.2.x') expect(name).to eql('foo-bar') expect(range).to eql(Semantic::VersionRange.parse('1.2.x')) expect(expr).to eql('1.2.x') end it 'parses a dependency with a version range expression in the (deprecated) versionRange key' do name, range, expr = subject.parse_module_dependency('source', 'name' => 'foo-bar', 'versionRequirement' => '1.2.x') expect(name).to eql('foo-bar') expect(range).to eql(Semantic::VersionRange.parse('1.2.x')) expect(expr).to eql('1.2.x') end it 'does not raise an error on invalid version range expressions' do name, range, expr = subject.parse_module_dependency('source', 'name' => 'foo-bar', 'version_requirement' => 'nope') expect(name).to eql('foo-bar') expect(range).to eql(Semantic::VersionRange::EMPTY_RANGE) expect(expr).to eql('nope') end end end diff --git a/spec/unit/network/http/api/v1_spec.rb b/spec/unit/network/http/api/v1_spec.rb index 3ec1b08d2..3f3999608 100755 --- a/spec/unit/network/http/api/v1_spec.rb +++ b/spec/unit/network/http/api/v1_spec.rb @@ -1,498 +1,513 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/http' require 'puppet/network/http/api/v1' require 'puppet/indirector_testing' describe Puppet::Network::HTTP::API::V1 do let(:not_found_code) { Puppet::Network::HTTP::Error::HTTPNotFoundError::CODE } let(:not_acceptable_code) { Puppet::Network::HTTP::Error::HTTPNotAcceptableError::CODE } let(:not_authorized_code) { Puppet::Network::HTTP::Error::HTTPNotAuthorizedError::CODE } let(:indirection) { Puppet::IndirectorTesting.indirection } let(:handler) { Puppet::Network::HTTP::API::V1.new } let(:response) { Puppet::Network::HTTP::MemoryResponse.new } def a_request_that_heads(data, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => "text/pson", }, :method => "HEAD", :path => "/production/#{indirection.name}/#{data.value}", :params => {}, }) end def a_request_that_submits(data, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => request[:content_type_header] || "text/pson", }, :method => "PUT", :path => "/production/#{indirection.name}/#{data.value}", :params => {}, :body => request[:body].nil? ? data.render("pson") : request[:body] }) end def a_request_that_destroys(data, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => "text/pson", }, :method => "DELETE", :path => "/production/#{indirection.name}/#{data.value}", :params => {}, :body => '' }) end def a_request_that_finds(data, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => "text/pson", }, :method => "GET", :path => "/production/#{indirection.name}/#{data.value}", :params => {}, :body => '' }) end def a_request_that_searches(key, request = {}) Puppet::Network::HTTP::Request.from_hash({ :headers => { 'accept' => request[:accept_header], 'content-type' => "text/pson", }, :method => "GET", :path => "/production/#{indirection.name}s/#{key}", :params => {}, :body => '' }) end before do Puppet::IndirectorTesting.indirection.terminus_class = :memory Puppet::IndirectorTesting.indirection.terminus.clear handler.stubs(:check_authorization) handler.stubs(:warn_if_near_expiration) end describe "when converting a URI into a request" do + let(:environment) { Puppet::Node::Environment.create(:env, []) } + let(:env_loaders) { Puppet::Environments::Static.new(environment) } + before do handler.stubs(:handler).returns "foo" end + around do |example| + Puppet.override(:environments => env_loaders) do + example.run + end + end + it "should require the http method, the URI, and the query parameters" do # Not a terribly useful test, but an important statement for the spec lambda { handler.uri2indirection("/foo") }.should raise_error(ArgumentError) end it "should use the first field of the URI as the environment" do handler.uri2indirection("GET", "/env/foo/bar", {})[3][:environment].to_s.should == "env" end it "should fail if the environment is not alphanumeric" do lambda { handler.uri2indirection("GET", "/env ness/foo/bar", {}) }.should raise_error(ArgumentError) end it "should use the environment from the URI even if one is specified in the parameters" do handler.uri2indirection("GET", "/env/foo/bar", {:environment => "otherenv"})[3][:environment].to_s.should == "env" end it "should not pass a buck_path parameter through (See Bugs #13553, #13518, #13511)" do handler.uri2indirection("GET", "/env/foo/bar", { :bucket_path => "/malicious/path" })[3].should_not include({ :bucket_path => "/malicious/path" }) end it "should pass allowed parameters through" do handler.uri2indirection("GET", "/env/foo/bar", { :allowed_param => "value" })[3].should include({ :allowed_param => "value" }) end it "should return the environment as a Puppet::Node::Environment" do handler.uri2indirection("GET", "/env/foo/bar", {})[3][:environment].should be_a(Puppet::Node::Environment) end it "should not pass a buck_path parameter through (See Bugs #13553, #13518, #13511)" do handler.uri2indirection("GET", "/env/foo/bar", { :bucket_path => "/malicious/path" })[3].should_not include({ :bucket_path => "/malicious/path" }) end it "should pass allowed parameters through" do handler.uri2indirection("GET", "/env/foo/bar", { :allowed_param => "value" })[3].should include({ :allowed_param => "value" }) end it "should use the second field of the URI as the indirection name" do handler.uri2indirection("GET", "/env/foo/bar", {})[0].should == "foo" end it "should fail if the indirection name is not alphanumeric" do lambda { handler.uri2indirection("GET", "/env/foo ness/bar", {}) }.should raise_error(ArgumentError) end it "should use the remainder of the URI as the indirection key" do handler.uri2indirection("GET", "/env/foo/bar", {})[2].should == "bar" end it "should support the indirection key being a /-separated file path" do handler.uri2indirection("GET", "/env/foo/bee/baz/bomb", {})[2].should == "bee/baz/bomb" end it "should fail if no indirection key is specified" do lambda { handler.uri2indirection("GET", "/env/foo/", {}) }.should raise_error(ArgumentError) lambda { handler.uri2indirection("GET", "/env/foo", {}) }.should raise_error(ArgumentError) end it "should choose 'find' as the indirection method if the http method is a GET and the indirection name is singular" do handler.uri2indirection("GET", "/env/foo/bar", {})[1].should == :find end it "should choose 'find' as the indirection method if the http method is a POST and the indirection name is singular" do handler.uri2indirection("POST", "/env/foo/bar", {})[1].should == :find end it "should choose 'head' as the indirection method if the http method is a HEAD and the indirection name is singular" do handler.uri2indirection("HEAD", "/env/foo/bar", {})[1].should == :head end it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is plural" do handler.uri2indirection("GET", "/env/foos/bar", {})[1].should == :search end it "should change indirection name to 'status' if the http method is a GET and the indirection name is statuses" do handler.uri2indirection("GET", "/env/statuses/bar", {})[0].should == 'status' end it "should change indirection name to 'probe' if the http method is a GET and the indirection name is probes" do handler.uri2indirection("GET", "/env/probes/bar", {})[0].should == 'probe' end it "should choose 'delete' as the indirection method if the http method is a DELETE and the indirection name is singular" do handler.uri2indirection("DELETE", "/env/foo/bar", {})[1].should == :destroy end it "should choose 'save' as the indirection method if the http method is a PUT and the indirection name is singular" do handler.uri2indirection("PUT", "/env/foo/bar", {})[1].should == :save end it "should fail if an indirection method cannot be picked" do lambda { handler.uri2indirection("UPDATE", "/env/foo/bar", {}) }.should raise_error(ArgumentError) end it "should URI unescape the indirection key" do escaped = URI.escape("foo bar") indirection_name, method, key, params = handler.uri2indirection("GET", "/env/foo/#{escaped}", {}) key.should == "foo bar" end end describe "when converting a request into a URI" do - let(:request) { Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => "myenv") } + let(:environment) { Puppet::Node::Environment.create(:myenv, []) } + let(:request) { Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => environment) } + + before do + handler.stubs(:handler).returns "foo" + end it "should use the environment as the first field of the URI" do handler.class.indirection2uri(request).split("/")[1].should == "myenv" end it "should use the indirection as the second field of the URI" do handler.class.indirection2uri(request).split("/")[2].should == "foo" end it "should pluralize the indirection name if the method is 'search'" do request.stubs(:method).returns :search handler.class.indirection2uri(request).split("/")[2].should == "foos" end it "should use the escaped key as the remainder of the URI" do escaped = URI.escape("with spaces") handler.class.indirection2uri(request).split("/")[3].sub(/\?.+/, '').should == escaped end it "should add the query string to the URI" do request.expects(:query_string).returns "?query" handler.class.indirection2uri(request).should =~ /\?query$/ end end describe "when converting a request into a URI with body" do - let(:request) { Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => "myenv") } + let(:environment) { Puppet::Node::Environment.create(:myenv, []) } + let(:request) { Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => environment) } it "should use the environment as the first field of the URI" do handler.class.request_to_uri_and_body(request).first.split("/")[1].should == "myenv" end it "should use the indirection as the second field of the URI" do handler.class.request_to_uri_and_body(request).first.split("/")[2].should == "foo" end it "should use the escaped key as the remainder of the URI" do escaped = URI.escape("with spaces") handler.class.request_to_uri_and_body(request).first.split("/")[3].sub(/\?.+/, '').should == escaped end it "should return the URI and body separately" do handler.class.request_to_uri_and_body(request).should == ["/myenv/foo/with%20spaces", "foo=bar"] end end describe "when processing a request" do it "should return not_authorized_code if the request is not authorized" do request = a_request_that_heads(Puppet::IndirectorTesting.new("my data")) handler.expects(:check_authorization).raises(Puppet::Network::AuthorizationError.new("forbidden")) handler.call(request, response) expect(response.code).to eq(not_authorized_code) end it "should return 'not found' if the indirection does not support remote requests" do request = a_request_that_heads(Puppet::IndirectorTesting.new("my data")) indirection.expects(:allow_remote_requests?).returns(false) handler.call(request, response) expect(response.code).to eq(not_found_code) end it "should return 'not found' if the environment does not exist" do Puppet.override(:environments => Puppet::Environments::Static.new()) do request = a_request_that_heads(Puppet::IndirectorTesting.new("my data")) handler.call(request, response) expect(response.code).to eq(not_found_code) end end it "should serialize a controller exception when an exception is thrown while finding the model instance" do request = a_request_that_finds(Puppet::IndirectorTesting.new("key")) handler.expects(:do_find).raises(ArgumentError, "The exception") handler.call(request, response) expect(response.code).to eq(400) expect(response.body).to eq("The exception") expect(response.type).to eq("text/plain") end end describe "when finding a model instance" do it "uses the first supported format for the response" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_finds(data, :accept_header => "unknown, pson") handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "responds with a not_acceptable_code error when no accept header is provided" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_finds(data, :accept_header => nil) handler.call(request, response) expect(response.code).to eq(not_acceptable_code) end it "raises an error when no accepted formats are known" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_finds(data, :accept_header => "unknown, also/unknown") handler.call(request, response) expect(response.code).to eq(not_acceptable_code) end it "should pass the result through without rendering it if the result is a string" do data = Puppet::IndirectorTesting.new("my data") data_string = "my data string" request = a_request_that_finds(data, :accept_header => "text/pson") indirection.expects(:find).returns(data_string) handler.call(request, response) expect(response.body).to eq(data_string) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "should return a not_found_code when no model instance can be found" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_finds(data, :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.code).to eq(not_found_code) end end describe "when searching for model instances" do it "uses the first supported format for the response" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_searches("my", :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) expect(response.body).to eq(Puppet::IndirectorTesting.render_multiple(:pson, [data])) end it "should return [] when searching returns an empty array" do request = a_request_that_searches("nothing", :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.body).to eq("[]") expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "should return a not_found_code when searching returns nil" do request = a_request_that_searches("nothing", :accept_header => "unknown, text/pson") indirection.expects(:search).returns(nil) handler.call(request, response) expect(response.code).to eq(not_found_code) end end describe "when destroying a model instance" do it "destroys the data indicated in the request" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_destroys(data) handler.call(request, response) Puppet::IndirectorTesting.indirection.find("my data").should be_nil end it "responds with pson when no Accept header is given" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_destroys(data, :accept_header => nil) handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "uses the first supported format for the response" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_destroys(data, :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "raises an error and does not destroy when no accepted formats are known" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_submits(data, :accept_header => "unknown, also/unknown") handler.call(request, response) expect(response.code).to eq(not_acceptable_code) Puppet::IndirectorTesting.indirection.find("my data").should_not be_nil end end describe "when saving a model instance" do it "allows an empty body when the format supports it" do class Puppet::IndirectorTesting::Nonvalidatingmemory < Puppet::IndirectorTesting::Memory def validate_key(_) # nothing end end indirection.terminus_class = :nonvalidatingmemory data = Puppet::IndirectorTesting.new("test") request = a_request_that_submits(data, :content_type_header => "application/x-raw", :body => '') handler.call(request, response) # PUP-3272 this test fails when yaml is removed and pson is used. Instead of returning an # empty string, the a string '""' is returned - Don't know what the expecation is, if this is # corrent or not. # (helindbe) # Puppet::IndirectorTesting.indirection.find("test").name.should == '' end it "saves the data sent in the request" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_submits(data) handler.call(request, response) saved = Puppet::IndirectorTesting.indirection.find("my data") expect(saved.name).to eq(data.name) end it "responds with pson when no Accept header is given" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_submits(data, :accept_header => nil) handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "uses the first supported format for the response" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_submits(data, :accept_header => "unknown, text/pson") handler.call(request, response) expect(response.body).to eq(data.render(:pson)) expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson)) end it "raises an error and does not save when no accepted formats are known" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_submits(data, :accept_header => "unknown, also/unknown") handler.call(request, response) expect(Puppet::IndirectorTesting.indirection.find("my data")).to be_nil expect(response.code).to eq(not_acceptable_code) end end describe "when performing head operation" do it "should not generate a response when a model head call succeeds" do data = Puppet::IndirectorTesting.new("my data") indirection.save(data, "my data") request = a_request_that_heads(data) handler.call(request, response) expect(response.code).to eq(nil) end it "should return a not_found_code when the model head call returns false" do data = Puppet::IndirectorTesting.new("my data") request = a_request_that_heads(data) handler.call(request, response) expect(response.code).to eq(not_found_code) expect(response.type).to eq("text/plain") expect(response.body).to eq("Not Found: Could not find indirector_testing my data") end end end diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index d2d27ad58..6710a6488 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -1,590 +1,488 @@ #! /usr/bin/env ruby require 'spec_helper' require 'tmpdir' require 'puppet/node/environment' require 'puppet/util/execution' require 'puppet_spec/modules' require 'puppet/parser/parser_factory' describe Puppet::Node::Environment do - let(:env) { Puppet::Node::Environment.new("testing") } + let(:env) { Puppet::Node::Environment.create("testing", []) } include PuppetSpec::Files - after do - Puppet::Node::Environment.clear - end context 'the environment' do - it "should convert an environment to string when converting to YAML" do + it "converts an environment to string when converting to YAML" do env.to_yaml.should match(/--- testing/) end - it "should use the filetimeout for the ttl for the module list" do - Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout]) - end - - it "should use the default environment if no name is provided while initializing an environment" do - Puppet[:environment] = "one" - Puppet::Node::Environment.new.name.should == :one - end - - it "should treat environment instances as singletons" do - Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one")) - end + describe ".create" do + it "creates equivalent environments whether specifying name as a symbol or a string" do + expect(Puppet::Node::Environment.create(:one, [])).to eq(Puppet::Node::Environment.create("one", [])) + end - it "should treat an environment specified as names or strings as equivalent" do - Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one")) + it "interns name" do + expect(Puppet::Node::Environment.create("one", []).name).to equal(:one) + end + it "does not produce environment singletons" do + expect(Puppet::Node::Environment.create("one", [])).to_not equal(Puppet::Node::Environment.create("one", [])) + end end - it "should return its name when converted to a string" do - Puppet::Node::Environment.new(:one).to_s.should == "one" + it "returns its name when converted to a string" do + expect(env.to_s).to eq("testing") end - it "should just return any provided environment if an environment is provided as the name" do - one = Puppet::Node::Environment.new(:one) - Puppet::Node::Environment.new(one).should equal(one) + it "has an inspect method for debugging" do + e = Puppet::Node::Environment.create(:test, ['/modules/path', '/other/modules'], '/manifests/path') + expect("a #{e} env").to eq("a test env") + expect(e.inspect).to match(%r{}) end describe "equality" do it "works as a hash key" do base = Puppet::Node::Environment.create(:first, ["modules"], "manifests") same = Puppet::Node::Environment.create(:first, ["modules"], "manifests") different = Puppet::Node::Environment.create(:first, ["different"], "manifests") hash = {} hash[base] = "base env" hash[same] = "same env" hash[different] = "different env" expect(hash[base]).to eq("same env") expect(hash[different]).to eq("different env") expect(hash).to have(2).item end it "is equal when name, modules, and manifests are the same" do base = Puppet::Node::Environment.create(:base, ["modules"], "manifests") different_name = Puppet::Node::Environment.create(:different, base.full_modulepath, base.manifest) expect(base).to_not eq("not an environment") expect(base).to eq(base) expect(base.hash).to eq(base.hash) expect(base.override_with(:modulepath => ["different"])).to_not eq(base) expect(base.override_with(:modulepath => ["different"]).hash).to_not eq(base.hash) expect(base.override_with(:manifest => "different")).to_not eq(base) expect(base.override_with(:manifest => "different").hash).to_not eq(base.hash) expect(different_name).to_not eq(base) expect(different_name.hash).to_not eq(base.hash) end end describe "overriding an existing environment" do let(:original_path) { [tmpdir('original')] } let(:new_path) { [tmpdir('new')] } let(:environment) { Puppet::Node::Environment.create(:overridden, original_path, 'orig.pp', '/config/script') } it "overrides modulepath" do overridden = environment.override_with(:modulepath => new_path) expect(overridden).to_not be_equal(environment) expect(overridden.name).to eq(:overridden) expect(overridden.manifest).to eq(File.expand_path('orig.pp')) expect(overridden.modulepath).to eq(new_path) expect(overridden.config_version).to eq('/config/script') end it "overrides manifest" do overridden = environment.override_with(:manifest => 'new.pp') expect(overridden).to_not be_equal(environment) expect(overridden.name).to eq(:overridden) expect(overridden.manifest).to eq(File.expand_path('new.pp')) expect(overridden.modulepath).to eq(original_path) expect(overridden.config_version).to eq('/config/script') end it "overrides config_version" do overridden = environment.override_with(:config_version => '/new/script') expect(overridden).to_not be_equal(environment) expect(overridden.name).to eq(:overridden) expect(overridden.manifest).to eq(File.expand_path('orig.pp')) expect(overridden.modulepath).to eq(original_path) expect(overridden.config_version).to eq('/new/script') end end - describe "watching a file" do - let(:filename) { "filename" } - - it "accepts a File" do - file = tmpfile(filename) - env.known_resource_types.expects(:watch_file).with(file.to_s) - env.watch_file(file) - end - - it "accepts a String" do - env.known_resource_types.expects(:watch_file).with(filename) - env.watch_file(filename) - end - end - describe "when managing known resource types" do before do - @collection = Puppet::Resource::TypeCollection.new(env) env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) end - it "should create a resource type collection if none exists" do - Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection - env.known_resource_types.should equal(@collection) + it "creates a resource type collection if none exists" do + expect(env.known_resource_types).to be_kind_of(Puppet::Resource::TypeCollection) end - it "should reuse any existing resource type collection" do - env.known_resource_types.should equal(env.known_resource_types) + it "memoizes resource type collection" do + expect(env.known_resource_types).to equal(env.known_resource_types) end - it "should perform the initial import when creating a new collection" do + it "performs the initial import when creating a new collection" do env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) env.known_resource_types end - it "should return the same collection even if stale if it's the same thread" do - Puppet::Resource::TypeCollection.stubs(:new).returns @collection - env.known_resource_types.stubs(:stale?).returns true - - env.known_resource_types.should equal(@collection) - end - - it "should generate a new TypeCollection if the current one requires reparsing" do + it "generates a new TypeCollection if the current one requires reparsing" do old_type_collection = env.known_resource_types - old_type_collection.stubs(:require_reparse?).returns true + old_type_collection.stubs(:parse_failed?).returns true env.check_for_reparse new_type_collection = env.known_resource_types - new_type_collection.should be_a Puppet::Resource::TypeCollection - new_type_collection.should_not equal(old_type_collection) + expect(new_type_collection).to be_a Puppet::Resource::TypeCollection + expect(new_type_collection).to_not equal(old_type_collection) end end - it "should validate the modulepath directories" do + it "validates the modulepath directories" do real_file = tmpdir('moduledir') - path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR) + path = ['/one', '/two', real_file] - Puppet[:modulepath] = path + env = Puppet::Node::Environment.create(:test, path) - env.modulepath.should == [real_file] + expect(env.modulepath).to eq([real_file]) end - it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do + it "prefixes the value of the 'PUPPETLIB' environment variable to the module path if present" do first_puppetlib = tmpdir('puppetlib1') second_puppetlib = tmpdir('puppetlib2') first_moduledir = tmpdir('moduledir1') second_moduledir = tmpdir('moduledir2') Puppet::Util.withenv("PUPPETLIB" => [first_puppetlib, second_puppetlib].join(File::PATH_SEPARATOR)) do - Puppet[:modulepath] = [first_moduledir, second_moduledir].join(File::PATH_SEPARATOR) + env = Puppet::Node::Environment.create(:testing, [first_moduledir, second_moduledir]) - env.modulepath.should == [first_puppetlib, second_puppetlib, first_moduledir, second_moduledir] + expect(env.modulepath).to eq([first_puppetlib, second_puppetlib, first_moduledir, second_moduledir]) end end - it "does not register validation_errors when not using directory environments" do - expect(Puppet::Node::Environment.create(:directory, [], '/some/non/default/manifest.pp').validation_errors).to be_empty - end - - describe "when operating in the context of directory environments" do + describe "validating manifest settings" do before(:each) do - Puppet[:environmentpath] = "$confdir/environments" Puppet[:default_manifest] = "/default/manifests/site.pp" end it "has no validation errors when disable_per_environment_manifest is false" do expect(Puppet::Node::Environment.create(:directory, [], '/some/non/default/manifest.pp').validation_errors).to be_empty end context "when disable_per_environment_manifest is true" do let(:config) { mock('config') } let(:global_modulepath) { ["/global/modulepath"] } let(:envconf) { Puppet::Settings::EnvironmentConf.new("/some/direnv", config, global_modulepath) } before(:each) do Puppet[:disable_per_environment_manifest] = true end def assert_manifest_conflict(expectation, envconf_manifest_value) config.expects(:setting).with(:manifest).returns( mock('setting', :value => envconf_manifest_value) ) environment = Puppet::Node::Environment.create(:directory, [], '/default/manifests/site.pp') loader = Puppet::Environments::Static.new(environment) loader.stubs(:get_conf).returns(envconf) Puppet.override(:environments => loader) do if expectation expect(environment.validation_errors).to have_matching_element(/The 'disable_per_environment_manifest' setting is true.*and the.*environment.*conflicts/) else expect(environment.validation_errors).to be_empty end end end it "has conflicting_manifest_settings when environment.conf manifest was set" do assert_manifest_conflict(true, '/some/envconf/manifest/site.pp') end it "does not have conflicting_manifest_settings when environment.conf manifest is empty" do assert_manifest_conflict(false, '') end it "does not have conflicting_manifest_settings when environment.conf manifest is nil" do assert_manifest_conflict(false, nil) end it "does not have conflicting_manifest_settings when environment.conf manifest is an exact, uninterpolated match of default_manifest" do assert_manifest_conflict(false, '/default/manifests/site.pp') end end end describe "when modeling a specific environment" do - it "should have a method for returning the environment name" do - Puppet::Node::Environment.new("testing").name.should == :testing - end - - it "should provide an array-like accessor method for returning any environment-specific setting" do - env.should respond_to(:[]) - end - - it "obtains its core values from the puppet settings instance as a legacy env" do - Puppet.settings.parse_config(<<-CONF) - [testing] - manifest = /some/manifest - modulepath = /some/modulepath - config_version = /some/script - CONF - - env = Puppet::Node::Environment.new("testing") - expect(env.full_modulepath).to eq([File.expand_path('/some/modulepath')]) - expect(env.manifest).to eq(File.expand_path('/some/manifest')) - expect(env.config_version).to eq('/some/script') - end - - it "should ask the Puppet settings instance for the setting qualified with the environment name" do - Puppet.settings.parse_config(<<-CONF) - [testing] - server = myval - CONF - - env[:server].should == "myval" - end - - it "should be able to return an individual module that exists in its module path" do - env.stubs(:modules).returns [Puppet::Module.new('one', "/one", mock("env"))] - - mod = env.module('one') - mod.should be_a(Puppet::Module) - mod.name.should == 'one' - end - - it "should not return a module if the module doesn't exist" do - env.stubs(:modules).returns [Puppet::Module.new('one', "/one", mock("env"))] - - env.module('two').should be_nil - end - - it "should return nil if asked for a module that does not exist in its path" do - modpath = tmpdir('modpath') - env = Puppet::Node::Environment.create(:testing, [modpath]) - - env.module("one").should be_nil - end + let(:first_modulepath) { tmpdir('firstmodules') } + let(:second_modulepath) { tmpdir('secondmodules') } + let(:env) { Puppet::Node::Environment.create(:modules_test, [first_modulepath, second_modulepath]) } + let(:module_options) { + { + :environment => env, + :metadata => { + :author => 'puppetlabs', + }, + } + } describe "module data" do - before do - dir = tmpdir("deep_path") + describe ".module" do - @first = File.join(dir, "first") - @second = File.join(dir, "second") - Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" + it "returns an individual module that exists in its module path" do + one = PuppetSpec::Modules.create('one', first_modulepath, module_options) + expect(env.module('one')).to eq(one) + end - FileUtils.mkdir_p(@first) - FileUtils.mkdir_p(@second) + it "returns nil if asked for a module that does not exist in its path" do + expect(env.module("doesnotexist")).to be_nil + end end describe "#modules_by_path" do - it "should return an empty list if there are no modules" do - env.modules_by_path.should == { - @first => [], - @second => [] - } + it "returns an empty list if there are no modules" do + expect(env.modules_by_path).to eq({ + first_modulepath => [], + second_modulepath => [] + }) end - it "should include modules even if they exist in multiple dirs in the modulepath" do - modpath1 = File.join(@first, "foo") - FileUtils.mkdir_p(modpath1) - modpath2 = File.join(@second, "foo") - FileUtils.mkdir_p(modpath2) + it "includes modules even if they exist in multiple dirs in the modulepath" do + one = PuppetSpec::Modules.create('one', first_modulepath, module_options) + two = PuppetSpec::Modules.create('two', second_modulepath, module_options) - env.modules_by_path.should == { - @first => [Puppet::Module.new('foo', modpath1, env)], - @second => [Puppet::Module.new('foo', modpath2, env)] - } + expect(env.modules_by_path).to eq({ + first_modulepath => [one], + second_modulepath => [two], + }) end - it "should ignore modules with invalid names" do - PuppetSpec::Modules.generate_files('foo', @first) - PuppetSpec::Modules.generate_files('foo2', @first) - PuppetSpec::Modules.generate_files('foo-bar', @first) - PuppetSpec::Modules.generate_files('foo_bar', @first) - PuppetSpec::Modules.generate_files('foo=bar', @first) - PuppetSpec::Modules.generate_files('foo bar', @first) - PuppetSpec::Modules.generate_files('foo.bar', @first) - PuppetSpec::Modules.generate_files('-foo', @first) - PuppetSpec::Modules.generate_files('foo-', @first) - PuppetSpec::Modules.generate_files('foo--bar', @first) - - env.modules_by_path[@first].collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} + it "ignores modules with invalid names" do + PuppetSpec::Modules.generate_files('foo', first_modulepath) + PuppetSpec::Modules.generate_files('foo2', first_modulepath) + PuppetSpec::Modules.generate_files('foo-bar', first_modulepath) + PuppetSpec::Modules.generate_files('foo_bar', first_modulepath) + PuppetSpec::Modules.generate_files('foo=bar', first_modulepath) + PuppetSpec::Modules.generate_files('foo bar', first_modulepath) + PuppetSpec::Modules.generate_files('foo.bar', first_modulepath) + PuppetSpec::Modules.generate_files('-foo', first_modulepath) + PuppetSpec::Modules.generate_files('foo-', first_modulepath) + PuppetSpec::Modules.generate_files('foo--bar', first_modulepath) + + expect(env.modules_by_path[first_modulepath].collect{|mod| mod.name}.sort).to eq(%w{foo foo-bar foo2 foo_bar}) end end describe "#module_requirements" do - it "should return a list of what modules depend on other modules" do + it "returns a list of what modules depend on other modules" do PuppetSpec::Modules.create( 'foo', - @first, + first_modulepath, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => ">= 1.0.0" }] } ) PuppetSpec::Modules.create( 'bar', - @second, + second_modulepath, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/foo', "version_requirement" => "<= 2.0.0" }] } ) PuppetSpec::Modules.create( 'baz', - @first, + first_modulepath, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs-bar', "version_requirement" => "3.0.0" }] } ) PuppetSpec::Modules.create( 'alpha', - @first, + first_modulepath, :metadata => { :author => 'puppetlabs', :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "~3.0.0" }] } ) - env.module_requirements.should == { + expect(env.module_requirements).to eq({ 'puppetlabs/alpha' => [], 'puppetlabs/foo' => [ { "name" => "puppetlabs/bar", "version" => "9.9.9", "version_requirement" => "<= 2.0.0" } ], 'puppetlabs/bar' => [ { "name" => "puppetlabs/alpha", "version" => "9.9.9", "version_requirement" => "~3.0.0" }, { "name" => "puppetlabs/baz", "version" => "9.9.9", "version_requirement" => "3.0.0" }, { "name" => "puppetlabs/foo", "version" => "9.9.9", "version_requirement" => ">= 1.0.0" } ], 'puppetlabs/baz' => [] - } + }) end end describe ".module_by_forge_name" do - it "should find modules by forge_name" do + it "finds modules by forge_name" do mod = PuppetSpec::Modules.create( 'baz', - @first, - :metadata => {:author => 'puppetlabs'}, - :environment => env + first_modulepath, + module_options, ) - env.module_by_forge_name('puppetlabs/baz').should == mod + expect(env.module_by_forge_name('puppetlabs/baz')).to eq(mod) end - it "should not find modules with same name by the wrong author" do + it "does not find modules with same name by the wrong author" do mod = PuppetSpec::Modules.create( 'baz', - @first, + first_modulepath, :metadata => {:author => 'sneakylabs'}, :environment => env ) - env.module_by_forge_name('puppetlabs/baz').should == nil + expect(env.module_by_forge_name('puppetlabs/baz')).to eq(nil) end - it "should return nil when the module can't be found" do - env.module_by_forge_name('ima/nothere').should be_nil + it "returns nil when the module can't be found" do + expect(env.module_by_forge_name('ima/nothere')).to be_nil end end describe ".modules" do - it "should return an empty list if there are no modules" do - env.modules.should == [] + it "returns an empty list if there are no modules" do + expect(env.modules).to eq([]) end - it "should return a module named for every directory in each module path" do + it "returns a module named for every directory in each module path" do %w{foo bar}.each do |mod_name| - PuppetSpec::Modules.generate_files(mod_name, @first) + PuppetSpec::Modules.generate_files(mod_name, first_modulepath) end %w{bee baz}.each do |mod_name| - PuppetSpec::Modules.generate_files(mod_name, @second) + PuppetSpec::Modules.generate_files(mod_name, second_modulepath) end - env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort + expect(env.modules.collect{|mod| mod.name}.sort).to eq(%w{foo bar bee baz}.sort) end - it "should remove duplicates" do - PuppetSpec::Modules.generate_files('foo', @first) - PuppetSpec::Modules.generate_files('foo', @second) + it "removes duplicates" do + PuppetSpec::Modules.generate_files('foo', first_modulepath) + PuppetSpec::Modules.generate_files('foo', second_modulepath) - env.modules.collect{|mod| mod.name}.sort.should == %w{foo} + expect(env.modules.collect{|mod| mod.name}.sort).to eq(%w{foo}) end - it "should ignore modules with invalid names" do - PuppetSpec::Modules.generate_files('foo', @first) - PuppetSpec::Modules.generate_files('foo2', @first) - PuppetSpec::Modules.generate_files('foo-bar', @first) - PuppetSpec::Modules.generate_files('foo_bar', @first) - PuppetSpec::Modules.generate_files('foo=bar', @first) - PuppetSpec::Modules.generate_files('foo bar', @first) + it "ignores modules with invalid names" do + PuppetSpec::Modules.generate_files('foo', first_modulepath) + PuppetSpec::Modules.generate_files('foo2', first_modulepath) + PuppetSpec::Modules.generate_files('foo-bar', first_modulepath) + PuppetSpec::Modules.generate_files('foo_bar', first_modulepath) + PuppetSpec::Modules.generate_files('foo=bar', first_modulepath) + PuppetSpec::Modules.generate_files('foo bar', first_modulepath) - env.modules.collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} + expect(env.modules.collect{|mod| mod.name}.sort).to eq(%w{foo foo-bar foo2 foo_bar}) end - it "should create modules with the correct environment" do - PuppetSpec::Modules.generate_files('foo', @first) + it "creates modules with the correct environment" do + PuppetSpec::Modules.generate_files('foo', first_modulepath) - env.modules.each {|mod| mod.environment.should == env } + env.modules.each do |mod| + expect(mod.environment).to eq(env) + end end - it "should log an exception if a module contains invalid metadata" do + it "logs an exception if a module contains invalid metadata" do PuppetSpec::Modules.generate_files( 'foo', - @first, + first_modulepath, :metadata => { :author => 'puppetlabs' # missing source, version, etc } ) Puppet.expects(:log_exception).with(is_a(Puppet::Module::MissingMetadata)) env.modules end end end end describe "when performing initial import" do - def parser_and_environment(name) - env = Puppet::Node::Environment.new(name) - parser = Puppet::Parser::ParserFactory.parser(env) - Puppet::Parser::ParserFactory.stubs(:parser).returns(parser) - - [parser, env] + it "loads from Puppet[:code]" do + Puppet[:code] = "define foo {}" + krt = env.known_resource_types + expect(krt.find_definition('', 'foo')).to be_kind_of(Puppet::Resource::Type) end - it "should set the parser's string to the 'code' setting and parse if code is available" do - Puppet[:code] = "my code" - parser, env = parser_and_environment('testing') - - parser.expects(:string=).with "my code" - parser.expects(:parse) - - env.instance_eval { perform_initial_import } + it "parses from the the environment's manifests if Puppet[:code] is not set" do + filename = tmpfile('a_manifest.pp') + File.open(filename, 'w') do |f| + f.puts("define from_manifest {}") + end + env = Puppet::Node::Environment.create(:testing, [], filename) + krt = env.known_resource_types + expect(krt.find_definition('', 'from_manifest')).to be_kind_of(Puppet::Resource::Type) end - it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do - filename = tmpfile('myfile') - Puppet[:manifest] = filename - parser, env = parser_and_environment('testing') - - parser.expects(:file=).with filename - parser.expects(:parse) - - env.instance_eval { perform_initial_import } + it "prefers Puppet[:code] over manifest files" do + Puppet[:code] = "define from_code_setting {}" + filename = tmpfile('a_manifest.pp') + File.open(filename, 'w') do |f| + f.puts("define from_manifest {}") + end + env = Puppet::Node::Environment.create(:testing, [], filename) + krt = env.known_resource_types + expect(krt.find_definition('', 'from_code_setting')).to be_kind_of(Puppet::Resource::Type) end - it "should pass the manifest file to the parser even if it does not exist on disk" do - filename = tmpfile('myfile') - Puppet[:code] = "" - Puppet[:manifest] = filename - parser, env = parser_and_environment('testing') - - parser.expects(:file=).with(filename).once - parser.expects(:parse).once - - env.instance_eval { perform_initial_import } + it "initial import proceeds even if manifest file does not exist on disk" do + filename = tmpfile('a_manifest.pp') + env = Puppet::Node::Environment.create(:testing, [], filename) + expect(env.known_resource_types).to be_kind_of(Puppet::Resource::TypeCollection) end - it "should fail helpfully if there is an error importing" do - Puppet::FileSystem.stubs(:exist?).returns true - parser, env = parser_and_environment('testing') - - parser.expects(:file=).once - parser.expects(:parse).raises ArgumentError + it "returns an empty TypeCollection if neither code nor manifests is present" do + expect(env.known_resource_types).to be_kind_of(Puppet::Resource::TypeCollection) + end + it "fails helpfully if there is an error importing" do + Puppet[:code] = "oops {" expect do env.known_resource_types - end.to raise_error(Puppet::Error) + end.to raise_error(Puppet::Error, /Could not parse for environment #{env.name}/) end - it "should not do anything if the ignore_import settings is set" do + it "does not do anything if the ignore_import settings is set" do Puppet[:ignoreimport] = true - parser, env = parser_and_environment('testing') - - parser.expects(:string=).never - parser.expects(:file=).never - parser.expects(:parse).never - - env.instance_eval { perform_initial_import } + expect(env.known_resource_types).to be_kind_of(Puppet::Resource::TypeCollection) end it "should mark the type collection as needing a reparse when there is an error parsing" do - parser, env = parser_and_environment('testing') - - parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") - + Puppet[:code] = "oops {" expect do env.known_resource_types end.to raise_error(Puppet::Error, /Syntax error at .../) - env.known_resource_types.require_reparse?.should be_true + expect(env.known_resource_types.parse_failed?).to be_true end end end - describe '#current' do - it 'should return the current context' do - env = Puppet::Node::Environment.new(:test) - Puppet::Context.any_instance.expects(:lookup).with(:current_environment).returns(env) - Puppet.expects(:deprecation_warning).once - Puppet::Node::Environment.current.should equal(env) - end - end - end diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index 9db2c8ec5..dd38fae75 100755 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -1,299 +1,321 @@ #! /usr/bin/env ruby require 'spec_helper' require 'matchers/json' describe Puppet::Node do include JSONMatchers let(:environment) { Puppet::Node::Environment.create(:bar, []) } let(:env_loader) { Puppet::Environments::Static.new(environment) } describe "when managing its environment" do - it "should use any set environment" do - Puppet.override(:environments => env_loader) do - Puppet::Node.new("foo", :environment => "bar").environment.should == environment - end - end - it "should support providing an actual environment instance" do - Puppet::Node.new("foo", :environment => environment).environment.name.should == :bar + it "provides an environment instance" do + expect(Puppet::Node.new("foo", :environment => environment).environment.name).to eq(:bar) end - it "should determine its environment from its parameters if no environment is set" do - Puppet.override(:environments => env_loader) do - Puppet::Node.new("foo", :parameters => {"environment" => :bar}).environment.should == environment + context "present in a loader" do + around do |example| + Puppet.override(:environments => env_loader) do + example.run + end end - end - it "should use the configured environment if no environment is provided" do - Puppet[:environment] = environment.name.to_s + it "uses any set environment" do + expect(Puppet::Node.new("foo", :environment => "bar").environment).to eq(environment) + end - Puppet.override(:environments => env_loader) do - Puppet::Node.new("foo").environment.should == environment + it "determines its environment from its parameters if no environment is set" do + expect(Puppet::Node.new("foo", :parameters => {"environment" => :bar}).environment).to eq(environment) end - end - it "should allow the environment to be set after initialization" do - node = Puppet::Node.new("foo") - node.environment = :bar - node.environment.name.should == :bar - end + it "uses the configured environment if no environment is provided" do + Puppet[:environment] = environment.name.to_s + expect(Puppet::Node.new("foo").environment).to eq(environment) + end + + it "allows the environment to be set after initialization" do + node = Puppet::Node.new("foo") + node.environment = :bar + expect(node.environment.name).to eq(:bar) + end - it "should allow its environment to be set by parameters after initialization" do - node = Puppet::Node.new("foo") - node.parameters["environment"] = :bar - node.environment.name.should == :bar + it "allows its environment to be set by parameters after initialization" do + node = Puppet::Node.new("foo") + node.parameters["environment"] = :bar + expect(node.environment.name).to eq(:bar) + end end end - it "can round-trip through pson" do - facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") - node = Puppet::Node.new("hello", - :environment => 'kjhgrg', - :classes => ['erth', 'aiu'], - :parameters => {"hostname"=>"food"} - ) - new_node = Puppet::Node.convert_from('pson', node.render('pson')) - new_node.environment.should == node.environment - new_node.parameters.should == node.parameters - new_node.classes.should == node.classes - new_node.name.should == node.name - end + describe "serialization" do + around do |example| + Puppet.override(:environments => env_loader) do + example.run + end + end - it "validates against the node json schema", :unless => Puppet.features.microsoft_windows? do - facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") - node = Puppet::Node.new("hello", - :environment => 'kjhgrg', - :classes => ['erth', 'aiu'], - :parameters => {"hostname"=>"food"} - ) - expect(node.to_pson).to validate_against('api/schemas/node.json') - end + it "can round-trip through pson" do + facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") + node = Puppet::Node.new("hello", + :environment => 'bar', + :classes => ['erth', 'aiu'], + :parameters => {"hostname"=>"food"} + ) + new_node = Puppet::Node.convert_from('pson', node.render('pson')) + expect(new_node.environment).to eq(node.environment) + expect(new_node.parameters).to eq(node.parameters) + expect(new_node.classes).to eq(node.classes) + expect(new_node.name).to eq(node.name) + end + + it "validates against the node json schema", :unless => Puppet.features.microsoft_windows? do + facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") + node = Puppet::Node.new("hello", + :environment => 'bar', + :classes => ['erth', 'aiu'], + :parameters => {"hostname"=>"food"} + ) + expect(node.to_pson).to validate_against('api/schemas/node.json') + end - it "when missing optional parameters validates against the node json schema", :unless => Puppet.features.microsoft_windows? do - facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") - node = Puppet::Node.new("hello", - :environment => 'kjhgrg' - ) - expect(node.to_pson).to validate_against('api/schemas/node.json') + it "when missing optional parameters validates against the node json schema", :unless => Puppet.features.microsoft_windows? do + facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") + node = Puppet::Node.new("hello", + :environment => 'bar' + ) + expect(node.to_pson).to validate_against('api/schemas/node.json') + end end describe "when converting to json" do before do @node = Puppet::Node.new("mynode") end - it "should provide its name" do - @node.should set_json_attribute('name').to("mynode") + it "provide its name" do + expect(@node).to set_json_attribute('name').to("mynode") end - it "should include the classes if set" do + it "includes the classes if set" do @node.classes = %w{a b c} - @node.should set_json_attribute("classes").to(%w{a b c}) + expect(@node).to set_json_attribute("classes").to(%w{a b c}) end - it "should not include the classes if there are none" do - @node.should_not set_json_attribute('classes') + it "does not include the classes if there are none" do + expect(@node).to_not set_json_attribute('classes') end - it "should include parameters if set" do + it "includes parameters if set" do @node.parameters = {"a" => "b", "c" => "d"} - @node.should set_json_attribute('parameters').to({"a" => "b", "c" => "d"}) + expect(@node).to set_json_attribute('parameters').to({"a" => "b", "c" => "d"}) end - it "should not include the parameters if there are none" do - @node.should_not set_json_attribute('parameters') + it "does not include the parameters if there are none" do + expect(@node).to_not set_json_attribute('parameters') end - it "should include the environment" do + it "includes the environment" do @node.environment = "production" - @node.should set_json_attribute('environment').to('production') + expect(@node).to set_json_attribute('environment').to('production') end end describe "when converting from json" do before do @node = Puppet::Node.new("mynode") @format = Puppet::Network::FormatHandler.format('pson') end def from_json(json) @format.intern(Puppet::Node, json) end - it "should set its name" do - Puppet::Node.should read_json_attribute('name').from(@node.to_pson).as("mynode") + it "sets its name" do + expect(Puppet::Node).to read_json_attribute('name').from(@node.to_pson).as("mynode") end - it "should include the classes if set" do + it "includes the classes if set" do @node.classes = %w{a b c} - Puppet::Node.should read_json_attribute('classes').from(@node.to_pson).as(%w{a b c}) + expect(Puppet::Node).to read_json_attribute('classes').from(@node.to_pson).as(%w{a b c}) end - it "should include parameters if set" do + it "includes parameters if set" do @node.parameters = {"a" => "b", "c" => "d"} - Puppet::Node.should read_json_attribute('parameters').from(@node.to_pson).as({"a" => "b", "c" => "d"}) + expect(Puppet::Node).to read_json_attribute('parameters').from(@node.to_pson).as({"a" => "b", "c" => "d"}) end it "deserializes environment to environment_name as a string" do @node.environment = environment - Puppet::Node.should read_json_attribute('environment_name').from(@node.to_pson).as('bar') + expect(Puppet::Node).to read_json_attribute('environment_name').from(@node.to_pson).as('bar') end end end describe Puppet::Node, "when initializing" do before do @node = Puppet::Node.new("testnode") end - it "should set the node name" do - @node.name.should == "testnode" + it "sets the node name" do + expect(@node.name).to eq("testnode") end - it "should not allow nil node names" do - proc { Puppet::Node.new(nil) }.should raise_error(ArgumentError) + it "does not allow nil node names" do + expect { Puppet::Node.new(nil) }.to raise_error(ArgumentError) end - it "should default to an empty parameter hash" do - @node.parameters.should == {} + it "defaults to an empty parameter hash" do + expect(@node.parameters).to eq({}) end - it "should default to an empty class array" do - @node.classes.should == [] + it "defaults to an empty class array" do + expect(@node.classes).to eq([]) end - it "should note its creation time" do - @node.time.should be_instance_of(Time) + it "notes its creation time" do + expect(@node.time).to be_instance_of(Time) end - it "should accept parameters passed in during initialization" do + it "accepts parameters passed in during initialization" do params = {"a" => "b"} @node = Puppet::Node.new("testing", :parameters => params) - @node.parameters.should == params + expect(@node.parameters).to eq(params) end - it "should accept classes passed in during initialization" do + it "accepts classes passed in during initialization" do classes = %w{one two} @node = Puppet::Node.new("testing", :classes => classes) - @node.classes.should == classes + expect(@node.classes).to eq(classes) end - it "should always return classes as an array" do + it "always returns classes as an array" do @node = Puppet::Node.new("testing", :classes => "myclass") - @node.classes.should == ["myclass"] + expect(@node.classes).to eq(["myclass"]) end end describe Puppet::Node, "when merging facts" do before do @node = Puppet::Node.new("testnode") - Puppet::Node::Facts.indirection.stubs(:find).with(@node.name, instance_of(Hash)).returns(Puppet::Node::Facts.new(@node.name, "one" => "c", "two" => "b")) + Puppet[:facts_terminus] = :memory + Puppet::Node::Facts.indirection.save(Puppet::Node::Facts.new(@node.name, "one" => "c", "two" => "b")) end - it "should fail intelligently if it cannot find facts" do - Puppet::Node::Facts.indirection.expects(:find).with(@node.name, instance_of(Hash)).raises "foo" - lambda { @node.fact_merge }.should raise_error(Puppet::Error) + it "recovers with a Puppet::Error if something is thrown from the facts indirection" do + Puppet::Node::Facts.indirection.expects(:find).raises "something bad happened in the indirector" + expect { @node.fact_merge }.to raise_error(Puppet::Error, /Could not retrieve facts for testnode: something bad happened in the indirector/) end - it "should prefer parameters already set on the node over facts from the node" do + it "prefers parameters already set on the node over facts from the node" do @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.fact_merge - @node.parameters["one"].should == "a" + expect(@node.parameters["one"]).to eq("a") end - it "should add passed parameters to the parameter list" do + it "adds passed parameters to the parameter list" do @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.fact_merge - @node.parameters["two"].should == "b" + expect(@node.parameters["two"]).to eq("b") end - it "should accept arbitrary parameters to merge into its parameters" do + it "accepts arbitrary parameters to merge into its parameters" do @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.merge "two" => "three" - @node.parameters["two"].should == "three" + expect(@node.parameters["two"]).to eq("three") end - it "should add the environment to the list of parameters" do - Puppet[:environment] = "one" - @node = Puppet::Node.new("testnode", :environment => "one") - @node.merge "two" => "three" - @node.parameters["environment"].should == "one" - end + context "with an env loader" do + let(:environment) { Puppet::Node::Environment.create(:one, []) } + let(:env_loader) { Puppet::Environments::Static.new(environment) } - it "should not set the environment if it is already set in the parameters" do - Puppet[:environment] = "one" - @node = Puppet::Node.new("testnode", :environment => "one") - @node.merge "environment" => "two" - @node.parameters["environment"].should == "two" + around do |example| + Puppet.override(:environments => env_loader) do + example.run + end + end + + it "adds the environment to the list of parameters" do + Puppet[:environment] = "one" + @node = Puppet::Node.new("testnode", :environment => "one") + @node.merge "two" => "three" + expect(@node.parameters["environment"]).to eq("one") + end + + it "nots set the environment if it is already set in the parameters" do + Puppet[:environment] = "one" + @node = Puppet::Node.new("testnode", :environment => "one") + @node.merge "environment" => "two" + expect(@node.parameters["environment"]).to eq("two") + end end end describe Puppet::Node, "when indirecting" do - it "should default to the 'plain' node terminus" do + it "defaults to the 'plain' node terminus" do Puppet::Node.indirection.reset_terminus_class - Puppet::Node.indirection.terminus_class.should == :plain + expect(Puppet::Node.indirection.terminus_class).to eq(:plain) end end describe Puppet::Node, "when generating the list of names to search through" do before do @node = Puppet::Node.new("foo.domain.com", :parameters => {"hostname" => "yay", "domain" => "domain.com"}) end - it "should return an array of names" do - @node.names.should be_instance_of(Array) + it "returns an array of names" do + expect(@node.names).to be_instance_of(Array) end describe "and the node name is fully qualified" do - it "should contain an entry for each part of the node name" do - @node.names.should be_include("foo.domain.com") - @node.names.should be_include("foo.domain") - @node.names.should be_include("foo") + it "contains an entry for each part of the node name" do + expect(@node.names).to include("foo.domain.com") + expect(@node.names).to include("foo.domain") + expect(@node.names).to include("foo") end end - it "should include the node's fqdn" do - @node.names.should be_include("yay.domain.com") + it "includes the node's fqdn" do + expect(@node.names).to include("yay.domain.com") end - it "should combine and include the node's hostname and domain if no fqdn is available" do - @node.names.should be_include("yay.domain.com") + it "combines and include the node's hostname and domain if no fqdn is available" do + expect(@node.names).to include("yay.domain.com") end - it "should contain an entry for each name available by stripping a segment of the fqdn" do + it "contains an entry for each name available by stripping a segment of the fqdn" do @node.parameters["fqdn"] = "foo.deep.sub.domain.com" - @node.names.should be_include("foo.deep.sub.domain") - @node.names.should be_include("foo.deep.sub") + expect(@node.names).to include("foo.deep.sub.domain") + expect(@node.names).to include("foo.deep.sub") end describe "and :node_name is set to 'cert'" do before do Puppet[:strict_hostname_checking] = false Puppet[:node_name] = "cert" end - it "should use the passed-in key as the first value" do - @node.names[0].should == "foo.domain.com" + it "uses the passed-in key as the first value" do + expect(@node.names[0]).to eq("foo.domain.com") end describe "and strict hostname checking is enabled" do - it "should only use the passed-in key" do + it "only uses the passed-in key" do Puppet[:strict_hostname_checking] = true - @node.names.should == ["foo.domain.com"] + expect(@node.names).to eq(["foo.domain.com"]) end end end describe "and :node_name is set to 'facter'" do before do Puppet[:strict_hostname_checking] = false Puppet[:node_name] = "facter" end - it "should use the node's 'hostname' fact as the first value" do - @node.names[0].should == "yay" + it "uses the node's 'hostname' fact as the first value" do + expect(@node.names[0]).to eq("yay") end end end diff --git a/spec/unit/parser/compiler_spec.rb b/spec/unit/parser/compiler_spec.rb index dc7b4e537..1f42c2cbf 100755 --- a/spec/unit/parser/compiler_spec.rb +++ b/spec/unit/parser/compiler_spec.rb @@ -1,928 +1,938 @@ require 'spec_helper' require 'puppet_spec/compiler' require 'matchers/resource' class CompilerTestResource attr_accessor :builtin, :virtual, :evaluated, :type, :title def initialize(type, title) @type = type @title = title end def [](attr) return nil if attr == :stage :main end def ref "#{type.to_s.capitalize}[#{title}]" end def evaluated? @evaluated end def builtin_type? @builtin end def virtual? @virtual end def class? false end def stage? false end def evaluate end def file "/fake/file/goes/here" end def line "42" end end describe Puppet::Parser::Compiler do include PuppetSpec::Files include Matchers::Resource def resource(type, title) Puppet::Parser::Resource.new(type, title, :scope => @scope) end + let(:environment) { Puppet::Node::Environment.create(:testing, []) } + before :each do # Push me faster, I wanna go back in time! (Specifically, freeze time # across the test since we have a bunch of version == timestamp code # hidden away in the implementation and we keep losing the race.) # --daniel 2011-04-21 now = Time.now Time.stubs(:now).returns(now) - environment = Puppet::Node::Environment.create(:testing, []) @node = Puppet::Node.new("testnode", :facts => Puppet::Node::Facts.new("facts", {}), :environment => environment) @known_resource_types = environment.known_resource_types @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(@compiler, :source => stub('source')) @scope_resource = Puppet::Parser::Resource.new(:file, "/my/file", :scope => @scope) @scope.resource = @scope_resource end it "should have a class method that compiles, converts, and returns a catalog" do compiler = stub 'compiler' Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler catalog = stub 'catalog' compiler.expects(:compile).returns catalog converted_catalog = stub 'converted_catalog' catalog.expects(:to_resource).returns converted_catalog Puppet::Parser::Compiler.compile(@node).should equal(converted_catalog) end it "should fail intelligently when a class-level compile fails" do Puppet::Parser::Compiler.expects(:new).raises ArgumentError lambda { Puppet::Parser::Compiler.compile(@node) }.should raise_error(Puppet::Error) end it "should use the node's environment as its environment" do @compiler.environment.should equal(@node.environment) end it "fails if the node's environment has validation errors" do conflicted_environment = Puppet::Node::Environment.create(:testing, [], '/some/environment.conf/manifest.pp') conflicted_environment.stubs(:validation_errors).returns(['bad environment']) @node.environment = conflicted_environment expect { Puppet::Parser::Compiler.compile(@node) }.to raise_error(Puppet::Error, /Compilation has been halted because.*bad environment/) end it "should include the resource type collection helper" do Puppet::Parser::Compiler.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper) end it "should be able to return a class list containing all added classes" do @compiler.add_class "" @compiler.add_class "one" @compiler.add_class "two" @compiler.classlist.sort.should == %w{one two}.sort end it "should clear the global caches before compile" do compiler = stub 'compiler' Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler catalog = stub 'catalog' compiler.expects(:compile).returns catalog catalog.expects(:to_resource) $known_resource_types = "rspec" $env_module_directories = "rspec" Puppet::Parser::Compiler.compile(@node) $known_resource_types = nil $env_module_directories = nil end describe "when initializing" do it "should set its node attribute" do @compiler.node.should equal(@node) end it "should detect when ast nodes are absent" do @compiler.ast_nodes?.should be_false end it "should detect when ast nodes are present" do @known_resource_types.expects(:nodes?).returns true @compiler.ast_nodes?.should be_true end it "should copy the known_resource_types version to the catalog" do @compiler.catalog.version.should == @known_resource_types.version end it "should copy any node classes into the class list" do node = Puppet::Node.new("mynode") node.classes = %w{foo bar} compiler = Puppet::Parser::Compiler.new(node) compiler.classlist.should =~ ['foo', 'bar'] end it "should transform node class hashes into a class list" do node = Puppet::Node.new("mynode") node.classes = {'foo'=>{'one'=>'p1'}, 'bar'=>{'two'=>'p2'}} compiler = Puppet::Parser::Compiler.new(node) compiler.classlist.should =~ ['foo', 'bar'] end it "should add a 'main' stage to the catalog" do @compiler.catalog.resource(:stage, :main).should be_instance_of(Puppet::Parser::Resource) end end describe "when managing scopes" do it "should create a top scope" do @compiler.topscope.should be_instance_of(Puppet::Parser::Scope) end it "should be able to create new scopes" do @compiler.newscope(@compiler.topscope).should be_instance_of(Puppet::Parser::Scope) end it "should set the parent scope of the new scope to be the passed-in parent" do scope = mock 'scope' newscope = @compiler.newscope(scope) newscope.parent.should equal(scope) end it "should set the parent scope of the new scope to its topscope if the parent passed in is nil" do scope = mock 'scope' newscope = @compiler.newscope(nil) newscope.parent.should equal(@compiler.topscope) end end describe "when compiling" do def compile_methods [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, :finish, :store, :extract, :evaluate_relationships] end # Stub all of the main compile methods except the ones we're specifically interested in. def compile_stub(*except) (compile_methods - except).each { |m| @compiler.stubs(m) } end it "should set node parameters as variables in the top scope" do params = {"a" => "b", "c" => "d"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compiler.compile @compiler.topscope['a'].should == "b" @compiler.topscope['c'].should == "d" end it "should set the client and server versions on the catalog" do params = {"clientversion" => "2", "serverversion" => "3"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compiler.compile @compiler.catalog.client_version.should == "2" @compiler.catalog.server_version.should == "3" end it "should evaluate the main class if it exists" do compile_stub(:evaluate_main) main_class = @known_resource_types.add Puppet::Resource::Type.new(:hostclass, "") main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) } @compiler.topscope.expects(:source=).with(main_class) @compiler.compile end it "should create a new, empty 'main' if no main class exists" do compile_stub(:evaluate_main) @compiler.compile @known_resource_types.find_hostclass([""], "").should be_instance_of(Puppet::Resource::Type) end it "should add an edge between the main stage and main class" do @compiler.compile (stage = @compiler.catalog.resource(:stage, "main")).should be_instance_of(Puppet::Parser::Resource) (klass = @compiler.catalog.resource(:class, "")).should be_instance_of(Puppet::Parser::Resource) @compiler.catalog.edge?(stage, klass).should be_true end it "should evaluate all added collections" do colls = [] # And when the collections fail to evaluate. colls << mock("coll1-false") colls << mock("coll2-false") colls.each { |c| c.expects(:evaluate).returns(false) } @compiler.add_collection(colls[0]) @compiler.add_collection(colls[1]) compile_stub(:evaluate_generators) @compiler.compile end it "should ignore builtin resources" do resource = resource(:file, "testing") @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end it "should evaluate unevaluated resources" do resource = CompilerTestResource.new(:file, "testing") @compiler.add_resource(@scope, resource) # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.evaluated = true } @compiler.compile end it "should not evaluate already-evaluated resources" do resource = resource(:file, "testing") resource.stubs(:evaluated?).returns true @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end it "should evaluate unevaluated resources created by evaluating other resources" do resource = CompilerTestResource.new(:file, "testing") @compiler.add_resource(@scope, resource) resource2 = CompilerTestResource.new(:file, "other") # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.evaluated = true; @compiler.add_resource(@scope, resource2) } resource2.expects(:evaluate).with { |*whatever| resource2.evaluated = true } @compiler.compile end describe "when finishing" do before do @compiler.send(:evaluate_main) @catalog = @compiler.catalog end def add_resource(name, parent = nil) resource = Puppet::Parser::Resource.new "file", name, :scope => @scope @compiler.add_resource(@scope, resource) @catalog.add_edge(parent, resource) if parent resource end it "should call finish() on all resources" do # Add a resource that does respond to :finish resource = Puppet::Parser::Resource.new "file", "finish", :scope => @scope resource.expects(:finish) @compiler.add_resource(@scope, resource) # And one that does not dnf_resource = stub_everything "dnf", :ref => "File[dnf]", :type => "file" @compiler.add_resource(@scope, dnf_resource) @compiler.send(:finish) end it "should call finish() in add_resource order" do resources = sequence('resources') resource1 = add_resource("finish1") resource1.expects(:finish).in_sequence(resources) resource2 = add_resource("finish2") resource2.expects(:finish).in_sequence(resources) @compiler.send(:finish) end it "should add each container's metaparams to its contained resources" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) @compiler.send(:finish) resource1[:noop].should be_true end it "should add metaparams recursively" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2[:noop].should be_true end it "should prefer metaparams from immediate parents" do main = @catalog.resource(:class, :main) main[:noop] = true resource1 = add_resource("meh", main) resource2 = add_resource("foo", resource1) resource1[:noop] = false @compiler.send(:finish) resource2[:noop].should be_false end it "should merge tags downward" do main = @catalog.resource(:class, :main) main.tag("one") resource1 = add_resource("meh", main) resource1.tag "two" resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2.tags.should be_include("one") resource2.tags.should be_include("two") end it "should work if only middle resources have metaparams set" do main = @catalog.resource(:class, :main) resource1 = add_resource("meh", main) resource1[:noop] = true resource2 = add_resource("foo", resource1) @compiler.send(:finish) resource2[:noop].should be_true end end it "should return added resources in add order" do resource1 = resource(:file, "yay") @compiler.add_resource(@scope, resource1) resource2 = resource(:file, "youpi") @compiler.add_resource(@scope, resource2) @compiler.resources.should == [resource1, resource2] end it "should add resources that do not conflict with existing resources" do resource = resource(:file, "yay") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_vertex(resource) end it "should fail to add resources that conflict with existing resources" do path = make_absolute("/foo") file1 = resource(:file, path) file2 = resource(:file, path) @compiler.add_resource(@scope, file1) lambda { @compiler.add_resource(@scope, file2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) end it "should add an edge from the scope resource to the added resource" do resource = resource(:file, "yay") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_edge(@scope.resource, resource) end it "should not add non-class resources that don't specify a stage to the 'main' stage" do main = @compiler.catalog.resource(:stage, :main) resource = resource(:file, "foo") @compiler.add_resource(@scope, resource) @compiler.catalog.should_not be_edge(main, resource) end it "should not add any parent-edges to stages" do stage = resource(:stage, "other") @compiler.add_resource(@scope, stage) @scope.resource = resource(:class, "foo") @compiler.catalog.edge?(@scope.resource, stage).should be_false end it "should not attempt to add stages to other stages" do other_stage = resource(:stage, "other") second_stage = resource(:stage, "second") @compiler.add_resource(@scope, other_stage) @compiler.add_resource(@scope, second_stage) second_stage[:stage] = "other" @compiler.catalog.edge?(other_stage, second_stage).should be_false end it "should have a method for looking up resources" do resource = resource(:yay, "foo") @compiler.add_resource(@scope, resource) @compiler.findresource("Yay[foo]").should equal(resource) end it "should be able to look resources up by type and title" do resource = resource(:yay, "foo") @compiler.add_resource(@scope, resource) @compiler.findresource("Yay", "foo").should equal(resource) end it "should not evaluate virtual defined resources" do resource = resource(:file, "testing") resource.virtual = true @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end end describe "when evaluating collections" do it "should evaluate each collection" do 2.times { |i| coll = mock 'coll%s' % i @compiler.add_collection(coll) # This is the hard part -- we have to emulate the fact that # collections delete themselves if they are done evaluating. coll.expects(:evaluate).with do @compiler.delete_collection(coll) end } @compiler.compile end it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:unresolved_resources).returns(nil) @compiler.add_collection(coll) lambda { @compiler.compile }.should_not raise_error end it "should fail when there are unevaluated resource collections that refer to a specific resource" do coll = stub 'coll', :evaluate => false coll.expects(:unresolved_resources).returns(:something) @compiler.add_collection(coll) lambda { @compiler.compile }.should raise_error(Puppet::ParseError, 'Failed to realize virtual resources something') end it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:unresolved_resources).returns([:one, :two]) @compiler.add_collection(coll) lambda { @compiler.compile }.should raise_error(Puppet::ParseError, 'Failed to realize virtual resources one, two') end end describe "when evaluating relationships" do it "should evaluate each relationship with its catalog" do dep = stub 'dep' dep.expects(:evaluate).with(@compiler.catalog) @compiler.add_relationship dep @compiler.evaluate_relationships end end describe "when told to evaluate missing classes" do it "should fail if there's no source listed for the scope" do scope = stub 'scope', :source => nil proc { @compiler.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError) end it "should raise an error if a class is not found" do @scope.expects(:find_hostclass).with("notfound", {:assume_fqname => false}).returns(nil) lambda{ @compiler.evaluate_classes(%w{notfound}, @scope) }.should raise_error(Puppet::Error, /Could not find class/) end it "should raise an error when it can't find class" do klasses = {'foo'=>nil} @node.classes = klasses @compiler.topscope.expects(:find_hostclass).with('foo', {:assume_fqname => false}).returns(nil) lambda{ @compiler.compile }.should raise_error(Puppet::Error, /Could not find class foo for testnode/) end end describe "when evaluating found classes" do before do Puppet.settings[:data_binding_terminus] = "none" @class = stub 'class', :name => "my::class" @scope.stubs(:find_hostclass).with("myclass", {:assume_fqname => false}).returns(@class) @resource = stub 'resource', :ref => "Class[myclass]", :type => "file" end + around do |example| + Puppet.override( + :environments => Puppet::Environments::Static.new(environment), + :description => "Static loader for specs" + ) do + example.run + end + end + it "should evaluate each class" do @compiler.catalog.stubs(:tag) @class.expects(:ensure_in_catalog).with(@scope) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope) end describe "and the classes are specified as a hash with parameters" do before do @node.classes = {} @ast_obj = Puppet::Parser::AST::Leaf.new(:value => 'foo') end # Define the given class with default parameters def define_class(name, parameters) @node.classes[name] = parameters klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'p1' => @ast_obj, 'p2' => @ast_obj}) @compiler.topscope.known_resource_types.add klass end def compile @catalog = @compiler.compile end it "should record which classes are evaluated" do classes = {'foo'=>{}, 'bar::foo'=>{}, 'bar'=>{}} classes.each { |c, params| define_class(c, params) } compile() classes.each { |name, p| @catalog.classes.should include(name) } end it "should provide default values for parameters that have no values specified" do define_class('foo', {}) compile() @catalog.resource(:class, 'foo')['p1'].should == "foo" end it "should use any provided values" do define_class('foo', {'p1' => 'real_value'}) compile() @catalog.resource(:class, 'foo')['p1'].should == "real_value" end it "should support providing some but not all values" do define_class('foo', {'p1' => 'real_value'}) compile() @catalog.resource(:class, 'Foo')['p1'].should == "real_value" @catalog.resource(:class, 'Foo')['p2'].should == "foo" end it "should ensure each node class is in catalog and has appropriate tags" do klasses = ['bar::foo'] @node.classes = klasses ast_obj = Puppet::Parser::AST::Leaf.new(:value => 'foo') klasses.each do |name| klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'p1' => ast_obj, 'p2' => ast_obj}) @compiler.topscope.known_resource_types.add klass end catalog = @compiler.compile r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' } r2.tags.should == Puppet::Util::TagSet.new(['bar::foo', 'class', 'bar', 'foo']) end end it "should fail if required parameters are missing" do klass = {'foo'=>{'a'=>'one'}} @node.classes = klass klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {'a' => nil, 'b' => nil}) @compiler.topscope.known_resource_types.add klass lambda { @compiler.compile }.should raise_error(Puppet::ParseError, "Must pass b to Class[Foo]") end it "should fail if invalid parameters are passed" do klass = {'foo'=>{'3'=>'one'}} @node.classes = klass klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {}) @compiler.topscope.known_resource_types.add klass lambda { @compiler.compile }.should raise_error(Puppet::ParseError, "Invalid parameter 3 on Class[Foo]") end it "should ensure class is in catalog without params" do @node.classes = klasses = {'foo'=>nil} foo = Puppet::Resource::Type.new(:hostclass, 'foo') @compiler.topscope.known_resource_types.add foo catalog = @compiler.compile catalog.classes.should include 'foo' end it "should not evaluate the resources created for found classes unless asked" do @compiler.catalog.stubs(:tag) @resource.expects(:evaluate).never @class.expects(:ensure_in_catalog).returns(@resource) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope) end it "should immediately evaluate the resources created for found classes when asked" do @compiler.catalog.stubs(:tag) @resource.expects(:evaluate) @class.expects(:ensure_in_catalog).returns(@resource) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes that have already been evaluated" do @compiler.catalog.stubs(:tag) @scope.stubs(:class_scope).with(@class).returns(@scope) @compiler.expects(:add_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compiler.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes previously evaluated with different capitalization" do @compiler.catalog.stubs(:tag) @scope.stubs(:find_hostclass).with("MyClass",{:assume_fqname => false}).returns(@class) @scope.stubs(:class_scope).with(@class).returns(@scope) @compiler.expects(:add_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compiler.evaluate_classes(%w{MyClass}, @scope, false) end end describe "when evaluating AST nodes with no AST nodes present" do it "should do nothing" do @compiler.expects(:ast_nodes?).returns(false) @compiler.known_resource_types.expects(:nodes).never Puppet::Parser::Resource.expects(:new).never @compiler.send(:evaluate_ast_node) end end describe "when evaluating AST nodes with AST nodes present" do before do @compiler.known_resource_types.stubs(:nodes?).returns true # Set some names for our test @node.stubs(:names).returns(%w{a b c}) @compiler.known_resource_types.stubs(:node).with("a").returns(nil) @compiler.known_resource_types.stubs(:node).with("b").returns(nil) @compiler.known_resource_types.stubs(:node).with("c").returns(nil) # It should check this last, of course. @compiler.known_resource_types.stubs(:node).with("default").returns(nil) end it "should fail if the named node cannot be found" do proc { @compiler.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError) end it "should evaluate the first node class matching the node name" do node_class = stub 'node', :name => "c", :evaluate_code => nil @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) @compiler.compile end it "should match the default node if no matching node can be found" do node_class = stub 'node', :name => "default", :evaluate_code => nil @compiler.known_resource_types.stubs(:node).with("default").returns(node_class) node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil, :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) @compiler.compile end it "should evaluate the node resource immediately rather than using lazy evaluation" do node_class = stub 'node', :name => "c" @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :type => "node" node_class.expects(:ensure_in_catalog).returns(node_resource) node_resource.expects(:evaluate) @compiler.send(:evaluate_ast_node) end end describe "when evaluating node classes" do include PuppetSpec::Compiler describe "when provided classes in array format" do let(:node) { Puppet::Node.new('someone', :classes => ['something']) } describe "when the class exists" do it "should succeed if the class is already included" do manifest = <<-MANIFEST class something {} include something MANIFEST catalog = compile_to_catalog(manifest, node) catalog.resource('Class', 'Something').should_not be_nil end it "should evaluate the class without parameters if it's not already included" do manifest = "class something {}" catalog = compile_to_catalog(manifest, node) catalog.resource('Class', 'Something').should_not be_nil end end it "should fail if the class doesn't exist" do expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) end end describe "when provided classes in hash format" do describe "for classes without parameters" do let(:node) { Puppet::Node.new('someone', :classes => {'something' => {}}) } describe "when the class exists" do it "should succeed if the class is already included" do manifest = <<-MANIFEST class something {} include something MANIFEST catalog = compile_to_catalog(manifest, node) catalog.resource('Class', 'Something').should_not be_nil end it "should evaluate the class if it's not already included" do manifest = <<-MANIFEST class something {} MANIFEST catalog = compile_to_catalog(manifest, node) catalog.resource('Class', 'Something').should_not be_nil end end it "should fail if the class doesn't exist" do expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) end end describe "for classes with parameters" do let(:node) { Puppet::Node.new('someone', :classes => {'something' => {'configuron' => 'defrabulated'}}) } describe "when the class exists" do it "should fail if the class is already included" do manifest = <<-MANIFEST class something($configuron=frabulated) {} include something MANIFEST expect { compile_to_catalog(manifest, node) }.to raise_error(Puppet::Error, /Class\[Something\] is already declared/) end it "should evaluate the class if it's not already included" do manifest = <<-MANIFEST class something($configuron=frabulated) {} MANIFEST catalog = compile_to_catalog(manifest, node) resource = catalog.resource('Class', 'Something') resource['configuron'].should == 'defrabulated' end end it "should fail if the class doesn't exist" do expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) end it 'evaluates classes declared with parameters before unparameterized classes' do node = Puppet::Node.new('someone', :classes => { 'app::web' => {}, 'app' => { 'port' => 8080 } }) manifest = <<-MANIFEST class app($port = 80) { } class app::web($port = $app::port) inherits app { notify { expected: message => "$port" } } MANIFEST catalog = compile_to_catalog(manifest, node) expect(catalog).to have_resource("Class[App]").with_parameter(:port, 8080) expect(catalog).to have_resource("Class[App::Web]") expect(catalog).to have_resource("Notify[expected]").with_parameter(:message, "8080") end end end end describe "when managing resource overrides" do before do @override = stub 'override', :ref => "File[/foo]", :type => "my" @resource = resource(:file, "/foo") end it "should be able to store overrides" do lambda { @compiler.add_override(@override) }.should_not raise_error end it "should apply overrides to the appropriate resources" do @compiler.add_resource(@scope, @resource) @resource.expects(:merge).with(@override) @compiler.add_override(@override) @compiler.compile end it "should accept overrides before the related resource has been created" do @resource.expects(:merge).with(@override) # First store the override @compiler.add_override(@override) # Then the resource @compiler.add_resource(@scope, @resource) # And compile, so they get resolved @compiler.compile end it "should fail if the compile is finished and resource overrides have not been applied" do @compiler.add_override(@override) lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Could not find resource(s) File[/foo] for overriding' end end end diff --git a/spec/unit/parser/files_spec.rb b/spec/unit/parser/files_spec.rb index 020ce740b..2c97a735c 100755 --- a/spec/unit/parser/files_spec.rb +++ b/spec/unit/parser/files_spec.rb @@ -1,168 +1,102 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/files' describe Puppet::Parser::Files do include PuppetSpec::Files - let(:environment) { Puppet::Node::Environment.create(:testing, []) } + let(:modulepath) { tmpdir("modulepath") } + let(:environment) { Puppet::Node::Environment.create(:testing, [modulepath]) } + let(:mymod) { File.join(modulepath, "mymod") } + let(:mymod_files) { File.join(mymod, "files") } + let(:mymod_a_file) { File.join(mymod_files, "some.txt") } + let(:mymod_templates) { File.join(mymod, "templates") } + let(:mymod_a_template) { File.join(mymod_templates, "some.erb") } + let(:mymod_manifests) { File.join(mymod, "manifests") } + let(:mymod_init_manifest) { File.join(mymod_manifests, "init.pp") } + let(:mymod_another_manifest) { File.join(mymod_manifests, "another.pp") } + let(:an_absolute_file_path_outside_of_module) { make_absolute("afilenamesomewhere") } before do - @basepath = make_absolute("/somepath") - end - - describe "when searching for files" do - it "should return fully-qualified files directly" do - Puppet::Parser::Files.expects(:modulepath).never - Puppet::Parser::Files.find_file(@basepath + "/my/file", environment).should == @basepath + "/my/file" - end - - it "should return the first found file" do - mod = mock 'module' - mod.expects(:file).returns("/one/mymod/files/myfile") - environment.expects(:module).with("mymod").returns mod - - Puppet::Parser::Files.find_file("mymod/myfile", environment).should == "/one/mymod/files/myfile" - end - - it "should return nil if template is not found" do - Puppet::Parser::Files.find_file("foomod/myfile", environment).should be_nil - end - end - - describe "when searching for templates" do - it "should return fully-qualified templates directly" do - Puppet::Parser::Files.expects(:modulepath).never - Puppet::Parser::Files.find_template(@basepath + "/my/template", environment).should == @basepath + "/my/template" - end - - it "should return the template from the first found module" do - mod = mock 'module' - mod.expects(:template).returns("/one/mymod/templates/mytemplate") - environment.expects(:module).with("mymod").returns mod - - Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/one/mymod/templates/mytemplate" - end - - it "should return the file in the templatedir if it exists" do - Puppet[:templatedir] = "/my/templates" - Puppet[:modulepath] = "/one:/two" - File.stubs(:directory?).returns(true) - Puppet::FileSystem.stubs(:exist?).returns(true) - Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == File.join(Puppet[:templatedir], "mymod/mytemplate") + FileUtils.mkdir_p(mymod_files) + File.open(mymod_a_file, 'w') do |f| + f.puts('something') end - - it "should not raise an error if no valid templatedir exists and the template exists in a module" do - mod = mock 'module' - mod.expects(:template).returns("/one/mymod/templates/mytemplate") - environment.expects(:module).with("mymod").returns mod - Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(nil) - - Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/one/mymod/templates/mytemplate" + FileUtils.mkdir_p(mymod_templates) + File.open(mymod_a_template, 'w') do |f| + f.puts('<%= "something" %>') end - - it "should return unqualified templates if they exist in the template dir" do - Puppet::FileSystem.stubs(:exist?).returns true - Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"]) - - Puppet::Parser::Files.find_template("mytemplate", environment).should == "/my/templates/mytemplate" + FileUtils.mkdir_p(mymod_manifests) + File.open(mymod_init_manifest, 'w') do |f| + f.puts('class mymod { }') end - - it "should only return templates if they actually exist" do - Puppet::FileSystem.expects(:exist?).with("/my/templates/mytemplate").returns true - Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"]) - Puppet::Parser::Files.find_template("mytemplate", environment).should == "/my/templates/mytemplate" - end - - it "should return nil when asked for a template that doesn't exist" do - Puppet::FileSystem.expects(:exist?).with("/my/templates/mytemplate").returns false - Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"]) - Puppet::Parser::Files.find_template("mytemplate", environment).should be_nil + File.open(mymod_another_manifest, 'w') do |f| + f.puts('class mymod::another { }') end + end - it "should accept relative templatedirs" do - Puppet::FileSystem.stubs(:exist?).returns true - Puppet[:templatedir] = "my/templates" - File.expects(:directory?).with(File.expand_path("my/templates")).returns(true) - Puppet::Parser::Files.find_template("mytemplate", environment).should == File.expand_path("my/templates/mytemplate") + describe "when searching for files" do + it "returns fully-qualified file names directly" do + expect(Puppet::Parser::Files.find_file(an_absolute_file_path_outside_of_module, environment)).to eq(an_absolute_file_path_outside_of_module) end - it "should use the environment templatedir if no module is found and an environment is specified" do - Puppet::FileSystem.stubs(:exist?).returns true - Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/myenv/templates"]) - Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/myenv/templates/mymod/mytemplate" + it "returns the full path to the file if given a modulename/relative_filepath selector " do + expect(Puppet::Parser::Files.find_file("mymod/some.txt", environment)).to eq(mymod_a_file) end - it "should use first dir from environment templatedir if no module is found and an environment is specified" do - Puppet::FileSystem.stubs(:exist?).returns true - Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/myenv/templates", "/two/templates"]) - Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/myenv/templates/mymod/mytemplate" + it "returns nil if the module is not found" do + expect(Puppet::Parser::Files.find_file("mod_does_not_exist/myfile", environment)).to be_nil end - it "should use a valid dir when templatedir is a path for unqualified templates and the first dir contains template" do - Puppet::Parser::Files.stubs(:templatepath).returns(["/one/templates", "/two/templates"]) - Puppet::FileSystem.expects(:exist?).with("/one/templates/mytemplate").returns(true) - Puppet::Parser::Files.find_template("mytemplate", environment).should == "/one/templates/mytemplate" + it "also returns nil if the module is found, but the file is not" do + expect(Puppet::Parser::Files.find_file("mymod/file_does_not_exist", environment)).to be_nil end + end - it "should use a valid dir when templatedir is a path for unqualified templates and only second dir contains template" do - Puppet::Parser::Files.stubs(:templatepath).returns(["/one/templates", "/two/templates"]) - Puppet::FileSystem.expects(:exist?).with("/one/templates/mytemplate").returns(false) - Puppet::FileSystem.expects(:exist?).with("/two/templates/mytemplate").returns(true) - Puppet::Parser::Files.find_template("mytemplate", environment).should == "/two/templates/mytemplate" + describe "when searching for templates" do + it "returns fully-qualified templates directly" do + expect(Puppet::Parser::Files.find_template(an_absolute_file_path_outside_of_module, environment)).to eq(an_absolute_file_path_outside_of_module) end - it "should use the node environment if specified" do - mod = mock 'module' - environment.expects(:module).with("mymod").returns mod - - mod.expects(:template).returns("/my/modules/mymod/templates/envtemplate") - - Puppet::Parser::Files.find_template("mymod/envtemplate", environment).should == "/my/modules/mymod/templates/envtemplate" + it "returns the full path to the template if given a modulename/relative_templatepath selector" do + expect(Puppet::Parser::Files.find_template("mymod/some.erb", environment)).to eq(mymod_a_template) end - it "should return nil if no template can be found" do - Puppet::Parser::Files.find_template("foomod/envtemplate", environment).should be_nil + it "returns nil if the module is not found" do + expect(Puppet::Parser::Files.find_template("module_does_not_exist/mytemplate", environment)).to be_nil end - end - - describe "when searching for manifests" do - it "should ignore invalid modules" do - mod = mock 'module' - environment.expects(:module).with("mymod").raises(Puppet::Module::InvalidName, "name is invalid") - Puppet.expects(:value).with(:modulepath).never - Dir.stubs(:glob).returns %w{foo} - Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", environment) + it "returns nil if the module is found, but the template is not " do + expect(Puppet::Parser::Files.find_template("mymod/template_does_not_exist", environment)).to be_nil end end describe "when searching for manifests in a module" do - def a_module_in_environment(env, name) - mod = Puppet::Module.new(name, "/one/#{name}", env) - env.stubs(:module).with(name).returns mod - mod.stubs(:match_manifests).with("init.pp").returns(["/one/#{name}/manifests/init.pp"]) + let(:no_manifests_found) { [nil, []] } + + it "ignores invalid module names" do + expect(Puppet::Parser::Files.find_manifests_in_modules("mod.has.invalid.name/init.pp", environment)).to eq(no_manifests_found) end it "returns no files when no module is found" do - module_name, files = Puppet::Parser::Files.find_manifests_in_modules("not_here_module/foo", environment) - expect(files).to be_empty - expect(module_name).to be_nil + expect(Puppet::Parser::Files.find_manifests_in_modules("not_here_module/init.pp", environment)).to eq(no_manifests_found) end - it "should return the name of the module and the manifests from the first found module" do - a_module_in_environment(environment, "mymod") + it "returns the name of the module and the manifests from the first found module" do + expect(Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", environment) + ).to eq(["mymod", [mymod_init_manifest]]) + end - Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", environment).should == - ["mymod", ["/one/mymod/manifests/init.pp"]] + it "always includes init.pp if present" do + expect(Puppet::Parser::Files.find_manifests_in_modules("mymod/another.pp", environment) + ).to eq(["mymod", [mymod_init_manifest, mymod_another_manifest]]) end it "does not find the module when it is a different environment" do different_env = Puppet::Node::Environment.create(:different, []) - a_module_in_environment(environment, "mymod") - Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", different_env).should_not include("mymod") + expect(Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", different_env)).to eq(no_manifests_found) end end end diff --git a/spec/unit/parser/templatewrapper_spec.rb b/spec/unit/parser/templatewrapper_spec.rb index 2b1388d0a..38dcb7000 100755 --- a/spec/unit/parser/templatewrapper_spec.rb +++ b/spec/unit/parser/templatewrapper_spec.rb @@ -1,108 +1,101 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/templatewrapper' describe Puppet::Parser::TemplateWrapper do let(:known_resource_types) { Puppet::Resource::TypeCollection.new("env") } let(:scope) do compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) compiler.environment.stubs(:known_resource_types).returns known_resource_types Puppet::Parser::Scope.new compiler end let(:tw) { Puppet::Parser::TemplateWrapper.new(scope) } - it "marks the file for watching" do - full_file_name = given_a_template_file("fake_template", "content") - - known_resource_types.expects(:watch_file).with(full_file_name) - tw.file = "fake_template" - end - it "fails if a template cannot be found" do Puppet::Parser::Files.expects(:find_template).returns nil expect { tw.file = "fake_template" }.to raise_error(Puppet::ParseError) end it "stringifies as template[] for a file based template" do Puppet::Parser::Files.stubs(:find_template).returns("/tmp/fake_template") tw.file = "fake_template" tw.to_s.should eql("template[/tmp/fake_template]") end it "stringifies as template[inline] for a string-based template" do tw.to_s.should eql("template[inline]") end it "reads and evaluates a file-based template" do given_a_template_file("fake_template", "template contents") tw.file = "fake_template" tw.result.should eql("template contents") end it "provides access to the name of the template via #file" do full_file_name = given_a_template_file("fake_template", "<%= file %>") tw.file = "fake_template" tw.result.should == full_file_name end it "evaluates a given string as a template" do tw.result("template contents").should eql("template contents") end it "provides the defined classes with #classes" do catalog = mock 'catalog', :classes => ["class1", "class2"] scope.expects(:catalog).returns( catalog ) tw.classes.should == ["class1", "class2"] end it "provides all the tags with #all_tags" do catalog = mock 'catalog', :tags => ["tag1", "tag2"] scope.expects(:catalog).returns( catalog ) tw.all_tags.should == ["tag1","tag2"] end it "provides the tags defined in the current scope with #tags" do scope.expects(:tags).returns( ["tag1", "tag2"] ) tw.tags.should == ["tag1","tag2"] end it "raises error on access to removed in-scope variables via method calls" do scope["in_scope_variable"] = "is good" expect { tw.result("<%= in_scope_variable %>") }.to raise_error(/undefined local variable or method `in_scope_variable'/ ) end it "reports that variable is available when it is in scope" do scope["in_scope_variable"] = "is good" tw.result("<%= has_variable?('in_scope_variable') %>").should == "true" end it "reports that a variable is not available when it is not in scope" do tw.result("<%= has_variable?('not_in_scope_variable') %>").should == "false" end it "provides access to in-scope variables via instance variables" do scope["one"] = "foo" tw.result("<%= @one %>").should == "foo" end %w{! . ; :}.each do |badchar| it "translates #{badchar} to _ in instance variables" do scope["one#{badchar}"] = "foo" tw.result("<%= @one_ %>").should == "foo" end end def given_a_template_file(name, contents) full_name = "/full/path/to/#{name}" Puppet::Parser::Files.stubs(:find_template). with(name, anything()). returns(full_name) File.stubs(:read).with(full_name).returns(contents) full_name end end diff --git a/spec/unit/parser/type_loader_spec.rb b/spec/unit/parser/type_loader_spec.rb index 759ade0f8..502f93348 100755 --- a/spec/unit/parser/type_loader_spec.rb +++ b/spec/unit/parser/type_loader_spec.rb @@ -1,202 +1,210 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/type_loader' require 'puppet/parser/parser_factory' require 'puppet_spec/modules' require 'puppet_spec/files' describe Puppet::Parser::TypeLoader do include PuppetSpec::Modules include PuppetSpec::Files let(:empty_hostclass) { Puppet::Parser::AST::Hostclass.new('') } let(:loader) { Puppet::Parser::TypeLoader.new(:myenv) } + let(:my_env) { Puppet::Node::Environment.create(:myenv, []) } + + around do |example| + envs = Puppet::Environments::Static.new(my_env) + + Puppet.override(:environments => envs) do + example.run + end + end it "should support an environment" do loader = Puppet::Parser::TypeLoader.new(:myenv) loader.environment.name.should == :myenv end it "should delegate its known resource types to its environment" do loader.known_resource_types.should be_instance_of(Puppet::Resource::TypeCollection) end describe "when loading names from namespaces" do it "should do nothing if the name to import is an empty string" do loader.try_load_fqname(:hostclass, "").should be_nil end it "should attempt to import each generated name" do loader.expects(:import_from_modules).with("foo/bar").returns([]) loader.expects(:import_from_modules).with("foo").returns([]) loader.try_load_fqname(:hostclass, "foo::bar") end it "should attempt to load each possible name going from most to least specific" do path_order = sequence('path') ['foo/bar/baz', 'foo/bar', 'foo'].each do |path| Puppet::Parser::Files.expects(:find_manifests_in_modules).with(path, anything).returns([nil, []]).in_sequence(path_order) end loader.try_load_fqname(:hostclass, 'foo::bar::baz') end end describe "when importing" do let(:stub_parser) { stub 'Parser', :file= => nil, :parse => empty_hostclass } before(:each) do Puppet::Parser::ParserFactory.stubs(:parser).with(anything).returns(stub_parser) end it "should return immediately when imports are being ignored" do Puppet::Parser::Files.expects(:find_manifests_in_modules).never Puppet[:ignoreimport] = true loader.import("foo", "/path").should be_nil end it "should find all manifests matching the file or pattern" do Puppet::Parser::Files.expects(:find_manifests_in_modules).with("myfile", anything).returns ["modname", %w{one}] loader.import("myfile", "/path") end it "should pass the environment when looking for files" do Puppet::Parser::Files.expects(:find_manifests_in_modules).with(anything, loader.environment).returns ["modname", %w{one}] loader.import("myfile", "/path") end it "should fail if no files are found" do Puppet::Parser::Files.expects(:find_manifests_in_modules).returns [nil, []] lambda { loader.import("myfile", "/path") }.should raise_error(Puppet::ImportError) end it "should parse each found file" do Puppet::Parser::Files.expects(:find_manifests_in_modules).returns ["modname", [make_absolute("/one")]] loader.expects(:parse_file).with(make_absolute("/one")).returns(Puppet::Parser::AST::Hostclass.new('')) loader.import("myfile", "/path") end it "should not attempt to import files that have already been imported" do loader = Puppet::Parser::TypeLoader.new(:myenv) Puppet::Parser::Files.expects(:find_manifests_in_modules).twice.returns ["modname", %w{/one}] loader.import("myfile", "/path").should_not be_empty loader.import("myfile", "/path").should be_empty end end describe "when importing all" do - before do - @base = tmpdir("base") + let(:base) { tmpdir("base") } + let(:modulebase1) { File.join(base, "first") } + let(:modulebase2) { File.join(base, "second") } + let(:my_env) { Puppet::Node::Environment.create(:myenv, [modulebase1, modulebase2]) } + before do # Create two module path directories - @modulebase1 = File.join(@base, "first") - FileUtils.mkdir_p(@modulebase1) - @modulebase2 = File.join(@base, "second") - FileUtils.mkdir_p(@modulebase2) - - Puppet[:modulepath] = "#{@modulebase1}#{File::PATH_SEPARATOR}#{@modulebase2}" + FileUtils.mkdir_p(modulebase1) + FileUtils.mkdir_p(modulebase2) end def mk_module(basedir, name) PuppetSpec::Modules.create(name, basedir) end # We have to pass the base path so that we can # write to modules that are in the second search path def mk_manifests(base, mod, files) files.collect do |file| name = mod.name + "::" + file.gsub("/", "::") path = File.join(base, mod.name, "manifests", file + ".pp") FileUtils.mkdir_p(File.split(path)[0]) # write out the class File.open(path, "w") { |f| f.print "class #{name} {}" } name end end it "should load all puppet manifests from all modules in the specified environment" do - @module1 = mk_module(@modulebase1, "one") - @module2 = mk_module(@modulebase2, "two") + module1 = mk_module(modulebase1, "one") + module2 = mk_module(modulebase2, "two") - mk_manifests(@modulebase1, @module1, %w{a b}) - mk_manifests(@modulebase2, @module2, %w{c d}) + mk_manifests(modulebase1, module1, %w{a b}) + mk_manifests(modulebase2, module2, %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end it "should not load manifests from duplicate modules later in the module path" do - @module1 = mk_module(@modulebase1, "one") + module1 = mk_module(modulebase1, "one") # duplicate - @module2 = mk_module(@modulebase2, "one") + module2 = mk_module(modulebase2, "one") - mk_manifests(@modulebase1, @module1, %w{a}) - mk_manifests(@modulebase2, @module2, %w{c}) + mk_manifests(modulebase1, module1, %w{a}) + mk_manifests(modulebase2, module2, %w{c}) loader.import_all loader.environment.known_resource_types.hostclass("one::c").should be_nil end it "should load manifests from subdirectories" do - @module1 = mk_module(@modulebase1, "one") + module1 = mk_module(modulebase1, "one") - mk_manifests(@modulebase1, @module1, %w{a a/b a/b/c}) + mk_manifests(modulebase1, module1, %w{a a/b a/b/c}) loader.import_all loader.environment.known_resource_types.hostclass("one::a::b").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("one::a::b::c").should be_instance_of(Puppet::Resource::Type) end it "should skip modules that don't have manifests" do - @module1 = mk_module(@modulebase1, "one") - @module2 = mk_module(@modulebase2, "two") - mk_manifests(@modulebase2, @module2, %w{c d}) + module1 = mk_module(modulebase1, "one") + module2 = mk_module(modulebase2, "two") + mk_manifests(modulebase2, module2, %w{c d}) loader.import_all loader.environment.known_resource_types.hostclass("one::a").should be_nil loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type) loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type) end end describe "when parsing a file" do - it "should create a new parser instance for each file using the current environment" do + it "requests a new parser instance for each file" do parser = stub 'Parser', :file= => nil, :parse => empty_hostclass - Puppet::Parser::ParserFactory.expects(:parser).twice.with(loader.environment).returns(parser) + Puppet::Parser::ParserFactory.expects(:parser).twice.returns(parser) loader.parse_file("/my/file") loader.parse_file("/my/other_file") end - it "should assign the parser its file and parse" do + it "assigns the parser its file and then parses" do parser = mock 'parser' - Puppet::Parser::ParserFactory.expects(:parser).with(loader.environment).returns(parser) + Puppet::Parser::ParserFactory.expects(:parser).returns(parser) parser.expects(:file=).with("/my/file") parser.expects(:parse).returns(empty_hostclass) loader.parse_file("/my/file") end end it "should be able to add classes to the current resource type collection" do file = tmpfile("simple_file.pp") File.open(file, "w") { |f| f.puts "class foo {}" } loader.import(File.basename(file), File.dirname(file)) loader.known_resource_types.hostclass("foo").should be_instance_of(Puppet::Resource::Type) end end diff --git a/spec/unit/pops/evaluator/evaluating_parser_spec.rb b/spec/unit/pops/evaluator/evaluating_parser_spec.rb index ce95ca71d..48558f930 100644 --- a/spec/unit/pops/evaluator/evaluating_parser_spec.rb +++ b/spec/unit/pops/evaluator/evaluating_parser_spec.rb @@ -1,1320 +1,1323 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'puppet/parser/e4_parser_adapter' # relative to this spec file (./) does not work as this file is loaded by rspec #require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper') describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do include PuppetSpec::Pops include PuppetSpec::Scope before(:each) do Puppet[:strict_variables] = true # Tests needs a known configuration of node/scope/compiler since it parses and evaluates # snippets as the compiler will evaluate them, butwithout the overhead of compiling a complete # catalog for each tested expression. # @parser = Puppet::Pops::Parser::EvaluatingParser.new @node = Puppet::Node.new('node.example.com') - @node.environment = Puppet::Node::Environment.create(:testing, []) + @node.environment = environment @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(@compiler) @scope.source = Puppet::Resource::Type.new(:node, 'node.example.com') @scope.parent = @compiler.topscope end - let(:parser) { @parser } + let(:environment) { Puppet::Node::Environment.create(:testing, []) } + let(:parser) { @parser } let(:scope) { @scope } types = Puppet::Pops::Types::TypeFactory context "When evaluator evaluates literals" do { "1" => 1, "010" => 8, "0x10" => 16, "3.14" => 3.14, "0.314e1" => 3.14, "31.4e-1" => 3.14, "'1'" => '1', "'banana'" => 'banana', '"banana"' => 'banana', "banana" => 'banana', "banana::split" => 'banana::split', "false" => false, "true" => true, "Array" => types.array_of_data(), "/.*/" => /.*/ }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator evaluates Lists and Hashes" do { "[]" => [], "[1,2,3]" => [1,2,3], "[1,[2.0, 2.1, [2.2]],[3.0, 3.1]]" => [1,[2.0, 2.1, [2.2]],[3.0, 3.1]], "[2 + 2]" => [4], "[1,2,3] == [1,2,3]" => true, "[1,2,3] != [2,3,4]" => true, "[1,2,3] == [2,2,3]" => false, "[1,2,3] != [1,2,3]" => false, "[1,2,3][2]" => 3, "[1,2,3] + [4,5]" => [1,2,3,4,5], "[1,2,3] + [[4,5]]" => [1,2,3,[4,5]], "[1,2,3] + 4" => [1,2,3,4], "[1,2,3] << [4,5]" => [1,2,3,[4,5]], "[1,2,3] << {'a' => 1, 'b'=>2}" => [1,2,3,{'a' => 1, 'b'=>2}], "[1,2,3] << 4" => [1,2,3,4], "[1,2,3,4] - [2,3]" => [1,4], "[1,2,3,4] - [2,5]" => [1,3,4], "[1,2,3,4] - 2" => [1,3,4], "[1,2,3,[2],4] - 2" => [1,3,[2],4], "[1,2,3,[2,3],4] - [[2,3]]" => [1,2,3,4], "[1,2,3,3,2,4,2,3] - [2,3]" => [1,4], "[1,2,3,['a',1],['b',2]] - {'a' => 1, 'b'=>2}" => [1,2,3], "[1,2,3,{'a'=>1,'b'=>2}] - [{'a' => 1, 'b'=>2}]" => [1,2,3], "[1,2,3] + {'a' => 1, 'b'=>2}" => [1,2,3,['a',1],['b',2]], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[1,2,3][a]" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "{}" => {}, "{'a'=>1,'b'=>2}" => {'a'=>1,'b'=>2}, "{'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}" => {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}, "{'a'=> 2 + 2}" => {'a'=> 4}, "{'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} != {'x'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} == {'a'=> 2, 'b'=>3}" => false, "{'a'=> 1, 'b'=>2} != {'a'=> 1, 'b'=>2}" => false, "{a => 1, b => 2}[b]" => 2, "{2+2 => sum, b => 2}[4]" => 'sum', "{'a'=>1, 'b'=>2} + {'c'=>3}" => {'a'=>1,'b'=>2,'c'=>3}, "{'a'=>1, 'b'=>2} + {'b'=>3}" => {'a'=>1,'b'=>3}, "{'a'=>1, 'b'=>2} + ['c', 3, 'b', 3]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} + [['c', 3], ['b', 3]]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} - {'b' => 3}" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - ['b', 'c']" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - 'c'" => {'a'=>1, 'b'=>2}, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{'a' => 1, 'b'=>2} << 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When the evaluator perform comparisons" do { "'a' == 'a'" => true, "'a' == 'b'" => false, "'a' != 'a'" => false, "'a' != 'b'" => true, "'a' < 'b' " => true, "'a' < 'a' " => false, "'b' < 'a' " => false, "'a' <= 'b'" => true, "'a' <= 'a'" => true, "'b' <= 'a'" => false, "'a' > 'b' " => false, "'a' > 'a' " => false, "'b' > 'a' " => true, "'a' >= 'b'" => false, "'a' >= 'a'" => true, "'b' >= 'a'" => true, "'a' == 'A'" => true, "'a' != 'A'" => false, "'a' > 'A'" => false, "'a' >= 'A'" => true, "'A' < 'a'" => false, "'A' <= 'a'" => true, "1 == 1" => true, "1 == 2" => false, "1 != 1" => false, "1 != 2" => true, "1 < 2 " => true, "1 < 1 " => false, "2 < 1 " => false, "1 <= 2" => true, "1 <= 1" => true, "2 <= 1" => false, "1 > 2 " => false, "1 > 1 " => false, "2 > 1 " => true, "1 >= 2" => false, "1 >= 1" => true, "2 >= 1" => true, "1 == 1.0 " => true, "1 < 1.1 " => true, "1.0 == 1 " => true, "1.0 < 2 " => true, "'1.0' < 'a'" => true, "'1.0' < '' " => false, "'1.0' < ' '" => false, "'a' > '1.0'" => true, "/.*/ == /.*/ " => true, "/.*/ != /a.*/" => true, "true == true " => true, "false == false" => true, "true == false" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "a > 1" => /String > Integer/, "a >= 1" => /String >= Integer/, "a < 1" => /String < Integer/, "a <= 1" => /String <= Integer/, "1 > a" => /Integer > String/, "1 >= a" => /Integer >= String/, "1 < a" => /Integer < String/, "1 <= a" => /Integer <= String/, }.each do | source, error| it "should not allow comparison of String and Number '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(error) end end { "'a' =~ /.*/" => true, "'a' =~ '.*'" => true, "/.*/ != /a.*/" => true, "'a' !~ /b.*/" => true, "'a' !~ 'b.*'" => true, '$x = a; a =~ "$x.*"' => true, "a =~ Pattern['a.*']" => true, "a =~ Regexp['a.*']" => false, # String is not subtype of Regexp. PUP-957 "$x = /a.*/ a =~ $x" => true, "$x = Pattern['a.*'] a =~ $x" => true, "1 =~ Integer" => true, "1 !~ Integer" => false, "[1,2,3] =~ Array[Integer[1,10]]" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "666 =~ /6/" => :error, "[a] =~ /a/" => :error, "{a=>1} =~ /a/" => :error, "/a/ =~ /a/" => :error, "Array =~ /A/" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "1 in [1,2,3]" => true, "4 in [1,2,3]" => false, "a in {x=>1, a=>2}" => true, "z in {x=>1, a=>2}" => false, "ana in bananas" => true, "xxx in bananas" => false, "/ana/ in bananas" => true, "/xxx/ in bananas" => false, "ANA in bananas" => false, # ANA is a type, not a String "String[1] in bananas" => false, # Philosophically true though :-) "'ANA' in bananas" => true, "ana in 'BANANAS'" => true, "/ana/ in 'BANANAS'" => false, "/ANA/ in 'BANANAS'" => true, "xxx in 'BANANAS'" => false, "[2,3] in [1,[2,3],4]" => true, "[2,4] in [1,[2,3],4]" => false, "[a,b] in ['A',['A','B'],'C']" => true, "[x,y] in ['A',['A','B'],'C']" => false, "a in {a=>1}" => true, "x in {a=>1}" => false, "'A' in {a=>1}" => true, "'X' in {a=>1}" => false, "a in {'A'=>1}" => true, "x in {'A'=>1}" => false, "/xxx/ in {'aaaxxxbbb'=>1}" => true, "/yyy/ in {'aaaxxxbbb'=>1}" => false, "15 in [1, 0xf]" => true, "15 in [1, '0xf']" => false, "'15' in [1, 0xf]" => false, "15 in [1, 115]" => false, "1 in [11, '111']" => false, "'1' in [11, '111']" => false, "Array[Integer] in [2, 3]" => false, "Array[Integer] in [2, [3, 4]]" => true, "Array[Integer] in [2, [a, 4]]" => false, "Integer in { 2 =>'a'}" => true, "Integer[5,10] in [1,5,3]" => true, "Integer[5,10] in [1,2,3]" => false, "Integer in {'a'=>'a'}" => false, "Integer in {'a'=>1}" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "if /(ana)/ in bananas {$1}" => 'ana', "if /(xyz)/ in bananas {$1} else {$1}" => nil, "$a = bananas =~ /(ana)/; $b = /(xyz)/ in bananas; $1" => 'ana', "$a = xyz =~ /(xyz)/; $b = /(ana)/ in bananas; $1" => 'ana', "if /p/ in [pineapple, bananas] {$0}" => 'p', "if /b/ in [pineapple, bananas] {$0}" => 'b', }.each do |source, result| it "sets match variables for a regexp search using in such that '#{source}' produces '#{result}'" do parser.evaluate_string(scope, source, __FILE__).should == result end end { 'Any' => ['Data', 'Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Collection', 'Array', 'Hash', 'CatalogEntry', 'Resource', 'Class', 'Undef', 'File', 'NotYetKnownResourceType'], # Note, Data > Collection is false (so not included) 'Data' => ['Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Array', 'Hash',], 'Scalar' => ['Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern'], 'Numeric' => ['Integer', 'Float'], 'CatalogEntry' => ['Class', 'Resource', 'File', 'NotYetKnownResourceType'], 'Integer[1,10]' => ['Integer[2,3]'], }.each do |general, specials| specials.each do |special | it "should compute that #{general} > #{special}" do parser.evaluate_string(scope, "#{general} > #{special}", __FILE__).should == true end it "should compute that #{special} < #{general}" do parser.evaluate_string(scope, "#{special} < #{general}", __FILE__).should == true end it "should compute that #{general} != #{special}" do parser.evaluate_string(scope, "#{special} != #{general}", __FILE__).should == true end end end { 'Integer[1,10] > Integer[2,3]' => true, 'Integer[1,10] == Integer[2,3]' => false, 'Integer[1,10] > Integer[0,5]' => false, 'Integer[1,10] > Integer[1,10]' => false, 'Integer[1,10] >= Integer[1,10]' => true, 'Integer[1,10] == Integer[1,10]' => true, }.each do |source, result| it "should parse and evaluate the integer range comparison expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator performs arithmetic" do context "on Integers" do { "2+2" => 4, "2 + 2" => 4, "7 - 3" => 4, "6 * 3" => 18, "6 / 3" => 2, "6 % 3" => 0, "10 % 3" => 1, "-(6/3)" => -2, "-6/3 " => -2, "8 >> 1" => 4, "8 << 1" => 16, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end context "on Floats" do { "2.2 + 2.2" => 4.4, "7.7 - 3.3" => 4.4, "6.1 * 3.1" => 18.91, "6.6 / 3.3" => 2.0, "-(6.0/3.0)" => -2.0, "-6.0/3.0 " => -2.0, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "3.14 << 2" => :error, "3.14 >> 2" => :error, "6.6 % 3.3" => 0.0, "10.0 % 3.0" => 1.0, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "on strings requiring boxing to Numeric" do { "'2' + '2'" => 4, "'-2' + '2'" => 0, "'- 2' + '2'" => 0, '"-\t 2" + "2"' => 0, "'+2' + '2'" => 4, "'+ 2' + '2'" => 4, "'2.2' + '2.2'" => 4.4, "'-2.2' + '2.2'" => 0.0, "'0xF7' + '010'" => 0xFF, "'0xF7' + '0x8'" => 0xFF, "'0367' + '010'" => 0xFF, "'012.3' + '010'" => 20.3, "'-0x2' + '0x4'" => 2, "'+0x2' + '0x4'" => 6, "'-02' + '04'" => 2, "'+02' + '04'" => 6, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'0888' + '010'" => :error, "'0xWTF' + '010'" => :error, "'0x12.3' + '010'" => :error, "'0x12.3' + '010'" => :error, '"-\n 2" + "2"' => :error, '"-\v 2" + "2"' => :error, '"-2\n" + "2"' => :error, '"-2\n " + "2"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end end end # arithmetic context "When the evaluator evaluates assignment" do { "$a = 5" => 5, "$a = 5; $a" => 5, "$a = 5; $b = 6; $a" => 5, "$a = $b = 5; $a == $b" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[a,b,c] = [1,2,3]" => /attempt to assign to 'an Array Expression'/, "[a,b,c] = {b=>2,c=>3,a=>1}" => /attempt to assign to 'an Array Expression'/, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to error with #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError, result) end end end context "When the evaluator evaluates conditionals" do { "if true {5}" => 5, "if false {5}" => nil, "if false {2} else {5}" => 5, "if false {2} elsif true {5}" => 5, "if false {2} elsif false {5}" => nil, "unless false {5}" => 5, "unless true {5}" => nil, "unless true {2} else {5}" => 5, "$a = if true {5} $a" => 5, "$a = if false {5} $a" => nil, "$a = if false {2} else {5} $a" => 5, "$a = if false {2} elsif true {5} $a" => 5, "$a = if false {2} elsif false {5} $a" => nil, "$a = unless false {5} $a" => 5, "$a = unless true {5} $a" => nil, "$a = unless true {2} else {5} $a" => 5, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "case 1 { 1 : { yes } }" => 'yes', "case 2 { 1,2,3 : { yes} }" => 'yes', "case 2 { 1,3 : { no } 2: { yes} }" => 'yes', "case 2 { 1,3 : { no } 5: { no } default: { yes }}" => 'yes', "case 2 { 1,3 : { no } 5: { no } }" => nil, "case 'banana' { 1,3 : { no } /.*ana.*/: { yes } }" => 'yes', "case 'banana' { /.*(ana).*/: { $1 } }" => 'ana', "case [1] { Array : { yes } }" => 'yes', "case [1] { Array[String] : { no } Array[Integer]: { yes } }" => 'yes', "case 1 { Integer : { yes } Type[Integer] : { no } }" => 'yes', "case Integer { Integer : { no } Type[Integer] : { yes } }" => 'yes', # supports unfold "case ringo { *[paul, john, ringo, george] : { 'beatle' } }" => 'beatle', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "2 ? { 1 => no, 2 => yes}" => 'yes', "3 ? { 1 => no, 2 => no, default => yes }" => 'yes', "3 ? { 1 => no, default => yes, 3 => no }" => 'no', "3 ? { 1 => no, 3 => no, default => yes }" => 'no', "4 ? { 1 => no, default => yes, 3 => no }" => 'yes', "4 ? { 1 => no, 3 => no, default => yes }" => 'yes', "'banana' ? { /.*(ana).*/ => $1 }" => 'ana', "[2] ? { Array[String] => yes, Array => yes}" => 'yes', "ringo ? *[paul, john, ringo, george] => 'beatle'" => 'beatle', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end it 'fails if a selector does not match' do expect{parser.evaluate_string(scope, "2 ? 3 => 4")}.to raise_error(/No matching entry for selector parameter with value '2'/) end end context "When evaluator evaluated unfold" do { "*[1,2,3]" => [1,2,3], "*1" => [1], "*'a'" => ['a'] }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate the expression '*{a=>10, b=>20} to [['a',10],['b',20]]" do result = parser.evaluate_string(scope, '*{a=>10, b=>20}', __FILE__) expect(result).to include(['a', 10]) expect(result).to include(['b', 20]) end end context "When evaluator performs [] operations" do { "[1,2,3][0]" => 1, "[1,2,3][2]" => 3, "[1,2,3][3]" => nil, "[1,2,3][-1]" => 3, "[1,2,3][-2]" => 2, "[1,2,3][-4]" => nil, "[1,2,3,4][0,2]" => [1,2], "[1,2,3,4][1,3]" => [2,3,4], "[1,2,3,4][-2,2]" => [3,4], "[1,2,3,4][-3,2]" => [2,3], "[1,2,3,4][3,5]" => [4], "[1,2,3,4][5,2]" => [], "[1,2,3,4][0,-1]" => [1,2,3,4], "[1,2,3,4][0,-2]" => [1,2,3], "[1,2,3,4][0,-4]" => [1], "[1,2,3,4][0,-5]" => [], "[1,2,3,4][-5,2]" => [1], "[1,2,3,4][-5,-3]" => [1,2], "[1,2,3,4][-6,-3]" => [1,2], "[1,2,3,4][2,-3]" => [], "[1,*[2,3],4]" => [1,2,3,4], "[1,*[2,3],4][1]" => 2, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{a=>1, b=>2, c=>3}[a]" => 1, "{a=>1, b=>2, c=>3}[c]" => 3, "{a=>1, b=>2, c=>3}[x]" => nil, "{a=>1, b=>2, c=>3}[c,b]" => [3,2], "{a=>1, b=>2, c=>3}[a,b,c]" => [1,2,3], "{a=>{b=>{c=>'it works'}}}[a][b][c]" => 'it works', "$a = {undef => 10} $a[free_lunch]" => nil, "$a = {undef => 10} $a[undef]" => 10, "$a = {undef => 10} $a[$a[free_lunch]]" => 10, "$a = {} $a[free_lunch] == undef" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'abc'[0]" => 'a', "'abc'[2]" => 'c', "'abc'[-1]" => 'c', "'abc'[-2]" => 'b', "'abc'[-3]" => 'a', "'abc'[-4]" => '', "'abc'[3]" => '', "abc[0]" => 'a', "abc[2]" => 'c', "abc[-1]" => 'c', "abc[-2]" => 'b', "abc[-3]" => 'a', "abc[-4]" => '', "abc[3]" => '', "'abcd'[0,2]" => 'ab', "'abcd'[1,3]" => 'bcd', "'abcd'[-2,2]" => 'cd', "'abcd'[-3,2]" => 'bc', "'abcd'[3,5]" => 'd', "'abcd'[5,2]" => '', "'abcd'[0,-1]" => 'abcd', "'abcd'[0,-2]" => 'abc', "'abcd'[0,-4]" => 'a', "'abcd'[0,-5]" => '', "'abcd'[-5,2]" => 'a', "'abcd'[-5,-3]" => 'ab', "'abcd'[-6,-3]" => 'ab', "'abcd'[2,-3]" => '', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Type operations (full set tested by tests covering type calculator) { "Array[Integer]" => types.array_of(types.integer), "Array[Integer,1]" => types.constrain_size(types.array_of(types.integer),1, :default), "Array[Integer,1,2]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1,2]]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1]]" => types.constrain_size(types.array_of(types.integer),1, :default), "Hash[Integer,Integer]" => types.hash_of(types.integer, types.integer), "Hash[Integer,Integer,1]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Hash[Integer,Integer,1,2]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1,2]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Resource[File]" => types.resource('File'), "Resource['File']" => types.resource(types.resource('File')), "File[foo]" => types.resource('file', 'foo'), "File[foo, bar]" => [types.resource('file', 'foo'), types.resource('file', 'bar')], "Pattern[a, /b/, Pattern[c], Regexp[d]]" => types.pattern('a', 'b', 'c', 'd'), "String[1,2]" => types.constrain_size(types.string,1, 2), "String[Integer[1,2]]" => types.constrain_size(types.string,1, 2), "String[Integer[1]]" => types.constrain_size(types.string,1, :default), }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # LHS where [] not supported, and missing key(s) { "Array[]" => :error, "'abc'[]" => :error, "Resource[]" => :error, "File[]" => :error, "String[]" => :error, "1[]" => :error, "3.14[]" => :error, "/.*/[]" => :error, "$a=[1] $a[]" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(/Syntax error/) end end # Errors when wrong number/type of keys are used { "Array[0]" => 'Array-Type[] arguments must be types. Got Fixnum', "Hash[0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Hash[Integer, 0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Array[Integer,1,2,3]" => 'Array-Type[] accepts 1 to 3 arguments. Got 4', "Array[Integer,String]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String-Type", "Hash[Integer,String, 1,2,3]" => 'Hash-Type[] accepts 1 to 4 arguments. Got 5', "'abc'[x]" => "The value 'x' cannot be converted to Numeric", "'abc'[1.0]" => "A String[] cannot use Float where Integer is expected", "'abc'[1,2,3]" => "String supports [] with one or two arguments. Got 3", "Resource[0]" => 'First argument to Resource[] must be a resource type or a String. Got Integer', "Resource[a, 0]" => 'Error creating type specialization of a Resource-Type, Cannot use Integer where a resource title String is expected', "File[0]" => 'Error creating type specialization of a File-Type, Cannot use Integer where a resource title String is expected', "String[a]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String", "Pattern[0]" => 'Error creating type specialization of a Pattern-Type, Cannot use Integer where String or Regexp or Pattern-Type or Regexp-Type is expected', "Regexp[0]" => 'Error creating type specialization of a Regexp-Type, Cannot use Integer where String or Regexp is expected', "Regexp[a,b]" => 'A Regexp-Type[] accepts 1 argument. Got 2', "true[0]" => "Operator '[]' is not applicable to a Boolean", "1[0]" => "Operator '[]' is not applicable to an Integer", "3.14[0]" => "Operator '[]' is not applicable to a Float", "/.*/[0]" => "Operator '[]' is not applicable to a Regexp", "[1][a]" => "The value 'a' cannot be converted to Numeric", "[1][0.0]" => "An Array[] cannot use Float where Integer is expected", "[1]['0.0']" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, 0.0]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1.0, -1]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, -1.0]" => "An Array[] cannot use Float where Integer is expected", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Regexp.new(Regexp.quote(result))) end end context "on catalog types" do it "[n] gets resource parameter [n]" do source = "notify { 'hello': message=>'yo'} Notify[hello][message]" parser.evaluate_string(scope, source, __FILE__).should == 'yo' end it "[n] gets class parameter [n]" do source = "class wonka($produces='chocolate'){ } include wonka Class[wonka][produces]" # This is more complicated since it needs to run like 3.x and do an import_ast adapted_parser = Puppet::Parser::E4ParserAdapter.new adapted_parser.file = __FILE__ ast = adapted_parser.parse(source) - Puppet.override({:global_scope => scope}, "test") do + Puppet.override({:global_scope => scope, + :environments => Puppet::Environments::Static.new(@node.environment) + }, "gets class parameter test") do scope.known_resource_types.import_ast(ast, '') ast.code.safeevaluate(scope).should == 'chocolate' end end # Resource default and override expressions and resource parameter access with [] { # Properties "notify { id: message=>explicit} Notify[id][message]" => "explicit", "Notify { message=>by_default} notify {foo:} Notify[foo][message]" => "by_default", "notify {foo:} Notify[foo]{message =>by_override} Notify[foo][message]" => "by_override", # Parameters "notify { id: withpath=>explicit} Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } notify { foo: } Notify[foo][withpath]" => "by_default", "notify {foo:} Notify[foo]{withpath=>by_override} Notify[foo][withpath]" => "by_override", # Metaparameters "notify { foo: tag => evoe} Notify[foo][tag]" => "evoe", # Does not produce the defaults for tag parameter (title, type or names of scopes) "notify { foo: } Notify[foo][tag]" => nil, # But a default may be specified on the type "Notify { tag=>by_default } notify { foo: } Notify[foo][tag]" => "by_default", "Notify { tag=>by_default } notify { foo: } Notify[foo]{ tag=>by_override } Notify[foo][tag]" => "by_override", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Virtual and realized resource default and overridden resource parameter access with [] { # Properties "@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@notify { id: message=>explicit } realize Notify[id] Notify[id][message]" => "explicit", "Notify { message=>by_default } @notify { id: } Notify[id][message]" => "by_default", "Notify { message=>by_default } @notify { id: tag=>thisone } Notify <| tag == thisone |>; Notify[id][message]" => "by_default", "@notify { id: } Notify[id]{message=>by_override} Notify[id][message]" => "by_override", # Parameters "@notify { id: withpath=>explicit } Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } @notify { id: } Notify[id][withpath]" => "by_default", "@notify { id: } realize Notify[id] Notify[id]{withpath=>by_override} Notify[id][withpath]" => "by_override", # Metaparameters "@notify { id: tag=>explicit } Notify[id][tag]" => "explicit", }.each do |source, result| it "parses and evaluates virtual and realized resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Exported resource attributes { "@@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@@notify { id: message=>explicit, tag=>thisone } Notify <<| tag == thisone |>> Notify[id][message]" => "explicit", }.each do |source, result| it "parses and evaluates exported resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Resource default and override expressions and resource parameter access error conditions { "notify { xid: message=>explicit} Notify[id][message]" => /Resource not found/, "notify { id: message=>explicit} Notify[id][mustard]" => /does not have a parameter called 'mustard'/, # NOTE: these meta-esque parameters are not recognized as such "notify { id: message=>explicit} Notify[id][title]" => /does not have a parameter called 'title'/, "notify { id: message=>explicit} Notify[id]['type']" => /does not have a parameter called 'type'/, "notify { id: message=>explicit } Notify[id]{message=>override}" => /'message' is already set on Notify\[id\]/ }.each do |source, result| it "should parse '#{source}' and raise error matching #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(result) end end context 'with errors' do { "Class['fail-whale']" => /Illegal name/, "Class[0]" => /An Integer cannot be used where a String is expected/, "Class[/.*/]" => /A Regexp cannot be used where a String is expected/, "Class[4.1415]" => /A Float cannot be used where a String is expected/, "Class[Integer]" => /An Integer-Type cannot be used where a String is expected/, "Class[File['tmp']]" => /A File\['tmp'\] Resource-Reference cannot be used where a String is expected/, }.each do | source, error_pattern| it "an error is flagged for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(error_pattern) end end end end # end [] operations end context "When the evaluator performs boolean operations" do { "true and true" => true, "false and true" => false, "true and false" => false, "false and false" => false, "true or true" => true, "false or true" => true, "true or false" => true, "false or false" => false, "! true" => false, "!! true" => true, "!! false" => false, "! 'x'" => false, "! ''" => false, "! undef" => true, "! [a]" => false, "! []" => false, "! {a=>1}" => false, "! {}" => false, "true and false and '0xwtf' + 1" => false, "false or true or '0xwtf' + 1" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "false || false || '0xwtf' + 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluator performs operations on literal undef" do it "computes non existing hash lookup as undef" do parser.evaluate_string(scope, "{a => 1}[b] == undef", __FILE__).should == true parser.evaluate_string(scope, "undef == {a => 1}[b]", __FILE__).should == true end end context "When evaluator performs calls" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { 'sprintf( "x%iy", $a )' => "x10y", # unfolds 'sprintf( *["x%iy", $a] )' => "x10y", '"x%iy".sprintf( $a )' => "x10y", '$b.reduce |$memo,$x| { $memo + $x }' => 6, 'reduce($b) |$memo,$x| { $memo + $x }' => 6, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end it "provides location information on error in unparenthesized call logic" do expect{parser.evaluate_string(scope, "include non_existing_class", __FILE__)}.to raise_error(Puppet::ParseError, /line 1\:1/) end it 'defaults can be given in a lambda and used only when arg is missing' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Integer', 'count' required_block_param end def test(count, block) block.call(*[].fill(10, 0, count)) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(1) |$x, $y=20| { $x + $y}")).to eql(30) expect(parser.evaluate_string(scope, "test(2) |$x, $y=20| { $x + $y}")).to eql(20) end it 'a given undef does not select the default value' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Any', 'lambda_arg' required_block_param end def test(lambda_arg, block) block.call(lambda_arg) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(undef) |$x=20| { $x == undef}")).to eql(true) end it 'a given undef is given as nil' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:assert_no_undef) do dispatch :assert_no_undef do param 'Any', 'x' end def assert_no_undef(x) case x when Array return unless x.include?(:undef) when Hash return unless x.keys.include?(:undef) || x.values.include?(:undef) else return unless x == :undef end raise "contains :undef" end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'assert_no_undef', the_func, __FILE__) expect{parser.evaluate_string(scope, "assert_no_undef(undef)")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef([undef])")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef({undef => 1})")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef({1 => undef})")}.to_not raise_error() end context 'using the 3x function api' do it 'can call a 3x function' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0] } parser.evaluate_string(scope, "bazinga(42)", __FILE__).should == 42 end it 'maps :undef to empty string' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0] } parser.evaluate_string(scope, "$a = {} bazinga($a[nope])", __FILE__).should == '' parser.evaluate_string(scope, "bazinga(undef)", __FILE__).should == '' end it 'does not map :undef to empty string in arrays' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0][0] } parser.evaluate_string(scope, "$a = {} $b = [$a[nope]] bazinga($b)", __FILE__).should == :undef parser.evaluate_string(scope, "bazinga([undef])", __FILE__).should == :undef end it 'does not map :undef to empty string in hashes' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0]['a'] } parser.evaluate_string(scope, "$a = {} $b = {a => $a[nope]} bazinga($b)", __FILE__).should == :undef parser.evaluate_string(scope, "bazinga({a => undef})", __FILE__).should == :undef end end end context "When evaluator performs string interpolation" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { '"value is $a yo"' => "value is 10 yo", '"value is \$a yo"' => "value is $a yo", '"value is ${a} yo"' => "value is 10 yo", '"value is \${a} yo"' => "value is ${a} yo", '"value is ${$a} yo"' => "value is 10 yo", '"value is ${$a*2} yo"' => "value is 20 yo", '"value is ${sprintf("x%iy",$a)} yo"' => "value is x10y yo", '"value is ${"x%iy".sprintf($a)} yo"' => "value is x10y yo", '"value is ${[1,2,3]} yo"' => "value is [1, 2, 3] yo", '"value is ${/.*/} yo"' => "value is /.*/ yo", '$x = undef "value is $x yo"' => "value is yo", '$x = default "value is $x yo"' => "value is default yo", '$x = Array[Integer] "value is $x yo"' => "value is Array[Integer] yo", '"value is ${Array[Integer]} yo"' => "value is Array[Integer] yo", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate an interpolation of a hash" do source = '"value is ${{a=>1,b=>2}} yo"' # This test requires testing against two options because a hash to string # produces a result that is unordered hashstr = {'a' => 1, 'b' => 2}.to_s alt_results = ["value is {a => 1, b => 2} yo", "value is {b => 2, a => 1} yo" ] populate parse_result = parser.evaluate_string(scope, source, __FILE__) alt_results.include?(parse_result).should == true end it 'should accept a variable with leading underscore when used directly' do source = '$_x = 10; "$_x"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end it 'should accept a variable with leading underscore when used as an expression' do source = '$_x = 10; "${_x}"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluating variables" do context "that are non existing an error is raised for" do it "unqualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity", __FILE__) }.to raise_error(/Unknown variable/) end it "qualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity::graviton", __FILE__) }.to raise_error(/Unknown variable/) end end it "a lex error should be raised for '$foo::::bar'" do expect { parser.evaluate_string(scope, "$foo::::bar") }.to raise_error(Puppet::LexError, /Illegal fully qualified name at line 1:7/) end { '$a = $0' => nil, '$a = $1' => nil, }.each do |source, value| it "it is ok to reference numeric unassigned variables '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end { '$00 = 0' => /must be a decimal value/, '$0xf = 0' => /must be a decimal value/, '$0777 = 0' => /must be a decimal value/, '$123a = 0' => /must be a decimal value/, }.each do |source, error_pattern| it "should raise an error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(error_pattern) end end context "an initial underscore in the last segment of a var name is allowed" do { '$_a = 1' => 1, '$__a = 1' => 1, }.each do |source, value| it "as in this example '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end end end context "When evaluating relationships" do it 'should form a relation with File[a] -> File[b]' do source = "File[a] -> File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'b']) end it 'should form a relation with resource -> resource' do source = "notify{a:} -> notify{b:}" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['Notify', 'a', '->', 'Notify', 'b']) end it 'should form a relation with [File[a], File[b]] -> [File[x], File[y]]' do source = "[File[a], File[b]] -> [File[x], File[y]]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'y']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'y']) end it 'should tolerate (eliminate) duplicates in operands' do source = "[File[a], File[a]] -> File[x]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.relationships.size.should == 1 end it 'should form a relation with <-' do source = "File[a] <- File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'a']) end it 'should form a relation with <-' do source = "File[a] <~ File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '~>', 'File', 'a']) end end context "When evaluating heredoc" do it "evaluates plain heredoc" do src = "@(END)\nThis is\nheredoc text\nEND\n" parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" end it "parses heredoc with margin" do src = [ "@(END)", " This is", " heredoc text", " | END", "" ].join("\n") parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" end it "parses heredoc with margin and right newline trim" do src = [ "@(END)", " This is", " heredoc text", " |- END", "" ].join("\n") parser.evaluate_string(scope, src).should == "This is\nheredoc text" end it "parses escape specification" do src = <<-CODE @(END/t) Tex\\tt\\n |- END CODE parser.evaluate_string(scope, src).should == "Tex\tt\\n" end it "parses syntax checked specification" do src = <<-CODE @(END:json) ["foo", "bar"] |- END CODE parser.evaluate_string(scope, src).should == '["foo", "bar"]' end it "parses syntax checked specification with error and reports it" do src = <<-CODE @(END:json) ['foo', "bar"] |- END CODE expect { parser.evaluate_string(scope, src)}.to raise_error(/Cannot parse invalid JSON string/) end it "parses interpolated heredoc expression" do src = <<-CODE $name = 'Fjodor' @("END") Hello $name |- END CODE parser.evaluate_string(scope, src).should == "Hello Fjodor" end end context "Handles Deprecations and Discontinuations" do it 'of import statements' do source = "\nimport foo" # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.evaluate_string(scope, source) }.to raise_error(/'import' has been discontinued.*line 2:1/) end end context "Detailed Error messages are reported" do it 'for illegal type references' do source = '1+1 { "title": }' # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.evaluate_string(scope, source) }.to raise_error( /Illegal Resource Type expression, expected result to be a type name, or untitled Resource.*line 1:2/) end it 'for non r-value producing <| |>' do expect { parser.parse_string("$a = File <| |>", nil) }.to raise_error(/A Virtual Query does not produce a value at line 1:6/) end it 'for non r-value producing <<| |>>' do expect { parser.parse_string("$a = File <<| |>>", nil) }.to raise_error(/An Exported Query does not produce a value at line 1:6/) end it 'for non r-value producing define' do Puppet.expects(:err).with("Invalid use of expression. A 'define' expression does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = define foo { }", nil) }.to raise_error(/2 errors/) end it 'for non r-value producing class' do Puppet.expects(:err).with("Invalid use of expression. A Host Class Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = class foo { }", nil) }.to raise_error(/2 errors/) end it 'for unclosed quote with indication of start position of string' do source = <<-SOURCE.gsub(/^ {6}/,'') $a = "xx yyy SOURCE # first char after opening " reported as being in error. expect { parser.parse_string(source) }.to raise_error(/Unclosed quote after '"' followed by 'xx\\nyy\.\.\.' at line 1:7/) end it 'for multiple errors with a summary exception' do Puppet.expects(:err).with("Invalid use of expression. A Node Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = node x { }",nil) }.to raise_error(/2 errors/) end it 'for a bad hostname' do expect { parser.parse_string("node 'macbook+owned+by+name' { }", nil) }.to raise_error(/The hostname 'macbook\+owned\+by\+name' contains illegal characters.*at line 1:6/) end it 'for a hostname with interpolation' do source = <<-SOURCE.gsub(/^ {6}/,'') $name = 'fred' node "macbook-owned-by$name" { } SOURCE expect { parser.parse_string(source, nil) }.to raise_error(/An interpolated expression is not allowed in a hostname of a node at line 2:23/) end end context 'does not leak variables' do it 'local variables are gone when lambda ends' do source = <<-SOURCE [1,2,3].each |$x| { $y = $x} $a = $y SOURCE expect do parser.evaluate_string(scope, source) end.to raise_error(/Unknown variable: 'y'/) end it 'lambda parameters are gone when lambda ends' do source = <<-SOURCE [1,2,3].each |$x| { $y = $x} $a = $x SOURCE expect do parser.evaluate_string(scope, source) end.to raise_error(/Unknown variable: 'x'/) end it 'does not leak match variables' do source = <<-SOURCE if 'xyz' =~ /(x)(y)(z)/ { notice $2 } case 'abc' { /(a)(b)(c)/ : { $x = $2 } } "-$x-$2-" SOURCE expect(parser.evaluate_string(scope, source)).to eq('-b--') end end matcher :have_relationship do |expected| calc = Puppet::Pops::Types::TypeCalculator.new match do |compiler| op_name = {'->' => :relationship, '~>' => :subscription} compiler.relationships.any? do | relation | relation.source.type == expected[0] && relation.source.title == expected[1] && relation.type == op_name[expected[2]] && relation.target.type == expected[3] && relation.target.title == expected[4] end end failure_message_for_should do |actual| "Relationship #{expected[0]}[#{expected[1]}] #{expected[2]} #{expected[3]}[#{expected[4]}] but was unknown to compiler" end end end diff --git a/spec/unit/resource/type_collection_spec.rb b/spec/unit/resource/type_collection_spec.rb index 58f7a5cd4..1d76cbfba 100755 --- a/spec/unit/resource/type_collection_spec.rb +++ b/spec/unit/resource/type_collection_spec.rb @@ -1,418 +1,374 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/resource/type_collection' require 'puppet/resource/type' describe Puppet::Resource::TypeCollection do include PuppetSpec::Files let(:environment) { Puppet::Node::Environment.create(:testing, []) } before do @instance = Puppet::Resource::Type.new(:hostclass, "foo") @code = Puppet::Resource::TypeCollection.new(environment) end it "should consider '<<' to be an alias to 'add' but should return self" do @code.expects(:add).with "foo" @code.expects(:add).with "bar" @code << "foo" << "bar" end it "should set itself as the code collection for added resource types" do node = Puppet::Resource::Type.new(:node, "foo") @code.add(node) @code.node("foo").should equal(node) node.resource_type_collection.should equal(@code) end it "should store node resource types as nodes" do node = Puppet::Resource::Type.new(:node, "foo") @code.add(node) @code.node("foo").should equal(node) end it "should fail if a duplicate node is added" do @code.add(Puppet::Resource::Type.new(:node, "foo")) expect do @code.add(Puppet::Resource::Type.new(:node, "foo")) end.to raise_error(Puppet::ParseError, /cannot redefine/) end it "should store hostclasses as hostclasses" do klass = Puppet::Resource::Type.new(:hostclass, "foo") @code.add(klass) @code.hostclass("foo").should equal(klass) end it "merge together hostclasses of the same name" do klass1 = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "first") klass2 = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "second") @code.add(klass1) @code.add(klass2) @code.hostclass("foo").doc.should == "firstsecond" end it "should store definitions as definitions" do define = Puppet::Resource::Type.new(:definition, "foo") @code.add(define) @code.definition("foo").should equal(define) end it "should fail if a duplicate definition is added" do @code.add(Puppet::Resource::Type.new(:definition, "foo")) expect do @code.add(Puppet::Resource::Type.new(:definition, "foo")) end.to raise_error(Puppet::ParseError, /cannot be redefined/) end it "should remove all nodes, classes, and definitions when cleared" do loader = Puppet::Resource::TypeCollection.new(environment) loader.add Puppet::Resource::Type.new(:hostclass, "class") loader.add Puppet::Resource::Type.new(:definition, "define") loader.add Puppet::Resource::Type.new(:node, "node") - watched_file = tmpfile('watched_file') - loader.watch_file(watched_file) loader.clear loader.hostclass("class").should be_nil loader.definition("define").should be_nil loader.node("node").should be_nil - loader.should_not be_watching_file(watched_file) end describe "when resolving namespaces" do [ ['', '::foo', ['foo']], ['a', '::foo', ['foo']], ['a::b', '::foo', ['foo']], [['a::b'], '::foo', ['foo']], [['a::b', 'c'], '::foo', ['foo']], [['A::B', 'C'], '::Foo', ['foo']], ['', '', ['']], ['a', '', ['']], ['a::b', '', ['']], [['a::b'], '', ['']], [['a::b', 'c'], '', ['']], [['A::B', 'C'], '', ['']], ['', 'foo', ['foo']], ['a', 'foo', ['a::foo', 'foo']], ['a::b', 'foo', ['a::b::foo', 'a::foo', 'foo']], ['A::B', 'Foo', ['a::b::foo', 'a::foo', 'foo']], [['a::b'], 'foo', ['a::b::foo', 'a::foo', 'foo']], [['a', 'b'], 'foo', ['a::foo', 'foo', 'b::foo']], [['a::b', 'c::d'], 'foo', ['a::b::foo', 'a::foo', 'foo', 'c::d::foo', 'c::foo']], [['a::b', 'a::c'], 'foo', ['a::b::foo', 'a::foo', 'foo', 'a::c::foo']], ].each do |namespaces, name, expected_result| it "should resolve #{name.inspect} in namespaces #{namespaces.inspect} correctly" do @code.instance_eval { resolve_namespaces(namespaces, name) }.should == expected_result end end end describe "when looking up names" do before do @type = Puppet::Resource::Type.new(:hostclass, "ns::klass") end it "should support looking up with multiple namespaces" do @code.add @type @code.find_hostclass(%w{boo baz ns}, "klass").should equal(@type) end it "should not attempt to import anything when the type is already defined" do @code.add @type @code.loader.expects(:import).never @code.find_hostclass(%w{ns}, "klass").should equal(@type) end describe "that need to be loaded" do it "should use the loader to load the files" do @code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass") @code.loader.expects(:try_load_fqname).with(:hostclass, "klass") @code.find_hostclass(["ns"], "klass") end it "should downcase the name and downcase and array-fy the namespaces before passing to the loader" do @code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass") @code.loader.expects(:try_load_fqname).with(:hostclass, "klass") @code.find_hostclass("Ns", "Klass") end it "should use the class returned by the loader" do @code.loader.expects(:try_load_fqname).returns(:klass) @code.expects(:hostclass).with("ns::klass").returns(false) @code.find_hostclass("ns", "klass").should == :klass end it "should return nil if the name isn't found" do @code.loader.stubs(:try_load_fqname).returns(nil) @code.find_hostclass("Ns", "Klass").should be_nil end it "already-loaded names at broader scopes should not shadow autoloaded names" do @code.add Puppet::Resource::Type.new(:hostclass, "bar") @code.loader.expects(:try_load_fqname).with(:hostclass, "foo::bar").returns(:foobar) @code.find_hostclass("foo", "bar").should == :foobar end it "should not try to autoload names that we couldn't autoload in a previous step if ignoremissingtypes is enabled" do Puppet[:ignoremissingtypes] = true @code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass").returns(nil) @code.loader.expects(:try_load_fqname).with(:hostclass, "klass").returns(nil) @code.find_hostclass("Ns", "Klass").should be_nil Puppet.expects(:debug).at_least_once.with {|msg| msg =~ /Not attempting to load hostclass/} @code.find_hostclass("Ns", "Klass").should be_nil end end end %w{hostclass node definition}.each do |data| describe "behavior of add for #{data}" do it "should return the added #{data}" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(data, "foo") loader.add(instance).should equal(instance) end it "should retrieve #{data} insensitive to case" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(data, "Bar") loader.add instance loader.send(data, "bAr").should equal(instance) end it "should return nil when asked for a #{data} that has not been added" do Puppet::Resource::TypeCollection.new(environment).send(data, "foo").should be_nil end end end describe "when finding a qualified instance" do it "should return any found instance if the instance name is fully qualified" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find_hostclass("namespace", "::foo::bar").should equal(instance) end it "should return nil if the instance name is fully qualified and no such instance exists" do loader = Puppet::Resource::TypeCollection.new(environment) loader.find_hostclass("namespace", "::foo::bar").should be_nil end it "should be able to find classes in the base namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo") loader.add instance loader.find_hostclass("", "foo").should equal(instance) end it "should return the partially qualified object if it exists in a provided namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass("foo", "bar::baz").should equal(instance) end it "should be able to find partially qualified objects in any of the provided namespaces" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass(["nons", "foo", "otherns"], "bar::baz").should equal(instance) end it "should return the unqualified object if it exists in a provided namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find_hostclass("foo", "bar").should equal(instance) end it "should return the unqualified object if it exists in the parent namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find_hostclass("foo::bar::baz", "bar").should equal(instance) end it "should should return the partially qualified object if it exists in the parent namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass("foo::bar", "bar::baz").should equal(instance) end it "should return the qualified object if it exists in the root namespace" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass("foo::bar", "foo::bar::baz").should equal(instance) end it "should return nil if the object cannot be found" do loader = Puppet::Resource::TypeCollection.new(environment) instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find_hostclass("foo::bar", "eh").should be_nil end describe "when topscope has a class that has the same name as a local class" do before do @loader = Puppet::Resource::TypeCollection.new(environment) [ "foo::bar", "bar" ].each do |name| @loader.add Puppet::Resource::Type.new(:hostclass, name) end end it "should favor the local class, if the name is unqualified" do @loader.find_hostclass("foo", "bar").name.should == 'foo::bar' end it "should only look in the topclass, if the name is qualified" do @loader.find_hostclass("foo", "::bar").name.should == 'bar' end it "should only look in the topclass, if we assume the name is fully qualified" do @loader.find_hostclass("foo", "bar", :assume_fqname => true).name.should == 'bar' end end it "should not look in the local scope for classes when the name is qualified" do @loader = Puppet::Resource::TypeCollection.new(environment) @loader.add Puppet::Resource::Type.new(:hostclass, "foo::bar") @loader.find_hostclass("foo", "::bar").should == nil end end it "should be able to find nodes" do node = Puppet::Resource::Type.new(:node, "bar") loader = Puppet::Resource::TypeCollection.new(environment) loader.add(node) loader.find_node(stub("ignored"), "bar").should == node end it "should indicate whether any nodes are defined" do loader = Puppet::Resource::TypeCollection.new(environment) loader.add_node(Puppet::Resource::Type.new(:node, "foo")) loader.should be_nodes end it "should indicate whether no nodes are defined" do Puppet::Resource::TypeCollection.new(environment).should_not be_nodes end describe "when finding nodes" do before :each do @loader = Puppet::Resource::TypeCollection.new(environment) end it "should return any node whose name exactly matches the provided node name" do node = Puppet::Resource::Type.new(:node, "foo") @loader << node @loader.node("foo").should equal(node) end it "should return the first regex node whose regex matches the provided node name" do node1 = Puppet::Resource::Type.new(:node, /\w/) node2 = Puppet::Resource::Type.new(:node, /\d/) @loader << node1 << node2 @loader.node("foo10").should equal(node1) end it "should preferentially return a node whose name is string-equal over returning a node whose regex matches a provided name" do node1 = Puppet::Resource::Type.new(:node, /\w/) node2 = Puppet::Resource::Type.new(:node, "foo") @loader << node1 << node2 @loader.node("foo").should equal(node2) end end - describe "when managing files" do - before do - @loader = Puppet::Resource::TypeCollection.new(environment) - Puppet::Util::WatchedFile.stubs(:new).returns stub("watched_file") - end - - it "should have a method for specifying a file should be watched" do - @loader.should respond_to(:watch_file) - end - - it "should have a method for determining if a file is being watched" do - @loader.watch_file("/foo/bar") - @loader.should be_watching_file("/foo/bar") - end - - it "should use WatchedFile to watch files" do - Puppet::Util::WatchedFile.expects(:new).with("/foo/bar").returns stub("watched_file") - @loader.watch_file("/foo/bar") - end - - it "should be considered stale if any files have changed" do - file1 = stub 'file1', :changed? => false - file2 = stub 'file2', :changed? => true - Puppet::Util::WatchedFile.expects(:new).times(2).returns(file1).then.returns(file2) - @loader.watch_file("/foo/bar") - @loader.watch_file("/other/bar") - - @loader.should be_stale - end - - it "should not be considered stable if no files have changed" do - file1 = stub 'file1', :changed? => false - file2 = stub 'file2', :changed? => false - Puppet::Util::WatchedFile.expects(:new).times(2).returns(file1).then.returns(file2) - @loader.watch_file("/foo/bar") - @loader.watch_file("/other/bar") - - @loader.should_not be_stale - end - end - describe "when determining the configuration version" do before do @code = Puppet::Resource::TypeCollection.new(environment) end it "should default to the current time" do time = Time.now Time.stubs(:now).returns time @code.version.should == time.to_i end context "when config_version script is specified" do let(:environment) { Puppet::Node::Environment.create(:testing, [], '', '/my/foo') } it "should use the output of the environment's config_version setting if one is provided" do Puppet::Util::Execution.expects(:execute).with(["/my/foo"]).returns "output\n" @code.version.should == "output" end it "should raise a puppet parser error if executing config_version fails" do Puppet::Util::Execution.expects(:execute).raises(Puppet::ExecutionFailure.new("msg")) lambda { @code.version }.should raise_error(Puppet::ParseError) end end end end diff --git a/spec/unit/resource/type_spec.rb b/spec/unit/resource/type_spec.rb index fbe96da6d..b1ddf847b 100755 --- a/spec/unit/resource/type_spec.rb +++ b/spec/unit/resource/type_spec.rb @@ -1,773 +1,772 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/resource/type' require 'puppet/pops' require 'matchers/json' describe Puppet::Resource::Type do include JSONMatchers it "should have a 'name' attribute" do Puppet::Resource::Type.new(:hostclass, "foo").name.should == "foo" end [:code, :doc, :line, :file, :resource_type_collection].each do |attr| it "should have a '#{attr}' attribute" do type = Puppet::Resource::Type.new(:hostclass, "foo") type.send(attr.to_s + "=", "yay") type.send(attr).should == "yay" end end [:hostclass, :node, :definition].each do |type| it "should know when it is a #{type}" do Puppet::Resource::Type.new(type, "foo").send("#{type}?").should be_true end end it "should indirect 'resource_type'" do Puppet::Resource::Type.indirection.name.should == :resource_type end it "should default to 'parser' for its terminus class" do Puppet::Resource::Type.indirection.terminus_class.should == :parser end describe "when converting to json" do before do @type = Puppet::Resource::Type.new(:hostclass, "foo") end def from_json(json) Puppet::Resource::Type.from_data_hash(json) end def double_convert Puppet::Resource::Type.from_data_hash(PSON.parse(@type.to_pson)) end it "should include the name and type" do double_convert.name.should == @type.name double_convert.type.should == @type.type end it "should validate with only name and kind" do expect(@type.to_pson).to validate_against('api/schemas/resource_type.json') end it "should validate with all fields set" do @type.set_arguments("one" => nil, "two" => "foo") @type.line = 100 @type.doc = "A weird type" @type.file = "/etc/manifests/thing.pp" @type.parent = "one::two" expect(@type.to_pson).to validate_against('api/schemas/resource_type.json') end it "should include any arguments" do @type.set_arguments("one" => nil, "two" => "foo") double_convert.arguments.should == {"one" => nil, "two" => "foo"} end it "should not include arguments if none are present" do @type.to_pson["arguments"].should be_nil end [:line, :doc, :file, :parent].each do |attr| it "should include #{attr} when set" do @type.send(attr.to_s + "=", "value") double_convert.send(attr).should == "value" end it "should not include #{attr} when not set" do @type.to_pson[attr.to_s].should be_nil end end it "should not include docs if they are empty" do @type.doc = "" @type.to_pson["doc"].should be_nil end end describe "when a node" do it "should allow a regex as its name" do lambda { Puppet::Resource::Type.new(:node, /foo/) }.should_not raise_error end it "should allow an AST::HostName instance as its name" do regex = Puppet::Parser::AST::Regex.new(:value => /foo/) name = Puppet::Parser::AST::HostName.new(:value => regex) lambda { Puppet::Resource::Type.new(:node, name) }.should_not raise_error end it "should match against the regexp in the AST::HostName when a HostName instance is provided" do regex = Puppet::Parser::AST::Regex.new(:value => /\w/) name = Puppet::Parser::AST::HostName.new(:value => regex) node = Puppet::Resource::Type.new(:node, name) node.match("foo").should be_true end it "should return the value of the hostname if provided a string-form AST::HostName instance as the name" do name = Puppet::Parser::AST::HostName.new(:value => "foo") node = Puppet::Resource::Type.new(:node, name) node.name.should == "foo" end describe "and the name is a regex" do it "should have a method that indicates that this is the case" do Puppet::Resource::Type.new(:node, /w/).should be_name_is_regex end it "should set its namespace to ''" do Puppet::Resource::Type.new(:node, /w/).namespace.should == "" end it "should return the regex converted to a string when asked for its name" do Puppet::Resource::Type.new(:node, /ww/).name.should == "ww" end it "should downcase the regex when returning the name as a string" do Puppet::Resource::Type.new(:node, /W/).name.should == "w" end it "should remove non-alpha characters when returning the name as a string" do Puppet::Resource::Type.new(:node, /w*w/).name.should_not include("*") end it "should remove leading dots when returning the name as a string" do Puppet::Resource::Type.new(:node, /.ww/).name.should_not =~ /^\./ end it "should have a method for matching its regex name against a provided name" do Puppet::Resource::Type.new(:node, /.ww/).should respond_to(:match) end it "should return true when its regex matches the provided name" do Puppet::Resource::Type.new(:node, /\w/).match("foo").should be_true end it "should return true when its regex matches the provided name" do Puppet::Resource::Type.new(:node, /\w/).match("foo").should be_true end it "should return false when its regex does not match the provided name" do (!!Puppet::Resource::Type.new(:node, /\d/).match("foo")).should be_false end it "should return true when its name, as a string, is matched against an equal string" do Puppet::Resource::Type.new(:node, "foo").match("foo").should be_true end it "should return false when its name is matched against an unequal string" do Puppet::Resource::Type.new(:node, "foo").match("bar").should be_false end it "should match names insensitive to case" do Puppet::Resource::Type.new(:node, "fOo").match("foO").should be_true end end end describe "when initializing" do it "should require a resource super type" do Puppet::Resource::Type.new(:hostclass, "foo").type.should == :hostclass end it "should fail if provided an invalid resource super type" do lambda { Puppet::Resource::Type.new(:nope, "foo") }.should raise_error(ArgumentError) end it "should set its name to the downcased, stringified provided name" do Puppet::Resource::Type.new(:hostclass, "Foo::Bar".intern).name.should == "foo::bar" end it "should set its namespace to the downcased, stringified qualified name for classes" do Puppet::Resource::Type.new(:hostclass, "Foo::Bar::Baz".intern).namespace.should == "foo::bar::baz" end [:definition, :node].each do |type| it "should set its namespace to the downcased, stringified qualified portion of the name for #{type}s" do Puppet::Resource::Type.new(type, "Foo::Bar::Baz".intern).namespace.should == "foo::bar" end end %w{code line file doc}.each do |arg| it "should set #{arg} if provided" do type = Puppet::Resource::Type.new(:hostclass, "foo", arg.to_sym => "something") type.send(arg).should == "something" end end it "should set any provided arguments with the keys as symbols" do type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {:foo => "bar", :baz => "biz"}) type.should be_valid_parameter("foo") type.should be_valid_parameter("baz") end it "should set any provided arguments with they keys as strings" do type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar", "baz" => "biz"}) type.should be_valid_parameter(:foo) type.should be_valid_parameter(:baz) end it "should function if provided no arguments" do type = Puppet::Resource::Type.new(:hostclass, "foo") type.should_not be_valid_parameter(:foo) end end describe "when testing the validity of an attribute" do it "should return true if the parameter was typed at initialization" do Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar"}).should be_valid_parameter("foo") end it "should return true if it is a metaparam" do Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("require") end it "should return true if the parameter is named 'name'" do Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("name") end it "should return false if it is not a metaparam and was not provided at initialization" do Puppet::Resource::Type.new(:hostclass, "foo").should_not be_valid_parameter("yayness") end end describe "when setting its parameters in the scope" do def variable_expression(name) varexpr = Puppet::Pops::Model::Factory.QNAME(name).var().current Puppet::Parser::AST::PopsBridge::Expression.new(:value => varexpr) end before do @scope = Puppet::Parser::Scope.new(Puppet::Parser::Compiler.new(Puppet::Node.new("foo")), :source => stub("source")) @resource = Puppet::Parser::Resource.new(:foo, "bar", :scope => @scope) @type = Puppet::Resource::Type.new(:definition, "foo") @resource.environment.known_resource_types.add @type end ['module_name', 'name', 'title'].each do |variable| it "should allow #{variable} to be evaluated as param default" do @type.instance_eval { @module_name = "bar" } @type.set_arguments :foo => variable_expression(variable) @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == 'bar' end end # this test is to clarify a crazy edge case # if you specify these special names as params, the resource # will override the special variables it "should allow the resource to override defaults" do @type.set_arguments :name => nil @resource[:name] = 'foobar' @type.set_arguments :foo => variable_expression('name') @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == 'foobar' end it "should set each of the resource's parameters as variables in the scope" do @type.set_arguments :foo => nil, :boo => nil @resource[:foo] = "bar" @resource[:boo] = "baz" @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == "bar" @scope['boo'].should == "baz" end it "should set the variables as strings" do @type.set_arguments :foo => nil @resource[:foo] = "bar" @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == "bar" end it "should fail if any of the resource's parameters are not valid attributes" do @type.set_arguments :foo => nil @resource[:boo] = "baz" lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError) end it "should evaluate and set its default values as variables for parameters not provided by the resource" do @type.set_arguments :foo => Puppet::Parser::AST::Leaf.new(:value => "something") @type.set_resource_parameters(@resource, @scope) @scope['foo'].should == "something" end it "should set all default values as parameters in the resource" do @type.set_arguments :foo => Puppet::Parser::AST::Leaf.new(:value => "something") @type.set_resource_parameters(@resource, @scope) @resource[:foo].should == "something" end it "should fail if the resource does not provide a value for a required argument" do @type.set_arguments :foo => nil lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError) end it "should set the resource's title as a variable if not otherwise provided" do @type.set_resource_parameters(@resource, @scope) @scope['title'].should == "bar" end it "should set the resource's name as a variable if not otherwise provided" do @type.set_resource_parameters(@resource, @scope) @scope['name'].should == "bar" end it "should set its module name in the scope if available" do @type.instance_eval { @module_name = "mymod" } @type.set_resource_parameters(@resource, @scope) @scope["module_name"].should == "mymod" end it "should set its caller module name in the scope if available" do @scope.expects(:parent_module_name).returns "mycaller" @type.set_resource_parameters(@resource, @scope) @scope["caller_module_name"].should == "mycaller" end end describe "when describing and managing parent classes" do before do environment = Puppet::Node::Environment.create(:testing, []) @krt = environment.known_resource_types @parent = Puppet::Resource::Type.new(:hostclass, "bar") @krt.add @parent @child = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar") @krt.add @child @scope = Puppet::Parser::Scope.new(Puppet::Parser::Compiler.new(Puppet::Node.new("foo", :environment => environment))) end it "should be able to define a parent" do Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar") end it "should use the code collection to find the parent resource type" do @child.parent_type(@scope).should equal(@parent) end it "should be able to find parent nodes" do parent = Puppet::Resource::Type.new(:node, "bar") @krt.add parent child = Puppet::Resource::Type.new(:node, "foo", :parent => "bar") @krt.add child child.parent_type(@scope).should equal(parent) end it "should cache a reference to the parent type" do @krt.stubs(:hostclass).with("foo::bar").returns nil @krt.expects(:hostclass).with("bar").once.returns @parent @child.parent_type(@scope) @child.parent_type end it "should correctly state when it is another type's child" do @child.parent_type(@scope) @child.should be_child_of(@parent) end it "should be considered the child of a parent's parent" do @grandchild = Puppet::Resource::Type.new(:hostclass, "baz", :parent => "foo") @krt.add @grandchild @child.parent_type(@scope) @grandchild.parent_type(@scope) @grandchild.should be_child_of(@parent) end it "should correctly state when it is not another type's child" do @notchild = Puppet::Resource::Type.new(:hostclass, "baz") @krt.add @notchild @notchild.should_not be_child_of(@parent) end end describe "when evaluating its code" do before do @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @scope = Puppet::Parser::Scope.new @compiler @resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope) # This is so the internal resource lookup works, yo. @compiler.catalog.add_resource @resource @type = Puppet::Resource::Type.new(:hostclass, "foo") @resource.environment.known_resource_types.add @type end it "should add node regex captures to its scope" do @type = Puppet::Resource::Type.new(:node, /f(\w)o(.*)$/) match = @type.match('foo') code = stub 'code' @type.stubs(:code).returns code subscope = stub 'subscope', :compiler => @compiler @scope.expects(:newscope).with(:source => @type, :namespace => '', :resource => @resource).returns subscope elevel = 876 subscope.expects(:ephemeral_level).returns elevel subscope.expects(:ephemeral_from).with(match, nil, nil).returns subscope code.expects(:safeevaluate).with(subscope) subscope.expects(:unset_ephemeral_var).with(elevel) # Just to keep the stub quiet about intermediate calls @type.expects(:set_resource_parameters).with(@resource, subscope) @type.evaluate_code(@resource) end it "should add hostclass names to the classes list" do @type.evaluate_code(@resource) @compiler.catalog.classes.should be_include("foo") end it "should not add defined resource names to the classes list" do @type = Puppet::Resource::Type.new(:definition, "foo") @type.evaluate_code(@resource) @compiler.catalog.classes.should_not be_include("foo") end it "should set all of its parameters in a subscope" do subscope = stub 'subscope', :compiler => @compiler @scope.expects(:newscope).with(:source => @type, :namespace => 'foo', :resource => @resource).returns subscope @type.expects(:set_resource_parameters).with(@resource, subscope) @type.evaluate_code(@resource) end it "should not create a subscope for the :main class" do @resource.stubs(:title).returns(:main) @type.expects(:subscope).never @type.expects(:set_resource_parameters).with(@resource, @scope) @type.evaluate_code(@resource) end it "should store the class scope" do @type.evaluate_code(@resource) @scope.class_scope(@type).should be_instance_of(@scope.class) end it "should still create a scope but not store it if the type is a definition" do @type = Puppet::Resource::Type.new(:definition, "foo") @type.evaluate_code(@resource) @scope.class_scope(@type).should be_nil end it "should evaluate the AST code if any is provided" do code = stub 'code' @type.stubs(:code).returns code subscope = stub_everything("subscope", :compiler => @compiler) @scope.stubs(:newscope).returns subscope code.expects(:safeevaluate).with subscope @type.evaluate_code(@resource) end it "should noop if there is no code" do @type.expects(:code).returns nil @type.evaluate_code(@resource) end describe "and it has a parent class" do before do @parent_type = Puppet::Resource::Type.new(:hostclass, "parent") @type.parent = "parent" @parent_resource = Puppet::Parser::Resource.new(:class, "parent", :scope => @scope) @compiler.add_resource @scope, @parent_resource @type.resource_type_collection = @scope.known_resource_types @type.resource_type_collection.add @parent_type end it "should evaluate the parent's resource" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@parent_type).should_not be_nil end it "should not evaluate the parent's resource if it has already been evaluated" do @parent_resource.evaluate @type.parent_type(@scope) @parent_resource.expects(:evaluate).never @type.evaluate_code(@resource) end it "should use the parent's scope as its base scope" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id end end describe "and it has a parent node" do before do @type = Puppet::Resource::Type.new(:node, "foo") @parent_type = Puppet::Resource::Type.new(:node, "parent") @type.parent = "parent" @parent_resource = Puppet::Parser::Resource.new(:node, "parent", :scope => @scope) @compiler.add_resource @scope, @parent_resource @type.resource_type_collection = @scope.known_resource_types @type.resource_type_collection.add(@parent_type) end it "should evaluate the parent's resource" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@parent_type).should_not be_nil end it "should not evaluate the parent's resource if it has already been evaluated" do @parent_resource.evaluate @type.parent_type(@scope) @parent_resource.expects(:evaluate).never @type.evaluate_code(@resource) end it "should use the parent's scope as its base scope" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id end end end describe "when creating a resource" do before do - @node = Puppet::Node.new("foo", :environment => 'env') + env = Puppet::Node::Environment.create('env', []) + @node = Puppet::Node.new("foo", :environment => env) @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(@compiler) @top = Puppet::Resource::Type.new :hostclass, "top" @middle = Puppet::Resource::Type.new :hostclass, "middle", :parent => "top" - @code = Puppet::Resource::TypeCollection.new("env") + @code = env.known_resource_types @code.add @top @code.add @middle - - @node.environment.stubs(:known_resource_types).returns(@code) end it "should create a resource instance" do @top.ensure_in_catalog(@scope).should be_instance_of(Puppet::Parser::Resource) end it "should set its resource type to 'class' when it is a hostclass" do Puppet::Resource::Type.new(:hostclass, "top").ensure_in_catalog(@scope).type.should == "Class" end it "should set its resource type to 'node' when it is a node" do Puppet::Resource::Type.new(:node, "top").ensure_in_catalog(@scope).type.should == "Node" end it "should fail when it is a definition" do lambda { Puppet::Resource::Type.new(:definition, "top").ensure_in_catalog(@scope) }.should raise_error(ArgumentError) end it "should add the created resource to the scope's catalog" do @top.ensure_in_catalog(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should add specified parameters to the resource" do @top.ensure_in_catalog(@scope, {'one'=>'1', 'two'=>'2'}) @compiler.catalog.resource(:class, "top")['one'].should == '1' @compiler.catalog.resource(:class, "top")['two'].should == '2' end it "should not require params for a param class" do @top.ensure_in_catalog(@scope, {}) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should evaluate the parent class if one exists" do @middle.ensure_in_catalog(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should evaluate the parent class if one exists" do @middle.ensure_in_catalog(@scope, {}) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should fail if you try to create duplicate class resources" do othertop = Puppet::Parser::Resource.new(:class, 'top',:source => @source, :scope => @scope ) # add the same class resource to the catalog @compiler.catalog.add_resource(othertop) lambda { @top.ensure_in_catalog(@scope, {}) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) end it "should fail to evaluate if a parent class is defined but cannot be found" do othertop = Puppet::Resource::Type.new :hostclass, "something", :parent => "yay" @code.add othertop lambda { othertop.ensure_in_catalog(@scope) }.should raise_error(Puppet::ParseError) end it "should not create a new resource if one already exists" do @compiler.catalog.expects(:resource).with(:class, "top").returns("something") @compiler.catalog.expects(:add_resource).never @top.ensure_in_catalog(@scope) end it "should return the existing resource when not creating a new one" do @compiler.catalog.expects(:resource).with(:class, "top").returns("something") @compiler.catalog.expects(:add_resource).never @top.ensure_in_catalog(@scope).should == "something" end it "should not create a new parent resource if one already exists and it has a parent class" do @top.ensure_in_catalog(@scope) top_resource = @compiler.catalog.resource(:class, "top") @middle.ensure_in_catalog(@scope) @compiler.catalog.resource(:class, "top").should equal(top_resource) end # #795 - tag before evaluation. it "should tag the catalog with the resource tags when it is evaluated" do @middle.ensure_in_catalog(@scope) @compiler.catalog.should be_tagged("middle") end it "should tag the catalog with the parent class tags when it is evaluated" do @middle.ensure_in_catalog(@scope) @compiler.catalog.should be_tagged("top") end end describe "when merging code from another instance" do def code(str) factory = Puppet::Pops::Model::Factory.literal(str) end it "should fail unless it is a class" do lambda { Puppet::Resource::Type.new(:node, "bar").merge("foo") }.should raise_error(Puppet::Error) end it "should fail unless the source instance is a class" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:node, "foo") lambda { dest.merge(source) }.should raise_error(Puppet::Error) end it "should fail if both classes have different parent classes" do code = Puppet::Resource::TypeCollection.new("env") {"a" => "b", "c" => "d"}.each do |parent, child| code.add Puppet::Resource::Type.new(:hostclass, parent) code.add Puppet::Resource::Type.new(:hostclass, child, :parent => parent) end lambda { code.hostclass("b").merge(code.hostclass("d")) }.should raise_error(Puppet::Error) end it "should fail if it's named 'main' and 'freeze_main' is enabled" do Puppet.settings[:freeze_main] = true code = Puppet::Resource::TypeCollection.new("env") code.add Puppet::Resource::Type.new(:hostclass, "") other = Puppet::Resource::Type.new(:hostclass, "") lambda { code.hostclass("").merge(other) }.should raise_error(Puppet::Error) end it "should copy the other class's parent if it has not parent" do dest = Puppet::Resource::Type.new(:hostclass, "bar") parent = Puppet::Resource::Type.new(:hostclass, "parent") source = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "parent") dest.merge(source) dest.parent.should == "parent" end it "should copy the other class's documentation as its docs if it has no docs" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness") dest.merge(source) dest.doc.should == "yayness" end it "should append the other class's docs to its docs if it has any" do dest = Puppet::Resource::Type.new(:hostclass, "bar", :doc => "fooness") source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness") dest.merge(source) dest.doc.should == "foonessyayness" end it "should set the other class's code as its code if it has none" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:hostclass, "foo", :code => code("bar")) dest.merge(source) dest.code.value.should == "bar" end it "should append the other class's code to its code if it has any" do # PUP-3274, the code merging at the top still uses AST::BlockExpression # But does not do mutating changes to code blocks, instead a new block is created # with references to the two original blocks. # TODO: fix this when the code merging is changed at the very top in 4x. # dcode = Puppet::Parser::AST::BlockExpression.new(:children => [code("dest")]) dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => dcode) scode = Puppet::Parser::AST::BlockExpression.new(:children => [code("source")]) source = Puppet::Resource::Type.new(:hostclass, "foo", :code => scode) dest.merge(source) dest.code.children.collect { |l| l.children[0].value }.should == %w{dest source} end end end diff --git a/spec/unit/settings/config_file_spec.rb b/spec/unit/settings/config_file_spec.rb index b071d32e2..9353eea3f 100644 --- a/spec/unit/settings/config_file_spec.rb +++ b/spec/unit/settings/config_file_spec.rb @@ -1,176 +1,149 @@ #! /usr/bin/env ruby -S rspec require 'spec_helper' require 'puppet/settings/config_file' describe Puppet::Settings::ConfigFile do NOTHING = {} def the_parse_of(*lines) config.parse_file(filename, lines.join("\n")) end let(:identity_transformer) { Proc.new { |value| value } } let(:config) { Puppet::Settings::ConfigFile.new(identity_transformer) } let(:filename) { "a/fake/filename.conf" } Conf = Puppet::Settings::ConfigFile::Conf Section = Puppet::Settings::ConfigFile::Section Meta = Puppet::Settings::ConfigFile::Meta NO_META = Puppet::Settings::ConfigFile::NO_META it "interprets an empty file to contain a main section with no entries" do result = the_parse_of("") expect(result).to eq(Conf.new.with_section(Section.new(:main))) end it "interprets an empty main section the same as an empty file" do the_parse_of("").should == config.parse_file(filename, "[main]") end it "places an entry in no section in main" do result = the_parse_of("var = value") expect(result).to eq(Conf.new.with_section(Section.new(:main).with_setting(:var, "value", NO_META))) end it "places an entry after a section header in that section" do - result = the_parse_of("[section]", "var = value") + result = the_parse_of("[agent]", "var = value") expect(result).to eq(Conf.new. with_section(Section.new(:main)). - with_section(Section.new(:section). + with_section(Section.new(:agent). with_setting(:var, "value", NO_META))) end it "does not include trailing whitespace in the value" do result = the_parse_of("var = value\t ") expect(result).to eq(Conf.new. with_section(Section.new(:main). with_setting(:var, "value", NO_META))) end it "does not include leading whitespace in the name" do result = the_parse_of(" \t var=value") expect(result).to eq(Conf.new. with_section(Section.new(:main). with_setting(:var, "value", NO_META))) end it "skips lines that are commented out" do result = the_parse_of("#var = value") expect(result).to eq(Conf.new.with_section(Section.new(:main))) end it "skips lines that are entirely whitespace" do result = the_parse_of(" \t ") expect(result).to eq(Conf.new.with_section(Section.new(:main))) end it "errors when a line is not a known form" do expect { the_parse_of("unknown") }.to raise_error Puppet::Settings::ParseError, /Could not match line/ end it "errors providing correct line number when line is not a known form" do multi_line_config = <<-EOF [main] foo=bar badline EOF expect { the_parse_of(multi_line_config) }.to( raise_error(Puppet::Settings::ParseError, /Could not match line/) do |exception| expect(exception.line).to eq(3) end ) end it "stores file meta information in the _meta section" do result = the_parse_of("var = value { owner = me, group = you, mode = 0666 }") expect(result).to eq(Conf.new.with_section(Section.new(:main). with_setting(:var, "value", Meta.new("me", "you", "0666")))) end it "errors when there is unknown meta information" do expect { the_parse_of("var = value { unknown = no }") }. to raise_error ArgumentError, /Invalid file option 'unknown'/ end it "errors when the mode is not numeric" do expect { the_parse_of("var = value { mode = no }") }. to raise_error ArgumentError, "File modes must be numbers" end it "errors when the options are not key-value pairs" do expect { the_parse_of("var = value { mode }") }. to raise_error ArgumentError, "Could not parse 'value { mode }'" end - it "errors when an application_defaults section is created" do - expect { the_parse_of("[application_defaults]") }. - to raise_error Puppet::Error, - "Illegal section 'application_defaults' in config file #{filename} at line 1" - end + it "may specify legal sections" do + text = <<-EOF + [legal] + a = 'b' + [illegal] + one = 'e' + two = 'f' + EOF - it "errors when a global_defaults section is created" do - expect { the_parse_of("[main]\n[global_defaults]") }. + expect { config.parse_file(filename, text, [:legal]) }. to raise_error Puppet::Error, - "Illegal section 'global_defaults' in config file #{filename} at line 2" - end - - it "issues a single deprecation warning when any section other than main, master, agent or user is parsed" do - text = "[legacy] - one = 'e' - two = 'f' - [also_deprecated] - one = 'g' - " - - config.parse_file(filename, text) - - expect(@logs.map(&:message).grep(/Sections other than/)).to have_exactly(1).item - end - - it "does not issue a deprecation warning for main, master, agent or user sections" do - text = "[main] - one = 'a' - [master] - one = 'b' - [agent] - one = 'c' - [user] - one = 'd' - " - - Puppet.expects(:deprecation_warning).never - - config.parse_file(filename, text) + /Illegal section 'legal' in config file #{filename} at line 1/ end it "transforms values with the given function" do config = Puppet::Settings::ConfigFile.new(Proc.new { |value| value + " changed" }) result = config.parse_file(filename, "var = value") expect(result).to eq(Conf.new. with_section(Section.new(:main). with_setting(:var, "value changed", NO_META))) end it "does not try to transform an entry named 'mode'" do config = Puppet::Settings::ConfigFile.new(Proc.new { raise "Should not transform" }) result = config.parse_file(filename, "mode = value") expect(result).to eq(Conf.new. with_section(Section.new(:main). with_setting(:mode, "value", NO_META))) end end diff --git a/spec/unit/settings_spec.rb b/spec/unit/settings_spec.rb index 0cfc31366..babfdbd07 100755 --- a/spec/unit/settings_spec.rb +++ b/spec/unit/settings_spec.rb @@ -1,1836 +1,1741 @@ #! /usr/bin/env ruby require 'spec_helper' require 'ostruct' require 'puppet/settings/errors' require 'puppet_spec/files' require 'matchers/resource' describe Puppet::Settings do include PuppetSpec::Files include Matchers::Resource let(:main_config_file_default_location) do File.join(Puppet::Util::RunMode[:master].conf_dir, "puppet.conf") end let(:user_config_file_default_location) do File.join(Puppet::Util::RunMode[:user].conf_dir, "puppet.conf") end describe "when specifying defaults" do before do @settings = Puppet::Settings.new end it "should start with no defined parameters" do @settings.params.length.should == 0 end it "should not allow specification of default values associated with a section as an array" do expect { @settings.define_settings(:section, :myvalue => ["defaultval", "my description"]) }.to raise_error end it "should not allow duplicate parameter specifications" do @settings.define_settings(:section, :myvalue => { :default => "a", :desc => "b" }) lambda { @settings.define_settings(:section, :myvalue => { :default => "c", :desc => "d" }) }.should raise_error(ArgumentError) end it "should allow specification of default values associated with a section as a hash" do @settings.define_settings(:section, :myvalue => {:default => "defaultval", :desc => "my description"}) end it "should consider defined parameters to be valid" do @settings.define_settings(:section, :myvalue => { :default => "defaultval", :desc => "my description" }) @settings.valid?(:myvalue).should be_true end it "should require a description when defaults are specified with a hash" do lambda { @settings.define_settings(:section, :myvalue => {:default => "a value"}) }.should raise_error(ArgumentError) end it "should support specifying owner, group, and mode when specifying files" do @settings.define_settings(:section, :myvalue => {:type => :file, :default => "/some/file", :owner => "service", :mode => "boo", :group => "service", :desc => "whatever"}) end it "should support specifying a short name" do @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) end it "should support specifying the setting type" do @settings.define_settings(:section, :myvalue => {:default => "/w", :desc => "b", :type => :string}) @settings.setting(:myvalue).should be_instance_of(Puppet::Settings::StringSetting) end it "should fail if an invalid setting type is specified" do lambda { @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :type => :foo}) }.should raise_error(ArgumentError) end it "should fail when short names conflict" do @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) lambda { @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) }.should raise_error(ArgumentError) end end describe "when initializing application defaults do" do let(:default_values) do values = {} PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key| values[key] = 'default value' end values end before do @settings = Puppet::Settings.new @settings.define_settings(:main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS) end it "should fail if the app defaults hash is missing any required values" do incomplete_default_values = default_values.reject { |key, _| key == :confdir } expect { @settings.initialize_app_defaults(default_values.reject { |key, _| key == :confdir }) }.to raise_error(Puppet::Settings::SettingsError) end # ultimately I'd like to stop treating "run_mode" as a normal setting, because it has so many special # case behaviors / uses. However, until that time... we need to make sure that our private run_mode= # setter method gets properly called during app initialization. it "sets the preferred run mode when initializing the app defaults" do @settings.initialize_app_defaults(default_values.merge(:run_mode => :master)) @settings.preferred_run_mode.should == :master end end describe "#call_hooks_deferred_to_application_initialization" do let(:good_default) { "yay" } let(:bad_default) { "$doesntexist" } before(:each) do @settings = Puppet::Settings.new end describe "when ignoring dependency interpolation errors" do let(:options) { {:ignore_interpolation_dependency_errors => true} } describe "if interpolation error" do it "should not raise an error" do hook_values = [] @settings.define_settings(:section, :badhook => {:default => bad_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v }}) expect do @settings.send(:call_hooks_deferred_to_application_initialization, options) end.to_not raise_error end end describe "if no interpolation error" do it "should not raise an error" do hook_values = [] @settings.define_settings(:section, :goodhook => {:default => good_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v }}) expect do @settings.send(:call_hooks_deferred_to_application_initialization, options) end.to_not raise_error end end end describe "when not ignoring dependency interpolation errors" do [ {}, {:ignore_interpolation_dependency_errors => false}].each do |options| describe "if interpolation error" do it "should raise an error" do hook_values = [] @settings.define_settings( :section, :badhook => { :default => bad_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v } } ) expect do @settings.send(:call_hooks_deferred_to_application_initialization, options) end.to raise_error(Puppet::Settings::InterpolationError) end it "should contain the setting name in error message" do hook_values = [] @settings.define_settings( :section, :badhook => { :default => bad_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v } } ) expect do @settings.send(:call_hooks_deferred_to_application_initialization, options) end.to raise_error(Puppet::Settings::InterpolationError, /badhook/) end end describe "if no interpolation error" do it "should not raise an error" do hook_values = [] @settings.define_settings( :section, :goodhook => { :default => good_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v } } ) expect do @settings.send(:call_hooks_deferred_to_application_initialization, options) end.to_not raise_error end end end end end describe "when setting values" do before do @settings = Puppet::Settings.new @settings.define_settings :main, :myval => { :default => "val", :desc => "desc" } @settings.define_settings :main, :bool => { :type => :boolean, :default => true, :desc => "desc" } end it "should provide a method for setting values from other objects" do @settings[:myval] = "something else" @settings[:myval].should == "something else" end it "should support a getopt-specific mechanism for setting values" do @settings.handlearg("--myval", "newval") @settings[:myval].should == "newval" end it "should support a getopt-specific mechanism for turning booleans off" do @settings.override_default(:bool, true) @settings.handlearg("--no-bool", "") @settings[:bool].should == false end it "should support a getopt-specific mechanism for turning booleans on" do # Turn it off first @settings.override_default(:bool, false) @settings.handlearg("--bool", "") @settings[:bool].should == true end it "should consider a cli setting with no argument to be a boolean" do # Turn it off first @settings.override_default(:bool, false) @settings.handlearg("--bool") @settings[:bool].should == true end it "should consider a cli setting with an empty string as an argument to be an empty argument, if the setting itself is not a boolean" do @settings.override_default(:myval, "bob") @settings.handlearg("--myval", "") @settings[:myval].should == "" end it "should consider a cli setting with a boolean as an argument to be a boolean" do # Turn it off first @settings.override_default(:bool, false) @settings.handlearg("--bool", "true") @settings[:bool].should == true end it "should not consider a cli setting of a non boolean with a boolean as an argument to be a boolean" do @settings.override_default(:myval, "bob") @settings.handlearg("--no-myval", "") @settings[:myval].should == "" end it "should flag string settings from the CLI" do @settings.handlearg("--myval", "12") @settings.set_by_cli?(:myval).should be_true end it "should flag bool settings from the CLI" do @settings.handlearg("--bool") @settings.set_by_cli?(:bool).should be_true end it "should not flag settings memory as from CLI" do @settings[:myval] = "12" @settings.set_by_cli?(:myval).should be_false end it "should clear the cache when setting getopt-specific values" do @settings.define_settings :mysection, :one => { :default => "whah", :desc => "yay" }, :two => { :default => "$one yay", :desc => "bah" } @settings.expects(:unsafe_flush_cache) @settings[:two].should == "whah yay" @settings.handlearg("--one", "else") @settings[:two].should == "else yay" end it "should clear the cache when the preferred_run_mode is changed" do @settings.expects(:flush_cache) @settings.preferred_run_mode = :master end it "should not clear other values when setting getopt-specific values" do @settings[:myval] = "yay" @settings.handlearg("--no-bool", "") @settings[:myval].should == "yay" end it "should clear the list of used sections" do @settings.expects(:clearused) @settings[:myval] = "yay" end describe "call_hook" do Puppet::Settings::StringSetting.available_call_hook_values.each do |val| describe "when :#{val}" do describe "and definition invalid" do it "should raise error if no hook defined" do expect do @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val}) end.to raise_error(ArgumentError, /no :hook/) end it "should include the setting name in the error message" do expect do @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val}) end.to raise_error(ArgumentError, /for :hooker/) end end describe "and definition valid" do before(:each) do hook_values = [] @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val, :hook => lambda { |v| hook_values << v }}) end it "should call the hook when value written" do @settings.setting(:hooker).expects(:handle).with("something").once @settings[:hooker] = "something" end end end end it "should have a default value of :on_write_only" do @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| hook_values << v }}) @settings.setting(:hooker).call_hook.should == :on_write_only end describe "when nil" do it "should generate a warning" do Puppet.expects(:warning) @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => nil, :hook => lambda { |v| hook_values << v }}) end it "should use default" do @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => nil, :hook => lambda { |v| hook_values << v }}) @settings.setting(:hooker).call_hook.should == :on_write_only end end describe "when invalid" do it "should raise an error" do expect do @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :foo, :hook => lambda { |v| hook_values << v }}) end.to raise_error(ArgumentError, /invalid.*call_hook/i) end end describe "when :on_define_and_write" do it "should call the hook at definition" do hook_values = [] @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| hook_values << v }}) @settings.setting(:hooker).call_hook.should == :on_define_and_write hook_values.should == %w{yay} end end describe "when :on_initialize_and_write" do before(:each) do @hook_values = [] @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| @hook_values << v }}) end it "should not call the hook at definition" do @hook_values.should == [] @hook_values.should_not == %w{yay} end it "should call the hook at initialization" do app_defaults = {} Puppet::Settings::REQUIRED_APP_SETTINGS.each do |key| app_defaults[key] = "foo" end app_defaults[:run_mode] = :user @settings.define_settings(:main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS) @settings.setting(:hooker).expects(:handle).with("yay").once @settings.initialize_app_defaults app_defaults end end end it "should call passed blocks when values are set" do values = [] @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }}) values.should == [] @settings[:hooker] = "something" values.should == %w{something} end it "should call passed blocks when values are set via the command line" do values = [] @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }}) values.should == [] @settings.handlearg("--hooker", "yay") values.should == %w{yay} end it "should provide an option to call passed blocks during definition" do values = [] @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| values << v }}) values.should == %w{yay} end it "should pass the fully interpolated value to the hook when called on definition" do values = [] @settings.define_settings(:section, :one => { :default => "test", :desc => "a" }) @settings.define_settings(:section, :hooker => {:default => "$one/yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| values << v }}) values.should == %w{test/yay} end it "should munge values using the setting-specific methods" do @settings[:bool] = "false" @settings[:bool].should == false end it "should prefer values set in ruby to values set on the cli" do @settings[:myval] = "memarg" @settings.handlearg("--myval", "cliarg") @settings[:myval].should == "memarg" end - it "should clear the list of environments" do - Puppet::Node::Environment.expects(:clear).at_least(1) - @settings[:myval] = "memarg" - end - it "should raise an error if we try to set a setting that hasn't been defined'" do lambda{ @settings[:why_so_serious] = "foo" }.should raise_error(ArgumentError, /unknown setting/) end it "allows overriding cli args based on the cli-set value" do @settings.handlearg("--myval", "cliarg") @settings.set_value(:myval, "modified #{@settings[:myval]}", :cli) expect(@settings[:myval]).to eq("modified cliarg") end end describe "when returning values" do before do @settings = Puppet::Settings.new @settings.define_settings :section, :config => { :type => :file, :default => "/my/file", :desc => "eh" }, :one => { :default => "ONE", :desc => "a" }, :two => { :default => "$one TWO", :desc => "b"}, :three => { :default => "$one $two THREE", :desc => "c"}, :four => { :default => "$two $three FOUR", :desc => "d"}, :five => { :default => nil, :desc => "e" } Puppet::FileSystem.stubs(:exist?).returns true end it "should provide a mechanism for returning set values" do @settings[:one] = "other" @settings[:one].should == "other" end it "setting a value to nil causes it to return to its default" do default_values = { :one => "skipped value" } [:logdir, :confdir, :vardir].each do |key| default_values[key] = 'default value' end @settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS @settings.initialize_app_defaults(default_values) @settings[:one] = "value will disappear" @settings[:one] = nil @settings[:one].should == "ONE" end it "should interpolate default values for other parameters into returned parameter values" do @settings[:one].should == "ONE" @settings[:two].should == "ONE TWO" @settings[:three].should == "ONE ONE TWO THREE" end it "should interpolate default values that themselves need to be interpolated" do @settings[:four].should == "ONE TWO ONE ONE TWO THREE FOUR" end it "should provide a method for returning uninterpolated values" do @settings[:two] = "$one tw0" @settings.uninterpolated_value(:two).should == "$one tw0" @settings.uninterpolated_value(:four).should == "$two $three FOUR" end it "should interpolate set values for other parameters into returned parameter values" do @settings[:one] = "on3" @settings[:two] = "$one tw0" @settings[:three] = "$one $two thr33" @settings[:four] = "$one $two $three f0ur" @settings[:one].should == "on3" @settings[:two].should == "on3 tw0" @settings[:three].should == "on3 on3 tw0 thr33" @settings[:four].should == "on3 on3 tw0 on3 on3 tw0 thr33 f0ur" end it "should not cache interpolated values such that stale information is returned" do @settings[:two].should == "ONE TWO" @settings[:one] = "one" @settings[:two].should == "one TWO" end - it "should not cache values such that information from one environment is returned for another environment" do - text = "[env1]\none = oneval\n[env2]\none = twoval\n" - @settings.stubs(:read_file).returns(text) - @settings.send(:parse_config_files) - - @settings.value(:one, "env1").should == "oneval" - @settings.value(:one, "env2").should == "twoval" - end - it "should have a run_mode that defaults to user" do @settings.preferred_run_mode.should == :user end it "interpolates a boolean false without raising an error" do @settings.define_settings(:section, :trip_wire => { :type => :boolean, :default => false, :desc => "a trip wire" }, :tripping => { :default => '$trip_wire', :desc => "once tripped if interpolated was false" }) @settings[:tripping].should == "false" end end describe "when choosing which value to return" do before do @settings = Puppet::Settings.new @settings.define_settings :section, :config => { :type => :file, :default => "/my/file", :desc => "a" }, :one => { :default => "ONE", :desc => "a" }, :two => { :default => "TWO", :desc => "b" } Puppet::FileSystem.stubs(:exist?).returns true @settings.preferred_run_mode = :agent end it "should return default values if no values have been set" do @settings[:one].should == "ONE" end it "should return values set on the cli before values set in the configuration file" do text = "[main]\none = fileval\n" @settings.stubs(:read_file).returns(text) @settings.handlearg("--one", "clival") @settings.send(:parse_config_files) @settings[:one].should == "clival" end it "should return values set in the mode-specific section before values set in the main section" do text = "[main]\none = mainval\n[agent]\none = modeval\n" @settings.stubs(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:one].should == "modeval" end it "should not return values outside of its search path" do text = "[other]\none = oval\n" file = "/some/file" @settings.stubs(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:one].should == "ONE" end - it "should return values in a specified environment" do - text = "[env]\none = envval\n" - @settings.stubs(:read_file).returns(text) - @settings.send(:parse_config_files) - @settings.value(:one, "env").should == "envval" - end - it 'should use the current environment for $environment' do - @settings.define_settings :main, :myval => { :default => "$environment/foo", :desc => "mydocs" } + @settings.define_settings :main, :config_version => { :default => "$environment/foo", :desc => "mydocs" } - @settings.value(:myval, "myenv").should == "myenv/foo" - end - - it "should interpolate found values using the current environment" do - text = "[main]\none = mainval\n[myname]\none = nameval\ntwo = $one/two\n" - @settings.stubs(:read_file).returns(text) - @settings.send(:parse_config_files) - - @settings.value(:two, "myname").should == "nameval/two" - end - - it "should return values in a specified environment before values in the main or name sections" do - text = "[env]\none = envval\n[main]\none = mainval\n[myname]\none = nameval\n" - @settings.stubs(:read_file).returns(text) - @settings.send(:parse_config_files) - @settings.value(:one, "env").should == "envval" - end - - context "when interpolating a dynamic environments setting" do - let(:dynamic_manifestdir) { "manifestdir=/somewhere/$environment/manifests" } - let(:environment) { "environment=anenv" } - - before(:each) do - @settings.define_settings :main, - :manifestdir => { :default => "/manifests", :desc => "manifestdir setting" }, - :environment => { :default => "production", :desc => "environment setting" } - end - - it "interpolates default environment when no environment specified" do - text = <<-EOF -[main] -#{dynamic_manifestdir} - EOF - @settings.stubs(:read_file).returns(text) - @settings.send(:parse_config_files) - expect(@settings.value(:manifestdir)).to eq("/somewhere/production/manifests") - end - - it "interpolates the set environment when no environment specified" do - text = <<-EOF -[main] -#{dynamic_manifestdir} -#{environment} - EOF - @settings.stubs(:read_file).returns(text) - @settings.send(:parse_config_files) - expect(@settings.value(:manifestdir)).to eq("/somewhere/anenv/manifests") - end + @settings.value(:config_version, "myenv").should == "myenv/foo" end end - describe "when locating config files" do before do @settings = Puppet::Settings.new end describe "when root" do it "should look for the main config file default location config settings haven't been overridden'" do Puppet.features.stubs(:root?).returns(true) Puppet::FileSystem.expects(:exist?).with(main_config_file_default_location).returns(false) Puppet::FileSystem.expects(:exist?).with(user_config_file_default_location).never @settings.send(:parse_config_files) end end describe "when not root" do it "should look for user config file default location if config settings haven't been overridden'" do Puppet.features.stubs(:root?).returns(false) seq = sequence "load config files" Puppet::FileSystem.expects(:exist?).with(user_config_file_default_location).returns(false).in_sequence(seq) @settings.send(:parse_config_files) end end end describe "when parsing its configuration" do before do @settings = Puppet::Settings.new @settings.stubs(:service_user_available?).returns true @settings.stubs(:service_group_available?).returns true @file = make_absolute("/some/file") @userconfig = make_absolute("/test/userconfigfile") @settings.define_settings :section, :user => { :default => "suser", :desc => "doc" }, :group => { :default => "sgroup", :desc => "doc" } @settings.define_settings :section, :config => { :type => :file, :default => @file, :desc => "eh" }, :one => { :default => "ONE", :desc => "a" }, :two => { :default => "$one TWO", :desc => "b" }, :three => { :default => "$one $two THREE", :desc => "c" } @settings.stubs(:user_config_file).returns(@userconfig) Puppet::FileSystem.stubs(:exist?).with(@file).returns true Puppet::FileSystem.stubs(:exist?).with(@userconfig).returns false end it "should not ignore the report setting" do @settings.define_settings :section, :report => { :default => "false", :desc => "a" } # This is needed in order to make sure we pass on windows myfile = File.expand_path(@file) @settings[:config] = myfile text = <<-CONF [puppetd] report=true CONF Puppet::FileSystem.expects(:exist?).with(myfile).returns(true) @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:report].should be_true end it "should use its current ':config' value for the file to parse" do myfile = make_absolute("/my/file") # do not stub expand_path here, as this leads to a stack overflow, when mocha tries to use it @settings[:config] = myfile Puppet::FileSystem.expects(:exist?).with(myfile).returns(true) Puppet::FileSystem.expects(:read).with(myfile).returns "[main]" @settings.send(:parse_config_files) end it "should not try to parse non-existent files" do Puppet::FileSystem.expects(:exist?).with(@file).returns false File.expects(:read).with(@file).never @settings.send(:parse_config_files) end it "should return values set in the configuration file" do text = "[main] one = fileval " @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:one].should == "fileval" end #484 - this should probably be in the regression area it "should not throw an exception on unknown parameters" do text = "[main]\nnosuchparam = mval\n" @settings.expects(:read_file).returns(text) lambda { @settings.send(:parse_config_files) }.should_not raise_error end it "should convert booleans in the configuration file into Ruby booleans" do text = "[main] one = true two = false " @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:one].should == true @settings[:two].should == false end it "should convert integers in the configuration file into Ruby Integers" do text = "[main] one = 65 " @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:one].should == 65 end it "should support specifying all metadata (owner, group, mode) in the configuration file" do @settings.define_settings :section, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" } otherfile = make_absolute("/other/file") @settings.parse_config(<<-CONF) [main] myfile = #{otherfile} {owner = service, group = service, mode = 644} CONF @settings[:myfile].should == otherfile @settings.metadata(:myfile).should == {:owner => "suser", :group => "sgroup", :mode => "644"} end it "should support specifying a single piece of metadata (owner, group, or mode) in the configuration file" do @settings.define_settings :section, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" } otherfile = make_absolute("/other/file") @settings.parse_config(<<-CONF) [main] myfile = #{otherfile} {owner = service} CONF @settings[:myfile].should == otherfile @settings.metadata(:myfile).should == {:owner => "suser"} end it "should support loading metadata (owner, group, or mode) from a run_mode section in the configuration file" do default_values = {} PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key| default_values[key] = 'default value' end @settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS @settings.define_settings :master, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" } otherfile = make_absolute("/other/file") text = "[master] myfile = #{otherfile} {mode = 664} " @settings.expects(:read_file).returns(text) # will start initialization as user @settings.preferred_run_mode.should == :user @settings.send(:parse_config_files) # change app run_mode to master @settings.initialize_app_defaults(default_values.merge(:run_mode => :master)) @settings.preferred_run_mode.should == :master # initializing the app should have reloaded the metadata based on run_mode @settings[:myfile].should == otherfile @settings.metadata(:myfile).should == {:mode => "664"} end it "does not use the metadata from the same setting in a different section" do default_values = {} PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key| default_values[key] = 'default value' end file = make_absolute("/file") default_mode = "0600" @settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS @settings.define_settings :master, :myfile => { :type => :file, :default => file, :desc => "a", :mode => default_mode } text = "[master] myfile = #{file}/foo [agent] myfile = #{file} {mode = 664} " @settings.expects(:read_file).returns(text) # will start initialization as user @settings.preferred_run_mode.should == :user @settings.send(:parse_config_files) # change app run_mode to master @settings.initialize_app_defaults(default_values.merge(:run_mode => :master)) @settings.preferred_run_mode.should == :master # initializing the app should have reloaded the metadata based on run_mode @settings[:myfile].should == "#{file}/foo" @settings.metadata(:myfile).should == { :mode => default_mode } end it "should call hooks associated with values set in the configuration file" do values = [] @settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[main] mysetting = setval " @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) values.should == ["setval"] end it "should not call the same hook for values set multiple times in the configuration file" do values = [] @settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[user] mysetting = setval [main] mysetting = other " @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) values.should == ["setval"] end - it "should pass the environment-specific value to the hook when one is available" do - values = [] - @settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} - @settings.define_settings :section, :environment => { :default => "yay", :desc => "a" } - @settings.define_settings :section, :environments => { :default => "yay,foo", :desc => "a" } - - text = "[main] - mysetting = setval - [yay] - mysetting = other - " - @settings.expects(:read_file).returns(text) - @settings.send(:parse_config_files) - values.should == ["other"] - end - it "should pass the interpolated value to the hook when one is available" do values = [] @settings.define_settings :section, :base => {:default => "yay", :desc => "a", :hook => proc { |v| values << v }} @settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }} text = "[main] mysetting = $base/setval " @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) values.should == ["yay/setval"] end it "should allow hooks invoked at parse time to be deferred" do hook_invoked = false @settings.define_settings :section, :deferred => {:desc => '', :hook => proc { |v| hook_invoked = true }, :call_hook => :on_initialize_and_write, } @settings.define_settings(:main, :logdir => { :type => :directory, :default => nil, :desc => "logdir" }, :confdir => { :type => :directory, :default => nil, :desc => "confdir" }, :vardir => { :type => :directory, :default => nil, :desc => "vardir" }) text = <<-EOD [main] deferred=$confdir/goose EOD @settings.stubs(:read_file).returns(text) @settings.initialize_global_settings hook_invoked.should be_false @settings.initialize_app_defaults(:logdir => '/path/to/logdir', :confdir => '/path/to/confdir', :vardir => '/path/to/vardir') hook_invoked.should be_true @settings[:deferred].should eq(File.expand_path('/path/to/confdir/goose')) end it "does not require the value for a setting without a hook to resolve during global setup" do hook_invoked = false @settings.define_settings :section, :can_cause_problems => {:desc => '' } @settings.define_settings(:main, :logdir => { :type => :directory, :default => nil, :desc => "logdir" }, :confdir => { :type => :directory, :default => nil, :desc => "confdir" }, :vardir => { :type => :directory, :default => nil, :desc => "vardir" }) text = <<-EOD [main] can_cause_problems=$confdir/goose EOD @settings.stubs(:read_file).returns(text) @settings.initialize_global_settings @settings.initialize_app_defaults(:logdir => '/path/to/logdir', :confdir => '/path/to/confdir', :vardir => '/path/to/vardir') @settings[:can_cause_problems].should eq(File.expand_path('/path/to/confdir/goose')) end it "should allow empty values" do @settings.define_settings :section, :myarg => { :default => "myfile", :desc => "a" } text = "[main] myarg = " @settings.stubs(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:myarg].should == "" end describe "deprecations" do let(:settings) { Puppet::Settings.new } let(:app_defaults) { { :logdir => "/dev/null", :confdir => "/dev/null", :vardir => "/dev/null", } } def assert_accessing_setting_is_deprecated(settings, setting) - Puppet.expects(:deprecation_warning).with("Accessing '#{setting}' as a setting is deprecated. See http://links.puppetlabs.com/env-settings-deprecations") - Puppet.expects(:deprecation_warning).with("Modifying '#{setting}' as a setting is deprecated. See http://links.puppetlabs.com/env-settings-deprecations") + Puppet.expects(:deprecation_warning).with("Accessing '#{setting}' as a setting is deprecated.") + Puppet.expects(:deprecation_warning).with("Modifying '#{setting}' as a setting is deprecated.") settings[setting.intern] = apath = File.expand_path('foo') expect(settings[setting.intern]).to eq(apath) end before(:each) do settings.define_settings(:main, { :logdir => { :default => 'a', :desc => 'a' }, :confdir => { :default => 'b', :desc => 'b' }, :vardir => { :default => 'c', :desc => 'c' }, }) end context "complete" do let(:completely_deprecated_settings) do settings.define_settings(:main, { - :manifestdir => { + :completely_deprecated_setting => { :default => 'foo', :desc => 'a deprecated setting', :deprecated => :completely, } }) settings end it "warns when set in puppet.conf" do - Puppet.expects(:deprecation_warning).with(regexp_matches(/manifestdir is deprecated\./), 'setting-manifestdir') + Puppet.expects(:deprecation_warning).with(regexp_matches(/completely_deprecated_setting is deprecated\./), 'setting-completely_deprecated_setting') completely_deprecated_settings.parse_config(<<-CONF) - manifestdir='should warn' + completely_deprecated_setting='should warn' CONF completely_deprecated_settings.initialize_app_defaults(app_defaults) end it "warns when set on the commandline" do - Puppet.expects(:deprecation_warning).with(regexp_matches(/manifestdir is deprecated\./), 'setting-manifestdir') + Puppet.expects(:deprecation_warning).with(regexp_matches(/completely_deprecated_setting is deprecated\./), 'setting-completely_deprecated_setting') - args = ["--manifestdir", "/some/value"] + args = ["--completely_deprecated_setting", "/some/value"] completely_deprecated_settings.send(:parse_global_options, args) completely_deprecated_settings.initialize_app_defaults(app_defaults) end it "warns when set in code" do - assert_accessing_setting_is_deprecated(completely_deprecated_settings, 'manifestdir') + assert_accessing_setting_is_deprecated(completely_deprecated_settings, 'completely_deprecated_setting') end end context "partial" do let(:partially_deprecated_settings) do settings.define_settings(:main, { - :modulepath => { + :partially_deprecated_setting => { :default => 'foo', :desc => 'a partially deprecated setting', :deprecated => :allowed_on_commandline, } }) settings end it "warns for a deprecated setting allowed on the command line set in puppet.conf" do - Puppet.expects(:deprecation_warning).with(regexp_matches(/modulepath is deprecated in puppet\.conf/), 'puppet-conf-setting-modulepath') + Puppet.expects(:deprecation_warning).with(regexp_matches(/partially_deprecated_setting is deprecated in puppet\.conf/), 'puppet-conf-setting-partially_deprecated_setting') partially_deprecated_settings.parse_config(<<-CONF) - modulepath='should warn' + partially_deprecated_setting='should warn' CONF partially_deprecated_settings.initialize_app_defaults(app_defaults) end it "does not warn when manifest is set on command line" do Puppet.expects(:deprecation_warning).never - args = ["--modulepath", "/some/value"] + args = ["--partially_deprecated_setting", "/some/value"] partially_deprecated_settings.send(:parse_global_options, args) partially_deprecated_settings.initialize_app_defaults(app_defaults) end it "warns when set in code" do - assert_accessing_setting_is_deprecated(partially_deprecated_settings, 'modulepath') + assert_accessing_setting_is_deprecated(partially_deprecated_settings, 'partially_deprecated_setting') end end end end describe "when there are multiple config files" do let(:main_config_text) { "[main]\none = main\ntwo = main2" } let(:user_config_text) { "[main]\none = user\n" } let(:seq) { sequence "config_file_sequence" } before :each do @settings = Puppet::Settings.new @settings.define_settings(:section, { :confdir => { :default => nil, :desc => "Conf dir" }, :config => { :default => "$confdir/puppet.conf", :desc => "Config" }, :one => { :default => "ONE", :desc => "a" }, :two => { :default => "TWO", :desc => "b" }, }) end context "running non-root without explicit config file" do before :each do Puppet.features.stubs(:root?).returns(false) Puppet::FileSystem.expects(:exist?). with(user_config_file_default_location). returns(true).in_sequence(seq) @settings.expects(:read_file). with(user_config_file_default_location). returns(user_config_text).in_sequence(seq) end it "should return values from the user config file" do @settings.send(:parse_config_files) @settings[:one].should == "user" end it "should not return values from the main config file" do @settings.send(:parse_config_files) @settings[:two].should == "TWO" end end context "running as root without explicit config file" do before :each do Puppet.features.stubs(:root?).returns(true) Puppet::FileSystem.expects(:exist?). with(main_config_file_default_location). returns(true).in_sequence(seq) @settings.expects(:read_file). with(main_config_file_default_location). returns(main_config_text).in_sequence(seq) end it "should return values from the main config file" do @settings.send(:parse_config_files) @settings[:one].should == "main" end it "should not return values from the user config file" do @settings.send(:parse_config_files) @settings[:two].should == "main2" end end context "running with an explicit config file as a user (e.g. Apache + Passenger)" do before :each do Puppet.features.stubs(:root?).returns(false) @settings[:confdir] = File.dirname(main_config_file_default_location) Puppet::FileSystem.expects(:exist?). with(main_config_file_default_location). returns(true).in_sequence(seq) @settings.expects(:read_file). with(main_config_file_default_location). returns(main_config_text).in_sequence(seq) end it "should return values from the main config file" do @settings.send(:parse_config_files) @settings[:one].should == "main" end it "should not return values from the user config file" do @settings.send(:parse_config_files) @settings[:two].should == "main2" end end end describe "when reparsing its configuration" do before do @file = make_absolute("/test/file") @userconfig = make_absolute("/test/userconfigfile") @settings = Puppet::Settings.new @settings.define_settings :section, :config => { :type => :file, :default => @file, :desc => "a" }, :one => { :default => "ONE", :desc => "a" }, :two => { :default => "$one TWO", :desc => "b" }, :three => { :default => "$one $two THREE", :desc => "c" } Puppet::FileSystem.stubs(:exist?).with(@file).returns true Puppet::FileSystem.stubs(:exist?).with(@userconfig).returns false @settings.stubs(:user_config_file).returns(@userconfig) end it "does not create the WatchedFile instance and should not parse if the file does not exist" do Puppet::FileSystem.expects(:exist?).with(@file).returns false Puppet::Util::WatchedFile.expects(:new).never @settings.expects(:parse_config_files).never @settings.reparse_config_files end context "and watched file exists" do before do @watched_file = Puppet::Util::WatchedFile.new(@file) Puppet::Util::WatchedFile.expects(:new).with(@file).returns @watched_file end it "uses a WatchedFile instance to determine if the file has changed" do @watched_file.expects(:changed?) @settings.reparse_config_files end it "does not reparse if the file has not changed" do @watched_file.expects(:changed?).returns false @settings.expects(:parse_config_files).never @settings.reparse_config_files end it "reparses if the file has changed" do @watched_file.expects(:changed?).returns true @settings.expects(:parse_config_files) @settings.reparse_config_files end it "replaces in-memory values with on-file values" do @watched_file.stubs(:changed?).returns(true) @settings[:one] = "init" # Now replace the value text = "[main]\none = disk-replace\n" @settings.stubs(:read_file).returns(text) @settings.reparse_config_files @settings[:one].should == "disk-replace" end end it "should retain parameters set by cli when configuration files are reparsed" do @settings.handlearg("--one", "clival") text = "[main]\none = on-disk\n" @settings.stubs(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:one].should == "clival" end it "should remove in-memory values that are no longer set in the file" do # Init the value text = "[main]\none = disk-init\n" @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:one].should == "disk-init" # Now replace the value text = "[main]\ntwo = disk-replace\n" @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) # The originally-overridden value should be replaced with the default @settings[:one].should == "ONE" # and we should now have the new value in memory @settings[:two].should == "disk-replace" end it "should retain in-memory values if the file has a syntax error" do # Init the value text = "[main]\none = initial-value\n" @settings.expects(:read_file).with(@file).returns(text) @settings.send(:parse_config_files) @settings[:one].should == "initial-value" # Now replace the value with something bogus text = "[main]\nkenny = killed-by-what-follows\n1 is 2, blah blah florp\n" @settings.expects(:read_file).with(@file).returns(text) @settings.send(:parse_config_files) # The originally-overridden value should not be replaced with the default @settings[:one].should == "initial-value" # and we should not have the new value in memory @settings[:kenny].should be_nil end end it "should provide a method for creating a catalog of resources from its configuration" do Puppet::Settings.new.should respond_to(:to_catalog) end describe "when creating a catalog" do before do @settings = Puppet::Settings.new @settings.stubs(:service_user_available?).returns true @prefix = Puppet.features.posix? ? "" : "C:" end it "should add all file resources to the catalog if no sections have been specified" do @settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a"}, :seconddir => { :type => :directory, :default => @prefix+"/seconddir", :desc => "a"} @settings.define_settings :other, :otherdir => { :type => :directory, :default => @prefix+"/otherdir", :desc => "a" } catalog = @settings.to_catalog [@prefix+"/maindir", @prefix+"/seconddir", @prefix+"/otherdir"].each do |path| catalog.resource(:file, path).should be_instance_of(Puppet::Resource) end end it "should add only files in the specified sections if section names are provided" do @settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" } @settings.define_settings :other, :otherdir => { :type => :directory, :default => @prefix+"/otherdir", :desc => "a" } catalog = @settings.to_catalog(:main) catalog.resource(:file, @prefix+"/otherdir").should be_nil catalog.resource(:file, @prefix+"/maindir").should be_instance_of(Puppet::Resource) end it "should not try to add the same file twice" do @settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" } @settings.define_settings :other, :otherdir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" } lambda { @settings.to_catalog }.should_not raise_error end it "should ignore files whose :to_resource method returns nil" do @settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" } @settings.setting(:maindir).expects(:to_resource).returns nil Puppet::Resource::Catalog.any_instance.expects(:add_resource).never @settings.to_catalog end - it "should ignore manifestdir if environmentpath is set" do - @settings.define_settings :main, - :manifestdir => { :type => :directory, :default => @prefix+"/manifestdir", :desc => "a" }, - :environmentpath => { :type => :path, :default => @prefix+"/envs", :desc => "a" } - - catalog = @settings.to_catalog(:main) - - expect(catalog).to_not have_resource("File[#{@prefix}/manifestdir]") - end - describe "on Microsoft Windows" do before :each do Puppet.features.stubs(:root?).returns true Puppet.features.stubs(:microsoft_windows?).returns true @settings.define_settings :foo, :mkusers => { :type => :boolean, :default => true, :desc => "e" }, :user => { :default => "suser", :desc => "doc" }, :group => { :default => "sgroup", :desc => "doc" } @settings.define_settings :other, :otherdir => { :type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"} @catalog = @settings.to_catalog end it "it should not add users and groups to the catalog" do @catalog.resource(:user, "suser").should be_nil @catalog.resource(:group, "sgroup").should be_nil end end describe "adding default directory environment to the catalog" do let(:tmpenv) { tmpdir("envs") } let(:default_path) { "#{tmpenv}/environments" } before(:each) do @settings.define_settings :main, :environment => { :default => "production", :desc => "env"}, :environmentpath => { :type => :path, :default => default_path, :desc => "envpath"} end it "adds if environmentpath exists" do envpath = "#{tmpenv}/custom_envpath" @settings[:environmentpath] = envpath Dir.mkdir(envpath) catalog = @settings.to_catalog expect(catalog.resource_keys).to include(["File", "#{envpath}/production"]) end it "adds the first directory of environmentpath" do envdir = "#{tmpenv}/custom_envpath" envpath = "#{envdir}#{File::PATH_SEPARATOR}/some/other/envdir" @settings[:environmentpath] = envpath Dir.mkdir(envdir) catalog = @settings.to_catalog expect(catalog.resource_keys).to include(["File", "#{envdir}/production"]) end it "handles a non-existent environmentpath" do catalog = @settings.to_catalog expect(catalog.resource_keys).to be_empty end it "handles a default environmentpath" do Dir.mkdir(default_path) catalog = @settings.to_catalog expect(catalog.resource_keys).to include(["File", "#{default_path}/production"]) end it "does not add if the path to the default directory environment exists as a symlink", :if => Puppet.features.manages_symlinks? do Dir.mkdir(default_path) Puppet::FileSystem.symlink("#{tmpenv}/nowhere", File.join(default_path, 'production')) catalog = @settings.to_catalog expect(catalog.resource_keys).to_not include(["File", "#{default_path}/production"]) end end describe "when adding users and groups to the catalog" do before do Puppet.features.stubs(:root?).returns true Puppet.features.stubs(:microsoft_windows?).returns false @settings.define_settings :foo, :mkusers => { :type => :boolean, :default => true, :desc => "e" }, :user => { :default => "suser", :desc => "doc" }, :group => { :default => "sgroup", :desc => "doc" } @settings.define_settings :other, :otherdir => {:type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"} @catalog = @settings.to_catalog end it "should add each specified user and group to the catalog if :mkusers is a valid setting, is enabled, and we're running as root" do @catalog.resource(:user, "suser").should be_instance_of(Puppet::Resource) @catalog.resource(:group, "sgroup").should be_instance_of(Puppet::Resource) end it "should only add users and groups to the catalog from specified sections" do @settings.define_settings :yay, :yaydir => { :type => :directory, :default => "/yaydir", :desc => "a", :owner => "service", :group => "service"} catalog = @settings.to_catalog(:other) catalog.resource(:user, "jane").should be_nil catalog.resource(:group, "billy").should be_nil end it "should not add users or groups to the catalog if :mkusers not running as root" do Puppet.features.stubs(:root?).returns false catalog = @settings.to_catalog catalog.resource(:user, "suser").should be_nil catalog.resource(:group, "sgroup").should be_nil end it "should not add users or groups to the catalog if :mkusers is not a valid setting" do Puppet.features.stubs(:root?).returns true settings = Puppet::Settings.new settings.define_settings :other, :otherdir => {:type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"} catalog = settings.to_catalog catalog.resource(:user, "suser").should be_nil catalog.resource(:group, "sgroup").should be_nil end it "should not add users or groups to the catalog if :mkusers is a valid setting but is disabled" do @settings[:mkusers] = false catalog = @settings.to_catalog catalog.resource(:user, "suser").should be_nil catalog.resource(:group, "sgroup").should be_nil end it "should not try to add users or groups to the catalog twice" do @settings.define_settings :yay, :yaydir => {:type => :directory, :default => "/yaydir", :desc => "a", :owner => "service", :group => "service"} # This would fail if users/groups were added twice lambda { @settings.to_catalog }.should_not raise_error end it "should set :ensure to :present on each created user and group" do @catalog.resource(:user, "suser")[:ensure].should == :present @catalog.resource(:group, "sgroup")[:ensure].should == :present end it "should set each created user's :gid to the service group" do @settings.to_catalog.resource(:user, "suser")[:gid].should == "sgroup" end it "should not attempt to manage the root user" do Puppet.features.stubs(:root?).returns true @settings.define_settings :foo, :foodir => {:type => :directory, :default => "/foodir", :desc => "a", :owner => "root", :group => "service"} @settings.to_catalog.resource(:user, "root").should be_nil end end end it "should be able to be converted to a manifest" do Puppet::Settings.new.should respond_to(:to_manifest) end describe "when being converted to a manifest" do it "should produce a string with the code for each resource joined by two carriage returns" do @settings = Puppet::Settings.new @settings.define_settings :main, :maindir => { :type => :directory, :default => "/maindir", :desc => "a"}, :seconddir => { :type => :directory, :default => "/seconddir", :desc => "a"} main = stub 'main_resource', :ref => "File[/maindir]" main.expects(:to_manifest).returns "maindir" second = stub 'second_resource', :ref => "File[/seconddir]" second.expects(:to_manifest).returns "seconddir" @settings.setting(:maindir).expects(:to_resource).returns main @settings.setting(:seconddir).expects(:to_resource).returns second @settings.to_manifest.split("\n\n").sort.should == %w{maindir seconddir} end end describe "when using sections of the configuration to manage the local host" do before do @settings = Puppet::Settings.new @settings.stubs(:service_user_available?).returns true @settings.stubs(:service_group_available?).returns true @settings.define_settings :main, :noop => { :default => false, :desc => "", :type => :boolean } @settings.define_settings :main, :maindir => { :type => :directory, :default => make_absolute("/maindir"), :desc => "a" }, :seconddir => { :type => :directory, :default => make_absolute("/seconddir"), :desc => "a"} @settings.define_settings :main, :user => { :default => "suser", :desc => "doc" }, :group => { :default => "sgroup", :desc => "doc" } @settings.define_settings :other, :otherdir => {:type => :directory, :default => make_absolute("/otherdir"), :desc => "a", :owner => "service", :group => "service", :mode => '0755'} @settings.define_settings :third, :thirddir => { :type => :directory, :default => make_absolute("/thirddir"), :desc => "b"} @settings.define_settings :files, :myfile => {:type => :file, :default => make_absolute("/myfile"), :desc => "a", :mode => '0755'} end it "should provide a method that creates directories with the correct modes" do Puppet::Util::SUIDManager.expects(:asuser).with("suser", "sgroup").yields Dir.expects(:mkdir).with(make_absolute("/otherdir"), '0755') @settings.mkdir(:otherdir) end it "should create a catalog with the specified sections" do @settings.expects(:to_catalog).with(:main, :other).returns Puppet::Resource::Catalog.new("foo") @settings.use(:main, :other) end it "should canonicalize the sections" do @settings.expects(:to_catalog).with(:main, :other).returns Puppet::Resource::Catalog.new("foo") @settings.use("main", "other") end it "should ignore sections that have already been used" do @settings.expects(:to_catalog).with(:main).returns Puppet::Resource::Catalog.new("foo") @settings.use(:main) @settings.expects(:to_catalog).with(:other).returns Puppet::Resource::Catalog.new("foo") @settings.use(:main, :other) end it "should convert the created catalog to a RAL catalog" do @catalog = Puppet::Resource::Catalog.new("foo") @settings.expects(:to_catalog).with(:main).returns @catalog @catalog.expects(:to_ral).returns @catalog @settings.use(:main) end it "should specify that it is not managing a host catalog" do catalog = Puppet::Resource::Catalog.new("foo") catalog.expects(:apply) @settings.expects(:to_catalog).returns catalog catalog.stubs(:to_ral).returns catalog catalog.expects(:host_config=).with false @settings.use(:main) end it "should support a method for re-using all currently used sections" do @settings.expects(:to_catalog).with(:main, :third).times(2).returns Puppet::Resource::Catalog.new("foo") @settings.use(:main, :third) @settings.reuse end it "should fail with an appropriate message if any resources fail" do @catalog = Puppet::Resource::Catalog.new("foo") @catalog.stubs(:to_ral).returns @catalog @settings.expects(:to_catalog).returns @catalog @trans = mock("transaction") @catalog.expects(:apply).yields(@trans) @trans.expects(:any_failed?).returns(true) resource = Puppet::Type.type(:notify).new(:title => 'failed') status = Puppet::Resource::Status.new(resource) event = Puppet::Transaction::Event.new( :name => 'failure', :status => 'failure', :message => 'My failure') status.add_event(event) report = Puppet::Transaction::Report.new('apply') report.add_resource_status(status) @trans.expects(:report).returns report @settings.expects(:raise).with(includes("My failure")) @settings.use(:whatever) end end describe "when dealing with printing configs" do before do @settings = Puppet::Settings.new #these are the magic default values @settings.stubs(:value).with(:configprint).returns("") @settings.stubs(:value).with(:genconfig).returns(false) @settings.stubs(:value).with(:genmanifest).returns(false) @settings.stubs(:value).with(:environment).returns(nil) end describe "when checking print_config?" do it "should return false when the :configprint, :genconfig and :genmanifest are not set" do @settings.print_configs?.should be_false end it "should return true when :configprint has a value" do @settings.stubs(:value).with(:configprint).returns("something") @settings.print_configs?.should be_true end it "should return true when :genconfig has a value" do @settings.stubs(:value).with(:genconfig).returns(true) @settings.print_configs?.should be_true end it "should return true when :genmanifest has a value" do @settings.stubs(:value).with(:genmanifest).returns(true) @settings.print_configs?.should be_true end end describe "when printing configs" do describe "when :configprint has a value" do it "should call print_config_options" do @settings.stubs(:value).with(:configprint).returns("something") @settings.expects(:print_config_options) @settings.print_configs end it "should get the value of the option using the environment" do @settings.stubs(:value).with(:configprint).returns("something") @settings.stubs(:include?).with("something").returns(true) @settings.expects(:value).with(:environment).returns("env") @settings.expects(:value).with("something", "env").returns("foo") @settings.stubs(:puts).with("foo") @settings.print_configs end it "should print the value of the option" do @settings.stubs(:value).with(:configprint).returns("something") @settings.stubs(:include?).with("something").returns(true) @settings.stubs(:value).with("something", nil).returns("foo") @settings.expects(:puts).with("foo") @settings.print_configs end it "should print the value pairs if there are multiple options" do @settings.stubs(:value).with(:configprint).returns("bar,baz") @settings.stubs(:include?).with("bar").returns(true) @settings.stubs(:include?).with("baz").returns(true) @settings.stubs(:value).with("bar", nil).returns("foo") @settings.stubs(:value).with("baz", nil).returns("fud") @settings.expects(:puts).with("bar = foo") @settings.expects(:puts).with("baz = fud") @settings.print_configs end it "should return true after printing" do @settings.stubs(:value).with(:configprint).returns("something") @settings.stubs(:include?).with("something").returns(true) @settings.stubs(:value).with("something", nil).returns("foo") @settings.stubs(:puts).with("foo") @settings.print_configs.should be_true end it "should return false if a config param is not found" do @settings.stubs :puts @settings.stubs(:value).with(:configprint).returns("something") @settings.stubs(:include?).with("something").returns(false) @settings.print_configs.should be_false end end describe "when genconfig is true" do before do @settings.stubs :puts end it "should call to_config" do @settings.stubs(:value).with(:genconfig).returns(true) @settings.expects(:to_config) @settings.print_configs end it "should return true from print_configs" do @settings.stubs(:value).with(:genconfig).returns(true) @settings.stubs(:to_config) @settings.print_configs.should be_true end end describe "when genmanifest is true" do before do @settings.stubs :puts end it "should call to_config" do @settings.stubs(:value).with(:genmanifest).returns(true) @settings.expects(:to_manifest) @settings.print_configs end it "should return true from print_configs" do @settings.stubs(:value).with(:genmanifest).returns(true) @settings.stubs(:to_manifest) @settings.print_configs.should be_true end end end end describe "when determining if the service user is available" do let(:settings) do settings = Puppet::Settings.new settings.define_settings :main, :user => { :default => nil, :desc => "doc" } settings end def a_user_type_for(username) user = mock 'user' Puppet::Type.type(:user).expects(:new).with { |args| args[:name] == username }.returns user user end it "should return false if there is no user setting" do settings.should_not be_service_user_available end it "should return false if the user provider says the user is missing" do settings[:user] = "foo" a_user_type_for("foo").expects(:exists?).returns false settings.should_not be_service_user_available end it "should return true if the user provider says the user is present" do settings[:user] = "foo" a_user_type_for("foo").expects(:exists?).returns true settings.should be_service_user_available end it "caches the result of determining if the user is present" do settings[:user] = "foo" a_user_type_for("foo").expects(:exists?).returns true settings.should be_service_user_available settings.should be_service_user_available end end describe "when determining if the service group is available" do let(:settings) do settings = Puppet::Settings.new settings.define_settings :main, :group => { :default => nil, :desc => "doc" } settings end def a_group_type_for(groupname) group = mock 'group' Puppet::Type.type(:group).expects(:new).with { |args| args[:name] == groupname }.returns group group end it "should return false if there is no group setting" do settings.should_not be_service_group_available end it "should return false if the group provider says the group is missing" do settings[:group] = "foo" a_group_type_for("foo").expects(:exists?).returns false settings.should_not be_service_group_available end it "should return true if the group provider says the group is present" do settings[:group] = "foo" a_group_type_for("foo").expects(:exists?).returns true settings.should be_service_group_available end it "caches the result of determining if the group is present" do settings[:group] = "foo" a_group_type_for("foo").expects(:exists?).returns true settings.should be_service_group_available settings.should be_service_group_available end end describe "when dealing with command-line options" do let(:settings) { Puppet::Settings.new } it "should get options from Puppet.settings.optparse_addargs" do settings.expects(:optparse_addargs).returns([]) settings.send(:parse_global_options, []) end it "should add options to OptionParser" do settings.stubs(:optparse_addargs).returns( [["--option","-o", "Funny Option", :NONE]]) settings.expects(:handlearg).with("--option", true) settings.send(:parse_global_options, ["--option"]) end it "should not die if it sees an unrecognized option, because the app/face may handle it later" do expect { settings.send(:parse_global_options, ["--topuppet", "value"]) } .to_not raise_error end it "should not pass an unrecognized option to handleargs" do settings.expects(:handlearg).with("--topuppet", "value").never expect { settings.send(:parse_global_options, ["--topuppet", "value"]) } .to_not raise_error end it "should pass valid puppet settings options to handlearg even if they appear after an unrecognized option" do settings.stubs(:optparse_addargs).returns( [["--option","-o", "Funny Option", :NONE]]) settings.expects(:handlearg).with("--option", true) settings.send(:parse_global_options, ["--invalidoption", "--option"]) end it "should transform boolean option to normal form" do Puppet::Settings.clean_opt("--[no-]option", true).should == ["--option", true] end it "should transform boolean option to no- form" do Puppet::Settings.clean_opt("--[no-]option", false).should == ["--no-option", false] end it "should set preferred run mode from --run_mode string without error" do args = ["--run_mode", "master"] settings.expects(:handlearg).with("--run_mode", "master").never expect { settings.send(:parse_global_options, args) } .to_not raise_error Puppet.settings.preferred_run_mode.should == :master args.empty?.should == true end it "should set preferred run mode from --run_mode= string without error" do args = ["--run_mode=master"] settings.expects(:handlearg).with("--run_mode", "master").never expect { settings.send(:parse_global_options, args) } .to_not raise_error Puppet.settings.preferred_run_mode.should == :master args.empty?.should == true end end describe "default_certname" do describe "using hostname and domainname" do before :each do Puppet::Settings.stubs(:hostname_fact).returns("testhostname") Puppet::Settings.stubs(:domain_fact).returns("domain.test.") end it "should use both to generate fqdn" do Puppet::Settings.default_certname.should =~ /testhostname\.domain\.test/ end it "should remove trailing dots from fqdn" do Puppet::Settings.default_certname.should == 'testhostname.domain.test' end end describe "using just hostname" do before :each do Puppet::Settings.stubs(:hostname_fact).returns("testhostname") Puppet::Settings.stubs(:domain_fact).returns("") end it "should use only hostname to generate fqdn" do Puppet::Settings.default_certname.should == "testhostname" end it "should removing trailing dots from fqdn" do Puppet::Settings.default_certname.should == "testhostname" end end end end diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb index bf438d680..d5a06246a 100755 --- a/spec/unit/type/file/content_spec.rb +++ b/spec/unit/type/file/content_spec.rb @@ -1,516 +1,521 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/network/http_pool' require 'puppet/network/resolver' describe Puppet::Type.type(:file).attrclass(:content), :uses_checksums => true do include PuppetSpec::Files let(:filename) { tmpfile('testfile') } - - let(:catalog) { Puppet::Resource::Catalog.new } - + let(:environment) { Puppet::Node::Environment.create(:testing, []) } + let(:catalog) { Puppet::Resource::Catalog.new(:test, environment) } let(:resource) { Puppet::Type.type(:file).new :path => filename, :catalog => catalog } before do File.open(filename, 'w') {|f| f.write "initial file content"} described_class.stubs(:standalone?).returns(false) end + around do |example| + Puppet.override(:environments => Puppet::Environments::Static.new(environment)) do + example.run + end + end + describe "when determining the checksum type" do let(:content) { described_class.new(:resource => resource) } it "should use the type specified in the source checksum if a source is set" do resource[:source] = File.expand_path("/foo") resource.parameter(:source).expects(:checksum).returns "{md5lite}eh" content.checksum_type.should == :md5lite end it "should use the type specified by the checksum parameter if no source is set" do resource[:checksum] = :md5lite content.checksum_type.should == :md5lite end with_digest_algorithms do it "should use the type specified by digest_algorithm by default" do content.checksum_type.should == digest_algorithm.intern end end end describe "when determining the actual content to write" do let(:content) { described_class.new(:resource => resource) } it "should use the set content if available" do content.should = "ehness" content.actual_content.should == "ehness" end it "should not use the content from the source if the source is set" do source = mock 'source' resource.expects(:parameter).never.with(:source).returns source content.actual_content.should be_nil end end describe "when setting the desired content" do let(:content) { described_class.new(:resource => resource) } it "should make the actual content available via an attribute" do content.stubs(:checksum_type).returns "md5" content.should = "this is some content" content.actual_content.should == "this is some content" end with_digest_algorithms do it "should store the checksum as the desired content" do d = digest("this is some content") content.stubs(:checksum_type).returns digest_algorithm content.should = "this is some content" content.should.must == "{#{digest_algorithm}}#{d}" end it "should not checksum 'absent'" do content.should = :absent content.should.must == :absent end it "should accept a checksum as the desired content" do d = digest("this is some content") string = "{#{digest_algorithm}}#{d}" content.should = string content.should.must == string end end it "should convert the value to ASCII-8BIT", :if => "".respond_to?(:encode) do content.should= "Let's make a \u{2603}" content.actual_content.should == "Let's make a \xE2\x98\x83".force_encoding(Encoding::ASCII_8BIT) end end describe "when retrieving the current content" do let(:content) { described_class.new(:resource => resource) } it "should return :absent if the file does not exist" do resource.expects(:stat).returns nil content.retrieve.should == :absent end it "should not manage content on directories" do stat = mock 'stat', :ftype => "directory" resource.expects(:stat).returns stat content.retrieve.should be_nil end it "should not manage content on links" do stat = mock 'stat', :ftype => "link" resource.expects(:stat).returns stat content.retrieve.should be_nil end it "should always return the checksum as a string" do resource[:checksum] = :mtime stat = mock 'stat', :ftype => "file" resource.expects(:stat).returns stat time = Time.now resource.parameter(:checksum).expects(:mtime_file).with(resource[:path]).returns time content.retrieve.should == "{mtime}#{time}" end with_digest_algorithms do it "should return the checksum of the file if it exists and is a normal file" do stat = mock 'stat', :ftype => "file" resource.expects(:stat).returns stat resource.parameter(:checksum).expects("#{digest_algorithm}_file".intern).with(resource[:path]).returns "mysum" content.retrieve.should == "{#{digest_algorithm}}mysum" end end end describe "when testing whether the content is in sync" do let(:content) { described_class.new(:resource => resource) } before do resource[:ensure] = :file end it "should return true if the resource shouldn't be a regular file" do resource.expects(:should_be_file?).returns false content.should = "foo" content.must be_safe_insync("whatever") end it "should warn that no content will be synced to links when ensure is :present" do resource[:ensure] = :present resource[:content] = 'foo' resource.stubs(:should_be_file?).returns false resource.stubs(:stat).returns mock("stat", :ftype => "link") resource.expects(:warning).with {|msg| msg =~ /Ensure set to :present but file type is/} content.insync? :present end it "should return false if the current content is :absent" do content.should = "foo" content.should_not be_safe_insync(:absent) end it "should return false if the file should be a file but is not present" do resource.expects(:should_be_file?).returns true content.should = "foo" content.should_not be_safe_insync(:absent) end describe "and the file exists" do with_digest_algorithms do before do resource.stubs(:stat).returns mock("stat") resource[:checksum] = digest_algorithm content.should = "some content" end it "should return false if the current contents are different from the desired content" do content.should_not be_safe_insync("other content") end it "should return true if the sum for the current contents is the same as the sum for the desired content" do content.must be_safe_insync("{#{digest_algorithm}}" + digest("some content")) end [true, false].product([true, false]).each do |cfg, param| describe "and Puppet[:show_diff] is #{cfg} and show_diff => #{param}" do before do Puppet[:show_diff] = cfg resource.stubs(:show_diff?).returns param resource[:loglevel] = "debug" end if cfg and param it "should display a diff" do content.expects(:diff).returns("my diff").once content.expects(:debug).with("\nmy diff").once content.should_not be_safe_insync("other content") end else it "should not display a diff" do content.expects(:diff).never content.should_not be_safe_insync("other content") end end end end end end describe "and :replace is false" do before do resource.stubs(:replace?).returns false end it "should be insync if the file exists and the content is different" do resource.stubs(:stat).returns mock('stat') content.must be_safe_insync("whatever") end it "should be insync if the file exists and the content is right" do resource.stubs(:stat).returns mock('stat') content.must be_safe_insync("something") end it "should not be insync if the file does not exist" do content.should = "foo" content.should_not be_safe_insync(:absent) end end end describe "when changing the content" do let(:content) { described_class.new(:resource => resource) } before do resource.stubs(:[]).with(:path).returns "/boo" resource.stubs(:stat).returns "eh" end it "should use the file's :write method to write the content" do resource.expects(:write).with(:content) content.sync end it "should return :file_changed if the file already existed" do resource.expects(:stat).returns "something" resource.stubs(:write) content.sync.should == :file_changed end it "should return :file_created if the file did not exist" do resource.expects(:stat).returns nil resource.stubs(:write) content.sync.should == :file_created end end describe "when writing" do let(:content) { described_class.new(:resource => resource) } let(:fh) { File.open(filename, 'wb') } it "should attempt to read from the filebucket if no actual content nor source exists" do content.should = "{md5}foo" content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo" content.write(fh) fh.close end describe "from actual content" do before(:each) do content.stubs(:actual_content).returns("this is content") end it "should write to the given file handle" do fh = mock 'filehandle' fh.expects(:print).with("this is content") content.write(fh) end it "should return the current checksum value" do resource.parameter(:checksum).expects(:sum_stream).returns "checksum" content.write(fh).should == "checksum" end end describe "from a file bucket" do it "should fail if a file bucket cannot be retrieved" do content.should = "{md5}foo" content.resource.expects(:bucket).returns nil expect { content.write(fh) }.to raise_error(Puppet::Error) end it "should fail if the file bucket cannot find any content" do content.should = "{md5}foo" bucket = stub 'bucket' content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").raises "foobar" expect { content.write(fh) }.to raise_error(Puppet::Error) end it "should write the returned content to the file" do content.should = "{md5}foo" bucket = stub 'bucket' content.resource.expects(:bucket).returns bucket bucket.expects(:getfile).with("foo").returns "mycontent" fh = mock 'filehandle' fh.expects(:print).with("mycontent") content.write(fh) end end describe "from local source" do let(:source_content) { "source file content\r\n"*10 } before(:each) do sourcename = tmpfile('source') resource[:backup] = false resource[:source] = sourcename File.open(sourcename, 'wb') {|f| f.write source_content} # This needs to be invoked to properly initialize the content property, # or attempting to write a file will fail. resource.newattr(:content) end it "should copy content from the source to the file" do source = resource.parameter(:source) resource.write(source) Puppet::FileSystem.binread(filename).should == source_content end with_digest_algorithms do it "should return the checksum computed" do File.open(filename, 'wb') do |file| resource[:checksum] = digest_algorithm content.write(file).should == "{#{digest_algorithm}}#{digest(source_content)}" end end end end describe 'from remote source' do let(:source_content) { "source file content\n"*10 } let(:source) { resource.newattr(:source) } let(:response) { stub_everything('response') } let(:conn) { mock('connection') } before(:each) do resource[:backup] = false # This needs to be invoked to properly initialize the content property, # or attempting to write a file will fail. resource.newattr(:content) response.stubs(:read_body).multiple_yields(*source_content.lines) conn.stubs(:request_get).yields(response) end it 'should use an explicit fileserver if source starts with puppet://' do response.stubs(:code).returns('200') source.stubs(:metadata).returns stub_everything('metadata', :source => 'puppet://somehostname/test/foo', :ftype => 'file') Puppet::Network::HttpPool.expects(:http_instance).with('somehostname', anything).returns(conn) resource.write(source) end it 'should use the default fileserver if source starts with puppet:///' do response.stubs(:code).returns('200') source.stubs(:metadata).returns stub_everything('metadata', :source => 'puppet:///test/foo', :ftype => 'file') Puppet::Network::HttpPool.expects(:http_instance).with(Puppet.settings[:server], anything).returns(conn) resource.write(source) end it 'should percent encode reserved characters' do response.stubs(:code).returns('200') Puppet::Network::HttpPool.stubs(:http_instance).returns(conn) source.stubs(:metadata).returns stub_everything('metadata', :source => 'puppet:///test/foo bar', :ftype => 'file') conn.unstub(:request_get) - conn.expects(:request_get).with('/none/file_content/test/foo%20bar', anything).yields(response) + conn.expects(:request_get).with('/testing/file_content/test/foo%20bar', anything).yields(response) resource.write(source) end describe 'when handling file_content responses' do before(:each) do Puppet::Network::HttpPool.stubs(:http_instance).returns(conn) source.stubs(:metadata).returns stub_everything('metadata', :source => 'puppet:///test/foo', :ftype => 'file') end it 'should not write anything if source is not found' do response.stubs(:code).returns('404') expect { resource.write(source) }.to raise_error(Net::HTTPError, /404/) expect(File.read(filename)).to eq('initial file content') end it 'should raise an HTTP error in case of server error' do response.stubs(:code).returns('500') expect { resource.write(source) }.to raise_error(Net::HTTPError, /500/) end context 'and the request was successful' do before(:each) { response.stubs(:code).returns '200' } it 'should write the contents to the file' do resource.write(source) expect(Puppet::FileSystem.binread(filename)).to eq(source_content) end with_digest_algorithms do it 'should return the checksum computed' do File.open(filename, 'w') do |file| resource[:checksum] = digest_algorithm expect(content.write(file)).to eq("{#{digest_algorithm}}#{digest(source_content)}") end end end end end end # These are testing the implementation rather than the desired behaviour; while that bites, there are a whole # pile of other methods in the File type that depend on intimate details of this implementation and vice-versa. # If these blow up, you are gonna have to review the callers to make sure they don't explode! --daniel 2011-02-01 describe "each_chunk_from should work" do it "when content is a string" do content.each_chunk_from('i_am_a_string') { |chunk| chunk.should == 'i_am_a_string' } end # The following manifest is a case where source and content.should are both set # file { "/tmp/mydir" : # source => '/tmp/sourcedir', # recurse => true, # } it "when content checksum comes from source" do source_param = Puppet::Type.type(:file).attrclass(:source) source = source_param.new(:resource => resource) content.should = "{md5}123abcd" content.expects(:chunk_file_from_source).returns('from_source') content.each_chunk_from(source) { |chunk| chunk.should == 'from_source' } end it "when no content, source, but ensure present" do resource[:ensure] = :present content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # you might do this if you were just auditing it "when no content, source, but ensure file" do resource[:ensure] = :file content.each_chunk_from(nil) { |chunk| chunk.should == '' } end it "when source_or_content is nil and content not a checksum" do content.each_chunk_from(nil) { |chunk| chunk.should == '' } end # the content is munged so that if it's a checksum nil gets passed in it "when content is a checksum it should try to read from filebucket" do content.should = "{md5}123abcd" content.expects(:read_file_from_filebucket).once.returns('im_a_filebucket') content.each_chunk_from(nil) { |chunk| chunk.should == 'im_a_filebucket' } end it "when running as puppet apply" do Puppet[:default_file_terminus] = "file_server" source_or_content = stubs('source_or_content') source_or_content.expects(:content).once.returns :whoo content.each_chunk_from(source_or_content) { |chunk| chunk.should == :whoo } end it "when running from source with a local file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns true content.expects(:chunk_file_from_disk).with(source_or_content).once.yields 'woot' content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end it "when running from source with a remote file" do source_or_content = stubs('source_or_content') source_or_content.expects(:local?).returns false content.expects(:chunk_file_from_source).with(source_or_content).once.yields 'woot' content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' } end end end end diff --git a/spec/unit/util/cacher_spec.rb b/spec/unit/util/cacher_spec.rb deleted file mode 100755 index 4e8bd1ec1..000000000 --- a/spec/unit/util/cacher_spec.rb +++ /dev/null @@ -1,107 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' - -require 'puppet/util/cacher' - -class CacheTest - @@count = 0 - - def self.count - @@count - end - - include Puppet::Util::Cacher - - cached_attr(:instance_cache, 10) do - @@count += 1 - {:number => @@count} - end -end - -describe Puppet::Util::Cacher do - before :each do - CacheTest.set_attr_ttl(:instance_cache, 10) - @object = CacheTest.new - end - - it "should return a value calculated from the provided block" do - @object.instance_cache.should == {:number => CacheTest.count} - end - - it "should return the cached value from the getter every time if the value is not expired" do - @object.instance_cache.should equal(@object.instance_cache) - end - - it "should regenerate and return a new value using the provided block if the value has expired" do - initial = @object.instance_cache - - # Ensure the value is expired immediately - CacheTest.set_attr_ttl(:instance_cache, -10) - @object.send(:set_expiration, :instance_cache) - - @object.instance_cache.should_not equal(initial) - end - - it "should be able to cache false values" do - @object.expects(:init_instance_cache).once.returns false - @object.instance_cache.should be_false - @object.instance_cache.should be_false - end - - it "should cache values again after expiration" do - initial = @object.instance_cache - - # Ensure the value is expired immediately - CacheTest.set_attr_ttl(:instance_cache, -10) - @object.send(:set_expiration, :instance_cache) - - # Reset ttl so this new value doesn't get expired - CacheTest.set_attr_ttl(:instance_cache, 10) - after_expiration = @object.instance_cache - - after_expiration.should_not == initial - @object.instance_cache.should == after_expiration - end - - it "should allow writing of the attribute" do - initial = @object.instance_cache - - @object.instance_cache = "another value" - @object.instance_cache.should == "another value" - end - - it "should update the expiration when the cached attribute is set manually" do - # Freeze time - now = Time.now - Time.stubs(:now).returns now - - @object.instance_cache - - # Set expiration to something far in the future - CacheTest.set_attr_ttl(:instance_cache, 60) - @object.send(:set_expiration, :instance_cache) - - CacheTest.set_attr_ttl(:instance_cache, 10) - - @object.instance_cache = "foo" - @object.instance_variable_get(:@attr_expirations)[:instance_cache].should == now + 10 - end - - it "should allow specification of a ttl as a string" do - klass = Class.new do - include Puppet::Util::Cacher - end - - klass.cached_attr(:myattr, "5") { 10 } - - klass.attr_ttl(:myattr).should == 5 - end - - it "should fail helpfully if the ttl cannot be converted to an integer" do - klass = Class.new do - include Puppet::Util::Cacher - end - - lambda { klass.cached_attr(:myattr, "yep") { 10 } }.should raise_error(ArgumentError) - end -end