diff --git a/acceptance/lib/puppet_x/acceptance/external_cert_fixtures.rb b/acceptance/lib/puppet_x/acceptance/external_cert_fixtures.rb index 7eadc7426..5a8a0610a 100644 --- a/acceptance/lib/puppet_x/acceptance/external_cert_fixtures.rb +++ b/acceptance/lib/puppet_x/acceptance/external_cert_fixtures.rb @@ -1,324 +1,361 @@ module PuppetX module Acceptance class ExternalCertFixtures attr_reader :fixture_dir attr_reader :test_dir attr_reader :master_name attr_reader :agent_name ## # ExternalCerts provides a utility class to fill in fixture data and other # large blobs of text configuration for the acceptance testing of External CA # behavior. # # @param [String] fixture_dir The fixture directory to read from. # # @param [String] test_dir The directory on the remote system, used for # filling in templates. # # @param [String] master_name The common name the master should be reachable # at. This name should match up with the certificate files in the fixture # directory, e.g. master1.example.org. # # @param [String] agent_name The common name the agent is configured to use. # This name should match up with the certificate files in the fixture # directory, e.g. def initialize(fixture_dir, test_dir, master_name = "master1.example.org", agent_name = "agent1.example.org") @fixture_dir = fixture_dir @test_dir = test_dir @master_name = master_name @agent_name = agent_name end def master_short_name @master_short_name ||= master_name.gsub(/\..*/, '') end def host_entry - @host_entry ||= "127.0.0.3 #{master_name} #{master_short_name} puppet\n" + @host_entry ||= "127.0.0.3 #{master_name} #{master_short_name} puppet" end def root_ca_cert @root_ca_cert ||= File.read(File.join(fixture_dir, 'root', 'ca-root.crt')) end def agent_ca_cert @agent_ca_cert ||= File.read(File.join(fixture_dir, 'agent-ca', 'ca-agent-ca.crt')) end def master_ca_cert @master_ca_cert ||= File.read(File.join(fixture_dir, 'master-ca', 'ca-master-ca.crt')) end def master_ca_crl @master_ca_crl ||= File.read(File.join(fixture_dir, 'master-ca', 'ca-master-ca.crl')) end def agent_cert @agent_cert ||= File.read(File.join(fixture_dir, 'leaves', "#{agent_name}.issued_by.agent-ca.crt")) end def agent_key @agent_key ||= File.read(File.join(fixture_dir, 'leaves', "#{agent_name}.issued_by.agent-ca.key")) end def agent_email_cert @agent_email_cert ||= File.read(File.join(fixture_dir, 'leaves', "#{agent_name}.email.issued_by.agent-ca.crt")) end def agent_email_key @agent_email_cert ||= File.read(File.join(fixture_dir, 'leaves', "#{agent_name}.email.issued_by.agent-ca.key")) end def master_cert @master_cert ||= File.read(File.join(fixture_dir, 'leaves', "#{master_name}.issued_by.master-ca.crt")) end def master_key @master_key ||= File.read(File.join(fixture_dir, 'leaves', "#{master_name}.issued_by.master-ca.key")) end def master_cert_rogue @master_cert_rogue ||= File.read(File.join(fixture_dir, 'leaves', "#{master_name}.issued_by.agent-ca.crt")) end def master_key_rogue @master_key_rogue ||= File.read(File.join(fixture_dir, 'leaves', "#{master_name}.issued_by.agent-ca.key")) end ## Configuration files def agent_conf @agent_conf ||= <<-EO_AGENT_CONF [main] color = false certname = #{agent_name} server = #{master_name} certificate_revocation = false # localcacert must contain the Root CA certificate to complete the 2 level CA # chain when an intermediate CA certificate is being used. Either the HTTP # server must send the intermediate certificate during the handshake, or the # agent must use the `ssl_client_ca_auth` setting to provide the client # certificate. localcacert = #{test_dir}/ca_root.crt EO_AGENT_CONF end def agent_conf_email @agent_conf ||= <<-EO_AGENT_CONF [main] color = false certname = #{agent_name} server = #{master_name} certificate_revocation = false hostcert = #{test_dir}/agent_email.crt hostkey = #{test_dir}/agent_email.key localcacert = #{test_dir}/ca_root.crt EO_AGENT_CONF end def agent_conf_crl @agent_conf_crl ||= <<-EO_AGENT_CONF [main] certname = #{agent_name} server = #{master_name} # localcacert must contain the Root CA certificate to complete the 2 level CA # chain when an intermediate CA certificate is being used. Either the HTTP # server must send the intermediate certificate during the handshake, or the # agent must use the `ssl_client_ca_auth` setting to provide the client # certificate. localcacert = #{test_dir}/ca_root.crt EO_AGENT_CONF end def master_conf @master_conf ||= <<-EO_MASTER_CONF [master] ca = false certname = #{master_name} ssl_client_header = HTTP_X_CLIENT_DN ssl_client_verify_header = HTTP_X_CLIENT_VERIFY EO_MASTER_CONF end ## # Passenger Rack compliant config.ru which is responsible for starting the # Puppet master. def config_ru @config_ru ||= <<-EO_CONFIG_RU \$0 = "master" ARGV << "--rack" ARGV << "--confdir=#{test_dir}/etc/master" ARGV << "--vardir=#{test_dir}/etc/master/var" require 'puppet/util/command_line' run Puppet::Util::CommandLine.new.execute EO_CONFIG_RU end ## # auth_conf should return auth authorization file that allows *.example.org # access to to the full REST API. def auth_conf @auth_conf_content ||= File.read(File.join(fixture_dir, 'auth.conf')) end ## # Apache configuration with Passenger def httpd_conf @httpd_conf ||= <<-EO_HTTPD_CONF User apache Group apache ServerRoot "/etc/httpd" PidFile run/httpd.pid Timeout 60 KeepAlive Off MaxKeepAliveRequests 100 KeepAliveTimeout 15 StartServers 8 MinSpareServers 5 MaxSpareServers 20 ServerLimit 256 MaxClients 256 MaxRequestsPerChild 4000 StartServers 4 MaxClients 300 MinSpareThreads 25 MaxSpareThreads 75 ThreadsPerChild 25 MaxRequestsPerChild 0 LoadModule auth_basic_module modules/mod_auth_basic.so LoadModule auth_digest_module modules/mod_auth_digest.so LoadModule authn_file_module modules/mod_authn_file.so LoadModule authn_alias_module modules/mod_authn_alias.so LoadModule authn_anon_module modules/mod_authn_anon.so LoadModule authn_dbm_module modules/mod_authn_dbm.so LoadModule authn_default_module modules/mod_authn_default.so LoadModule authz_host_module modules/mod_authz_host.so LoadModule authz_user_module modules/mod_authz_user.so LoadModule authz_owner_module modules/mod_authz_owner.so LoadModule authz_groupfile_module modules/mod_authz_groupfile.so LoadModule authz_dbm_module modules/mod_authz_dbm.so LoadModule authz_default_module modules/mod_authz_default.so LoadModule ldap_module modules/mod_ldap.so LoadModule authnz_ldap_module modules/mod_authnz_ldap.so LoadModule include_module modules/mod_include.so LoadModule log_config_module modules/mod_log_config.so LoadModule logio_module modules/mod_logio.so LoadModule env_module modules/mod_env.so LoadModule ext_filter_module modules/mod_ext_filter.so LoadModule mime_magic_module modules/mod_mime_magic.so LoadModule expires_module modules/mod_expires.so LoadModule deflate_module modules/mod_deflate.so LoadModule headers_module modules/mod_headers.so LoadModule usertrack_module modules/mod_usertrack.so LoadModule setenvif_module modules/mod_setenvif.so LoadModule mime_module modules/mod_mime.so LoadModule dav_module modules/mod_dav.so LoadModule status_module modules/mod_status.so LoadModule autoindex_module modules/mod_autoindex.so LoadModule info_module modules/mod_info.so LoadModule dav_fs_module modules/mod_dav_fs.so LoadModule vhost_alias_module modules/mod_vhost_alias.so LoadModule negotiation_module modules/mod_negotiation.so LoadModule dir_module modules/mod_dir.so LoadModule actions_module modules/mod_actions.so LoadModule speling_module modules/mod_speling.so LoadModule userdir_module modules/mod_userdir.so LoadModule alias_module modules/mod_alias.so LoadModule substitute_module modules/mod_substitute.so LoadModule rewrite_module modules/mod_rewrite.so LoadModule proxy_module modules/mod_proxy.so LoadModule proxy_balancer_module modules/mod_proxy_balancer.so LoadModule proxy_ftp_module modules/mod_proxy_ftp.so LoadModule proxy_http_module modules/mod_proxy_http.so LoadModule proxy_ajp_module modules/mod_proxy_ajp.so LoadModule proxy_connect_module modules/mod_proxy_connect.so LoadModule cache_module modules/mod_cache.so LoadModule suexec_module modules/mod_suexec.so LoadModule disk_cache_module modules/mod_disk_cache.so LoadModule cgi_module modules/mod_cgi.so LoadModule version_module modules/mod_version.so LoadModule ssl_module modules/mod_ssl.so LoadModule passenger_module modules/mod_passenger.so ServerName #{master_name} DocumentRoot "#{test_dir}/etc/master/public" DefaultType text/plain TypesConfig /etc/mime.types # Same thing, just using a certificate issued by the Agent CA, which should not # be trusted by the clients. Listen 8140 https Listen 8141 https SSLEngine on SSLProtocol ALL -SSLv2 SSLCipherSuite ALL:!ADH:RC4+RSA:+HIGH:+MEDIUM:-LOW:-SSLv2:-EXP SSLCertificateFile "#{test_dir}/master.crt" SSLCertificateKeyFile "#{test_dir}/master.key" # The chain file is sent to the client during handshake. SSLCertificateChainFile "#{test_dir}/ca_master_bundle.crt" # The CA cert file is used to authenticate clients SSLCACertificateFile "#{test_dir}/ca_agent_bundle.crt" SSLVerifyClient optional SSLVerifyDepth 2 SSLOptions +StdEnvVars RequestHeader set X-SSL-Subject %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-DN %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-Verify %{SSL_CLIENT_VERIFY}e DocumentRoot "#{test_dir}/etc/master/public" PassengerRoot /usr/share/gems/gems/passenger-3.0.17 PassengerRuby /usr/bin/ruby RackAutoDetect On RackBaseURI / SSLEngine on SSLProtocol ALL -SSLv2 SSLCipherSuite ALL:!ADH:RC4+RSA:+HIGH:+MEDIUM:-LOW:-SSLv2:-EXP SSLCertificateFile "#{test_dir}/master_rogue.crt" SSLCertificateKeyFile "#{test_dir}/master_rogue.key" SSLCertificateChainFile "#{test_dir}/ca_agent_bundle.crt" SSLCACertificateFile "#{test_dir}/ca_agent_bundle.crt" SSLVerifyClient optional SSLVerifyDepth 2 SSLOptions +StdEnvVars RequestHeader set X-SSL-Subject %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-DN %{SSL_CLIENT_S_DN}e RequestHeader set X-Client-Verify %{SSL_CLIENT_VERIFY}e DocumentRoot "#{test_dir}/etc/master/public" PassengerRoot /usr/share/gems/gems/passenger-3.0.17 PassengerRuby /usr/bin/ruby RackAutoDetect On RackBaseURI / EO_HTTPD_CONF end + + ## + # webserver.conf for a trustworthy master for use with Jetty + def jetty_webserver_conf_for_trustworthy_master + @jetty_webserver_conf_for_trustworthy_master ||= <<-EO_WEBSERVER_CONF +webserver: { + client-auth: want + ssl-host: 0.0.0.0 + ssl-port: 8140 + + ssl-cert: "#{test_dir}/master.crt" + ssl-key: "#{test_dir}/master.key" + + ssl-cert-chain: "#{test_dir}/ca_master_bundle.crt" + ssl-ca-cert: "#{test_dir}/ca_agent_bundle.crt" +} + EO_WEBSERVER_CONF + end + + ## + # webserver.conf for a rogue master for use with Jetty + def jetty_webserver_conf_for_rogue_master + @jetty_webserver_conf_for_rogue_master ||= <<-EO_WEBSERVER_CONF +webserver: { + client-auth: want + ssl-host: 0.0.0.0 + ssl-port: 8140 + + ssl-cert: "#{test_dir}/master_rogue.crt" + ssl-key: "#{test_dir}/master_rogue.key" + + ssl-cert-chain: "#{test_dir}/ca_agent_bundle.crt" + ssl-ca-cert: "#{test_dir}/ca_agent_bundle.crt" +} + EO_WEBSERVER_CONF + end + end end end diff --git a/acceptance/tests/config/puppet_manages_own_configuration_in_robust_manner.rb b/acceptance/tests/config/puppet_manages_own_configuration_in_robust_manner.rb index dafef1cff..543c1d815 100644 --- a/acceptance/tests/config/puppet_manages_own_configuration_in_robust_manner.rb +++ b/acceptance/tests/config/puppet_manages_own_configuration_in_robust_manner.rb @@ -1,82 +1,82 @@ # User story: # A new user has installed puppet either from source or from a gem, which does # not put the "puppet" user or group on the system. They run the puppet master, # which fails because of the missing user and then correct their actions. They # expect that after correcting their actions, puppet will work correctly. test_name "Puppet manages its own configuration in a robust manner" -skip_test "JVM Puppet cannot change its user while running." if @options[:is_jvm_puppet] +skip_test "JVM Puppet cannot change its user while running." if @options[:is_puppetserver] # when owner/group works on windows for settings, this confine should be removed. confine :except, :platform => 'windows' # when managhome roundtrips for solaris, this confine should be removed confine :except, :platform => 'solaris' # pe setup includes ownership of external directories such as the passenger # document root, which puppet itself knows nothing about confine :except, :type => 'pe' # same issue for a foss passenger run if master.is_using_passenger? skip_test 'Cannot test with passenger.' end if master.use_service_scripts? # Beaker defaults to leaving puppet running when using service scripts, # Need to shut it down so we can modify user/group and test startup failure on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=stopped')) end step "Clear out yaml directory because of a bug in the indirector/yaml. (See #21145)" on master, 'rm -rf $(puppet master --configprint yamldir)' original_state = {} step "Record original state of system users" do hosts.each do |host| original_state[host] = {} original_state[host][:user] = user = host.execute('puppet config print user') original_state[host][:group] = group = host.execute('puppet config print group') original_state[host][:ug_resources] = on(host, puppet('resource', 'user', user)).stdout original_state[host][:ug_resources] += on(host, puppet('resource', 'group', group)).stdout original_state[host][:ug_resources] += "Group['#{group}'] -> User['#{user}']\n" end end teardown do # And cleaning up yaml dir again here because we are changing service # user and group ids back to the original uid and gid on master, 'rm -rf $(puppet master --configprint yamldir)' hosts.each do |host| apply_manifest_on(host, <<-ORIG) #{original_state[host][:ug_resources]} ORIG end with_puppet_running_on(master, {}) do agents.each do |agent| on agent, puppet('agent', '-t', '--server', master) end end end step "Remove system users" do hosts.each do |host| on host, puppet('resource', 'user', original_state[host][:user], 'ensure=absent') on host, puppet('resource', 'group', original_state[host][:group], 'ensure=absent') end end step "Ensure master fails to start when missing system user" do on master, puppet('master'), :acceptable_exit_codes => [74] do assert_match(/could not change to group "#{original_state[master][:group]}"/, result.output) assert_match(/Could not change to user #{original_state[master][:user]}/, result.output) end end step "Ensure master starts when making users after having previously failed startup" do with_puppet_running_on(master, :master => { :mkusers => true }) do agents.each do |agent| on agent, puppet('agent', '-t', '--server', master) end end end diff --git a/acceptance/tests/environment/cmdline_overrides_environment.rb b/acceptance/tests/environment/cmdline_overrides_environment.rb index 78a07413b..bbaad76eb 100644 --- a/acceptance/tests/environment/cmdline_overrides_environment.rb +++ b/acceptance/tests/environment/cmdline_overrides_environment.rb @@ -1,322 +1,322 @@ test_name "Commandline modulepath and manifest settings override environment" -skip_test "CLI-master tests are not applicable" if @options[:is_jvm_puppet] +skip_test "CLI-master tests are not applicable" if @options[:is_puppetserver] 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 = { '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/external_ca_support/apache_external_root_ca.rb b/acceptance/tests/external_ca_support/apache_external_root_ca.rb index 39bd194c1..b78d748d9 100644 --- a/acceptance/tests/external_ca_support/apache_external_root_ca.rb +++ b/acceptance/tests/external_ca_support/apache_external_root_ca.rb @@ -1,203 +1,203 @@ begin require 'puppet_x/acceptance/external_cert_fixtures' rescue LoadError $LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__)) require 'puppet_x/acceptance/external_cert_fixtures' end # This test only runs on EL-6 master roles. confine :to, :platform => 'el-6' confine :except, :type => 'pe' -skip_test "Test not supported on jvm" if @options[:is_jvm_puppet] +skip_test "Test not supported on jvm" if @options[:is_puppetserver] 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 our apache instance on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=stopped')) teardown do # And ensure that it is up again after everything is done on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=running')) end end # Verify that a trivial manifest can be run to completion. # Supported Setup: Single, Root CA # - Agent and Master SSL cert issued by the Root CA # - Revocation disabled on the agent `certificate_revocation = false` # - CA disabled on the master `ca = false` # # SUPPORT NOTES # # * If the x509 alt names extension is used when issuing SSL server certificates # for the Puppet master, then the client SSL certificate issued by an external # CA must posses the DNS common name in the alternate name field. This is # due to a bug in Ruby. If the CN is not duplicated in the Alt Names, then # the following error will appear on the agent with MRI 1.8.7: # # Warning: Server hostname 'master1.example.org' did not match server # certificate; expected one of master1.example.org, DNS:puppet, # DNS:master-ca.example.org # # See: https://bugs.ruby-lang.org/issues/6493 test_name "Puppet agent works with Apache, both configured with externally issued certificates from independent intermediate CA's" step "Copy certificates and configuration files to the master..." fixture_dir = File.expand_path('../fixtures', __FILE__) testdir = master.tmpdir('apache_external_root_ca') fixtures = PuppetX::Acceptance::ExternalCertFixtures.new(fixture_dir, testdir) # We need this variable in scope. disable_and_reenable_selinux = nil # Register our cleanup steps early in a teardown so that they will happen even # if execution aborts part way. teardown do step "Cleanup Apache (httpd) and /etc/hosts" # Restore /etc/hosts on master, "cp -p '#{testdir}/hosts' /etc/hosts" # stop the service before moving files around on master, "/etc/init.d/httpd stop" on master, "mv --force /etc/httpd/conf/httpd.conf{,.external_ca_test}" on master, "mv --force /etc/httpd/conf/httpd.conf{.orig,}" if disable_and_reenable_selinux step "Restore the original state of SELinux" on master, "setenforce 1" end end # Read all of the CA certificates. # Copy all of the x.509 fixture data over to the master. create_remote_file master, "#{testdir}/ca_root.crt", fixtures.root_ca_cert create_remote_file master, "#{testdir}/ca_agent.crt", fixtures.agent_ca_cert create_remote_file master, "#{testdir}/ca_master.crt", fixtures.master_ca_cert create_remote_file master, "#{testdir}/ca_master.crl", fixtures.master_ca_crl create_remote_file master, "#{testdir}/ca_master_bundle.crt", "#{fixtures.master_ca_cert}\n#{fixtures.root_ca_cert}\n" create_remote_file master, "#{testdir}/ca_agent_bundle.crt", "#{fixtures.agent_ca_cert}\n#{fixtures.root_ca_cert}\n" create_remote_file master, "#{testdir}/agent.crt", fixtures.agent_cert create_remote_file master, "#{testdir}/agent.key", fixtures.agent_key create_remote_file master, "#{testdir}/agent_email.crt", fixtures.agent_email_cert create_remote_file master, "#{testdir}/agent_email.key", fixtures.agent_email_key create_remote_file master, "#{testdir}/master.crt", fixtures.master_cert create_remote_file master, "#{testdir}/master.key", fixtures.master_key create_remote_file master, "#{testdir}/master_rogue.crt", fixtures.master_cert_rogue create_remote_file master, "#{testdir}/master_rogue.key", fixtures.master_key_rogue ## # Now create the master and agent puppet.conf # # We need to create the public directory for Passenger and the modules # directory to avoid `Error: Could not evaluate: Could not retrieve information # from environment production source(s) puppet://master1.example.org/plugins` on master, "mkdir -p #{testdir}/etc/{master/{public,modules/empty/lib},agent}" # Backup /etc/hosts on master, "cp -p /etc/hosts '#{testdir}/hosts'" # Make master1.example.org resolve if it doesn't already. on master, "grep -q -x '#{fixtures.host_entry}' /etc/hosts || echo '#{fixtures.host_entry}' >> /etc/hosts" create_remote_file master, "#{testdir}/etc/agent/puppet.conf", fixtures.agent_conf create_remote_file master, "#{testdir}/etc/agent/puppet.conf.crl", fixtures.agent_conf_crl create_remote_file master, "#{testdir}/etc/agent/puppet.conf.email", fixtures.agent_conf_email create_remote_file master, "#{testdir}/etc/master/puppet.conf", fixtures.master_conf # auth.conf to allow *.example.com access to the rest API create_remote_file master, "#{testdir}/etc/master/auth.conf", fixtures.auth_conf create_remote_file master, "#{testdir}/etc/master/config.ru", fixtures.config_ru step "Set filesystem permissions and ownership for the master" # These permissions are required for Passenger to start Puppet as puppet on master, "chown -R puppet:puppet #{testdir}/etc/master" # These permissions are just for testing, end users should protect their # private keys. on master, "chmod -R a+rX #{testdir}" agent_cmd_prefix = "--confdir #{testdir}/etc/agent --vardir #{testdir}/etc/agent/var" step "Configure EPEL" epel_release_path = "http://mirror.us.leaseweb.net/epel/6/i386/epel-release-6-8.noarch.rpm" on master, "rpm -q epel-release || (yum -y install #{epel_release_path} && yum -y upgrade epel-release)" step "Configure Apache and Passenger" packages = [ 'httpd', 'mod_ssl', 'mod_passenger', 'rubygem-passenger', 'policycoreutils-python' ] packages.each do |pkg| on master, "rpm -q #{pkg} || (yum -y install #{pkg})" end create_remote_file master, "#{testdir}/etc/httpd.conf", fixtures.httpd_conf on master, 'test -f /etc/httpd/conf/httpd.conf.orig || cp -p /etc/httpd/conf/httpd.conf{,.orig}' on master, "cat #{testdir}/etc/httpd.conf > /etc/httpd/conf/httpd.conf" step "Make SELinux and Apache play nicely together..." on master, "sestatus" do if stdout.match(/Current mode:.*enforcing/) disable_and_reenable_selinux = true else disable_and_reenable_selinux = false end end if disable_and_reenable_selinux on master, "setenforce 0" end step "Start the Apache httpd service..." on master, 'service httpd restart' # Move the agent SSL cert and key into place. # The filename must match the configured certname, otherwise Puppet will try # and generate a new certificate and key step "Configure the agent with the externally issued certificates" on master, "mkdir -p #{testdir}/etc/agent/ssl/{public_keys,certs,certificate_requests,private_keys,private}" create_remote_file master, "#{testdir}/etc/agent/ssl/certs/#{fixtures.agent_name}.pem", fixtures.agent_cert create_remote_file master, "#{testdir}/etc/agent/ssl/private_keys/#{fixtures.agent_name}.pem", fixtures.agent_key # Now, try and run the agent on the master against itself. step "Successfully run the puppet agent on the master" on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do assert_no_match /Creating a new SSL key/, stdout assert_no_match /\Wfailed\W/i, stderr assert_no_match /\Wfailed\W/i, stdout assert_no_match /\Werror\W/i, stderr assert_no_match /\Werror\W/i, stdout # Assert the exit code so we get a "Failed test" instead of an "Errored test" assert exit_code == 0 end step "Agent refuses to connect to a rogue master" on master, puppet_agent("#{agent_cmd_prefix} --ssl_client_ca_auth=#{testdir}/ca_master.crt --masterport=8141 --test"), :acceptable_exit_codes => (0..255) do assert_no_match /Creating a new SSL key/, stdout assert_match /certificate verify failed/i, stderr assert_match /The server presented a SSL certificate chain which does not include a CA listed in the ssl_client_ca_auth file/i, stderr assert exit_code == 1 end step "Master accepts client cert with email address in subject" on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_email}" on master, "cp #{testdir}/etc/agent/puppet.conf{.email,}" on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do assert_no_match /\Wfailed\W/i, stdout assert_no_match /\Wfailed\W/i, stderr assert_no_match /\Werror\W/i, stdout assert_no_match /\Werror\W/i, stderr # Assert the exit code so we get a "Failed test" instead of an "Errored test" assert exit_code == 0 end step "Agent refuses to connect to revoked master" on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_crl}" on master, "cp #{testdir}/etc/agent/puppet.conf{.crl,}" revoke_opts = "--hostcrl #{testdir}/ca_master.crl" on master, puppet_agent("#{agent_cmd_prefix} #{revoke_opts} --test"), :acceptable_exit_codes => (0..255) do assert_match /certificate revoked.*?example.org/, stderr assert exit_code == 1 end step "Finished testing External Certificates" diff --git a/acceptance/tests/external_ca_support/apache_external_root_ca.rb b/acceptance/tests/external_ca_support/jetty_external_root_ca.rb similarity index 52% copy from acceptance/tests/external_ca_support/apache_external_root_ca.rb copy to acceptance/tests/external_ca_support/jetty_external_root_ca.rb index 39bd194c1..be768b891 100644 --- a/acceptance/tests/external_ca_support/apache_external_root_ca.rb +++ b/acceptance/tests/external_ca_support/jetty_external_root_ca.rb @@ -1,203 +1,172 @@ begin require 'puppet_x/acceptance/external_cert_fixtures' rescue LoadError $LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__)) require 'puppet_x/acceptance/external_cert_fixtures' end -# This test only runs on EL-6 master roles. -confine :to, :platform => 'el-6' confine :except, :type => 'pe' -skip_test "Test not supported on jvm" if @options[:is_jvm_puppet] - -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 our apache instance - on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=stopped')) - - teardown do - # And ensure that it is up again after everything is done - on(master, puppet('resource', 'service', master['puppetservice'], 'ensure=running')) - end -end +skip_test "Test only supported on Jetty" unless @options[:is_puppetserver] # Verify that a trivial manifest can be run to completion. # Supported Setup: Single, Root CA # - Agent and Master SSL cert issued by the Root CA # - Revocation disabled on the agent `certificate_revocation = false` # - CA disabled on the master `ca = false` # # SUPPORT NOTES # # * If the x509 alt names extension is used when issuing SSL server certificates # for the Puppet master, then the client SSL certificate issued by an external # CA must posses the DNS common name in the alternate name field. This is # due to a bug in Ruby. If the CN is not duplicated in the Alt Names, then # the following error will appear on the agent with MRI 1.8.7: # # Warning: Server hostname 'master1.example.org' did not match server # certificate; expected one of master1.example.org, DNS:puppet, # DNS:master-ca.example.org # # See: https://bugs.ruby-lang.org/issues/6493 -test_name "Puppet agent works with Apache, both configured with externally issued certificates from independent intermediate CA's" +test_name "Puppet agent and master work when both configured with externally issued certificates from independent intermediate CAs" step "Copy certificates and configuration files to the master..." fixture_dir = File.expand_path('../fixtures', __FILE__) -testdir = master.tmpdir('apache_external_root_ca') +testdir = master.tmpdir('jetty_external_root_ca') fixtures = PuppetX::Acceptance::ExternalCertFixtures.new(fixture_dir, testdir) -# We need this variable in scope. -disable_and_reenable_selinux = nil +jetty_confdir = master['puppetserver-confdir'] # Register our cleanup steps early in a teardown so that they will happen even # if execution aborts part way. teardown do - step "Cleanup Apache (httpd) and /etc/hosts" - # Restore /etc/hosts + step "Restore /etc/hosts and webserver.conf" on master, "cp -p '#{testdir}/hosts' /etc/hosts" - # stop the service before moving files around - on master, "/etc/init.d/httpd stop" - on master, "mv --force /etc/httpd/conf/httpd.conf{,.external_ca_test}" - on master, "mv --force /etc/httpd/conf/httpd.conf{.orig,}" - - if disable_and_reenable_selinux - step "Restore the original state of SELinux" - on master, "setenforce 1" - end + on master, "cp -p '#{testdir}/webserver.conf.orig' '#{jetty_confdir}/webserver.conf'" end # Read all of the CA certificates. # Copy all of the x.509 fixture data over to the master. create_remote_file master, "#{testdir}/ca_root.crt", fixtures.root_ca_cert create_remote_file master, "#{testdir}/ca_agent.crt", fixtures.agent_ca_cert create_remote_file master, "#{testdir}/ca_master.crt", fixtures.master_ca_cert create_remote_file master, "#{testdir}/ca_master.crl", fixtures.master_ca_crl create_remote_file master, "#{testdir}/ca_master_bundle.crt", "#{fixtures.master_ca_cert}\n#{fixtures.root_ca_cert}\n" create_remote_file master, "#{testdir}/ca_agent_bundle.crt", "#{fixtures.agent_ca_cert}\n#{fixtures.root_ca_cert}\n" create_remote_file master, "#{testdir}/agent.crt", fixtures.agent_cert create_remote_file master, "#{testdir}/agent.key", fixtures.agent_key create_remote_file master, "#{testdir}/agent_email.crt", fixtures.agent_email_cert create_remote_file master, "#{testdir}/agent_email.key", fixtures.agent_email_key create_remote_file master, "#{testdir}/master.crt", fixtures.master_cert create_remote_file master, "#{testdir}/master.key", fixtures.master_key create_remote_file master, "#{testdir}/master_rogue.crt", fixtures.master_cert_rogue create_remote_file master, "#{testdir}/master_rogue.key", fixtures.master_key_rogue ## # Now create the master and agent puppet.conf # # We need to create the public directory for Passenger and the modules # directory to avoid `Error: Could not evaluate: Could not retrieve information # from environment production source(s) puppet://master1.example.org/plugins` on master, "mkdir -p #{testdir}/etc/{master/{public,modules/empty/lib},agent}" # Backup /etc/hosts on master, "cp -p /etc/hosts '#{testdir}/hosts'" # Make master1.example.org resolve if it doesn't already. on master, "grep -q -x '#{fixtures.host_entry}' /etc/hosts || echo '#{fixtures.host_entry}' >> /etc/hosts" create_remote_file master, "#{testdir}/etc/agent/puppet.conf", fixtures.agent_conf create_remote_file master, "#{testdir}/etc/agent/puppet.conf.crl", fixtures.agent_conf_crl create_remote_file master, "#{testdir}/etc/agent/puppet.conf.email", fixtures.agent_conf_email -create_remote_file master, "#{testdir}/etc/master/puppet.conf", fixtures.master_conf # auth.conf to allow *.example.com access to the rest API create_remote_file master, "#{testdir}/etc/master/auth.conf", fixtures.auth_conf create_remote_file master, "#{testdir}/etc/master/config.ru", fixtures.config_ru step "Set filesystem permissions and ownership for the master" -# These permissions are required for Passenger to start Puppet as puppet +# These permissions are required for the JVM to start Puppet as puppet on master, "chown -R puppet:puppet #{testdir}/etc/master" +on master, "chown -R puppet:puppet #{testdir}/*.crt" +on master, "chown -R puppet:puppet #{testdir}/*.key" +on master, "chown -R puppet:puppet #{testdir}/*.crl" # These permissions are just for testing, end users should protect their # private keys. on master, "chmod -R a+rX #{testdir}" agent_cmd_prefix = "--confdir #{testdir}/etc/agent --vardir #{testdir}/etc/agent/var" -step "Configure EPEL" -epel_release_path = "http://mirror.us.leaseweb.net/epel/6/i386/epel-release-6-8.noarch.rpm" -on master, "rpm -q epel-release || (yum -y install #{epel_release_path} && yum -y upgrade epel-release)" - -step "Configure Apache and Passenger" -packages = [ 'httpd', 'mod_ssl', 'mod_passenger', 'rubygem-passenger', 'policycoreutils-python' ] -packages.each do |pkg| - on master, "rpm -q #{pkg} || (yum -y install #{pkg})" -end - -create_remote_file master, "#{testdir}/etc/httpd.conf", fixtures.httpd_conf -on master, 'test -f /etc/httpd/conf/httpd.conf.orig || cp -p /etc/httpd/conf/httpd.conf{,.orig}' -on master, "cat #{testdir}/etc/httpd.conf > /etc/httpd/conf/httpd.conf" - -step "Make SELinux and Apache play nicely together..." - -on master, "sestatus" do - if stdout.match(/Current mode:.*enforcing/) - disable_and_reenable_selinux = true - else - disable_and_reenable_selinux = false - end -end - -if disable_and_reenable_selinux - on master, "setenforce 0" -end - -step "Start the Apache httpd service..." -on master, 'service httpd restart' - # Move the agent SSL cert and key into place. # The filename must match the configured certname, otherwise Puppet will try # and generate a new certificate and key step "Configure the agent with the externally issued certificates" on master, "mkdir -p #{testdir}/etc/agent/ssl/{public_keys,certs,certificate_requests,private_keys,private}" create_remote_file master, "#{testdir}/etc/agent/ssl/certs/#{fixtures.agent_name}.pem", fixtures.agent_cert create_remote_file master, "#{testdir}/etc/agent/ssl/private_keys/#{fixtures.agent_name}.pem", fixtures.agent_key -# Now, try and run the agent on the master against itself. -step "Successfully run the puppet agent on the master" -on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do - assert_no_match /Creating a new SSL key/, stdout - assert_no_match /\Wfailed\W/i, stderr - assert_no_match /\Wfailed\W/i, stdout - assert_no_match /\Werror\W/i, stderr - assert_no_match /\Werror\W/i, stdout - # Assert the exit code so we get a "Failed test" instead of an "Errored test" - assert exit_code == 0 -end +on master, "cp -p '#{jetty_confdir}/webserver.conf' '#{testdir}/webserver.conf.orig'" +create_remote_file master, "#{jetty_confdir}/webserver.conf", + fixtures.jetty_webserver_conf_for_trustworthy_master + +master_opts = { + 'master' => { + 'ca' => false, + 'certname' => fixtures.master_name, + 'ssl_client_header' => "HTTP_X_CLIENT_DN", + 'ssl_client_verify_header' => "HTTP_X_CLIENT_VERIFY" + } +} + +step "Start the Puppet master service..." +with_puppet_running_on(master, master_opts) do + # Now, try and run the agent on the master against itself. + step "Successfully run the puppet agent on the master" + on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do + assert_no_match /Creating a new SSL key/, stdout + assert_no_match /\Wfailed\W/i, stderr + assert_no_match /\Wfailed\W/i, stdout + assert_no_match /\Werror\W/i, stderr + assert_no_match /\Werror\W/i, stdout + # Assert the exit code so we get a "Failed test" instead of an "Errored test" + assert exit_code == 0 + end -step "Agent refuses to connect to a rogue master" -on master, puppet_agent("#{agent_cmd_prefix} --ssl_client_ca_auth=#{testdir}/ca_master.crt --masterport=8141 --test"), :acceptable_exit_codes => (0..255) do - assert_no_match /Creating a new SSL key/, stdout - assert_match /certificate verify failed/i, stderr - assert_match /The server presented a SSL certificate chain which does not include a CA listed in the ssl_client_ca_auth file/i, stderr - assert exit_code == 1 -end + step "Master accepts client cert with email address in subject" + on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_email}" + on master, "cp #{testdir}/etc/agent/puppet.conf{.email,}" + on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do + assert_no_match /\Wfailed\W/i, stdout + assert_no_match /\Wfailed\W/i, stderr + assert_no_match /\Werror\W/i, stdout + assert_no_match /\Werror\W/i, stderr + # Assert the exit code so we get a "Failed test" instead of an "Errored test" + assert exit_code == 0 + end + + step "Agent refuses to connect to revoked master" + on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_crl}" + on master, "cp #{testdir}/etc/agent/puppet.conf{.crl,}" -step "Master accepts client cert with email address in subject" -on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_email}" -on master, "cp #{testdir}/etc/agent/puppet.conf{.email,}" -on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do - assert_no_match /\Wfailed\W/i, stdout - assert_no_match /\Wfailed\W/i, stderr - assert_no_match /\Werror\W/i, stdout - assert_no_match /\Werror\W/i, stderr - # Assert the exit code so we get a "Failed test" instead of an "Errored test" - assert exit_code == 0 + revoke_opts = "--hostcrl #{testdir}/ca_master.crl" + on master, puppet_agent("#{agent_cmd_prefix} #{revoke_opts} --test"), :acceptable_exit_codes => (0..255) do + assert_match /certificate revoked.*?example.org/, stderr + assert exit_code == 1 + end end -step "Agent refuses to connect to revoked master" -on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_crl}" -on master, "cp #{testdir}/etc/agent/puppet.conf{.crl,}" +create_remote_file master, "#{jetty_confdir}/webserver.conf", + fixtures.jetty_webserver_conf_for_rogue_master -revoke_opts = "--hostcrl #{testdir}/ca_master.crl" -on master, puppet_agent("#{agent_cmd_prefix} #{revoke_opts} --test"), :acceptable_exit_codes => (0..255) do - assert_match /certificate revoked.*?example.org/, stderr - assert exit_code == 1 +with_puppet_running_on(master, master_opts) do + step "Agent refuses to connect to a rogue master" + on master, puppet_agent("#{agent_cmd_prefix} --ssl_client_ca_auth=#{testdir}/ca_master.crt --test"), :acceptable_exit_codes => (0..255) do + assert_no_match /Creating a new SSL key/, stdout + assert_match /certificate verify failed/i, stderr + assert_match /The server presented a SSL certificate chain which does not include a CA listed in the ssl_client_ca_auth file/i, stderr + assert exit_code == 1 + end end step "Finished testing External Certificates" diff --git a/acceptance/tests/helpful_error_message_when_hostname_not_match_server_certificate.rb b/acceptance/tests/helpful_error_message_when_hostname_not_match_server_certificate.rb index 115e69e2c..d43f6ca42 100644 --- a/acceptance/tests/helpful_error_message_when_hostname_not_match_server_certificate.rb +++ b/acceptance/tests/helpful_error_message_when_hostname_not_match_server_certificate.rb @@ -1,18 +1,45 @@ test_name "generate a helpful error message when hostname doesn't match server certificate" -skip_test "Certs need to be signed with DNS Alt names." if @options[:is_jvm_puppet] skip_test( 'Changing certnames of the master will break PE/Passenger installations' ) if master.is_using_passenger? +certname = "foobar_not_my_hostname" +dns_alt_names = "one_cert,two_cert,red_cert,blue_cert" + +# The DNS names in the certificate's Subject Alternative Name extension +# may appear in any order so sort the list of names alphabetically before +# comparison. +expected_sorted_dns_alt_names = "DNS:" + + dns_alt_names.split(",").push(certname).sort().join(", DNS:") + # Start the master with a certname not matching its hostname master_opts = { 'master' => { - 'certname' => 'foobar_not_my_hostname', - 'dns_alt_names' => 'one_cert,two_cert,red_cert,blue_cert' + 'certname' => certname, + 'dns_alt_names' => dns_alt_names } } + with_puppet_running_on master, master_opts do run_agent_on(agents, "--test --server #{master}", :acceptable_exit_codes => (1..255)) do - msg = "Server hostname '#{master}' did not match server certificate; expected one of foobar_not_my_hostname, DNS:blue_cert, DNS:foobar_not_my_hostname, DNS:one_cert, DNS:red_cert, DNS:two_cert" - assert_match(msg, stderr) + msg = "Server hostname '" + + Regexp.escape(master) + + "' did not match server certificate; expected one of " + + Regexp.escape(certname) + + ', (.*)$' + + exp = Regexp.new (msg) + + match_result = exp.match(stderr) + + assert(match_result, "Expected " + msg + " to match '" + stderr + "'") + + # Sort the expected DNS names in alphabetical order before comparison. + # The names extracted from the shell output might contain color output + # characters at the end (\e[0m), so strip those off before sorting. + actual_sorted_dns_alt_names = match_result[1].sub(/\e\[0m$/,''). + split(", ").sort().join(", ") + + assert_equal(expected_sorted_dns_alt_names, actual_sorted_dns_alt_names, + "Unexpected DNS alt names found in server certificate") end end diff --git a/acceptance/tests/loader/func4x_loadable_from_modules.rb b/acceptance/tests/loader/func4x_loadable_from_modules.rb new file mode 100644 index 000000000..53690a9ec --- /dev/null +++ b/acceptance/tests/loader/func4x_loadable_from_modules.rb @@ -0,0 +1,76 @@ +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') + 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', '--parser', 'future', '-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/node/check_woy_cache_works.rb b/acceptance/tests/node/check_woy_cache_works.rb index 57d7b89ac..881d1aa20 100644 --- a/acceptance/tests/node/check_woy_cache_works.rb +++ b/acceptance/tests/node/check_woy_cache_works.rb @@ -1,47 +1,55 @@ require 'securerandom' require 'puppet/acceptance/temp_file_utils' require 'yaml' extend Puppet::Acceptance::TempFileUtils test_name "ticket #16753 node data should be cached in yaml to allow it to be queried" node_name = "woy_node_#{SecureRandom.hex}" auth_contents = < { - 'rest_authconfig' => authfile + 'rest_authconfig' => authfile, + 'yamldir' => temp_yamldir, } } with_puppet_running_on master, master_opts do # only one agent is needed because we only care about the file written on the master run_agent_on(agents[0], "--no-daemonize --verbose --onetime --node_name_value #{node_name} --server #{master}") yamldir = on(master, puppet('master', '--configprint', 'yamldir')).stdout.chomp on master, puppet('node', 'search', '"*"', '--node_terminus', 'yaml', '--clientyamldir', yamldir, '--render-as', 'json') do assert_match(/"name":["\s]*#{node_name}/, stdout, "Expect node name '#{node_name}' to be present in node yaml content written by the WriteOnlyYaml terminus") end end diff --git a/acceptance/tests/resource/service/ticket_14297_handle_upstart.rb b/acceptance/tests/resource/service/ticket_14297_handle_upstart.rb index 966705583..1400443f1 100644 --- a/acceptance/tests/resource/service/ticket_14297_handle_upstart.rb +++ b/acceptance/tests/resource/service/ticket_14297_handle_upstart.rb @@ -1,52 +1,58 @@ test_name 'Upstart Testing' # only run these on ubuntu vms confine :to, :platform => 'ubuntu' # pick any ubuntu agent agent = agents.first -def check_service_for(pkg, type, agent) - if pkg == "apache2" - if type == "stop" - on agent, "service #{pkg} status", :acceptable_exit_codes => [1,2,3] +def manage_service_for(pkg, state, agent) + + return_code = 0 + + if pkg == 'rabbitmq-server' && state == 'stopped' + return_code = 3 + end + + manifest = <<-MANIFEST + service { '#{pkg}': + ensure => #{state}, + } ~> + exec { 'service #{pkg} status': + path => $path, + logoutput => true, + returns => #{return_code}, + } + MANIFEST + + apply_manifest_on(agent, manifest, :catch_failures => true) do + if pkg == 'rabbitmq-server' + if state == 'running' + assert_match(/Status of node/m, stdout, "Could not start #{pkg}.") + elsif + assert_match(/unable to connect to node/m, stdout, "Could not stop #{pkg}.") + end else - on agent, "service #{pkg} status", :acceptable_exit_codes => [0] + if state == 'running' + assert_match(/start/m, stdout, "Could not start #{pkg}.") + elsif + assert_match(/stop/m, stdout, "Could not stop #{pkg}.") + end end - else - on agent, "service #{pkg} status | grep #{type} -q" end end begin # in Precise these packages provide a mix of upstart with no linked init # script (tty2), upstart linked to an init script (rsyslog), and no upstart -# script - only an init script (apache2) - %w(tty2 rsyslog apache2).each do |pkg| - on agent, puppet_resource("package #{pkg} ensure=present") - - step "Ensure #{pkg} has started" - on agent, "service #{pkg} start", :acceptable_exit_codes => [0,1] +# script - only an init script (rabbitmq-server) + %w(tty2 rsyslog rabbitmq-server).each do |pkg| - step "Check that status for running #{pkg}" - check_service_for(pkg, "start", agent) - - step "Stop #{pkg} with `puppet resource'" - on agent, puppet_resource("service #{pkg} ensure=stopped") - - step "Check that status for stopped #{pkg}" - check_service_for(pkg, "stop", agent) - - step "Start #{pkg} with `puppet resource'" - on agent, puppet_resource("service #{pkg} ensure=running") - - step "Check that status for started #{pkg}" - check_service_for(pkg, "start", agent) - end + on agent, puppet_resource("package #{pkg} ensure=present") - on agent, puppet_resource("service") do - assert_match(/service \{ 'ssh':\n.* ensure => 'running',/, stdout, "SSH isn't running, something is wrong with upstart.") + # Cycle the services + manage_service_for(pkg, "running", agent) + manage_service_for(pkg, "stopped", agent) + manage_service_for(pkg, "running", agent) end -ensure - on agent, puppet_resource("package apache2 ensure=absent") end diff --git a/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb b/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb index c50cda655..135ebf0c4 100644 --- a/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb +++ b/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb @@ -1,121 +1,123 @@ test_name "ENC node information is used when store configs enabled (#16698)" confine :to, :platform => ['debian', 'ubuntu'] confine :except, :platform => 'lucid' +skip_test "Test not supported on jvm" if @options[:is_puppetserver] + testdir = master.tmpdir('use_enc') create_remote_file master, "#{testdir}/enc.rb", < [], 'parameters' => { 'data' => 'data from enc' }, }.to_yaml) END on master, "chmod 755 #{testdir}/enc.rb" create_remote_file(master, "#{testdir}/site.pp", 'notify { $data: }') on master, "chown -R #{master['user']}:#{master['group']} #{testdir}" on master, "chmod -R g+rwX #{testdir}" create_remote_file master, "#{testdir}/setup.pp", < $lsbmajdistrelease ? { 5 => '2.2.3', default => '3.2.16', }, default => '3.2.16', } # Trusty doesn't have a rubygems package anymore # Not sure which other Debian's might follow suit so # restricting this narrowly for now # if $lsbdistid == "Ubuntu" and $lsbdistrelease == "14.04" { package { activerecord: ensure => $active_record_version, provider => 'gem', } } else { package { rubygems: ensure => present; activerecord: ensure => $active_record_version, provider => 'gem', require => Package[rubygems]; } } if $osfamily == "Debian" { package { # This is the deb sqlite3 package sqlite3: ensure => present; libsqlite3-dev: ensure => present, require => Package[sqlite3]; } } elsif $osfamily == "RedHat" { $sqlite_gem_pkg_name = $operatingsystem ? { "Fedora" => "rubygem-sqlite3", default => "rubygem-sqlite3-ruby" } package { sqlite: ensure => present; $sqlite_gem_pkg_name: ensure => present, require => Package[sqlite] } } else { fail "Unknown OS $osfamily" } END # This is a brute force hack around PUP-1073 because the deb for the core # sqlite3 package and the rubygem for the sqlite3 driver are both named # 'sqlite3'. So we just run a second puppet apply. create_remote_file master, "#{testdir}/setup_sqlite_gem.pp", < 'sqlite3', ensure => present, provider => 'gem', } } END on master, puppet_apply("#{testdir}/setup.pp") on master, puppet_apply("#{testdir}/setup_sqlite_gem.pp") master_opts = { 'master' => { 'node_terminus' => 'exec', 'external_nodes' => "#{testdir}/enc.rb", 'storeconfigs' => true, 'dbadapter' => 'sqlite3', 'dblocation' => "#{testdir}/store_configs.sqlite3", 'manifest' => "#{testdir}/site.pp" } } with_puppet_running_on master, master_opts, testdir do agents.each do |agent| run_agent_on(agent, "--no-daemonize --onetime --server #{master} --verbose") assert_match(/data from enc/, stdout) end end diff --git a/ext/systemd/puppet.service b/ext/systemd/puppet.service index 604e47c45..3f89df8d7 100644 --- a/ext/systemd/puppet.service +++ b/ext/systemd/puppet.service @@ -1,12 +1,12 @@ [Unit] Description=Puppet agent Wants=basic.target After=basic.target network.target puppetmaster.service [Service] EnvironmentFile=-/etc/sysconfig/puppetagent EnvironmentFile=-/etc/sysconfig/puppet -ExecStart=/usr/bin/puppet agent ${PUPPET_EXTRA_OPTS} --no-daemonize +ExecStart=/usr/bin/puppet agent $PUPPET_EXTRA_OPTS --no-daemonize [Install] WantedBy=multi-user.target diff --git a/ext/systemd/puppetmaster.service b/ext/systemd/puppetmaster.service index 8a6f11749..7132a2694 100644 --- a/ext/systemd/puppetmaster.service +++ b/ext/systemd/puppetmaster.service @@ -1,11 +1,11 @@ [Unit] Description=Puppet master Wants=basic.target After=basic.target network.target [Service] EnvironmentFile=-/etc/sysconfig/puppetmaster -ExecStart=/usr/bin/puppet master ${PUPPETMASTER_EXTRA_OPTS} --no-daemonize +ExecStart=/usr/bin/puppet master $PUPPETMASTER_EXTRA_OPTS --no-daemonize [Install] WantedBy=multi-user.target diff --git a/lib/puppet/application/resource.rb b/lib/puppet/application/resource.rb index 4a3a4491a..8ed392ee2 100644 --- a/lib/puppet/application/resource.rb +++ b/lib/puppet/application/resource.rb @@ -1,228 +1,240 @@ require 'puppet/application' class Puppet::Application::Resource < Puppet::Application attr_accessor :host, :extra_params def preinit @extra_params = [] end option("--debug","-d") option("--verbose","-v") option("--edit","-e") + option("--to_yaml","-y") option("--host HOST","-H") do |arg| Puppet.warning("Accessing resources on the network is deprecated. See http://links.puppetlabs.com/deprecate-networked-resource") @host = arg end option("--types", "-t") do |arg| types = [] Puppet::Type.loadall Puppet::Type.eachtype do |t| next if t.name == :component types << t.name.to_s end puts types.sort exit end option("--param PARAM", "-p") do |arg| @extra_params << arg.to_sym end def help <<-'HELP' puppet-resource(8) -- The resource abstraction layer shell ======== SYNOPSIS -------- Uses the Puppet RAL to directly interact with the system. USAGE ----- puppet resource [-h|--help] [-d|--debug] [-v|--verbose] [-e|--edit] - [-H|--host ] [-p|--param ] [-t|--types] + [-H|--host ] [-p|--param ] [-t|--types] [-y|--to_yaml] [] [= ...] DESCRIPTION ----------- This command provides simple facilities for converting current system state into Puppet code, along with some ability to modify the current state using Puppet's RAL. By default, you must at least provide a type to list, in which case puppet resource will tell you everything it knows about all resources of that type. You can optionally specify an instance name, and puppet resource will only describe that single instance. If given a type, a name, and a series of = pairs, puppet resource will modify the state of the specified resource. Alternately, if given a type, a name, and the '--edit' flag, puppet resource will write its output to a file, open that file in an editor, and then apply the saved file as a Puppet transaction. OPTIONS ------- Note that any setting that's valid in the configuration file is also a valid long argument. For example, 'ssldir' is a valid setting, so you can specify '--ssldir ' as an argument. See the configuration file documentation at http://docs.puppetlabs.com/references/stable/configuration.html for the full list of acceptable parameters. A commented list of all configuration options can also be generated by running puppet with '--genconfig'. * --debug: Enable full debugging. * --edit: Write the results of the query to a file, open the file in an editor, and read the file back in as an executable Puppet manifest. * --host: When specified, connect to the resource server on the named host and retrieve the list of resouces of the type specified. * --help: Print this help message. * --param: Add more parameters to be outputted from queries. * --types: List all available types. * --verbose: Print extra information. +* --to_yaml: + Output found resources in yaml format, suitable to use with Hiera and create_resources. EXAMPLE ------- This example uses `puppet resource` to return a Puppet configuration for the user `luke`: $ puppet resource user luke user { 'luke': home => '/home/luke', uid => '100', ensure => 'present', comment => 'Luke Kanies,,,', gid => '1000', shell => '/bin/bash', groups => ['sysadmin','audio','video','puppet'] } AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def main type, name, params = parse_args(command_line.args) raise "You cannot edit a remote host" if options[:edit] and @host + raise "Editing with Yaml output is not supported" if options[:edit] and options[:to_yaml] resources = find_or_save_resources(type, name, params) - text = resources. - map { |resource| resource.prune_parameters(:parameters_to_include => @extra_params).to_manifest }. - join("\n") + + if options[:to_yaml] + text = resources. + map { |resource| resource.prune_parameters(:parameters_to_include => @extra_params).to_hierayaml }. + join("\n") + text.prepend("#{type.downcase}:\n") + else + text = resources. + map { |resource| resource.prune_parameters(:parameters_to_include => @extra_params).to_manifest }. + join("\n") + end options[:edit] ? handle_editing(text) : (puts text) end def setup Puppet::Util::Log.newdestination(:console) set_log_level end private def remote_key(type, name) Puppet::Resource.indirection.terminus_class = :rest port = Puppet[:puppetport] ["https://#{@host}:#{port}", "production", "resources", type, name].join('/') end def local_key(type, name) [type, name].join('/') end def handle_editing(text) require 'tempfile' # Prefer the current directory, which is more likely to be secure # and, in the case of interactive use, accessible to the user. tmpfile = Tempfile.new('x2puppet', Dir.pwd) begin # sync write, so nothing buffers before we invoke the editor. tmpfile.sync = true tmpfile.puts text # edit the content system(ENV["EDITOR"] || 'vi', tmpfile.path) # ...and, now, pass that file to puppet to apply. Because # many editors rename or replace the original file we need to # feed the pathname, not the file content itself, to puppet. system('puppet apply -v ' + tmpfile.path) ensure # The temporary file will be safely removed. tmpfile.close(true) end end def parse_args(args) type = args.shift or raise "You must specify the type to display" Puppet::Type.type(type) or raise "Could not find type #{type}" name = args.shift params = {} args.each do |setting| if setting =~ /^(\w+)=(.+)$/ params[$1] = $2 else raise "Invalid parameter setting #{setting}" end end [type, name, params] end def find_or_save_resources(type, name, params) key = @host ? remote_key(type, name) : local_key(type, name) if name if params.empty? [ Puppet::Resource.indirection.find( key ) ] else resource = Puppet::Resource.new( type, name, :parameters => params ) # save returns [resource that was saved, transaction log from applying the resource] save_result = Puppet::Resource.indirection.save(resource, key) [ save_result.first ] end else if type == "file" raise "Listing all file instances is not supported. Please specify a file or directory, e.g. puppet resource file /etc" end Puppet::Resource.indirection.search( key, {} ) end end end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 7b464ee53..cb9f3714a 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,2078 +1,2071 @@ 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).} STORECONFIGS_ONLY = %q{This setting is only used by the ActiveRecord storeconfigs and inventory backends, which are deprecated.} # 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 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 ", }, :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", }, :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.", :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 => "", :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 => { :default => false, :desc => "Turns the binding system on or off. This includes bindings in modules. The binding system aggregates data from modules and other locations and makes them available for lookup. The binding system is experimental and any or all of it may change.", :type => :boolean, }, :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.", :call_hook => :on_initialize_and_write, :hook => proc do |value| require 'puppet/node/facts' # Cache to YAML if we're uploading facts away if %w[rest inventory_service].include? value.to_s Puppet.info "configuring the YAML fact cache because a remote terminus is active" Puppet::Node::Facts.indirection.cache_class = :yaml end end }, :inventory_terminus => { :type => :terminus, :default => "$facts_terminus", :desc => "Should usually be the same as the 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." }, :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." }, :queue_type => { :default => "stomp", :desc => "Which type of queue to use for asynchronous processing.", }, :queue_type => { :default => "stomp", :desc => "Which type of queue to use for asynchronous processing.", }, :queue_source => { :default => "stomp://localhost:61613/", :desc => "Which type of queue to use for asynchronous processing. If your stomp server requires authentication, you can include it in the URI as long as your stomp client library is at least 1.1.1", }, :async_storeconfigs => { :default => false, :type => :boolean, :desc => "Whether to use a queueing system to provide asynchronous database integration. Requires that `puppet queue` be running.", :hook => proc do |value| if value # This reconfigures the termini for Node, Facts, and Catalog Puppet.settings.override_default(:storeconfigs, true) # But then we modify the configuration Puppet::Resource::Catalog.indirection.cache_class = :queue Puppet.settings.override_default(:catalog_cache_terminus, :queue) else raise "Cannot disable asynchronous storeconfigs in a running process" end end }, :thin_storeconfigs => { :default => false, :type => :boolean, :desc => "Boolean; whether Puppet should store only facts and exported resources in the storeconfigs database. This will improve the performance of exported resources with the older `active_record` backend, but will disable external tools that search the storeconfigs database. Thinning catalogs is generally unnecessary when using PuppetDB to store catalogs.", :hook => proc do |value| Puppet.settings.override_default(:storeconfigs, true) if value end }, :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, }, :zlib => { :default => true, :type => :boolean, :desc => "Boolean; whether to use the zlib library", }, :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.", }, :stringify_facts => { :default => true, :type => :boolean, :desc => "Flatten fact values to strings using #to_s. Means you can't have arrays or hashes as fact values. (DEPRECATED) This option will be removed in Puppet 4.0.", }, :trusted_node_data => { :default => false, :type => :boolean, :desc => "Stores trusted node data in a hash called $trusted. When true also prevents $trusted from being overridden in any scope.", }, :immutable_node_data => { :default => '$trusted_node_data', :type => :boolean, :desc => "When true, also prevents $trusted and $facts from being overridden in any scope", } ) 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 }}, :certdnsnames => { :default => '', :hook => proc do |value| unless value.nil? or value == '' then Puppet.warning < < { :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.", }, :certificate_expire_warning => { :default => "60d", :type => :duration, :desc => "The window of time leading up to a certificate's expiration that a notification will be logged. This applies to CA, master, and agent certificates. #{AS_DURATION}" }, :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, }, :manifest => { :default => "$manifestdir/site.pp", :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 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, }, :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.", - :hook => proc do |value| - uninterpolated_value = self.value(true) - if uninterpolated_value =~ /\$environment/ || value =~ /\$environment/ then - raise(Puppet::Settings::ValidationError, - "You cannot interpolate '$environment' within the 'default_manifest' setting.") - end - end }, :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.", }, :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 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, tagmail`.) 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(:metrics, :rrddir => { :type => :directory, :default => "$vardir/rrd", :mode => "0750", :owner => "service", :group => "service", :desc => "The directory where RRD database files are stored. Directories for each reporting host will be created under this directory." }, :rrdinterval => { :default => "$runinterval", :type => :duration, :desc => "How often RRD should expect data. This should match how often the hosts report back to the server. #{AS_DURATION}", } ) 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 }, :localconfig => { :default => "$statedir/localconfig", :type => :file, :owner => "root", :mode => "0660", :desc => "Where puppet agent caches the local configuration. An extension indicating the cache format is added automatically."}, :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.", }, :puppetport => { :default => 8139, :desc => "Which port puppet agent listens on.", }, :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}", }, :listen => { :default => false, :type => :boolean, :desc => "Whether puppet agent should listen for connections. If this is true, then puppet agent will accept incoming REST API requests, subject to the default ACLs and the ACLs set in the `rest_authconfig` file. Puppet agent can respond usefully to requests on the `run`, `facts`, `certificate`, and `resource` endpoints.", }, :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.", }, :catalog_format => { :default => "", :desc => "(Deprecated for 'preferred_serialization_format') What format to use to dump the catalog. Only supports 'marshal' and 'yaml'. Only matters on the client, since it asks the server for a specific format.", :hook => proc { |value| if value Puppet.deprecation_warning "Setting 'catalog_format' is deprecated; use 'preferred_serialization_format' instead." Puppet.settings.override_default(:preferred_serialization_format, value) end } }, :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.", }, :report_serialization_format => { :default => "pson", :type => :enum, :values => ["pson", "yaml"], :desc => "The serialization format to use when sending reports to the `report_server`. Possible values are `pson` and `yaml`. This setting affects puppet agent, but not puppet apply (which processes its own reports). This should almost always be set to `pson`. It can be temporarily set to `yaml` to let agents using this Puppet version connect to a puppet master running Puppet 3.0.0 through 3.2.x. Note that this is set to 'yaml' automatically if the agent detects an older master, so should never need to be set explicitly." }, :legacy_query_parameter_serialization => { :default => false, :type => :boolean, :desc => "The serialization format to use when sending file_metadata query parameters. Older versions of puppet master expect certain query parameters to be serialized as yaml, which is deprecated. This should almost always be false. It can be temporarily set to true to let agents using this Puppet version connect to a puppet master running Puppet 3.0.0 through 3.2.x. Note that this is set to true automatically if the agent detects an older master, so should never need to be set explicitly." }, :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.", }, :dynamicfacts => { :default => "memorysize,memoryfree,swapsize,swapfree", :desc => "(Deprecated) Facts that are dynamic; these facts will be ignored when deciding whether changed facts should result in a recompile. Multiple facts should be comma-separated.", :hook => proc { |value| if value Puppet.deprecation_warning "The dynamicfacts setting is deprecated and will be ignored." end } }, :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 can help reduce flapping if too many clients contact the server at one time. #{AS_DURATION}", }, :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.", }, :inventory_server => { :default => "$server", :desc => "The server to send facts to.", }, :inventory_port => { :default => "$masterport", :desc => "The port to communicate with the inventory_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.", }, :http_compression => { :default => false, :type => :boolean, :desc => "Allow http compression in REST communication with the master. This setting might improve performance for agent -> master communications over slow WANs. Your puppet master needs to support compression (usually by activating some settings in a reverse-proxy in front of the puppet master, which rules out webrick). It is harmless to activate this settings if your master doesn't support compression, but if it supports it, this setting might reduce performance on high-speed LANs.", }, :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. * `title-hash` (the default) will order resources randomly, but will use the same order across runs and across nodes. * `manifest` will use the order in which the resources were declared in their manifest files. * `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://$server/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://$server/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( :tagmail, :tagmap => { :default => "$confdir/tagmail.conf", :desc => "The mapping between reporting tags and email addresses.", }, :sendmail => { :default => which('sendmail') || '', :desc => "Where to find the sendmail binary with which to send email.", }, :reportfrom => { :default => lambda { "report@#{Puppet::Settings.default_certname.downcase}" }, :desc => "The 'from' email address for the reports.", }, :smtpserver => { :default => "none", :desc => "The server through which to send email reports.", }, :smtpport => { :default => 25, :desc => "The TCP port through which to send email reports.", }, :smtphelo => { :default => lambda { Facter.value 'fqdn' }, :desc => "The name by which we identify ourselves in SMTP HELO for reports. If you send to a smtpserver which does strict HELO checking (as with Postfix's `smtpd_helo_restrictions` access controls), you may need to ensure this resolves.", } ) define_settings( :rails, :dblocation => { :default => "$statedir/clientconfigs.sqlite3", :type => :file, :mode => "0660", :owner => "service", :group => "service", :desc => "The sqlite database file. #{STORECONFIGS_ONLY}" }, :dbadapter => { :default => "sqlite3", :desc => "The type of database to use. #{STORECONFIGS_ONLY}", }, :dbmigrate => { :default => false, :type => :boolean, :desc => "Whether to automatically migrate the database. #{STORECONFIGS_ONLY}", }, :dbname => { :default => "puppet", :desc => "The name of the database to use. #{STORECONFIGS_ONLY}", }, :dbserver => { :default => "localhost", :desc => "The database server for caching. Only used when networked databases are used.", }, :dbport => { :default => "", :desc => "The database password for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbuser => { :default => "puppet", :desc => "The database user for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbpassword => { :default => "puppet", :desc => "The database password for caching. Only used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbconnections => { :default => '', :desc => "The number of database connections for networked databases. Will be ignored unless the value is a positive integer. #{STORECONFIGS_ONLY}", }, :dbsocket => { :default => "", :desc => "The database socket location. Only used when networked databases are used. Will be ignored if the value is an empty string. #{STORECONFIGS_ONLY}", }, :railslog => { :default => "$logdir/rails.log", :type => :file, :mode => "0600", :owner => "service", :group => "service", :desc => "Where Rails-specific logs are sent. #{STORECONFIGS_ONLY}" }, :rails_loglevel => { :default => "info", :desc => "The log level for Rails connections. The value must be a valid log level within Rails. Production environments normally use `info` and other environments normally use `debug`. #{STORECONFIGS_ONLY}", } ) define_settings( :couchdb, :couchdb_url => { :default => "http://127.0.0.1:5984/puppet", :desc => "The url where the puppet couchdb database will be created. Only used when `facts_terminus` is set to `couch`.", } ) 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 ActiveRecord and an SQL database to store and query the data; this, in turn, will depend on Rails being available. 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 if not Puppet.settings[:async_storeconfigs] Puppet::Resource::Catalog.indirection.cache_class = :store_configs Puppet.settings.override_default(:catalog_cache_terminus, :store_configs) end Puppet::Node::Facts.indirection.cache_class = :store_configs Puppet::Resource.indirection.terminus_class = :store_configs end end }, :storeconfigs_backend => { :type => :terminus, :default => "active_record", :desc => "Configure the backend terminus used for StoreConfigs. By default, this uses the ActiveRecord store, which directly talks to the database from within the Puppet Master process." } ) 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 }, :parser => { :default => "current", :desc => <<-'EOT' Selects the parser to use for parsing puppet manifests (in puppet DSL language/'.pp' files). Available choices are `current` (the default) and `future`. The `current` parser means that the released version of the parser should be used. The `future` parser is a "time travel to the future" allowing early exposure to new language features. What these features are will vary from release to release and they may be invididually configurable. Available Since Puppet 3.2. 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/module_tool/applications/unpacker.rb b/lib/puppet/module_tool/applications/unpacker.rb index 8ffef3d64..1c609f200 100644 --- a/lib/puppet/module_tool/applications/unpacker.rb +++ b/lib/puppet/module_tool/applications/unpacker.rb @@ -1,100 +1,100 @@ require 'pathname' require 'tmpdir' require 'json' require 'puppet/file_system' module Puppet::ModuleTool module Applications class Unpacker < Application def self.unpack(filename, target) app = self.new(filename, :target_dir => target) app.unpack app.sanity_check app.move_into(target) end def self.harmonize_ownership(source, target) unless Puppet.features.microsoft_windows? source = Pathname.new(source) unless source.respond_to?(:stat) target = Pathname.new(target) unless target.respond_to?(:stat) FileUtils.chown_R(source.stat.uid, source.stat.gid, target) end end def initialize(filename, options = {}) @filename = Pathname.new(filename) super(options) @module_path = Pathname(options[:target_dir]) end def run unpack sanity_check module_dir = @module_path + module_name move_into(module_dir) # Return the Pathname object representing the directory where the # module release archive was unpacked the to. return module_dir end # @api private # Error on symlinks and other junk def sanity_check symlinks = Dir.glob("#{tmpdir}/**/*", File::FNM_DOTMATCH).map { |f| Pathname.new(f) }.select {|p| Puppet::FileSystem.symlink? p} tmpdirpath = Pathname.new tmpdir symlinks.each do |s| - Puppet.warning "Symlinks in modules are unsupported. Please investigate symlink #{s.relative_path_from tmpdirpath}->#{s.realpath.relative_path_from tmpdirpath}." + Puppet.warning "Symlinks in modules are unsupported. Please investigate symlink #{s.relative_path_from tmpdirpath}->#{Puppet::FileSystem.readlink(s)}." end end # @api private def unpack begin Puppet::ModuleTool::Tar.instance.unpack(@filename.to_s, tmpdir, [@module_path.stat.uid, @module_path.stat.gid].join(':')) rescue Puppet::ExecutionFailure => e raise RuntimeError, "Could not extract contents of module archive: #{e.message}" end end # @api private def root_dir return @root_dir if @root_dir # Grab the first directory containing a metadata.json file metadata_file = Dir["#{tmpdir}/**/metadata.json"].sort_by(&:length)[0] if metadata_file @root_dir = Pathname.new(metadata_file).dirname else raise "No valid metadata.json found!" end end # @api private def module_name metadata = JSON.parse((root_dir + 'metadata.json').read) name = metadata['name'][/-(.*)/, 1] end # @api private def move_into(dir) dir = Pathname.new(dir) dir.rmtree if dir.exist? FileUtils.mv(root_dir, dir) ensure FileUtils.rmtree(tmpdir) end # Obtain a suitable temporary path for unpacking tarballs # # @api private # @return [String] path to temporary unpacking location def tmpdir @dir ||= Dir.mktmpdir('tmp-unpacker', Puppet::Forge::Cache.base_path) end end end end diff --git a/lib/puppet/pops/loader/loader_paths.rb b/lib/puppet/pops/loader/loader_paths.rb index 09bb7e5b0..505887915 100644 --- a/lib/puppet/pops/loader/loader_paths.rb +++ b/lib/puppet/pops/loader/loader_paths.rb @@ -1,118 +1,118 @@ # LoaderPaths # === # The central loader knowledge about paths, what they represent and how to instantiate from them. # Contains helpers (*smart paths*) to deal with lazy resolution of paths. # # TODO: Currently only supports loading of functions (3 kinds) # module Puppet::Pops::Loader::LoaderPaths # Returns an array of SmartPath, each instantiated with a reference to the given loader (for root path resolution # and existence checks). The smart paths in the array appear in precedence order. The returned array may be # mutated. # def self.relative_paths_for_type(type, loader) result = case type when :function [FunctionPath4x.new(loader)] else # unknown types, simply produce an empty result; no paths to check, nothing to find... move along... [] end result end # # DO NOT REMOVE YET. needed later? when there is the need to decamel a classname # def de_camel(fq_name) # fq_name.to_s.gsub(/::/, '/'). # gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). # gsub(/([a-z\d])([A-Z])/,'\1_\2'). # tr("-", "_"). # downcase # end class SmartPath # Generic path, in the sense of "if there are any entities of this kind to load, where are they?" attr_reader :generic_path # Creates SmartPath for the given loader (loader knows how to check for existence etc.) def initialize(loader) @loader = loader end def generic_path() return @generic_path unless @generic_path.nil? root_path = @loader.path @generic_path = (root_path.nil? ? relative_path : File.join(root_path, relative_path)) end # Effective path is the generic path + the name part(s) + extension. # def effective_path(typed_name, start_index_in_name) "#{File.join(generic_path, typed_name.name_parts)}#{extension}" end def relative_path() raise NotImplementedError.new end def instantiator() raise NotImplementedError.new end end class RubySmartPath < SmartPath def extension ".rb" end # Duplication of extension information, but avoids one call def effective_path(typed_name, start_index_in_name) "#{File.join(generic_path, typed_name.name_parts)}.rb" end end class FunctionPath4x < RubySmartPath - FUNCTION_PATH_4X = File.join('lib', 'puppet', 'functions') + FUNCTION_PATH_4X = File.join('puppet', 'functions') def relative_path FUNCTION_PATH_4X end def instantiator() Puppet::Pops::Loader::RubyFunctionInstantiator end end # SmartPaths # === # Holds effective SmartPath instances per type # class SmartPaths def initialize(path_based_loader) @loader = path_based_loader @smart_paths = {} end # Ensures that the paths for the type have been probed and pruned to what is existing relative to # the given root. # # @param type [Symbol] the entity type to load # @return [Array] array of effective paths for type (may be empty) # def effective_paths(type) smart_paths = @smart_paths loader = @loader unless effective_paths = smart_paths[type] # type not yet processed, does the various directories for the type exist ? # Get the relative dirs for the type paths_for_type = Puppet::Pops::Loader::LoaderPaths.relative_paths_for_type(type, loader) # Check which directories exist in the loader's content/index effective_paths = smart_paths[type] = paths_for_type.select { |sp| loader.meaningful_to_search?(sp) } end effective_paths end end end diff --git a/lib/puppet/pops/loader/module_loaders.rb b/lib/puppet/pops/loader/module_loaders.rb index 40ff5141d..3edd51561 100644 --- a/lib/puppet/pops/loader/module_loaders.rb +++ b/lib/puppet/pops/loader/module_loaders.rb @@ -1,242 +1,256 @@ # =ModuleLoaders # A ModuleLoader loads items from a single module. # The ModuleLoaders (ruby) module contains various such loaders. There is currently one concrete # implementation, ModuleLoaders::FileBased that loads content from the file system. # Other implementations can be created - if they are based on name to path mapping where the path # is relative to a root path, they can derive the base behavior from the ModuleLoaders::AbstractPathBasedModuleLoader class. # # Examples of such extensions could be a zip/jar/compressed file base loader. # # Notably, a ModuleLoader does not configure itself - it is given the information it needs (the root, its name etc.) # Logic higher up in the loader hierarchy of things makes decisions based on the "shape of modules", and "available # modules" to determine which module loader to use for each individual module. (There could be differences in # internal layout etc.) # # A module loader is also not aware of the mapping of name to relative paths - this is performed by the # included module Puppet::Pops::Loader::PathBasedInstantatorConfig which knows about the map from type/name to # relative path, and the logic that can instantiate what is expected to be found in the content of that path. # # @api private # module Puppet::Pops::Loader::ModuleLoaders + def self.system_loader_from(parent_loader, loaders) + # Puppet system may be installed in a fixed location via RPM, installed as a Gem, via source etc. + # The only way to find this across the different ways puppet can be installed is + # to search up the path from this source file's __FILE__ location until it finds the base of + # puppet. + # + puppet_lib = File.join(File.dirname(__FILE__), '../../..') + Puppet::Pops::Loader::ModuleLoaders::FileBased.new(parent_loader, + loaders, + nil, + puppet_lib, + 'puppet_system') + end + + def self.module_loader_from(parent_loader, loaders, module_name, module_path) + Puppet::Pops::Loader::ModuleLoaders::FileBased.new(parent_loader, + loaders, + module_name, + File.join(module_path, 'lib'), + module_name) + end + class AbstractPathBasedModuleLoader < Puppet::Pops::Loader::BaseLoader # The name of the module, or nil, if this is a global "component" attr_reader :module_name # The path to the location of the module/component - semantics determined by subclass attr_reader :path # A map of type to smart-paths that help with minimizing the number of paths to scan attr_reader :smart_paths # A Module Loader has a private loader, it is lazily obtained on request to provide the visibility # for entities contained in the module. Since a ModuleLoader also represents an environment and it is # created a different way, this loader can be set explicitly by the loaders bootstrap logic. # # @api private attr_accessor :private_loader # Initialize a kind of ModuleLoader for one module # @param parent_loader [Puppet::Pops::Loader] loader with higher priority # @param module_name [String] the name of the module (non qualified name), may be nil for a global "component" # @param path [String] the path to the root of the module (semantics defined by subclass) # @param loader_name [String] a name that is used for human identification (useful when module_name is nil) # def initialize(parent_loader, loaders, module_name, path, loader_name) super parent_loader, loader_name - # Irrespective of the path referencing a directory or file, the path must exist. - unless Puppet::FileSystem.exist?(path) - raise ArgumentError, "The given path '#{path}' does not exist!" - end - @module_name = module_name @path = path @smart_paths = Puppet::Pops::Loader::LoaderPaths::SmartPaths.new(self) @loaders = loaders end # Finds typed/named entity in this module # @param typed_name [Puppet::Pops::Loader::TypedName] the type/name to find # @return [Puppet::Pops::Loader::Loader::NamedEntry, nil found/created entry, or nil if not found # def find(typed_name) # Assume it is a global name, and that all parts of the name should be used when looking up name_part_index = 0 name_parts = typed_name.name_parts # Certain types and names can be disqualified up front if name_parts.size > 1 # The name is in a name space. # Then entity cannot possible be in this module unless the name starts with the module name. # Note: If "module" represents a "global component", the module_name is nil and cannot match which is # ok since such a "module" cannot have namespaced content). # return nil unless name_parts[0] == module_name # Skip the first part of the name when computing the path since the path already contains the name of the # module name_part_index = 1 else # The name is in the global name space. # The only globally name-spaced elements that may be loaded from modules are functions and resource types case typed_name.type when :function when :resource_type else # anything else cannot possibly be in this module # TODO: should not be allowed anyway... may have to revisit this decision return nil end end # Get the paths that actually exist in this module (they are lazily processed once and cached). # The result is an array (that may be empty). # Find the file to instantiate, and instantiate the entity if file is found origin = nil if (smart_path = smart_paths.effective_paths(typed_name.type).find do |sp| origin = sp.effective_path(typed_name, name_part_index) existing_path(origin) end) value = smart_path.instantiator.create(self, typed_name, origin, get_contents(origin)) # cache the entry and return it set_entry(typed_name, value, origin) else nil end end # Abstract method that subclasses override that checks if it is meaningful to search using a generic smart path. # This optimization is performed to not be tricked into searching an empty directory over and over again. # The implementation may perform a deep search for file content other than directories and cache this in # and index. It is guaranteed that a call to meaningful_to_search? takes place before checking any other # path with relative_path_exists?. # # This optimization exists because many modules have been created from a template and they have # empty directories for functions, types, etc. (It is also the place to create a cached index of the content). # # @param smart_path [String] a path relative to the module's root # @return [Boolean] true if there is content in the directory appointed by the relative path # def meaningful_to_search?(smart_path) raise NotImplementedError.new end # Abstract method that subclasses override to answer if the given relative path exists, and if so returns that path # # @param resolved_path [String] a path resolved by a smart path against the loader's root (if it has one) # @return [Boolean] true if the file exists # def existing_path(resolved_path) raise NotImplementedError.new end # Abstract method that subclasses override to produce the content of the effective path. # It should either succeed and return a String or fail with an exception. # # @param effective_path [String] a path as resolved by a smart path # @return [String] the content of the file # def get_contents(effective_path) raise NotImplementedError.new end # Abstract method that subclasses override to produce a source reference String used to identify the # system resource (resource in the URI sense). # # @param relative_path [String] a path relative to the module's root # @return [String] a reference to the source file (in file system, zip file, or elsewhere). # def get_source_ref(relative_path) raise NotImplementedError.new end # Produces the private loader for the module. If this module is not already resolved, this will trigger resolution # def private_loader @private_loader ||= @loaders.private_loader_for_module(module_name) end end # @api private # class FileBased < AbstractPathBasedModuleLoader attr_reader :smart_paths attr_reader :path_index # Create a kind of ModuleLoader for one module (Puppet Module, or module like) # # @param parent_loader [Puppet::Pops::Loader::Loader] typically the loader for the environment or root # @param module_name [String] the name of the module (non qualified name), may be nil for "modules" only containing globals # @param path [String] the path to the root of the module (semantics defined by subclass) # @param loader_name [String] a name that identifies the loader # def initialize(parent_loader, loaders, module_name, path, loader_name) super - unless Puppet::FileSystem.directory?(path) - raise ArgumentError, "The given module root path '#{path}' is not a directory (required for file system based module path entry)" - end @path_index = Set.new() end def existing_path(effective_path) # Optimized, checks index instead of visiting file system @path_index.include?(effective_path) ? effective_path : nil end def meaningful_to_search?(smart_path) ! add_to_index(smart_path).empty? end def to_s() "(ModuleLoader::FileBased '#{loader_name()}' '#{module_name()}')" end def add_to_index(smart_path) found = Dir.glob(File.join(smart_path.generic_path, '**', "*#{smart_path.extension}")) @path_index.merge(found) found end def get_contents(effective_path) Puppet::FileSystem.read(effective_path) end end # Loads from a gem specified as a URI, gem://gemname/optional/path/in/gem, or just a String gemname. # The source reference (shown in errors etc.) is the expanded path of the gem as this is believed to be more # helpful - given the location it should be quite obvious which gem it is, without the location, the user would # need to go on a hunt for where the file actually is located. # # TODO: How does this get instantiated? Does the gemname refelect the name of the module (the namespace) # or is that specified a different way? Can a gem be the container of multiple modules? # # @api private # class GemBased < FileBased include Puppet::Pops::Loader::GemSupport attr_reader :gem_ref # Create a kind of ModuleLoader for one module # The parameters are: # * parent_loader - typically the loader for the root # * module_name - the name of the module (non qualified name) # * gem_ref - [URI, String] gem reference to the root of the module (URI, gem://gemname/optional/path/in/gem), or # just the gem's name as a String. # def initialize(parent_loader, loaders, module_name, gem_ref, loader_name) @gem_ref = gem_ref super parent_loader, loaders, module_name, gem_dir(gem_ref), loader_name end def to_s() "(ModuleLoader::GemBased '#{loader_name()}' '#{@gem_ref}' [#{module_name()}])" end end end diff --git a/lib/puppet/pops/loaders.rb b/lib/puppet/pops/loaders.rb index ed113291a..f6a147fe1 100644 --- a/lib/puppet/pops/loaders.rb +++ b/lib/puppet/pops/loaders.rb @@ -1,240 +1,231 @@ 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() - module_name = nil - loader_name = 'puppet_system' - - # Puppet system may be installed in a fixed location via RPM, installed as a Gem, via source etc. - # The only way to find this across the different ways puppet can be installed is - # to search up the path from this source file's __FILE__ location until it finds the parent of - # lib/puppet... e.g.. dirname(__FILE__)/../../.. (i.e. /lib/puppet/pops/loaders.rb). - # - puppet_lib = File.join(File.dirname(__FILE__), '../../..') - Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, self, module_name, puppet_lib, loader_name) + 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}" loader = Puppet::Pops::Loader::SimpleEnvironmentLoader.new(puppet_system_loader, loader_name) # 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::FileBased.new(parent_loader, self, md.name, md.path, md.name) + 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/provider/package/pacman.rb b/lib/puppet/provider/package/pacman.rb index 4ca356b16..c947dac6d 100644 --- a/lib/puppet/provider/package/pacman.rb +++ b/lib/puppet/provider/package/pacman.rb @@ -1,234 +1,234 @@ require 'puppet/provider/package' require 'set' require 'uri' Puppet::Type.type(:package).provide :pacman, :parent => Puppet::Provider::Package do desc "Support for the Package Manager Utility (pacman) used in Archlinux. This provider supports the `install_options` attribute, which allows command-line flags to be passed to pacman. These options should be specified as a string (e.g. '--flag'), a hash (e.g. {'--flag' => 'value'}), or an array where each element is either a string or a hash." commands :pacman => "/usr/bin/pacman" # Yaourt is a common AUR helper which, if installed, we can use to query the AUR commands :yaourt => "/usr/bin/yaourt" if Puppet::FileSystem.exist? '/usr/bin/yaourt' - confine :operatingsystem => :archlinux - defaultfor :operatingsystem => :archlinux + confine :operatingsystem => [:archlinux, :manjarolinux] + defaultfor :operatingsystem => [:archlinux, :manjarolinux] has_feature :install_options has_feature :uninstall_options has_feature :upgradeable # If yaourt is installed, we can make use of it def yaourt? return Puppet::FileSystem.exist?('/usr/bin/yaourt') end # Install a package using 'pacman', or 'yaourt' if available. # Installs quietly, without confirmation or progressbar, updates package # list from servers defined in pacman.conf. def install if @resource[:source] install_from_file else install_from_repo end unless self.query raise Puppet::ExecutionFailure.new("Could not find package %s" % self.name) end end def install_from_repo if yaourt? cmd = %w{--noconfirm} cmd += install_options if @resource[:install_options] cmd << "-S" << @resource[:name] yaourt *cmd else cmd = %w{--noconfirm --noprogressbar} cmd += install_options if @resource[:install_options] cmd << "-Sy" << @resource[:name] pacman *cmd end end private :install_from_repo def install_from_file source = @resource[:source] begin source_uri = URI.parse source rescue => detail self.fail Puppet::Error, "Invalid source '#{source}': #{detail}", detail end source = case source_uri.scheme when nil then source when /https?/i then source when /ftp/i then source when /file/i then source_uri.path when /puppet/i fail "puppet:// URL is not supported by pacman" else fail "Source #{source} is not supported by pacman" end pacman "--noconfirm", "--noprogressbar", "-Sy" pacman "--noconfirm", "--noprogressbar", "-U", source end private :install_from_file def self.listcmd [command(:pacman), "-Q"] end # Pacman has a concept of package groups as well. # Package groups have no versions. def self.listgroupcmd [command(:pacman), "-Qg"] end # Get installed packages (pacman -Q) def self.installedpkgs packages = [] begin execpipe(listcmd()) do |process| # pacman -Q output is 'packagename version-rel' regex = %r{^(\S+)\s(\S+)} fields = [:name, :ensure] hash = {} process.each_line { |line| if match = regex.match(line) fields.zip(match.captures) { |field,value| hash[field] = value } hash[:provider] = self.name packages << new(hash) hash = {} else warning("Failed to match line %s" % line) end } end rescue Puppet::ExecutionFailure return nil end packages end # Get installed groups (pacman -Qg) def self.installedgroups packages = [] begin execpipe(listgroupcmd()) do |process| # pacman -Qg output is 'groupname packagename' # Groups need to be deduplicated groups = Set[] process.each_line { |line| groups.add(line.split[0]) } groups.each { |line| hash = { :name => line, :ensure => "1", # Groups don't have versions, so ensure => latest # will still cause a reinstall. :provider => self.name } packages << new(hash) } end rescue Puppet::ExecutionFailure return nil end packages end # Fetch the list of packages currently installed on the system. def self.instances packages = self.installedpkgs groups = self.installedgroups result = nil if (!packages && !groups) nil elsif (packages && groups) packages.concat(groups) else packages end end # Because Archlinux is a rolling release based distro, installing a package # should always result in the newest release. def update # Install in pacman can be used for update, too self.install end # We rescue the main check from Pacman with a check on the AUR using yaourt, if installed def latest pacman "-Sy" pacman_check = true # Query the main repos first begin if pacman_check output = pacman "-Sp", "--print-format", "%v", @resource[:name] return output.chomp else output = yaourt "-Qma", @resource[:name] output.split("\n").each do |line| return line.split[1].chomp if line =~ /^aur/ end end rescue Puppet::ExecutionFailure if pacman_check and self.yaourt? pacman_check = false # now try the AUR retry else raise end end end # Querys the pacman master list for information about the package. def query begin output = pacman("-Qi", @resource[:name]) if output =~ /Version.*:\s(.+)/ return { :ensure => $1 } end rescue Puppet::ExecutionFailure return { :ensure => :purged, :status => 'missing', :name => @resource[:name], :error => 'ok', } end nil end # Removes a package from the system. def uninstall cmd = %w{--noconfirm --noprogressbar} cmd += uninstall_options if @resource[:uninstall_options] cmd << "-R" << @resource[:name] pacman *cmd end private def install_options join_options(@resource[:install_options]) end def uninstall_options join_options(@resource[:uninstall_options]) end end diff --git a/lib/puppet/provider/package/rpm.rb b/lib/puppet/provider/package/rpm.rb index 3b4b6388a..93d36d993 100644 --- a/lib/puppet/provider/package/rpm.rb +++ b/lib/puppet/provider/package/rpm.rb @@ -1,187 +1,296 @@ require 'puppet/provider/package' # RPM packaging. Should work anywhere that has rpm installed. Puppet::Type.type(:package).provide :rpm, :source => :rpm, :parent => Puppet::Provider::Package do desc "RPM packaging support; should work anywhere with a working `rpm` binary. This provider supports the `install_options` and `uninstall_options` attributes, which allow command-line flags to be passed to rpm. These options should be specified as a string (e.g. '--flag'), a hash (e.g. {'--flag' => 'value'}), or an array where each element is either a string or a hash." has_feature :versionable has_feature :install_options has_feature :uninstall_options has_feature :virtual_packages # Note: self:: is required here to keep these constants in the context of what will # eventually become this Puppet::Type::Package::ProviderRpm class. # The query format by which we identify installed packages self::NEVRA_FORMAT = %Q{%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\\n} self::NEVRA_REGEX = %r{^(\S+) (\S+) (\S+) (\S+) (\S+)$} self::NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch] commands :rpm => "rpm" if command('rpm') confine :true => begin rpm('--version') rescue Puppet::ExecutionFailure false else true end end def self.current_version return @current_version unless @current_version.nil? output = rpm "--version" @current_version = output.gsub('RPM version ', '').strip end # rpm < 4.1 does not support --nosignature def self.nosignature '--nosignature' unless Puppet::Util::Package.versioncmp(current_version, '4.1') < 0 end # rpm < 4.0.2 does not support --nodigest def self.nodigest '--nodigest' unless Puppet::Util::Package.versioncmp(current_version, '4.0.2') < 0 end def self.instances packages = [] # list out all of the packages begin execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf '#{self::NEVRA_FORMAT}'") { |process| # now turn each returned line into a package object process.each_line { |line| hash = nevra_to_hash(line) packages << new(hash) unless hash.empty? } } rescue Puppet::ExecutionFailure raise Puppet::Error, "Failed to list packages", $!.backtrace end packages end # Find the fully versioned package name and the version alone. Returns # a hash with entries :instance => fully versioned package name, and # :ensure => version-release def query #NOTE: Prior to a fix for issue 1243, this method potentially returned a cached value #IF YOU CALL THIS METHOD, IT WILL CALL RPM #Use get(:property) to check if cached values are available cmd = ["-q", @resource[:name], "#{self.class.nosignature}", "#{self.class.nodigest}", "--qf", self.class::NEVRA_FORMAT] begin output = rpm(*cmd) rescue Puppet::ExecutionFailure return nil unless @resource.allow_virtual? # rpm -q exits 1 if package not found # retry the query for virtual packages cmd << '--whatprovides' begin output = rpm(*cmd) rescue Puppet::ExecutionFailure # couldn't find a virtual package either return nil end end # FIXME: We could actually be getting back multiple packages # for multilib and this will only return the first such package @property_hash.update(self.class.nevra_to_hash(output)) @property_hash.dup end # Here we just retrieve the version from the file specified in the source. def latest unless source = @resource[:source] @resource.fail "RPMs must specify a package source" end cmd = [command(:rpm), "-q", "--qf", self.class::NEVRA_FORMAT, "-p", source] h = self.class.nevra_to_hash(execfail(cmd, Puppet::Error)) h[:ensure] end def install unless source = @resource[:source] @resource.fail "RPMs must specify a package source" end # RPM gets pissy if you try to install an already # installed package if @resource.should(:ensure) == @property_hash[:ensure] or @resource.should(:ensure) == :latest && @property_hash[:ensure] == latest return end flag = ["-i"] flag = ["-U", "--oldpackage"] if @property_hash[:ensure] and @property_hash[:ensure] != :absent flag += install_options if resource[:install_options] rpm flag, source end def uninstall query if get(:arch) == :absent nvr = "#{get(:name)}-#{get(:version)}-#{get(:release)}" arch = ".#{get(:arch)}" # If they specified an arch in the manifest, erase that Otherwise, # erase the arch we got back from the query. If multiple arches are # installed and only the package name is specified (without the # arch), this will uninstall all of them on successive runs of the # client, one after the other # version of RPM prior to 4.2.1 can't accept the architecture as # part of the package name. unless Puppet::Util::Package.versioncmp(self.class.current_version, '4.2.1') < 0 if @resource[:name][-arch.size, arch.size] == arch nvr += arch else nvr += ".#{get(:arch)}" end end flag = ['-e'] flag += uninstall_options if resource[:uninstall_options] rpm flag, nvr end def update self.install end def install_options join_options(resource[:install_options]) end def uninstall_options join_options(resource[:uninstall_options]) end + # This is an attempt at implementing RPM's + # lib/rpmvercmp.c rpmvercmp(a, b) in Ruby. + # + # Some of the things in here look REALLY + # UGLY and/or arbitrary. Our goal is to + # match how RPM compares versions, quirks + # and all. + # + # I've kept a lot of C-like string processing + # in an effort to keep this as identical to RPM + # as possible. + # + # returns 1 if str1 is newer than str2, + # 0 if they are identical + # -1 if str1 is older than str2 + def rpmvercmp(str1, str2) + return 0 if str1 == str2 + + front_strip_re = /^[^A-Za-z0-9~]+/ + segment_re = /^[A-Za-z0-9]/ + # these represent RPM rpmio/rpmstring.c functions + risalnum = /[A-Za-z0-9]/ + risdigit = /^[0-9]+/ + risalpha = /[A-Za-z]/ + + while str1.length > 0 or str2.length > 0 + # trim anything that's !risalnum() and != '~' off the beginning of each string + str1 = str1.gsub(front_strip_re, '') + str2 = str2.gsub(front_strip_re, '') + + # "handle the tilde separator, it sorts before everything else" + if /^~/.match(str1) && /^~/.match(str2) + # if they both have ~, strip it + str1 = str1[1..-1] + str2 = str2[1..-1] + elsif /^~/.match(str1) + return -1 + elsif /^~/.match(str2) + return 1 + end + + break if str1.length == 0 or str2.length == 0 + + # "grab first completely alpha or completely numeric segment" + isnum = false + # if the first char of str1 is a digit, grab the chunk of continuous digits from each string + if risdigit.match(str1) + if str1 =~ /^[0-9]+/ + segment1 = $~.to_s + str1 = $~.post_match + else + segment1 = '' + end + if str2 =~ /^[0-9]+/ + segment2 = $~.to_s + str2 = $~.post_match + else + segment2 = '' + end + isnum = true + # else grab the chunk of continuous alphas from each string (which may be '') + else + if str1 =~ /^[A-Za-z]+/ + segment1 = $~.to_s + str1 = $~.post_match + else + segment1 = '' + end + if str2 =~ /^[A-Za-z]+/ + segment2 = $~.to_s + str2 = $~.post_match + else + segment2 = '' + end + end + + # if the segments we just grabbed from the strings are different types (i.e. one numeric one alpha), + # where alpha also includes ''; "numeric segments are always newer than alpha segments" + if segment2.length == 0 + return 1 if isnum + return -1 + end + + if isnum + # "throw away any leading zeros - it's a number, right?" + segment1 = segment1.gsub(/^0+/, '') + segment2 = segment2.gsub(/^0+/, '') + # "whichever number has more digits wins" + return 1 if segment1.length > segment2.length + return -1 if segment1.length < segment2.length + end + + # "strcmp will return which one is greater - even if the two segments are alpha + # or if they are numeric. don't return if they are equal because there might + # be more segments to compare" + rc = segment1 <=> segment2 + return rc if rc != 0 + end #end while loop + + # if we haven't returned anything yet, "whichever version still has characters left over wins" + if str1.length > str2.length + return 1 + elsif str1.length < str2.length + return -1 + else + return 0 + end + end + private # @param line [String] one line of rpm package query information # @return [Hash] of NEVRA_FIELDS strings parsed from package info # or an empty hash if we failed to parse # @api private def self.nevra_to_hash(line) line.strip! hash = {} if match = self::NEVRA_REGEX.match(line) self::NEVRA_FIELDS.zip(match.captures) { |f, v| hash[f] = v } hash[:provider] = self.name hash[:ensure] = "#{hash[:version]}-#{hash[:release]}" else Puppet.debug("Failed to match rpm line #{line}") end return hash end end diff --git a/lib/puppet/provider/package/yum.rb b/lib/puppet/provider/package/yum.rb index bfbe9df16..e93379f8b 100644 --- a/lib/puppet/provider/package/yum.rb +++ b/lib/puppet/provider/package/yum.rb @@ -1,199 +1,264 @@ -require 'puppet/util/package' - Puppet::Type.type(:package).provide :yum, :parent => :rpm, :source => :rpm do desc "Support via `yum`. Using this provider's `uninstallable` feature will not remove dependent packages. To remove dependent packages with this provider use the `purgeable` feature, but note this feature is destructive and should be used with the utmost care. This provider supports the `install_options` attribute, which allows command-line flags to be passed to yum. These options should be specified as a string (e.g. '--flag'), a hash (e.g. {'--flag' => 'value'}), or an array where each element is either a string or a hash." has_feature :install_options, :versionable, :virtual_packages commands :yum => "yum", :rpm => "rpm", :python => "python" self::YUMHELPER = File::join(File::dirname(__FILE__), "yumhelper.py") if command('rpm') confine :true => begin rpm('--version') rescue Puppet::ExecutionFailure false else true end end defaultfor :osfamily => :redhat def self.prefetch(packages) raise Puppet::Error, "The yum provider can only be used as root" if Process.euid != 0 super end # Retrieve the latest package version information for a given package name # and combination of repos to enable and disable. # # @note If multiple package versions are defined (such as in the case where a # package is built for multiple architectures), the first package found # will be used. # # @api private # @param package [String] The name of the package to query # @param enablerepo [Array] A list of repositories to enable for this query # @param disablerepo [Array] A list of repositories to disable for this query # @return [Hash] def self.latest_package_version(package, enablerepo, disablerepo) key = [enablerepo, disablerepo] @latest_versions ||= {} if @latest_versions[key].nil? @latest_versions[key] = fetch_latest_versions(enablerepo, disablerepo) end if @latest_versions[key][package] @latest_versions[key][package].first end end # Search for all installed packages that have newer versions, given a # combination of repositories to enable and disable. # # @api private # @param enablerepo [Array] A list of repositories to enable for this query # @param disablerepo [Array] A list of repositories to disable for this query # @return [Hash>>] All packages that were # found with a list of found versions for each package. def self.fetch_latest_versions(enablerepo, disablerepo) latest_versions = Hash.new {|h, k| h[k] = []} args = [self::YUMHELPER] args.concat(enablerepo.map { |repo| ['-e', repo] }.flatten) args.concat(disablerepo.map { |repo| ['-d', repo] }.flatten) python(args).scan(/^_pkg (.*)$/) do |match| hash = nevra_to_hash(match[0]) # Create entries for both the package name without a version and a # version since yum considers those as mostly interchangeable. short_name = hash[:name] long_name = "#{hash[:name]}.#{hash[:arch]}" latest_versions[short_name] << hash latest_versions[long_name] << hash end latest_versions end def self.clear @latest_versions = nil end def install wanted = @resource[:name] # If not allowing virtual packages, do a query to ensure a real package exists unless @resource.allow_virtual? yum *['-d', '0', '-e', '0', '-y', install_options, :list, wanted].compact end should = @resource.should(:ensure) self.debug "Ensuring => #{should}" operation = :install case should when true, false, Symbol # pass should = nil else # Add the package version wanted += "-#{should}" is = self.query - if is && Puppet::Util::Package.versioncmp(should, is[:ensure]) < 0 + if is && yum_compareEVR(yum_parse_evr(should), yum_parse_evr(is[:ensure])) < 0 self.debug "Downgrading package #{@resource[:name]} from version #{is[:ensure]} to #{should}" operation = :downgrade end end args = ["-d", "0", "-e", "0", "-y", install_options, operation, wanted].compact yum *args # If a version was specified, query again to see if it is a matching version if should is = self.query raise Puppet::Error, "Could not find package #{self.name}" unless is # FIXME: Should we raise an exception even if should == :latest # and yum updated us to a version other than @param_hash[:ensure] ? - raise Puppet::Error, "Failed to update to version #{should}, got version #{is[:ensure]} instead" if should != is[:ensure] + vercmp_result = yum_compareEVR(yum_parse_evr(should), yum_parse_evr(is[:ensure])) + raise Puppet::Error, "Failed to update to version #{should}, got version #{is[:ensure]} instead" if vercmp_result != 0 end end # What's the latest package version available? def latest upd = self.class.latest_package_version(@resource[:name], enablerepo, disablerepo) unless upd.nil? # FIXME: there could be more than one update for a package # because of multiarch return "#{upd[:epoch]}:#{upd[:version]}-#{upd[:release]}" else # Yum didn't find updates, pretend the current # version is the latest raise Puppet::DevError, "Tried to get latest on a missing package" if properties[:ensure] == :absent return properties[:ensure] end end def update # Install in yum can be used for update, too self.install end def purge yum "-y", :erase, @resource[:name] end + # parse a yum "version" specification + # this re-implements yum's + # rpmUtils.miscutils.stringToVersion() in ruby + def yum_parse_evr(s) + ei = s.index(':') + if ei + e = s[0,ei] + s = s[ei+1,s.length] + else + e = nil + end + e = String(Bignum(e)) rescue '0' + ri = s.index('-') + if ri + v = s[0,ri] + r = s[ri+1,s.length] + else + v = s + r = nil + end + return { :epoch => e, :version => v, :release => r } + end + + # how yum compares two package versions: + # rpmUtils.miscutils.compareEVR(), which massages data types and then calls + # rpm.labelCompare(), found in rpm.git/python/header-py.c, which + # sets epoch to 0 if null, then compares epoch, then ver, then rel + # using compare_values() and returns the first non-0 result, else 0. + # This function combines the logic of compareEVR() and labelCompare(). + # + # "version_should" can be v, v-r, or e:v-r. + # "version_is" will always be at least v-r, can be e:v-r + def yum_compareEVR(should_hash, is_hash) + # pass on to rpm labelCompare + rc = compare_values(should_hash[:epoch], is_hash[:epoch]) + return rc unless rc == 0 + rc = compare_values(should_hash[:version], is_hash[:version]) + return rc unless rc == 0 + + # here is our special case, PUP-1244. + # if should_hash[:release] is nil (not specified by the user), + # and comparisons up to here are equal, return equal. We need to + # evaluate to whatever level of detail the user specified, so we + # don't end up upgrading or *downgrading* when not intended. + # + # This should NOT be triggered if we're trying to ensure latest. + return 0 if should_hash[:release].nil? + + rc = compare_values(should_hash[:release], is_hash[:release]) + return rc + end + + # this method is a native implementation of the + # compare_values function in rpm's python bindings, + # found in python/header-py.c, as used by yum. + def compare_values(s1, s2) + if s1.nil? && s2.nil? + return 0 + elsif ( not s1.nil? ) && s2.nil? + return 1 + elsif s1.nil? && (not s2.nil?) + return -1 + end + return rpmvercmp(s1, s2) + end + # @deprecated def latest_info Puppet.deprecation_warning("#{self.class}##{__method__} is deprecated and is no longer used.") @latest_info end # @deprecated def latest_info=(latest) Puppet.deprecation_warning("#{self.class}##{__method__} is deprecated and is no longer used.") @latest_info = latest end private def enablerepo scan_options(resource[:install_options], '--enablerepo') end def disablerepo scan_options(resource[:install_options], '--disablerepo') end # Scan a structure that looks like the package type 'install_options' # structure for all hashes that have a specific key. # # @api private # @param options [Array, nil] The options structure. If the # options are nil an empty array will be returned. # @param key [String] The key to look for in all contained hashes # @return [Array] All hash values with the given key. def scan_options(options, key) return [] if options.nil? options.inject([]) do |repos, opt| if opt.is_a? Hash and opt[key] repos << opt[key] end repos end end end diff --git a/lib/puppet/provider/service/openbsd.rb b/lib/puppet/provider/service/openbsd.rb index 521462b78..c84fc3d87 100644 --- a/lib/puppet/provider/service/openbsd.rb +++ b/lib/puppet/provider/service/openbsd.rb @@ -1,342 +1,93 @@ Puppet::Type.type(:service).provide :openbsd, :parent => :init do desc "Provider for OpenBSD's rc.d daemon control scripts" + commands :rcctl => '/usr/sbin/rcctl' + confine :operatingsystem => :openbsd defaultfor :operatingsystem => :openbsd has_feature :flaggable - def self.rcconf() '/etc/rc.conf' end - def self.rcconf_local() '/etc/rc.conf.local' end - - def self.defpath - ["/etc/rc.d"] + def startcmd + [command(:rcctl), '-f', :start, @resource[:name]] end - def startcmd - [self.initscript, "-f", :start] + def stopcmd + [command(:rcctl), :stop, @resource[:name]] end def restartcmd - (@resource[:hasrestart] == :true) && [self.initscript, "-f", :restart] + (@resource[:hasrestart] == :true) && [command(:rcctl), '-f', :restart, @resource[:name]] end def statuscmd - [self.initscript, :check] - end - - # Fetch the state of all service resources - def self.prefetch(resources) - services = instances - resources.keys.each do |name| - if provider = services.find { |svc| svc.name == name } - resources[name].provider = provider - end - end - end - - # Return the list of rc scripts - # @api private - def self.rclist - unless @rclist - Puppet.debug "Building list of rc scripts" - @rclist = [] - defpath.each do |p| - Dir.glob(p + '/*').each do |item| - @rclist << item if File.executable?(item) - end - end - end - @rclist - end - - # Return a hash where the keys are the rc script names as symbols with flags - # as values - # @api private - def self.rcflags - unless @flag_hash - Puppet.debug "Retrieving flags for all discovered services" - - Puppet.debug "Reading the contents of the rc conf files" - - if File.exists?(rcconf_local()) - rcconf_local_contents = File.readlines(rcconf_local()) - else - rcconf_local_contents = [] - end - - if File.exists?(rcconf()) - rcconf_contents = File.readlines(rcconf()) - else - rcconf_contents = [] - end - - @flag_hash = {} - rclist().each do |rcitem| - - rcname = rcitem.split('/').last - - if flagline = rcconf_local_contents.find {|l| l =~ /^#{rcname}_flags/ } - flag = parse_rc_line(flagline) - @flag_hash[rcname.to_sym] ||= flag - end - - # For the defaults, if the flags are set to 'NO', we skip setting the - # flag here, since it will already be disabled, and this makes the - # output of `puppet resource service` a bit more correct. - if flagline = rcconf_contents.find {|l| l =~ /^#{rcname}_flags/ } - flag = parse_rc_line(flagline) - unless flag == "NO" - @flag_hash[rcname.to_sym] ||= flag - end - end - @flag_hash[rcname.to_sym] ||= nil - end - end - @flag_hash + [command(:rcctl), :check, @resource[:name]] end # @api private - def self.parse_rc_line(rc_line) - rc_line.sub!(/\s*#(.*)$/,'') - regex = /\w+_flags=(.*)/ - rc_line.match(regex)[1].gsub(/^"/,'').gsub(/"$/,'') - end - - # Read the rc.conf* files and determine the value of the flags - # @api private - def self.get_flags(rcname) - rcflags() - @flag_hash[rcname.to_sym] - end - + # When storing the name, take into account not everything has + # '_flags', like 'multicast_host' and 'pf'. def self.instances instances = [] - defpath.each do |path| - unless File.directory?(path) - Puppet.debug "Service path #{path} does not exist" - next - end - rclist().each do |d| - instances << new( - :name => File.basename(d), - :path => path, - :flags => get_flags(File.basename(d)), - :hasstatus => true - ) - end - end - instances - end - - # @api private - def rcvar_name - self.name + '_flags' - end - - # @api private - def read_rcconf_local_text() - if File.exists?(self.class.rcconf_local()) - File.read(self.class.rcconf_local()) - else - [] - end - end - - # @api private - def load_rcconf_local_array - if File.exists?(self.class.rcconf_local()) - File.readlines(self.class.rcconf_local()).map {|l| - l.chomp! - } - else - [] - end - end - - # @api private - def write_rc_contents(file, text) - Puppet::Util.replace_file(file, 0644) do |f| - f.write(text) - end - end - - # @api private - def set_content_flags(content,flags) - unless content.is_a? Array - debug "content must be an array at flags" - return "" - else - content.reject! {|l| l.nil? } - end - - if flags.nil? or flags.size == 0 - if in_base? - append = resource[:name] + '_flags=""' + begin + execpipe([command(:rcctl), :status]) do |process| + process.each_line do |line| + match = /^(.*?)(?:_flags)?=(.*)$/.match(line) + attributes_hash = { + :name => match[1], + :flags => match[2], + :hasstatus => true, + :provider => :openbsd, + } + + instances << new(attributes_hash); + end end - else - append = resource[:name] + '_flags="' + flags + '"' + instances + rescue Puppet::ExecutionFailure + return nil end - - if content.find {|l| l =~ /#{resource[:name]}_flags/ }.nil? - content << append - else - content.map {|l| l.gsub!(/^#{resource[:name]}_flags="(.*)?"(.*)?$/, append) } - end - content end - # @api private - def remove_content_flags(content) - content.reject {|l| l =~ /#{resource[:name]}_flags/ } - end - - # return an array of the currently enabled pkg_scripts - # @api private - def pkg_scripts - current = load_rcconf_local_array() - if scripts = current.find{|l| l =~ /^pkg_scripts/ } - if match = scripts.match(/^pkg_scripts="(.*)?"(.*)?$/) - match[1].split(' ') - else - [] - end - else - [] - end - end - - # return the array with the current resource added - # @api private - def pkg_scripts_append - [pkg_scripts(), resource[:name]].flatten.uniq - end - - # return the array without the current resource - # @api private - def pkg_scripts_remove - pkg_scripts().reject {|s| s == resource[:name] } - end - - # Modify the content array to contain the requsted pkg_scripts line and retun - # the resulting array - # @api private - def set_content_scripts(content,scripts) - unless content.is_a? Array - debug "content must be an array at scripts" - return "" - else - content.reject! {|l| l.nil? } - end - - scripts_line = 'pkg_scripts="' + scripts.join(' ') + '"' + def enabled? + output = execute([command(:rcctl), "status", @resource[:name]], + :failonfail => false, :combine => false, :squelch => false).chomp - if content.find {|l| l =~ /^pkg_scripts/ }.nil? - content << scripts_line + if output == 'NO' + self.debug("Is disabled") + return :false else - # Replace the found pkg_scripts line with our own - content.each_with_index {|l,i| - if l =~ /^pkg_scripts/ - content[i] = scripts_line - end - } + self.debug("Is enabled") + return :true end - content - end - - # Determine if the rc script is included in base - # @api private - def in_base? - script = File.readlines(self.class.rcconf).find {|s| s =~ /^#{rcvar_name}/ } - !script.nil? - end - - # @api private - def default_disabled? - line = File.readlines(self.class.rcconf).find {|l| l =~ /#{rcvar_name}/ } - self.class.parse_rc_line(line) == 'NO' end - def enabled? - if in_base? - if (@property_hash[:flags].nil? or @property_hash[:flags] == 'NO') - :false - else - :true - end + def enable + self.debug("Enabling") + if @resource[:flags] + rcctl(:enable, @resource[:name], :flags, @resource[:flags]) else - if (pkg_scripts().include?(@property_hash[:name])) - :true - else - :false - end + rcctl(:enable, @resource[:name]) end end - def enable - self.debug("Enabling #{self.name}") - end - - # We should also check for default state def disable - self.debug("Disabling #{self.name}") + self.debug("Disabling") + rcctl(:disable, @resource[:name]) end + # Uses the wrapper to prevent failure when the service is not running; + # rcctl(8) return non-zero in that case. def flags - @property_hash[:flags] + output = execute([command(:rcctl), "status", @resource[:name]], + :failonfail => false, :combine => false, :squelch => false).chomp + self.debug("Flags are: #{output}") + output end def flags=(value) - @property_hash[:flags] = value - end - - def flush - debug "Flusing resource for #{self.name}" - - # Here we load the contents of the rc.conf.local file into the contents - # variable, modify it if needed, and then compare that to the original. If - # they are different, we write it out. - - original = load_rcconf_local_array() - content = original - - debug @property_hash.inspect - - if resource[:enable] == :true - #set_flags(resource[:flags]) - content = set_content_flags(content, resource[:flags]) - - # We need only add append the resource name to the pkg_scripts if the - # package is not found in the base system. - - if not in_base? - content = set_content_scripts(content,pkg_scripts_append()) - end - elsif resource[:enable] == :false - - # By virtue of being excluded from the base system, all packages are - # disabled by default and need not be set in the rc.conf.local at all. - - if not in_base? - content = remove_content_flags(content) - content = set_content_scripts(content,pkg_scripts_remove()) - else - if default_disabled? - content = remove_content_flags(content) - else - content = set_content_flags(content, "NO") - end - end - end - - # Make sure to append a newline to the end of the file - unless content[-1] == "" - content << "" - end - output = content.join("\n") - - # Write the contents only if necessary, and only once - write_rc_contents(self.class.rcconf_local(), output) + self.debug("Changing flags from #{flags} to #{value}") + rcctl(:enable, @resource[:name], :flags, value) end end diff --git a/lib/puppet/provider/service/windows.rb b/lib/puppet/provider/service/windows.rb index c084ffbc9..2eb55f38b 100644 --- a/lib/puppet/provider/service/windows.rb +++ b/lib/puppet/provider/service/windows.rb @@ -1,106 +1,106 @@ # Windows Service Control Manager (SCM) provider Puppet::Type.type(:service).provide :windows, :parent => :service do desc <<-EOT Support for Windows Service Control Manager (SCM). This provider can start, stop, enable, and disable services, and the SCM provides working status methods for all services. Control of service groups (dependencies) is not yet supported, nor is running services as a specific user. EOT defaultfor :operatingsystem => :windows confine :operatingsystem => :windows has_feature :refreshable commands :net => 'net.exe' def enable w32ss = Win32::Service.configure( 'service_name' => @resource[:name], 'start_type' => Win32::Service::SERVICE_AUTO_START ) raise Puppet::Error.new("Win32 service enable of #{@resource[:name]} failed" ) if( w32ss.nil? ) - rescue Win32::Service::Error => detail + rescue => detail raise Puppet::Error.new("Cannot enable #{@resource[:name]}, error was: #{detail}", detail ) end def disable w32ss = Win32::Service.configure( 'service_name' => @resource[:name], 'start_type' => Win32::Service::SERVICE_DISABLED ) raise Puppet::Error.new("Win32 service disable of #{@resource[:name]} failed" ) if( w32ss.nil? ) - rescue Win32::Service::Error => detail + rescue => detail raise Puppet::Error.new("Cannot disable #{@resource[:name]}, error was: #{detail}", detail ) end def manual_start w32ss = Win32::Service.configure( 'service_name' => @resource[:name], 'start_type' => Win32::Service::SERVICE_DEMAND_START ) raise Puppet::Error.new("Win32 service manual enable of #{@resource[:name]} failed" ) if( w32ss.nil? ) - rescue Win32::Service::Error => detail + rescue => detail raise Puppet::Error.new("Cannot enable #{@resource[:name]} for manual start, error was: #{detail}", detail ) end def enabled? w32ss = Win32::Service.config_info( @resource[:name] ) raise Puppet::Error.new("Win32 service query of #{@resource[:name]} failed" ) unless( !w32ss.nil? && w32ss.instance_of?( Struct::ServiceConfigInfo ) ) debug("Service #{@resource[:name]} start type is #{w32ss.start_type}") case w32ss.start_type when Win32::Service.get_start_type(Win32::Service::SERVICE_AUTO_START), Win32::Service.get_start_type(Win32::Service::SERVICE_BOOT_START), Win32::Service.get_start_type(Win32::Service::SERVICE_SYSTEM_START) :true when Win32::Service.get_start_type(Win32::Service::SERVICE_DEMAND_START) :manual when Win32::Service.get_start_type(Win32::Service::SERVICE_DISABLED) :false else raise Puppet::Error.new("Unknown start type: #{w32ss.start_type}") end - rescue Win32::Service::Error => detail + rescue => detail raise Puppet::Error.new("Cannot get start type for #{@resource[:name]}, error was: #{detail}", detail ) end def start if enabled? == :false # If disabled and not managing enable, respect disabled and fail. if @resource[:enable].nil? raise Puppet::Error, "Will not start disabled service #{@resource[:name]} without managing enable. Specify 'enable => false' to override." # Otherwise start. If enable => false, we will later sync enable and # disable the service again. elsif @resource[:enable] == :true enable else manual_start end end net(:start, @resource[:name]) rescue Puppet::ExecutionFailure => detail raise Puppet::Error.new("Cannot start #{@resource[:name]}, error was: #{detail}", detail ) end def stop net(:stop, @resource[:name]) rescue Puppet::ExecutionFailure => detail raise Puppet::Error.new("Cannot stop #{@resource[:name]}, error was: #{detail}", detail ) end def status w32ss = Win32::Service.status( @resource[:name] ) raise Puppet::Error.new("Win32 service query of #{@resource[:name]} failed" ) unless( !w32ss.nil? && w32ss.instance_of?( Struct::ServiceStatus ) ) state = case w32ss.current_state when "stopped", "pause pending", "stop pending", "paused" then :stopped when "running", "continue pending", "start pending" then :running else raise Puppet::Error.new("Unknown service state '#{w32ss.current_state}' for service '#{@resource[:name]}'") end debug("Service #{@resource[:name]} is #{w32ss.current_state}") return state - rescue Win32::Service::Error => detail + rescue => detail raise Puppet::Error.new("Cannot get status of #{@resource[:name]}, error was: #{detail}", detail ) end # returns all providers for all existing services and startup state def self.instances Win32::Service.services.collect { |s| new(:name => s.service_name) } end end diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb index a5419512b..8abb31b0e 100644 --- a/lib/puppet/resource.rb +++ b/lib/puppet/resource.rb @@ -1,612 +1,632 @@ require 'puppet' require 'puppet/util/tagging' require 'puppet/util/pson' require 'puppet/parameter' # The simplest resource class. Eventually it will function as the # base class for all resource-like behaviour. # # @api public class Puppet::Resource # This stub class is only needed for serialization compatibility with 0.25.x. # Specifically, it exists to provide a compatibility API when using YAML # serialized objects loaded from StoreConfigs. Reference = Puppet::Resource include Puppet::Util::Tagging extend Puppet::Util::Pson include Enumerable attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict attr_reader :type, :title require 'puppet/indirector' extend Puppet::Indirector indirects :resource, :terminus_class => :ral ATTRIBUTES = [:file, :line, :exported] def self.from_data_hash(data) raise ArgumentError, "No resource type provided in serialized data" unless type = data['type'] raise ArgumentError, "No resource title provided in serialized data" unless title = data['title'] resource = new(type, title) if params = data['parameters'] params.each { |param, value| resource[param] = value } end if tags = data['tags'] tags.each { |tag| resource.tag(tag) } end ATTRIBUTES.each do |a| if value = data[a.to_s] resource.send(a.to_s + "=", value) end end resource end def self.from_pson(pson) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(pson) end def inspect "#{@type}[#{@title}]#{to_hash.inspect}" end def to_data_hash data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param| next hash unless value = self.send(param) hash[param.to_s] = value hash end data["exported"] ||= false params = self.to_hash.inject({}) do |hash, ary| param, value = ary # Don't duplicate the title as the namevar next hash if param == namevar and value == title hash[param] = Puppet::Resource.value_to_pson_data(value) hash end data["parameters"] = params unless params.empty? data end # This doesn't include document type as it is part of a catalog def to_pson_data_hash to_data_hash end def self.value_to_pson_data(value) if value.is_a? Array value.map{|v| value_to_pson_data(v) } elsif value.is_a? Puppet::Resource value.to_s else value end end def yaml_property_munge(x) case x when Hash x.inject({}) { |h,kv| k,v = kv h[k] = self.class.value_to_pson_data(v) h } else self.class.value_to_pson_data(x) end end YAML_ATTRIBUTES = [:@file, :@line, :@exported, :@type, :@title, :@tags, :@parameters] # Explicitly list the instance variables that should be serialized when # converting to YAML. # # @api private # @return [Array] The intersection of our explicit variable list and # all of the instance variables defined on this class. def to_yaml_properties YAML_ATTRIBUTES & super end def to_pson(*args) to_data_hash.to_pson(*args) end # Proxy these methods to the parameters hash. It's likely they'll # be overridden at some point, but this works for now. %w{has_key? keys length delete empty? <<}.each do |method| define_method(method) do |*args| parameters.send(method, *args) end end # Set a given parameter. Converts all passed names # to lower-case symbols. def []=(param, value) validate_parameter(param) if validate_parameters parameters[parameter_name(param)] = value end # Return a given parameter's value. Converts all passed names # to lower-case symbols. def [](param) parameters[parameter_name(param)] end def ==(other) return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title return false unless to_hash == other.to_hash true end # Compatibility method. def builtin? builtin_type? end # Is this a builtin resource type? def builtin_type? resource_type.is_a?(Class) end # Iterate over each param/value pair, as required for Enumerable. def each parameters.each { |p,v| yield p, v } end def include?(parameter) super || parameters.keys.include?( parameter_name(parameter) ) end %w{exported virtual strict}.each do |m| define_method(m+"?") do self.send(m) end end def class? @is_class ||= @type == "Class" end def stage? @is_stage ||= @type.to_s.downcase == "stage" end # Cache to reduce respond_to? lookups @@nondeprecating_type = {} # Construct a resource from data. # # Constructs a resource instance with the given `type` and `title`. Multiple # type signatures are possible for these arguments and most will result in an # expensive call to {Puppet::Node::Environment#known_resource_types} in order # to resolve `String` and `Symbol` Types to actual Ruby classes. # # @param type [Symbol, String] The name of the Puppet Type, as a string or # symbol. The actual Type will be looked up using # {Puppet::Node::Environment#known_resource_types}. This lookup is expensive. # @param type [String] The full resource name in the form of # `"Type[Title]"`. This method of calling should only be used when # `title` is `nil`. # @param type [nil] If a `nil` is passed, the title argument must be a string # of the form `"Type[Title]"`. # @param type [Class] A class that inherits from `Puppet::Type`. This method # of construction is much more efficient as it skips calls to # {Puppet::Node::Environment#known_resource_types}. # # @param title [String, :main, nil] The title of the resource. If type is `nil`, may also # be the full resource name in the form of `"Type[Title]"`. # # @api public def initialize(type, title = nil, attributes = {}) @parameters = {} if type.is_a?(Class) && type < Puppet::Type # Set the resource type to avoid an expensive `known_resource_types` # lookup. self.resource_type = type # From this point on, the constructor behaves the same as if `type` had # been passed as a symbol. type = type.name end # Set things like strictness first. attributes.each do |attr, value| next if attr == :parameters send(attr.to_s + "=", value) end @type, @title = extract_type_and_title(type, title) @type = munge_type_name(@type) if self.class? @title = :main if @title == "" @title = munge_type_name(@title) end if params = attributes[:parameters] extract_parameters(params) end if resource_type and ! @@nondeprecating_type[resource_type] if resource_type.respond_to?(:deprecate_params) resource_type.deprecate_params(title, attributes[:parameters]) else @@nondeprecating_type[resource_type] = true end end tag(self.type) tag(self.title) if valid_tag?(self.title) @reference = self # for serialization compatibility with 0.25.x if strict? and ! resource_type if self.class? raise ArgumentError, "Could not find declared class #{title}" else raise ArgumentError, "Invalid resource type #{type}" end end end def ref to_s end # Find our resource. def resolve catalog ? catalog.resource(to_s) : nil end # The resource's type implementation # @return [Puppet::Type, Puppet::Resource::Type] # @api private def resource_type @rstype ||= case type when "Class"; environment.known_resource_types.hostclass(title == :main ? "" : title) when "Node"; environment.known_resource_types.node(title) else Puppet::Type.type(type) || environment.known_resource_types.definition(type) end end # Set the resource's type implementation # @param type [Puppet::Type, Puppet::Resource::Type] # @api private def resource_type=(type) @rstype = type end def environment @environment ||= if catalog catalog.environment_instance else Puppet.lookup(:current_environment) { Puppet::Node::Environment::NONE } end end def environment=(environment) @environment = environment end # Produce a simple hash of our parameters. def to_hash parse_title.merge parameters end def to_s "#{type}[#{title}]" end def uniqueness_key # Temporary kludge to deal with inconsistant use patters h = self.to_hash h[namevar] ||= h[:name] h[:name] ||= h[namevar] h.values_at(*key_attributes.sort_by { |k| k.to_s }) end def key_attributes resource_type.respond_to?(:key_attributes) ? resource_type.key_attributes : [:name] end + # Convert our resource to yaml for Hiera purposes. + def to_hierayaml + # Collect list of attributes to align => and move ensure first + attr = parameters.keys + attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max } + + attr.sort! + if attr.first != :ensure && attr.include?(:ensure) + attr.delete(:ensure) + attr.unshift(:ensure) + end + + attributes = attr.collect { |k| + v = parameters[k] + " %-#{attr_max}s: %s\n" % [k, Puppet::Parameter.format_value_for_display(v)] + }.join + + " %s:\n%s" % [self.title, attributes] + end + # Convert our resource to Puppet code. def to_manifest # Collect list of attributes to align => and move ensure first attr = parameters.keys attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max } attr.sort! if attr.first != :ensure && attr.include?(:ensure) attr.delete(:ensure) attr.unshift(:ensure) end attributes = attr.collect { |k| v = parameters[k] " %-#{attr_max}s => %s,\n" % [k, Puppet::Parameter.format_value_for_display(v)] }.join "%s { '%s':\n%s}" % [self.type.to_s.downcase, self.title, attributes] end def to_ref ref end # Convert our resource to a RAL resource instance. Creates component # instances for resource types that don't exist. def to_ral typeklass = Puppet::Type.type(self.type) || Puppet::Type.type(:component) typeklass.new(self) end def name # this is potential namespace conflict # between the notion of an "indirector name" # and a "resource name" [ type, title ].join('/') end def missing_arguments resource_type.arguments.select do |param, default| param = param.to_sym parameters[param].nil? || parameters[param].value == :undef end end private :missing_arguments # Consult external data bindings for class parameter values which must be # namespaced in the backend. # # Example: # # class foo($port=0){ ... } # # We make a request to the backend for the key 'foo::port' not 'foo' # def lookup_external_default_for(param, scope) # Only lookup parameters for host classes return nil unless resource_type.type == :hostclass name = "#{resource_type.name}::#{param}" lookup_with_databinding(name, scope) end private :lookup_external_default_for def lookup_with_databinding(name, scope) begin Puppet::DataBinding.indirection.find( name, :environment => scope.environment.to_s, :variables => scope) rescue Puppet::DataBinding::LookupError => e raise Puppet::Error.new("Error from DataBinding '#{Puppet[:data_binding_terminus]}' while looking up '#{name}': #{e.message}", e) end end private :lookup_with_databinding def set_default_parameters(scope) return [] unless resource_type and resource_type.respond_to?(:arguments) unless is_a?(Puppet::Parser::Resource) fail Puppet::DevError, "Cannot evaluate default parameters for #{self} - not a parser resource" end missing_arguments.collect do |param, default| external_value = lookup_external_default_for(param, scope) if external_value.nil? && default.nil? next elsif external_value.nil? value = default.safeevaluate(scope) else value = external_value end self[param.to_sym] = value param end.compact end def copy_as_resource result = Puppet::Resource.new(type, title) result.file = self.file result.line = self.line result.exported = self.exported result.virtual = self.virtual result.tag(*self.tags) result.environment = environment result.instance_variable_set(:@rstype, resource_type) to_hash.each do |p, v| if v.is_a?(Puppet::Resource) v = Puppet::Resource.new(v.type, v.title) elsif v.is_a?(Array) # flatten resource references arrays v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) } v = v.collect do |av| av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource) av end end if Puppet[:parser] == 'current' # If the value is an array with only one value, then # convert it to a single value. This is largely so that # the database interaction doesn't have to worry about # whether it returns an array or a string. # # This behavior is not done in the future parser, but we can't issue a # deprecation warning either since there isn't anything that a user can # do about it. result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end else result[p] = v end end result end def valid_parameter?(name) resource_type.valid_parameter?(name) end # Verify that all required arguments are either present or # have been provided with defaults. # Must be called after 'set_default_parameters'. We can't join the methods # because Type#set_parameters needs specifically ordered behavior. def validate_complete return unless resource_type and resource_type.respond_to?(:arguments) resource_type.arguments.each do |param, default| param = param.to_sym fail Puppet::ParseError, "Must pass #{param} to #{self}" unless parameters.include?(param) end # Perform optional type checking if Puppet[:parser] == 'future' # Perform type checking arg_types = resource_type.argument_types # Parameters is a map from name, to parameter, and the parameter again has name and value parameters.each do |name, value| next unless t = arg_types[name.to_s] # untyped, and parameters are symbols here (aargh, strings in the type) unless Puppet::Pops::Types::TypeCalculator.instance?(t, value.value) inferred_type = Puppet::Pops::Types::TypeCalculator.infer(value.value) actual = Puppet::Pops::Types::TypeCalculator.generalize!(inferred_type) fail Puppet::ParseError, "Expected parameter '#{name}' of '#{self}' to have type #{t.to_s}, got #{actual.to_s}" end end end end def validate_parameter(name) raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name) end def prune_parameters(options = {}) properties = resource_type.properties.map(&:name) dup.collect do |attribute, value| if value.to_s.empty? or Array(value).empty? delete(attribute) elsif value.to_s == "absent" and attribute.to_s != "ensure" delete(attribute) end parameters_to_include = options[:parameters_to_include] || [] delete(attribute) unless properties.include?(attribute) || parameters_to_include.include?(attribute) end self end private # Produce a canonical method name. def parameter_name(param) param = param.to_s.downcase.to_sym if param == :name and namevar param = namevar end param end # The namevar for our resource type. If the type doesn't exist, # always use :name. def namevar if builtin_type? and t = resource_type and t.key_attributes.length == 1 t.key_attributes.first else :name end end def extract_parameters(params) params.each do |param, value| validate_parameter(param) if strict? self[param] = value end end def extract_type_and_title(argtype, argtitle) if (argtype.nil? || argtype == :component || argtype == :whit) && argtitle =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle.nil? && argtype =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle then [ argtype, argtitle ] elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ] elsif argtype.is_a?(Hash) then raise ArgumentError, "Puppet::Resource.new does not take a hash as the first argument. "+ "Did you mean (#{(argtype[:type] || argtype["type"]).inspect}, #{(argtype[:title] || argtype["title"]).inspect }) ?" else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference" end end def munge_type_name(value) return :main if value == :main return "Class" if value == "" or value.nil? or value.to_s.downcase == "component" value.to_s.split("::").collect { |s| s.capitalize }.join("::") end def parse_title h = {} type = resource_type if type.respond_to? :title_patterns type.title_patterns.each { |regexp, symbols_and_lambdas| if captures = regexp.match(title.to_s) symbols_and_lambdas.zip(captures[1..-1]).each do |symbol_and_lambda,capture| symbol, proc = symbol_and_lambda # Many types pass "identity" as the proc; we might as well give # them a shortcut to delivering that without the extra cost. # # Especially because the global type defines title_patterns and # uses the identity patterns. # # This was worth about 8MB of memory allocation saved in my # testing, so is worth the complexity for the API. if proc then h[symbol] = proc.call(capture) else h[symbol] = capture end end return h end } # If we've gotten this far, then none of the provided title patterns # matched. Since there's no way to determine the title then the # resource should fail here. raise Puppet::Error, "No set of title patterns matched the title \"#{title}\"." else return { :name => title.to_s } end end def parameters # @parameters could have been loaded from YAML, causing it to be nil (by # bypassing initialize). @parameters ||= {} end end diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb index 832d88eab..0ec2a4a64 100644 --- a/lib/puppet/resource/catalog.rb +++ b/lib/puppet/resource/catalog.rb @@ -1,554 +1,554 @@ require 'puppet/node' require 'puppet/indirector' require 'puppet/transaction' require 'puppet/util/pson' require 'puppet/util/tagging' require 'puppet/graph' # This class models a node catalog. It is the thing meant to be passed # from server to client, and it contains all of the information in the # catalog, including the resources and the relationships between them. # # @api public class Puppet::Resource::Catalog < Puppet::Graph::SimpleGraph class DuplicateResourceError < Puppet::Error include Puppet::ExternalFileError end extend Puppet::Indirector indirects :catalog, :terminus_setting => :catalog_terminus include Puppet::Util::Tagging extend Puppet::Util::Pson # The host name this is a catalog for. attr_accessor :name # The catalog version. Used for testing whether a catalog # is up to date. attr_accessor :version # How long this catalog took to retrieve. Used for reporting stats. attr_accessor :retrieval_duration # Whether this is a host catalog, which behaves very differently. # In particular, reports are sent, graphs are made, and state is # stored in the state database. If this is set incorrectly, then you often # end up in infinite loops, because catalogs are used to make things # that the host catalog needs. attr_accessor :host_config # Whether this catalog was retrieved from the cache, which affects # whether it is written back out again. attr_accessor :from_cache # Some metadata to help us compile and generally respond to the current state. attr_accessor :client_version, :server_version # A String representing the environment for this catalog attr_accessor :environment # The actual environment instance that was used during compilation attr_accessor :environment_instance # Add classes to our class list. def add_class(*classes) classes.each do |klass| @classes << klass end # Add the class names as tags, too. tag(*classes) end def title_key_for_ref( ref ) ref =~ /^([-\w:]+)\[(.*)\]$/m [$1, $2] end def add_resource(*resources) resources.each do |resource| add_one_resource(resource) end end # @param resource [A Resource] a resource in the catalog # @return [A Resource, nil] the resource that contains the given resource # @api public def container_of(resource) adjacent(resource, :direction => :in)[0] end def add_one_resource(resource) title_key = title_key_for_ref(resource.ref) if @resource_table[title_key] fail_on_duplicate_type_and_title(resource, title_key) end add_resource_to_table(resource, title_key) create_resource_aliases(resource) resource.catalog = self if resource.respond_to?(:catalog=) add_resource_to_graph(resource) end private :add_one_resource def add_resource_to_table(resource, title_key) @resource_table[title_key] = resource @resources << title_key end private :add_resource_to_table def add_resource_to_graph(resource) add_vertex(resource) @relationship_graph.add_vertex(resource) if @relationship_graph end private :add_resource_to_graph def create_resource_aliases(resource) if resource.respond_to?(:isomorphic?) and resource.isomorphic? and resource.name != resource.title self.alias(resource, resource.uniqueness_key) end end private :create_resource_aliases # Create an alias for a resource. def alias(resource, key) resource.ref =~ /^(.+)\[/ class_name = $1 || resource.class.name newref = [class_name, key].flatten if key.is_a? String ref_string = "#{class_name}[#{key}]" return if ref_string == resource.ref end # LAK:NOTE It's important that we directly compare the references, # because sometimes an alias is created before the resource is # added to the catalog, so comparing inside the below if block # isn't sufficient. if existing = @resource_table[newref] return if existing == resource resource_declaration = " at #{resource.file}:#{resource.line}" if resource.file and resource.line existing_declaration = " at #{existing.file}:#{existing.line}" if existing.file and existing.line msg = "Cannot alias #{resource.ref} to #{key.inspect}#{resource_declaration}; resource #{newref.inspect} already declared#{existing_declaration}" raise ArgumentError, msg end @resource_table[newref] = resource @aliases[resource.ref] ||= [] @aliases[resource.ref] << newref end # Apply our catalog to the local host. # @param options [Hash{Symbol => Object}] a hash of options # @option options [Puppet::Transaction::Report] :report # The report object to log this transaction to. This is optional, # and the resulting transaction will create a report if not # supplied. # @option options [Array[String]] :tags # Tags used to filter the transaction. If supplied then only # resources tagged with any of these tags will be evaluated. # @option options [Boolean] :ignoreschedules # Ignore schedules when evaluating resources # @option options [Boolean] :for_network_device # Whether this catalog is for a network device # # @return [Puppet::Transaction] the transaction created for this # application # # @api public def apply(options = {}) Puppet::Util::Storage.load if host_config? transaction = create_transaction(options) begin transaction.report.as_logging_destination do transaction.evaluate end rescue Puppet::Error => detail Puppet.log_exception(detail, "Could not apply complete catalog: #{detail}") rescue => detail Puppet.log_exception(detail, "Got an uncaught exception of type #{detail.class}: #{detail}") ensure # Don't try to store state unless we're a host config # too recursive. Puppet::Util::Storage.store if host_config? end yield transaction if block_given? transaction end # The relationship_graph form of the catalog. This contains all of the # dependency edges that are used for determining order. # # @param given_prioritizer [Puppet::Graph::Prioritizer] The prioritization # strategy to use when constructing the relationship graph. Defaults the # being determined by the `ordering` setting. # @return [Puppet::Graph::RelationshipGraph] # @api public def relationship_graph(given_prioritizer = nil) if @relationship_graph.nil? @relationship_graph = Puppet::Graph::RelationshipGraph.new(given_prioritizer || prioritizer) @relationship_graph.populate_from(self) end @relationship_graph end def clear(remove_resources = true) super() # We have to do this so that the resources clean themselves up. @resource_table.values.each { |resource| resource.remove } if remove_resources @resource_table.clear @resources = [] if @relationship_graph @relationship_graph.clear @relationship_graph = nil end end def classes @classes.dup end # Create a new resource and register it in the catalog. def create_resource(type, options) unless klass = Puppet::Type.type(type) raise ArgumentError, "Unknown resource type #{type}" end return unless resource = klass.new(options) add_resource(resource) resource end # Make sure all of our resources are "finished". def finalize make_default_resources @resource_table.values.each { |resource| resource.finish } write_graph(:resources) end def host_config? host_config end def initialize(name = nil, environment = Puppet::Node::Environment::NONE) super() @name = name @classes = [] @resource_table = {} @resources = [] @relationship_graph = nil @host_config = true @environment_instance = environment @environment = environment.to_s @aliases = {} if block_given? yield(self) finalize end end # Make the default objects necessary for function. def make_default_resources # We have to add the resources to the catalog, or else they won't get cleaned up after # the transaction. # First create the default scheduling objects Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) } # And filebuckets if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket add_resource(bucket) unless resource(bucket.ref) end end # Remove the resource from our catalog. Notice that we also call # 'remove' on the resource, at least until resource classes no longer maintain # references to the resource instances. def remove_resource(*resources) resources.each do |resource| title_key = title_key_for_ref(resource.ref) @resource_table.delete(title_key) if aliases = @aliases[resource.ref] aliases.each { |res_alias| @resource_table.delete(res_alias) } @aliases.delete(resource.ref) end remove_vertex!(resource) if vertex?(resource) @relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource) @resources.delete(title_key) resource.remove end end # Look a resource up by its reference (e.g., File[/etc/passwd]). def resource(type, title = nil) # Always create a resource reference, so that it always # canonicalizes how we are referring to them. if title res = Puppet::Resource.new(type, title) else # If they didn't provide a title, then we expect the first # argument to be of the form 'Class[name]', which our # Reference class canonicalizes for us. res = Puppet::Resource.new(nil, type) end res.catalog = self title_key = [res.type, res.title.to_s] uniqueness_key = [res.type, res.uniqueness_key].flatten @resource_table[title_key] || @resource_table[uniqueness_key] end def resource_refs resource_keys.collect{ |type, name| name.is_a?( String ) ? "#{type}[#{name}]" : nil}.compact end def resource_keys @resource_table.keys end def resources @resources.collect do |key| @resource_table[key] end end def self.from_data_hash(data) result = new(data['name'], Puppet::Node::Environment::NONE) if tags = data['tags'] result.tag(*tags) end if version = data['version'] result.version = version end if environment = data['environment'] result.environment = environment result.environment_instance = Puppet::Node::Environment.remote(environment.to_sym) end if resources = data['resources'] result.add_resource(*resources.collect do |res| Puppet::Resource.from_data_hash(res) end) end if edges = data['edges'] edges.each do |edge_hash| edge = Puppet::Relationship.from_data_hash(edge_hash) unless source = result.resource(edge.source) - raise ArgumentError, "Could not intern from data: Could not find relationship source #{edge.source.inspect}" + raise ArgumentError, "Could not intern from data: Could not find relationship source #{edge.source.inspect} for #{edge.target.to_s}" end edge.source = source unless target = result.resource(edge.target) - raise ArgumentError, "Could not intern from data: Could not find relationship target #{edge.target.inspect}" + raise ArgumentError, "Could not intern from data: Could not find relationship target #{edge.target.inspect} for #{edge.source.to_s}" end edge.target = target result.add_edge(edge) end end if classes = data['classes'] result.add_class(*classes) end result end def self.from_pson(data) Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.") self.from_data_hash(data) end def to_data_hash { 'tags' => tags, 'name' => name, 'version' => version, 'environment' => environment.to_s, 'resources' => @resources.collect { |v| @resource_table[v].to_pson_data_hash }, 'edges' => edges. collect { |e| e.to_pson_data_hash }, 'classes' => classes } end PSON.register_document_type('Catalog',self) def to_pson_data_hash { 'document_type' => 'Catalog', 'data' => to_data_hash, 'metadata' => { 'api_version' => 1 } } end def to_pson(*args) to_pson_data_hash.to_pson(*args) end # Convert our catalog into a RAL catalog. def to_ral to_catalog :to_ral end # Convert our catalog into a catalog of Puppet::Resource instances. def to_resource to_catalog :to_resource end # filter out the catalog, applying +block+ to each resource. # If the block result is false, the resource will # be kept otherwise it will be skipped def filter(&block) to_catalog :to_resource, &block end # Store the classes in the classfile. def write_class_file ::File.open(Puppet[:classfile], "w") do |f| f.puts classes.join("\n") end rescue => detail Puppet.err "Could not create class file #{Puppet[:classfile]}: #{detail}" end # Store the list of resources we manage def write_resource_file ::File.open(Puppet[:resourcefile], "w") do |f| to_print = resources.map do |resource| next unless resource.managed? if resource.name_var "#{resource.type}[#{resource[resource.name_var]}]" else "#{resource.ref.downcase}" end end.compact f.puts to_print.join("\n") end rescue => detail Puppet.err "Could not create resource file #{Puppet[:resourcefile]}: #{detail}" end # Produce the graph files if requested. def write_graph(name) # We only want to graph the main host catalog. return unless host_config? super end private def prioritizer @prioritizer ||= case Puppet[:ordering] when "title-hash" Puppet::Graph::TitleHashPrioritizer.new when "manifest" Puppet::Graph::SequentialPrioritizer.new when "random" Puppet::Graph::RandomPrioritizer.new else raise Puppet::DevError, "Unknown ordering type #{Puppet[:ordering]}" end end def create_transaction(options) transaction = Puppet::Transaction.new(self, options[:report], prioritizer) transaction.tags = options[:tags] if options[:tags] transaction.ignoreschedules = true if options[:ignoreschedules] transaction.for_network_device = options[:network_device] transaction end # Verify that the given resource isn't declared elsewhere. def fail_on_duplicate_type_and_title(resource, title_key) # Short-circuit the common case, return unless existing_resource = @resource_table[title_key] # If we've gotten this far, it's a real conflict msg = "Duplicate declaration: #{resource.ref} is already declared" msg << " in file #{existing_resource.file}:#{existing_resource.line}" if existing_resource.file and existing_resource.line msg << "; cannot redeclare" raise DuplicateResourceError.new(msg, resource.file, resource.line) end # An abstracted method for converting one catalog into another type of catalog. # This pretty much just converts all of the resources from one class to another, using # a conversion method. def to_catalog(convert) result = self.class.new(self.name, self.environment_instance) result.version = self.version map = {} resources.each do |resource| next if virtual_not_exported?(resource) next if block_given? and yield resource newres = resource.copy_as_resource newres.catalog = result if convert != :to_resource newres = newres.to_ral end # We can't guarantee that resources don't munge their names # (like files do with trailing slashes), so we have to keep track # of what a resource got converted to. map[resource.ref] = newres result.add_resource newres end message = convert.to_s.gsub "_", " " edges.each do |edge| # Skip edges between virtual resources. next if virtual_not_exported?(edge.source) next if block_given? and yield edge.source next if virtual_not_exported?(edge.target) next if block_given? and yield edge.target unless source = map[edge.source.ref] raise Puppet::DevError, "Could not find resource #{edge.source.ref} when converting #{message} resources" end unless target = map[edge.target.ref] raise Puppet::DevError, "Could not find resource #{edge.target.ref} when converting #{message} resources" end result.add_edge(source, target, edge.label) end map.clear result.add_class(*self.classes) result.tag(*self.tags) result end def virtual_not_exported?(resource) resource.virtual && !resource.exported end end diff --git a/lib/puppet/settings.rb b/lib/puppet/settings.rb index 3201b125f..499ee1502 100644 --- a/lib/puppet/settings.rb +++ b/lib/puppet/settings.rb @@ -1,1392 +1,1420 @@ 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. REQUIRED_APP_SETTINGS = [:logdir, :confdir, :vardir] # 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["hostname"].value end def self.domain_fact() Facter["domain"].value 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 # 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) 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) 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. @config.values.select(&:has_hook?).each do |setting| value_sets_for(env, self.preferred_run_mode).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(self.value(setting.name, env)) 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] 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 # The order in which to search for values. def searchpath(environment = nil) [:memory, :cli, environment, :run_mode, :main, :application_defaults, :overridden_defaults].compact 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 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)) end private DEPRECATION_REFS = { [:manifest, :modulepath, :config_version, :templatedir, :manifestdir] => "See http://links.puppetlabs.com/env-settings-deprecations" }.freeze # 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| 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) - catalog.add_resource( - Puppet::Resource.new(:file, - configured_environment_path, - :parameters => { :ensure => 'directory' }) - ) + 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 end 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 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) + 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) + def convert(value, setting_name) case value when nil nil when String - value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value| + failed_environment_interpolation = false + interpolated_value = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |expression| varname = $2 || $1 - if varname == ENVIRONMENT_SETTING && @environment - @environment - elsif varname == "run_mode" - @mode - elsif !(pval = interpolate(varname.to_sym)).nil? - pval + 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 - raise InterpolationError, "Could not find value for #{value}" + 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 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 end class ValuesFromSection 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 end # @api private class ValuesFromEnvironmentConf def initialize(environment_name) @environment_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 end end diff --git a/lib/puppet/ssl/validator/default_validator.rb b/lib/puppet/ssl/validator/default_validator.rb index 1f31499e2..674b3c224 100644 --- a/lib/puppet/ssl/validator/default_validator.rb +++ b/lib/puppet/ssl/validator/default_validator.rb @@ -1,154 +1,175 @@ require 'openssl' require 'puppet/ssl' # Perform peer certificate verification against the known CA. # If there is no CA information known, then no verification is performed # # @api private # class Puppet::SSL::Validator::DefaultValidator #< class Puppet::SSL::Validator attr_reader :peer_certs attr_reader :verify_errors attr_reader :ssl_configuration + FIVE_MINUTES_AS_SECONDS = 5 * 60 + # Creates a new DefaultValidator, optionally with an SSL Configuration and SSL Host. # # @param ssl_configuration [Puppet::SSL::Configuration] (a default configuration) ssl_configuration the SSL configuration to use # @param ssl_host [Puppet::SSL::Host] (Puppet::SSL::Host.localhost) the SSL host to use # # @api private # def initialize( ssl_configuration = Puppet::SSL::Configuration.new( Puppet[:localcacert], { :ca_chain_file => Puppet[:ssl_client_ca_chain], :ca_auth_file => Puppet[:ssl_client_ca_auth] }), ssl_host = Puppet::SSL::Host.localhost) reset! @ssl_configuration = ssl_configuration @ssl_host = ssl_host end # Resets this validator to its initial validation state. The ssl configuration is not changed. # # @api private # def reset! @peer_certs = [] @verify_errors = [] end # Performs verification of the SSL connection and collection of the # certificates for use in constructing the error message if the verification # failed. This callback will be executed once for each certificate in a # chain being verified. # # From the [OpenSSL # documentation](http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html): # The `verify_callback` function is used to control the behaviour when the # SSL_VERIFY_PEER flag is set. It must be supplied by the application and # receives two arguments: preverify_ok indicates, whether the verification of # the certificate in question was passed (preverify_ok=1) or not - # (preverify_ok=0). x509_ctx is a pointer to the complete context used for + # (preverify_ok=0). x509_store_ctx is a pointer to the complete context used for # the certificate chain verification. # # See {Puppet::Network::HTTP::Connection} for more information and where this # class is intended to be used. # # @param [Boolean] preverify_ok indicates whether the verification of the # certificate in question was passed (preverify_ok=true) - # @param [OpenSSL::SSL::SSLContext] ssl_context holds the SSLContext for the - # chain being verified. + # @param [OpenSSL::X509::StoreContext] store_context holds the X509 store context + # for the chain being verified. # # @return [Boolean] false if the peer is invalid, true otherwise. # # @api private # - def call(preverify_ok, ssl_context) - # We must make a copy since the scope of the ssl_context will be lost + def call(preverify_ok, store_context) + # We must make a copy since the scope of the store_context will be lost # across invocations of this method. - current_cert = ssl_context.current_cert - @peer_certs << Puppet::SSL::Certificate.from_instance(current_cert) - if preverify_ok + current_cert = store_context.current_cert + @peer_certs << Puppet::SSL::Certificate.from_instance(current_cert) + # If we've copied all of the certs in the chain out of the SSL library - if @peer_certs.length == ssl_context.chain.length + if @peer_certs.length == store_context.chain.length # (#20027) The peer cert must be issued by a specific authority preverify_ok = valid_peer? end else - if ssl_context.error_string - @verify_errors << "#{ssl_context.error_string} for #{current_cert.subject}" + error = store_context.error || 0 + error_string = store_context.error_string || "OpenSSL error #{error}" + + case error + when OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID + # current_crl can be nil + # https://github.com/ruby/ruby/blob/ruby_1_9_3/ext/openssl/ossl_x509store.c#L501-L510 + crl = store_context.current_crl + if crl + if crl.last_update && crl.last_update < Time.now + FIVE_MINUTES_AS_SECONDS + Puppet.debug("Ignoring CRL not yet valid, current time #{Time.now.utc}, CRL last updated #{crl.last_update.utc}") + preverify_ok = true + else + @verify_errors << "#{error_string} for #{crl.issuer}" + end + else + @verify_errors << error_string + end + else + current_cert = store_context.current_cert + @verify_errors << "#{error_string} for #{current_cert.subject}" end end preverify_ok rescue => ex @verify_errors << ex.message false end # Registers the instance's call method with the connection. # # @param [Net::HTTP] connection The connection to validate # # @return [void] # # @api private # def setup_connection(connection) if ssl_certificates_are_present? connection.cert_store = @ssl_host.ssl_store connection.ca_file = @ssl_configuration.ca_auth_file connection.cert = @ssl_host.certificate.content connection.key = @ssl_host.key.content connection.verify_mode = OpenSSL::SSL::VERIFY_PEER connection.verify_callback = self else connection.verify_mode = OpenSSL::SSL::VERIFY_NONE end end # Validates the peer certificates against the authorized certificates. # # @api private # def valid_peer? descending_cert_chain = @peer_certs.reverse.map {|c| c.content } authz_ca_certs = ssl_configuration.ca_auth_certificates if not has_authz_peer_cert(descending_cert_chain, authz_ca_certs) msg = "The server presented a SSL certificate chain which does not include a " << "CA listed in the ssl_client_ca_auth file. " msg << "Authorized Issuers: #{authz_ca_certs.collect {|c| c.subject}.join(', ')} " << "Peer Chain: #{descending_cert_chain.collect {|c| c.subject}.join(' => ')}" @verify_errors << msg false else true end end # Checks if the set of peer_certs contains at least one certificate issued # by a certificate listed in authz_certs # # @return [Boolean] # # @api private # def has_authz_peer_cert(peer_certs, authz_certs) peer_certs.any? do |peer_cert| authz_certs.any? do |authz_cert| peer_cert.verify(authz_cert.public_key) end end end # @api private # def ssl_certificates_are_present? Puppet::FileSystem.exist?(Puppet[:hostcert]) && Puppet::FileSystem.exist?(@ssl_configuration.ca_auth_file) end end diff --git a/spec/fixtures/unit/provider/service/openbsd/rcctl_status b/spec/fixtures/unit/provider/service/openbsd/rcctl_status new file mode 100644 index 000000000..f58ce12e3 --- /dev/null +++ b/spec/fixtures/unit/provider/service/openbsd/rcctl_status @@ -0,0 +1,6 @@ +accounting=NO +pf=YES +postgresql_flags=-l /var/postgresql/logfile +tftpd_flags=/tftpboot +wsmoused_flags=NO +xdm_flags= diff --git a/spec/integration/defaults_spec.rb b/spec/integration/defaults_spec.rb index 734785230..7d8ea3150 100755 --- a/spec/integration/defaults_spec.rb +++ b/spec/integration/defaults_spec.rb @@ -1,341 +1,335 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/defaults' require 'puppet/rails' describe "Puppet defaults" do describe "when default_manifest is set" do it "returns ./manifests by default" do expect(Puppet[:default_manifest]).to eq('./manifests') end - - it "errors when $environment is part of the value" do - expect { - Puppet[:default_manifest] = '/$environment/manifest.pp' - }.to raise_error Puppet::Settings::ValidationError, /cannot interpolate.*\$environment/ - end end describe "when disable_per_environment_manifest is set" do it "returns false by default" do expect(Puppet[:disable_per_environment_manifest]).to eq(false) end it "errors when set to true and default_manifest is not an absolute path" do expect { Puppet[:default_manifest] = './some/relative/manifest.pp' Puppet[:disable_per_environment_manifest] = true }.to raise_error Puppet::Settings::ValidationError, /'default_manifest' setting must be.*absolute/ end end describe "when setting the :factpath" do it "should add the :factpath to Facter's search paths" do Facter.expects(:search).with("/my/fact/path") Puppet.settings[:factpath] = "/my/fact/path" end end describe "when setting the :certname" do it "should fail if the certname is not downcased" do expect { Puppet.settings[:certname] = "Host.Domain.Com" }.to raise_error(ArgumentError) end end describe "when setting :node_name_value" do it "should default to the value of :certname" do Puppet.settings[:certname] = 'blargle' Puppet.settings[:node_name_value].should == 'blargle' end end describe "when setting the :node_name_fact" do it "should fail when also setting :node_name_value" do lambda do Puppet.settings[:node_name_value] = "some value" Puppet.settings[:node_name_fact] = "some_fact" end.should raise_error("Cannot specify both the node_name_value and node_name_fact settings") end it "should not fail when using the default for :node_name_value" do lambda do Puppet.settings[:node_name_fact] = "some_fact" end.should_not raise_error end end describe "when :certdnsnames is set" do it "should not fail" do expect { Puppet[:certdnsnames] = 'fred:wilma' }.to_not raise_error end it "should warn the value is ignored" do Puppet.expects(:warning).with {|msg| msg =~ /CVE-2011-3872/ } Puppet[:certdnsnames] = 'fred:wilma' end end describe "when setting the :catalog_format" do it "should log a deprecation notice" do Puppet.expects(:deprecation_warning) Puppet.settings[:catalog_format] = 'marshal' end it "should copy the value to :preferred_serialization_format" do Puppet.settings[:catalog_format] = 'marshal' Puppet.settings[:preferred_serialization_format].should == 'marshal' end end it "should have a clientyamldir setting" do Puppet.settings[:clientyamldir].should_not be_nil end it "should have different values for the yamldir and clientyamldir" do Puppet.settings[:yamldir].should_not == Puppet.settings[:clientyamldir] end it "should have a client_datadir setting" do Puppet.settings[:client_datadir].should_not be_nil end it "should have different values for the server_datadir and client_datadir" do Puppet.settings[:server_datadir].should_not == Puppet.settings[:client_datadir] end # See #1232 it "should not specify a user or group for the clientyamldir" do Puppet.settings.setting(:clientyamldir).owner.should be_nil Puppet.settings.setting(:clientyamldir).group.should be_nil end it "should use the service user and group for the yamldir" do Puppet.settings.stubs(:service_user_available?).returns true Puppet.settings.stubs(:service_group_available?).returns true Puppet.settings.setting(:yamldir).owner.should == Puppet.settings[:user] Puppet.settings.setting(:yamldir).group.should == Puppet.settings[:group] end it "should specify that the host private key should be owned by the service user" do Puppet.settings.stubs(:service_user_available?).returns true Puppet.settings.setting(:hostprivkey).owner.should == Puppet.settings[:user] end it "should specify that the host certificate should be owned by the service user" do Puppet.settings.stubs(:service_user_available?).returns true Puppet.settings.setting(:hostcert).owner.should == Puppet.settings[:user] end [:modulepath, :factpath].each do |setting| it "should configure '#{setting}' not to be a file setting, so multi-directory settings are acceptable" do Puppet.settings.setting(setting).should be_instance_of(Puppet::Settings::PathSetting) end end describe "on a Unix-like platform it", :as_platform => :posix do it "should add /usr/sbin and /sbin to the path if they're not there" do Puppet::Util.withenv("PATH" => "/usr/bin#{File::PATH_SEPARATOR}/usr/local/bin") do Puppet.settings[:path] = "none" # this causes it to ignore the setting ENV["PATH"].split(File::PATH_SEPARATOR).should be_include("/usr/sbin") ENV["PATH"].split(File::PATH_SEPARATOR).should be_include("/sbin") end end end describe "on a Windows-like platform it", :as_platform => :windows do it "should not add anything" do path = "c:\\windows\\system32#{File::PATH_SEPARATOR}c:\\windows" Puppet::Util.withenv("PATH" => path) do Puppet.settings[:path] = "none" # this causes it to ignore the setting ENV["PATH"].should == path end end end it "should default to pson for the preferred serialization format" do Puppet.settings.value(:preferred_serialization_format).should == "pson" end describe "when enabling storeconfigs" do before do Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:cache_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet.features.stubs(:rails?).returns true end it "should set the Catalog cache class to :store_configs" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:store_configs) Puppet.settings[:storeconfigs] = true end it "should not set the Catalog cache class to :store_configs if asynchronous storeconfigs is enabled" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:store_configs).never Puppet.settings[:async_storeconfigs] = true Puppet.settings[:storeconfigs] = true end it "should set the Facts cache class to :store_configs" do Puppet::Node::Facts.indirection.expects(:cache_class=).with(:store_configs) Puppet.settings[:storeconfigs] = true end it "does not change the Node cache" do Puppet::Node.indirection.expects(:cache_class=).never Puppet.settings[:storeconfigs] = true end end describe "when enabling asynchronous storeconfigs" do before do Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:cache_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet.features.stubs(:rails?).returns true end it "should set storeconfigs to true" do Puppet.settings[:async_storeconfigs] = true Puppet.settings[:storeconfigs].should be_true end it "should set the Catalog cache class to :queue" do Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:queue) Puppet.settings[:async_storeconfigs] = true end it "should set the Facts cache class to :store_configs" do Puppet::Node::Facts.indirection.expects(:cache_class=).with(:store_configs) Puppet.settings[:storeconfigs] = true end it "does not change the Node cache" do Puppet::Node.indirection.expects(:cache_class=).never Puppet.settings[:storeconfigs] = true end end describe "when enabling thin storeconfigs" do before do Puppet::Resource::Catalog.indirection.stubs(:cache_class=) Puppet::Node::Facts.indirection.stubs(:cache_class=) Puppet::Node.indirection.stubs(:cache_class=) Puppet.features.stubs(:rails?).returns true end it "should set storeconfigs to true" do Puppet.settings[:thin_storeconfigs] = true Puppet.settings[:storeconfigs].should be_true end end it "should have a setting for determining the configuration version and should default to an empty string" do Puppet.settings[:config_version].should == "" end describe "when enabling reports" do it "should use the default server value when report server is unspecified" do Puppet.settings[:server] = "server" Puppet.settings[:report_server].should == "server" end it "should use the default masterport value when report port is unspecified" do Puppet.settings[:masterport] = "1234" Puppet.settings[:report_port].should == "1234" end it "should use report_port when set" do Puppet.settings[:masterport] = "1234" Puppet.settings[:report_port] = "5678" Puppet.settings[:report_port].should == "5678" end end it "should have a :caname setting that defaults to the cert name" do Puppet.settings[:certname] = "foo" Puppet.settings[:ca_name].should == "Puppet CA: foo" end it "should have a 'prerun_command' that defaults to the empty string" do Puppet.settings[:prerun_command].should == "" end it "should have a 'postrun_command' that defaults to the empty string" do Puppet.settings[:postrun_command].should == "" end it "should have a 'certificate_revocation' setting that defaults to true" do Puppet.settings[:certificate_revocation].should be_true end it "should have an http_compression setting that defaults to false" do Puppet.settings[:http_compression].should be_false end describe "reportdir" do subject { Puppet.settings[:reportdir] } it { should == "#{Puppet[:vardir]}/reports" } end describe "reporturl" do subject { Puppet.settings[:reporturl] } it { should == "http://localhost:3000/reports/upload" } end describe "when configuring color" do subject { Puppet.settings[:color] } it { should == "ansi" } end describe "daemonize" do it "should default to true", :unless => Puppet.features.microsoft_windows? do Puppet.settings[:daemonize].should == true end describe "on Windows", :if => Puppet.features.microsoft_windows? do it "should default to false" do Puppet.settings[:daemonize].should == false end it "should raise an error if set to true" do expect { Puppet.settings[:daemonize] = true }.to raise_error(/Cannot daemonize on Windows/) end end end describe "diff" do it "should default to 'diff' on POSIX", :unless => Puppet.features.microsoft_windows? do Puppet.settings[:diff].should == 'diff' end it "should default to '' on Windows", :if => Puppet.features.microsoft_windows? do Puppet.settings[:diff].should == '' end end describe "when configuring hiera" do it "should have a hiera_config setting" do Puppet.settings[:hiera_config].should_not be_nil end end describe "when configuring the data_binding terminus" do it "should have a data_binding_terminus setting" do Puppet.settings[:data_binding_terminus].should_not be_nil end it "should be set to hiera by default" do Puppet.settings[:data_binding_terminus].should == :hiera end end describe "agent_catalog_run_lockfile" do it "(#2888) is not a file setting so it is absent from the Settings catalog" do Puppet.settings.setting(:agent_catalog_run_lockfile).should_not be_a_kind_of Puppet::Settings::FileSetting Puppet.settings.setting(:agent_catalog_run_lockfile).should be_a Puppet::Settings::StringSetting end end end diff --git a/spec/integration/environments/default_manifest_spec.rb b/spec/integration/environments/default_manifest_spec.rb index 6d4564037..543afc087 100644 --- a/spec/integration/environments/default_manifest_spec.rb +++ b/spec/integration/environments/default_manifest_spec.rb @@ -1,274 +1,263 @@ require 'spec_helper' module EnvironmentsDefaultManifestsSpec describe "default manifests" do - FS = Puppet::FileSystem shared_examples_for "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 - it "raises an exception if default_manifest has $environment in it" do - File.open(File.join(confdir, "puppet.conf"), "w") do |f| - f.puts(<<-EOF) - environmentpath=#{environmentpath} - default_manifest=/foo/$environment - EOF - end - - expect { Puppet.initialize_settings }.to raise_error(Puppet::Settings::ValidationError, /cannot interpolate.*\$environment.*in.*default_manifest/) - 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 describe 'using future parser' do before :each do Puppet[:parser] = 'future' end it_behaves_like 'puppet with default_manifest settings' end describe 'using current parser' do before :each do Puppet[:parser] = 'current' end it_behaves_like 'puppet with default_manifest settings' 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 new file mode 100644 index 000000000..bcdc136c7 --- /dev/null +++ b/spec/integration/environments/settings_interpolation_spec.rb @@ -0,0 +1,165 @@ +require 'pp' +require 'spec_helper' + +module SettingsInterpolationSpec +describe "interpolating $environment" do + 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 instaces of $environment in the setting" do + set_puppet_conf(confdir, <<-EOF) + environmentpath=$confdir/environments + #{setting}=#{value} + EOF + + Puppet.initialize_settings(cmdline_args) + expect(@logs.map(&:to_s).grep(/cannot interpolate \$environment within '#{setting}'/).count).to eq(1) + end + end + + describe "environment" do + let(:setting) { "environment" } + let(:value) { "whatareyouthinking$environment" } + let(:expected) { value } + + it_behaves_like "a setting that does not interpolate $environment" + 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 + + it "does not interpolate $environment and logs a warning when interpolating environmentpath" do + setting = 'environmentpath' + value = "$confdir/environments/$environment" + expected = "#{confdir}/environments/$environment" + + set_puppet_conf(confdir, <<-EOF) + #{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}'/) + end + end + + def assert_does_interpolate_environment(setting, value, expected_interpolation) + set_puppet_conf(confdir, <<-EOF) + #{setting}=#{value} + EOF + + 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}'/) + 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 + + 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 + end + + 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 +end diff --git a/spec/integration/provider/service/windows_spec.rb b/spec/integration/provider/service/windows_spec.rb new file mode 100644 index 000000000..79d18144d --- /dev/null +++ b/spec/integration/provider/service/windows_spec.rb @@ -0,0 +1,48 @@ +#! /usr/bin/env ruby +require 'spec_helper' + +describe Puppet::Type.type(:service).provider(:windows), '(integration)', + :if => Puppet.features.microsoft_windows? do + + require 'puppet/util/windows' + + before :each do + Puppet::Type.type(:service).stubs(:defaultprovider).returns described_class + end + + context 'should fail querying services that do not exist' do + let(:service) do + Puppet::Type.type(:service).new(:name => 'foobarservice1234') + end + + it "with a Puppet::Error when querying enabled?" do + expect { service.provider.enabled? }.to raise_error(Puppet::Error) + end + + it "with a Puppet::Error when querying status" do + expect { service.provider.status }.to raise_error(Puppet::Error) + end + end + + context 'should return valid values when querying a service that does exist' do + let(:service) do + Puppet::Type.type(:service).new(:name => 'lmhosts') + end + + it "with a valid boolean when asked if enabled" do + expect([:true, :false]).to include(service.provider.enabled?) + end + + it "with a valid status when asked about status" do + expect([ + :running, + :'continue pending', + :'pause pending', + :paused, + :running, + :'start pending', + :'stop pending', + :stopped]).to include(service.provider.status) + end + end +end diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb index 7f7aa7cb6..70d4c4fa6 100755 --- a/spec/integration/util/windows/security_spec.rb +++ b/spec/integration/util/windows/security_spec.rb @@ -1,864 +1,864 @@ #!/usr/bin/env ruby require 'spec_helper' if Puppet.features.microsoft_windows? class WindowsSecurityTester require 'puppet/util/windows/security' include Puppet::Util::Windows::Security end end describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_windows? do include PuppetSpec::Files before :all do @sids = { :current_user => Puppet::Util::Windows::SID.name_to_sid(Puppet::Util::Windows::ADSI::User.current_user_name), :system => Win32::Security::SID::LocalSystem, :admin => Puppet::Util::Windows::SID.name_to_sid("Administrator"), :administrators => Win32::Security::SID::BuiltinAdministrators, :guest => Puppet::Util::Windows::SID.name_to_sid("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, :none => Win32::Security::SID::Nobody, :everyone => Win32::Security::SID::Everyone } # The TCP/IP NetBIOS Helper service (aka 'lmhosts') has ended up # disabled on some VMs for reasons we couldn't track down. This # condition causes tests which rely on resolving UNC style paths # (like \\localhost) to fail with unhelpful error messages. # Put a check for this upfront to aid debug should this strike again. service = Puppet::Type.type(:service).new(:name => 'lmhosts') expect(service.provider.status).to eq(:running), 'lmhosts service is not running' end let (:sids) { @sids } let (:winsec) { WindowsSecurityTester.new } let (:klass) { Puppet::Util::Windows::File } def set_group_depending_on_current_user(path) if sids[:current_user] == sids[:system] # if the current user is SYSTEM, by setting the group to # guest, SYSTEM is automagically given full control, so instead # override that behavior with SYSTEM as group and a specific mode winsec.set_group(sids[:system], path) mode = winsec.get_mode(path) winsec.set_mode(mode & ~WindowsSecurityTester::S_IRWXG, path) else winsec.set_group(sids[:guest], path) end end def grant_everyone_full_access(path) sd = winsec.get_security_descriptor(path) everyone = 'S-1-1-0' inherit = Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE sd.dacl.allow(everyone, klass::FILE_ALL_ACCESS, inherit) winsec.set_security_descriptor(path, sd) end shared_examples_for "only child owner" do it "should allow child owner" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(0700, parent) check_delete(path) end it "should deny parent owner" do winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny group" do winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny other" do winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end end shared_examples_for "a securable object" do describe "on a volume that doesn't support ACLs" do [:owner, :group, :mode].each do |p| it "should return nil #{p}" do winsec.stubs(:supports_acl?).returns false winsec.send("get_#{p}", path).should be_nil end end end describe "on a volume that supports ACLs" do describe "for a normal user" do before :each do Puppet.features.stubs(:root?).returns(false) end after :each do winsec.set_mode(WindowsSecurityTester::S_IRWXU, parent) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if Puppet::FileSystem.exist?(path) end describe "#supports_acl?" do %w[c:/ c:\\ c:/windows/system32 \\\\localhost\\C$ \\\\127.0.0.1\\C$\\foo].each do |path| it "should accept #{path}" do winsec.should be_supports_acl(path) end end it "should raise an exception if it cannot get volume information" do expect { winsec.supports_acl?('foobar') }.to raise_error(Puppet::Error, /Failed to get volume information/) end end describe "#owner=" do it "should allow setting to the current user" do winsec.set_owner(sids[:current_user], path) end it "should raise an exception when setting to a different user" do lambda { winsec.set_owner(sids[:guest], path) }.should raise_error(Puppet::Error, /This security ID may not be assigned as the owner of this object./) end end describe "#owner" do it "it should not be empty" do winsec.get_owner(path).should_not be_empty end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_owner("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "#group=" do it "should allow setting to a group the current owner is a member of" do winsec.set_group(sids[:users], path) end # Unlike unix, if the user has permission to WRITE_OWNER, which the file owner has by default, # then they can set the primary group to a group that the user does not belong to. it "should allow setting to a group the current owner is not a member of" do winsec.set_group(sids[:power_users], path) end end describe "#group" do it "should not be empty" do winsec.get_group(path).should_not be_empty end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_group("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end it "should preserve inherited full control for SYSTEM when setting owner and group" do # new file has SYSTEM system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.should_not be_empty # when running under SYSTEM account, multiple ACEs come back # so we only care that we have at least one of these system_aces.any? do |ace| ace.mask == klass::FILE_ALL_ACCESS end.should be_true # changing the owner/group will no longer make the SD protected winsec.set_group(sids[:power_users], path) winsec.set_owner(sids[:administrators], path) system_aces.find do |ace| ace.mask == klass::FILE_ALL_ACCESS && ace.inherited? end.should_not be_nil end describe "#mode=" do (0000..0700).step(0100) do |mode| it "should enforce mode #{mode.to_s(8)}" do winsec.set_mode(mode, path) check_access(mode, path) end end it "should round-trip all 128 modes that do not require deny ACEs" do 0.upto(1).each do |s| 0.upto(7).each do |u| 0.upto(u).each do |g| 0.upto(g).each do |o| # if user is superset of group, and group superset of other, then # no deny ace is required, and mode can be converted to win32 # access mask, and back to mode without loss of information # (provided the owner and group are not the same) next if ((u & g) != g) or ((g & o) != o) mode = (s << 9 | u << 6 | g << 3 | o << 0) winsec.set_mode(mode, path) winsec.get_mode(path).to_s(8).should == mode.to_s(8) end end end end end it "should preserve full control for SYSTEM when setting mode" do # new file has SYSTEM system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.should_not be_empty # when running under SYSTEM account, multiple ACEs come back # so we only care that we have at least one of these system_aces.any? do |ace| ace.mask == klass::FILE_ALL_ACCESS end.should be_true # changing the mode will make the SD protected winsec.set_group(sids[:none], path) winsec.set_mode(0600, path) # and should have a non-inherited SYSTEM ACE(s) system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) system_aces.each do |ace| ace.mask.should == klass::FILE_ALL_ACCESS && ! ace.inherited? end end describe "for modes that require deny aces" do it "should map everyone to group and owner" do winsec.set_mode(0426, path) winsec.get_mode(path).to_s(8).should == "666" end it "should combine user and group modes when owner and group sids are equal" do winsec.set_group(winsec.get_owner(path), path) winsec.set_mode(0410, path) winsec.get_mode(path).to_s(8).should == "550" end end describe "for read-only objects" do before :each do winsec.set_group(sids[:none], path) winsec.set_mode(0600, path) Puppet::Util::Windows::File.add_attributes(path, klass::FILE_ATTRIBUTE_READONLY) (Puppet::Util::Windows::File.get_attributes(path) & klass::FILE_ATTRIBUTE_READONLY).should be_nonzero end it "should make them writable if any sid has write permission" do winsec.set_mode(WindowsSecurityTester::S_IWUSR, path) (Puppet::Util::Windows::File.get_attributes(path) & klass::FILE_ATTRIBUTE_READONLY).should == 0 end it "should leave them read-only if no sid has write permission and should allow full access for SYSTEM" do winsec.set_mode(WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IXGRP, path) (Puppet::Util::Windows::File.get_attributes(path) & klass::FILE_ATTRIBUTE_READONLY).should be_nonzero system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) # when running under SYSTEM account, and set_group / set_owner hasn't been called # SYSTEM full access will be restored system_aces.any? do |ace| ace.mask == klass::FILE_ALL_ACCESS end.should be_true end end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_mode(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "#mode" do it "should report when extra aces are encounted" do sd = winsec.get_security_descriptor(path) (544..547).each do |rid| sd.dacl.allow("S-1-5-32-#{rid}", klass::STANDARD_RIGHTS_ALL) end winsec.set_security_descriptor(path, sd) mode = winsec.get_mode(path) (mode & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA end it "should return deny aces" do sd = winsec.get_security_descriptor(path) sd.dacl.deny(sids[:guest], klass::FILE_GENERIC_WRITE) winsec.set_security_descriptor(path, sd) guest_aces = winsec.get_aces_for_path_by_sid(path, sids[:guest]) guest_aces.find do |ace| ace.type == Puppet::Util::Windows::AccessControlEntry::ACCESS_DENIED_ACE_TYPE end.should_not be_nil end it "should skip inherit-only ace" do sd = winsec.get_security_descriptor(path) dacl = Puppet::Util::Windows::AccessControlList.new dacl.allow( sids[:current_user], klass::STANDARD_RIGHTS_ALL | klass::SPECIFIC_RIGHTS_ALL ) dacl.allow( sids[:everyone], klass::FILE_GENERIC_READ, Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE | Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE ) winsec.set_security_descriptor(path, sd) (winsec.get_mode(path) & WindowsSecurityTester::S_IRWXO).should == 0 end it "should raise an exception if an invalid path is provided" do lambda { winsec.get_mode("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "inherited access control entries" do it "should be absent when the access control list is protected, and should not remove SYSTEM" do winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) mode = winsec.get_mode(path) [ WindowsSecurityTester::S_IEXTRA, WindowsSecurityTester::S_ISYSTEM_MISSING ].each do |flag| (mode & flag).should_not == flag end end it "should be present when the access control list is unprotected" do # add a bunch of aces to the parent with permission to add children allow = klass::STANDARD_RIGHTS_ALL | klass::SPECIFIC_RIGHTS_ALL inherit = Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE sd = winsec.get_security_descriptor(parent) sd.dacl.allow( "S-1-1-0", #everyone allow, inherit ) (544..547).each do |rid| sd.dacl.allow( "S-1-5-32-#{rid}", klass::STANDARD_RIGHTS_ALL, inherit ) end winsec.set_security_descriptor(parent, sd) # unprotect child, it should inherit from parent winsec.set_mode(WindowsSecurityTester::S_IRWXU, path, false) (winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA end end end describe "for an administrator", :if => Puppet.features.root? do before :each do is_dir = Puppet::FileSystem.directory?(path) winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, path) set_group_depending_on_current_user(path) winsec.set_owner(sids[:guest], path) expected_error = RUBY_VERSION =~ /^2\./ && is_dir ? Errno::EISDIR : Errno::EACCES lambda { File.open(path, 'r') }.should raise_error(expected_error) end after :each do if Puppet::FileSystem.exist?(path) winsec.set_owner(sids[:current_user], path) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) end end describe "#owner=" do it "should accept a user sid" do winsec.set_owner(sids[:admin], path) winsec.get_owner(path).should == sids[:admin] end it "should accept a group sid" do winsec.set_owner(sids[:power_users], path) winsec.get_owner(path).should == sids[:power_users] end it "should raise an exception if an invalid sid is provided" do lambda { winsec.set_owner("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_owner(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "#group=" do it "should accept a group sid" do winsec.set_group(sids[:power_users], path) winsec.get_group(path).should == sids[:power_users] end it "should accept a user sid" do winsec.set_group(sids[:admin], path) winsec.get_group(path).should == sids[:admin] end it "should combine owner and group rights when they are the same sid" do winsec.set_owner(sids[:power_users], path) winsec.set_group(sids[:power_users], path) winsec.set_mode(0610, path) winsec.get_owner(path).should == sids[:power_users] winsec.get_group(path).should == sids[:power_users] # note group execute permission added to user ace, and then group rwx value # reflected to match # Exclude missing system ace, since that's not relevant (winsec.get_mode(path) & 0777).to_s(8).should == "770" end it "should raise an exception if an invalid sid is provided" do lambda { winsec.set_group("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/) end it "should raise an exception if an invalid path is provided" do lambda { winsec.set_group(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./) end end describe "when the sid is NULL" do it "should retrieve an empty owner sid" it "should retrieve an empty group sid" end describe "when the sid refers to a deleted trustee" do it "should retrieve the user sid" do sid = nil - user = Puppet::Util::Windows::ADSI::User.create("delete_me_user") + user = Puppet::Util::Windows::ADSI::User.create("puppet#{rand(10000)}") user.commit begin sid = Puppet::Util::Windows::ADSI::User.new(user.name).sid.to_s winsec.set_owner(sid, path) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) ensure Puppet::Util::Windows::ADSI::User.delete(user.name) end winsec.get_owner(path).should == sid winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXU end it "should retrieve the group sid" do sid = nil - group = Puppet::Util::Windows::ADSI::Group.create("delete_me_group") + group = Puppet::Util::Windows::ADSI::Group.create("puppet#{rand(10000)}") group.commit begin sid = Puppet::Util::Windows::ADSI::Group.new(group.name).sid.to_s winsec.set_group(sid, path) winsec.set_mode(WindowsSecurityTester::S_IRWXG, path) ensure Puppet::Util::Windows::ADSI::Group.delete(group.name) end winsec.get_group(path).should == sid winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXG end end describe "#mode" do it "should deny all access when the DACL is empty, including SYSTEM" do sd = winsec.get_security_descriptor(path) # don't allow inherited aces to affect the test protect = true new_sd = Puppet::Util::Windows::SecurityDescriptor.new(sd.owner, sd.group, [], protect) winsec.set_security_descriptor(path, new_sd) winsec.get_mode(path).should == WindowsSecurityTester::S_ISYSTEM_MISSING end # REMIND: ruby crashes when trying to set a NULL DACL # it "should allow all when it is nil" do # winsec.set_owner(sids[:current_user], path) # winsec.open_file(path, WindowsSecurityTester::READ_CONTROL | WindowsSecurityTester::WRITE_DAC) do |handle| # winsec.set_security_info(handle, WindowsSecurityTester::DACL_SECURITY_INFORMATION | WindowsSecurityTester::PROTECTED_DACL_SECURITY_INFORMATION, nil) # end # winsec.get_mode(path).to_s(8).should == "777" # end end describe "when the parent directory" do before :each do winsec.set_owner(sids[:current_user], parent) winsec.set_owner(sids[:current_user], path) winsec.set_mode(0777, path, false) end describe "is writable and executable" do describe "and sticky bit is set" do it "should allow child owner" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(01700, parent) check_delete(path) end it "should allow parent owner" do winsec.set_owner(sids[:current_user], parent) winsec.set_group(sids[:guest], parent) winsec.set_mode(01700, parent) winsec.set_owner(sids[:current_user], path) winsec.set_group(sids[:guest], path) winsec.set_mode(0700, path) check_delete(path) end it "should deny group" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(01770, parent) winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny other" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(01777, parent) winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) lambda { check_delete(path) }.should raise_error(Errno::EACCES) end end describe "and sticky bit is not set" do it "should allow child owner" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(0700, parent) check_delete(path) end it "should allow parent owner" do winsec.set_owner(sids[:current_user], parent) winsec.set_group(sids[:guest], parent) winsec.set_mode(0700, parent) winsec.set_owner(sids[:current_user], path) winsec.set_group(sids[:guest], path) winsec.set_mode(0700, path) check_delete(path) end it "should allow group" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(0770, parent) winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) check_delete(path) end it "should allow other" do winsec.set_owner(sids[:guest], parent) winsec.set_group(sids[:current_user], parent) winsec.set_mode(0777, parent) winsec.set_owner(sids[:guest], path) winsec.set_group(sids[:current_user], path) winsec.set_mode(0700, path) check_delete(path) end end end describe "is not writable" do before :each do winsec.set_group(sids[:current_user], parent) winsec.set_mode(0555, parent) end it_behaves_like "only child owner" end describe "is not executable" do before :each do winsec.set_group(sids[:current_user], parent) winsec.set_mode(0666, parent) end it_behaves_like "only child owner" end end end end end describe "file" do let (:parent) do tmpdir('win_sec_test_file') end let (:path) do path = File.join(parent, 'childfile') File.new(path, 'w').close path end after :each do # allow temp files to be cleaned up grant_everyone_full_access(parent) end it_behaves_like "a securable object" do def check_access(mode, path) if (mode & WindowsSecurityTester::S_IRUSR).nonzero? check_read(path) else lambda { check_read(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IWUSR).nonzero? check_write(path) else lambda { check_write(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IXUSR).nonzero? lambda { check_execute(path) }.should raise_error(Errno::ENOEXEC) else lambda { check_execute(path) }.should raise_error(Errno::EACCES) end end def check_read(path) File.open(path, 'r').close end def check_write(path) File.open(path, 'w').close end def check_execute(path) Kernel.exec(path) end def check_delete(path) File.delete(path) end end describe "locked files" do let (:explorer) { File.join(Dir::WINDOWS, "explorer.exe") } it "should get the owner" do winsec.get_owner(explorer).should match /^S-1-5-/ end it "should get the group" do winsec.get_group(explorer).should match /^S-1-5-/ end it "should get the mode" do winsec.get_mode(explorer).should == (WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG | WindowsSecurityTester::S_IEXTRA) end end end describe "directory" do let (:parent) do tmpdir('win_sec_test_dir') end let (:path) do path = File.join(parent, 'childdir') Dir.mkdir(path) path end after :each do # allow temp files to be cleaned up grant_everyone_full_access(parent) end it_behaves_like "a securable object" do def check_access(mode, path) if (mode & WindowsSecurityTester::S_IRUSR).nonzero? check_read(path) else lambda { check_read(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IWUSR).nonzero? check_write(path) else lambda { check_write(path) }.should raise_error(Errno::EACCES) end if (mode & WindowsSecurityTester::S_IXUSR).nonzero? check_execute(path) else lambda { check_execute(path) }.should raise_error(Errno::EACCES) end end def check_read(path) Dir.entries(path) end def check_write(path) Dir.mkdir(File.join(path, "subdir")) end def check_execute(path) Dir.chdir(path) {} end def check_delete(path) Dir.rmdir(path) end end describe "inheritable aces" do it "should be applied to child objects" do mode640 = WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IWUSR | WindowsSecurityTester::S_IRGRP winsec.set_mode(mode640, path) newfile = File.join(path, "newfile.txt") File.new(newfile, "w").close newdir = File.join(path, "newdir") Dir.mkdir(newdir) [newfile, newdir].each do |p| mode = winsec.get_mode(p) (mode & 07777).to_s(8).should == mode640.to_s(8) end end end end context "security descriptor" do let(:path) { tmpfile('sec_descriptor') } let(:read_execute) { 0x201FF } let(:synchronize) { 0x100000 } before :each do FileUtils.touch(path) end it "preserves aces for other users" do dacl = Puppet::Util::Windows::AccessControlList.new sids_in_dacl = [sids[:current_user], sids[:users]] sids_in_dacl.each do |sid| dacl.allow(sid, read_execute) end sd = Puppet::Util::Windows::SecurityDescriptor.new(sids[:guest], sids[:guest], dacl, true) winsec.set_security_descriptor(path, sd) aces = winsec.get_security_descriptor(path).dacl.to_a aces.map(&:sid).should == sids_in_dacl aces.map(&:mask).all? { |mask| mask == read_execute }.should be_true end it "changes the sid for all aces that were assigned to the old owner" do sd = winsec.get_security_descriptor(path) sd.owner.should_not == sids[:guest] sd.dacl.allow(sd.owner, read_execute) sd.dacl.allow(sd.owner, synchronize) sd.owner = sids[:guest] winsec.set_security_descriptor(path, sd) dacl = winsec.get_security_descriptor(path).dacl aces = dacl.find_all { |ace| ace.sid == sids[:guest] } # only non-inherited aces will be reassigned to guest, so # make sure we find at least the two we added aces.size.should >= 2 end it "preserves INHERIT_ONLY_ACEs" do # inherit only aces can only be set on directories dir = tmpdir('inheritonlyace') inherit_flags = Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE | Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE sd = winsec.get_security_descriptor(dir) sd.dacl.allow(sd.owner, klass::FILE_ALL_ACCESS, inherit_flags) winsec.set_security_descriptor(dir, sd) sd = winsec.get_security_descriptor(dir) winsec.set_owner(sids[:guest], dir) sd = winsec.get_security_descriptor(dir) sd.dacl.find do |ace| ace.sid == sids[:guest] && ace.inherit_only? end.should_not be_nil end it "allows deny ACEs with inheritance" do # inheritance can only be set on directories dir = tmpdir('denyaces') inherit_flags = Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE sd = winsec.get_security_descriptor(dir) sd.dacl.deny(sids[:guest], klass::FILE_ALL_ACCESS, inherit_flags) winsec.set_security_descriptor(dir, sd) sd = winsec.get_security_descriptor(dir) sd.dacl.find do |ace| ace.sid == sids[:guest] && ace.flags != 0 end.should_not be_nil end context "when managing mode" do it "removes aces for sids that are neither the owner nor group" do # add a guest ace, it's never owner or group sd = winsec.get_security_descriptor(path) sd.dacl.allow(sids[:guest], read_execute) winsec.set_security_descriptor(path, sd) # setting the mode, it should remove extra aces winsec.set_mode(0770, path) # make sure it's gone dacl = winsec.get_security_descriptor(path).dacl aces = dacl.find_all { |ace| ace.sid == sids[:guest] } aces.should be_empty end end end end diff --git a/spec/lib/puppet_spec/matchers.rb b/spec/lib/puppet_spec/matchers.rb index c01d8e89d..033397cb8 100644 --- a/spec/lib/puppet_spec/matchers.rb +++ b/spec/lib/puppet_spec/matchers.rb @@ -1,147 +1,152 @@ require 'stringio' ######################################################################## # Backward compatibility for Jenkins outdated environment. module RSpec module Matchers module BlockAliases alias_method :to, :should unless method_defined? :to alias_method :to_not, :should_not unless method_defined? :to_not alias_method :not_to, :should_not unless method_defined? :not_to end end end ######################################################################## # Custom matchers... RSpec::Matchers.define :have_matching_element do |expected| match do |actual| actual.any? { |item| item =~ expected } end end +RSpec::Matchers.define :have_matching_log do |expected| + match do |actual| + actual.map(&:to_s).any? { |item| item =~ expected } + end +end RSpec::Matchers.define :exit_with do |expected| actual = nil match do |block| begin block.call rescue SystemExit => e actual = e.status end actual and actual == expected end failure_message_for_should do |block| "expected exit with code #{expected} but " + (actual.nil? ? " exit was not called" : "we exited with #{actual} instead") end failure_message_for_should_not do |block| "expected that exit would not be called with #{expected}" end description do "expect exit with #{expected}" end end RSpec::Matchers.define :have_printed do |expected| case expected when String, Regexp expected = expected else expected = expected.to_s end chain :and_exit_with do |code| @expected_exit_code = code end define_method :matches_exit_code? do |actual| @expected_exit_code.nil? || @expected_exit_code == actual end define_method :matches_output? do |actual| return false unless actual case expected when String actual.include?(expected) when Regexp expected.match(actual) else raise ArgumentError, "No idea how to match a #{actual.class.name}" end end match do |block| $stderr = $stdout = StringIO.new $stdout.set_encoding('UTF-8') if $stdout.respond_to?(:set_encoding) begin block.call rescue SystemExit => e raise unless @expected_exit_code @actual_exit_code = e.status ensure $stdout.rewind @actual = $stdout.read $stdout = STDOUT $stderr = STDERR end matches_output?(@actual) && matches_exit_code?(@actual_exit_code) end failure_message_for_should do |actual| if actual.nil? then "expected #{expected.inspect}, but nothing was printed" else if !@expected_exit_code.nil? && matches_output?(actual) "expected exit with code #{@expected_exit_code} but " + (@actual_exit_code.nil? ? " exit was not called" : "exited with #{@actual_exit_code} instead") else "expected #{expected.inspect} to be printed; got:\n#{actual}" end end end failure_message_for_should_not do |actual| if @expected_exit_code && matches_exit_code?(@actual_exit_code) "expected exit code to not be #{@actual_exit_code}" else "expected #{expected.inspect} to not be printed; got:\n#{actual}" end end description do "expect #{expected.inspect} to be printed" + (@expected_exit_code.nil ? '' : " with exit code #{@expected_exit_code}") end end RSpec::Matchers.define :equal_attributes_of do |expected| match do |actual| actual.instance_variables.all? do |attr| actual.instance_variable_get(attr) == expected.instance_variable_get(attr) end end end RSpec::Matchers.define :equal_resource_attributes_of do |expected| match do |actual| actual.keys do |attr| actual[attr] == expected[attr] end end end RSpec::Matchers.define :be_one_of do |*expected| match do |actual| expected.include? actual end failure_message_for_should do |actual| "expected #{actual.inspect} to be one of #{expected.map(&:inspect).join(' or ')}" end end diff --git a/spec/unit/functions/epp_spec.rb b/spec/unit/functions/epp_spec.rb index 382fd9548..da58748b3 100644 --- a/spec/unit/functions/epp_spec.rb +++ b/spec/unit/functions/epp_spec.rb @@ -1,155 +1,154 @@ - require 'spec_helper' describe "the epp function" do include PuppetSpec::Files before :all do Puppet::Parser::Functions.autoloader.loadall end before :each do Puppet[:parser] = 'future' end let :node do Puppet::Node.new('localhost') end let :compiler do Puppet::Parser::Compiler.new(node) end let :scope do Puppet::Parser::Scope.new(compiler) end context "when accessing scope variables as $ variables" do it "looks up the value from the scope" do scope["what"] = "are belong" eval_template("all your base <%= $what %> to us").should == "all your base are belong to us" end it "get nil accessing a variable that does not exist" do eval_template("<%= $kryptonite == undef %>").should == "true" end it "get nil accessing a variable that is undef" do scope['undef_var'] = nil eval_template("<%= $undef_var == undef %>").should == "true" end it "gets shadowed variable if args are given" do scope['phantom'] = 'of the opera' eval_template_with_args("<%= $phantom == dragos %>", 'phantom' => 'dragos').should == "true" end it "can use values from the enclosing scope for defaults" do scope['phantom'] = 'of the opera' eval_template("<%- |$phantom = $phantom| -%><%= $phantom %>").should == "of the opera" end it "uses the default value if the given value is undef/nil" do eval_template_with_args("<%- |$phantom = 'inside your mind'| -%><%= $phantom %>", 'phantom' => nil).should == "inside your mind" end it "gets shadowed variable if args are given and parameters are specified" do scope['x'] = 'wrong one' eval_template_with_args("<%- |$x| -%><%= $x == correct %>", 'x' => 'correct').should == "true" end it "raises an error if required variable is not given" do scope['x'] = 'wrong one' expect do eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'y' => 'correct') end.to raise_error(/no value given for required parameters x/) end it "raises an error if too many arguments are given" do scope['x'] = 'wrong one' expect do eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'x' => 'correct', 'y' => 'surplus') end.to raise_error(/Too many arguments: 2 for 1/) end end context "when given an empty template" do it "allows the template file to be empty" do expect(eval_template("")).to eq("") end it "allows the template to have empty body after parameters" do expect(eval_template_with_args("<%-|$x|%>", 'x'=>1)).to eq("") end end context "when using typed parameters" do it "allows a passed value that matches the parameter's type" do expect(eval_template_with_args("<%-|String $x|-%><%= $x == correct %>", 'x' => 'correct')).to eq("true") end it "does not allow slurped parameters" do expect do eval_template_with_args("<%-|*$x|-%><%= $x %>", 'x' => 'incorrect') end.to raise_error(/'captures rest' - not supported in an Epp Template/) end it "raises an error when the passed value does not match the parameter's type" do expect do eval_template_with_args("<%-|Integer $x|-%><%= $x %>", 'x' => 'incorrect') end.to raise_error(/expected.*Integer.*actual.*String/m) end it "raises an error when the default value does not match the parameter's type" do expect do eval_template("<%-|Integer $x = 'nope'|-%><%= $x %>") end.to raise_error(/expected.*Integer.*actual.*String/m) end it "allows an parameter to default to undef" do expect(eval_template("<%-|Optional[Integer] $x = undef|-%><%= $x == undef %>")).to eq("true") end end # although never a problem with epp it "is not interfered with by having a variable named 'string' (#14093)" do scope['string'] = "this output should not be seen" eval_template("some text that is static").should == "some text that is static" end it "has access to a variable named 'string' (#14093)" do scope['string'] = "the string value" eval_template("string was: <%= $string %>").should == "string was: the string value" end describe 'when loading from modules' do include PuppetSpec::Files it 'an epp template is found' do modules_dir = dir_containing('modules', { 'testmodule' => { 'templates' => { 'the_x.epp' => 'The x is <%= $x %>' } }}) Puppet.override({:current_environment => (env = Puppet::Node::Environment.create(:testload, [ modules_dir ]))}, "test") do node.environment = env expect(epp_function.call(scope, 'testmodule/the_x.epp', { 'x' => '3'} )).to eql("The x is 3") end end end def eval_template_with_args(content, args_hash) file_path = tmpdir('epp_spec_content') filename = File.join(file_path, "template.epp") File.open(filename, "w+") { |f| f.write(content) } Puppet::Parser::Files.stubs(:find_template).returns(filename) epp_function.call(scope, 'template', args_hash) end def eval_template(content) file_path = tmpdir('epp_spec_content') filename = File.join(file_path, "template.epp") File.open(filename, "w+") { |f| f.write(content) } Puppet::Parser::Files.stubs(:find_template).returns(filename) epp_function.call(scope, 'template') end def epp_function() epp_func = scope.compiler.loaders.public_environment_loader.load(:function, 'epp') end end diff --git a/spec/unit/pops/loaders/dependency_loader_spec.rb b/spec/unit/pops/loaders/dependency_loader_spec.rb index cbdefe897..31d937766 100644 --- a/spec/unit/pops/loaders/dependency_loader_spec.rb +++ b/spec/unit/pops/loaders/dependency_loader_spec.rb @@ -1,61 +1,66 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'dependency loader' do include PuppetSpec::Files let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } let(:loaders) { Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) } describe 'FileBased module loader' do it 'load something in global name space raises an error' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => 'Puppet::Functions.create_function("foo") { def foo; end; }' }}}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') - dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) + loader = loader_for('testmodule', module_dir) + expect do - dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + loader.load_typed(typed_name(:function, 'testmodule::foo')).value end.to raise_error(ArgumentError, /produced mis-matched name, expected 'testmodule::foo', got foo/) end it 'can load something in a qualified name space' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' }}}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') - dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) - function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + loader = loader_for('testmodule', module_dir) + + function = loader.load_typed(typed_name(:function, 'testmodule::foo')).value expect(function.class.name).to eq('testmodule::foo') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end it 'can load something in a qualified name space more than once' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' }}}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') - dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) - function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + loader = loader_for('testmodule', module_dir) + + function = loader.load_typed(typed_name(:function, 'testmodule::foo')).value expect(function.class.name).to eq('testmodule::foo') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) - function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + function = loader.load_typed(typed_name(:function, 'testmodule::foo')).value expect(function.class.name).to eq('testmodule::foo') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end end + def loader_for(name, dir) + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, loaders, name, dir) + Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) + end + def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end end diff --git a/spec/unit/pops/loaders/loader_paths_spec.rb b/spec/unit/pops/loaders/loader_paths_spec.rb index 2cd565a8c..7f65f44c1 100644 --- a/spec/unit/pops/loaders/loader_paths_spec.rb +++ b/spec/unit/pops/loaders/loader_paths_spec.rb @@ -1,55 +1,43 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'loader paths' do include PuppetSpec::Files let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } let(:unused_loaders) { nil } - describe 'the relative_path_for_types method' do - it 'produces paths to load in precendence order' do - module_dir = dir_containing('testmodule', { - 'functions' => {}, - 'lib' => { - 'puppet' => { - 'functions' => {}, - }}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') - - effective_paths = Puppet::Pops::Loader::LoaderPaths.relative_paths_for_type(:function, module_loader) - - expect(effective_paths.collect(&:generic_path)).to eq([ - File.join(module_dir, 'lib', 'puppet', 'functions') - ]) - end - - it 'module loader has smart-paths that prunes unavailable paths' do - module_dir = dir_containing('testmodule', {'lib' => {'puppet' => {'functions' => {'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' }}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') - - effective_paths = module_loader.smart_paths.effective_paths(:function) - - expect(effective_paths.size).to be_eql(1) - expect(effective_paths[0].generic_path).to be_eql(File.join(module_dir, 'lib', 'puppet', 'functions')) - end - - it 'all function smart-paths produces entries if they exist' do - module_dir = dir_containing('testmodule', { - 'lib' => { - 'puppet' => { - 'functions' => {'foo4x.rb' => 'ignored in this test'}, - }}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') - - effective_paths = module_loader.smart_paths.effective_paths(:function) - - expect(effective_paths.size).to eq(1) - expect(module_loader.path_index.size).to eq(1) - path_index = module_loader.path_index - expect(path_index).to include(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo4x.rb')) - end + it 'module loader has smart-paths that prunes unavailable paths' do + module_dir = dir_containing('testmodule', {'lib' => {'puppet' => {'functions' => + {'foo.rb' => + 'Puppet::Functions.create_function("testmodule::foo") { + def foo; end; + }' + } + }}}) + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, unused_loaders, 'testmodule', module_dir) + + effective_paths = module_loader.smart_paths.effective_paths(:function) + + expect(effective_paths.size).to be_eql(1) + expect(effective_paths[0].generic_path).to be_eql(File.join(module_dir, 'lib', 'puppet', 'functions')) + end + + it 'all function smart-paths produces entries if they exist' do + module_dir = dir_containing('testmodule', { + 'lib' => { + 'puppet' => { + 'functions' => {'foo4x.rb' => 'ignored in this test'}, + }}}) + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, unused_loaders, 'testmodule', module_dir) + + effective_paths = module_loader.smart_paths.effective_paths(:function) + + expect(effective_paths.size).to eq(1) + expect(module_loader.path_index.size).to eq(1) + path_index = module_loader.path_index + expect(path_index).to include(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo4x.rb')) end end diff --git a/spec/unit/pops/loaders/module_loaders_spec.rb b/spec/unit/pops/loaders/module_loaders_spec.rb index 058e765de..d15a8ee0d 100644 --- a/spec/unit/pops/loaders/module_loaders_spec.rb +++ b/spec/unit/pops/loaders/module_loaders_spec.rb @@ -1,90 +1,90 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'FileBased module loader' do include PuppetSpec::Files let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } let(:loaders) { Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) } it 'can load a 4x function API ruby function in global name space' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'foo4x.rb' => <<-CODE Puppet::Functions.create_function(:foo4x) do def foo4x() 'yay' end end CODE } } } }) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, loaders, 'testmodule', module_dir) function = module_loader.load_typed(typed_name(:function, 'foo4x')).value expect(function.class.name).to eq('foo4x') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end it 'can load a 4x function API ruby function in qualified name space' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo4x.rb' => <<-CODE Puppet::Functions.create_function('testmodule::foo4x') do def foo4x() 'yay' end end CODE } } } }}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, loaders, 'testmodule', module_dir) function = module_loader.load_typed(typed_name(:function, 'testmodule::foo4x')).value expect(function.class.name).to eq('testmodule::foo4x') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end it 'makes parent loader win over entries in child' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => <<-CODE Puppet::Functions.create_function('testmodule::foo') do def foo() 'yay' end end CODE }}}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, loaders, 'testmodule', module_dir) module_dir2 = dir_containing('testmodule2', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule2' => { 'foo.rb' => <<-CODE raise "should not get here" CODE }}}}}) module_loader2 = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(module_loader, loaders, 'testmodule2', module_dir2, 'test2') function = module_loader2.load_typed(typed_name(:function, 'testmodule::foo')).value expect(function.class.name).to eq('testmodule::foo') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end end diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb index c61ea1c4f..24449ef9d 100755 --- a/spec/unit/provider/package/rpm_spec.rb +++ b/spec/unit/provider/package/rpm_spec.rb @@ -1,390 +1,471 @@ #! /usr/bin/env ruby require 'spec_helper' provider_class = Puppet::Type.type(:package).provider(:rpm) describe provider_class do let (:packages) do <<-RPM_OUTPUT cracklib-dicts 0 2.8.9 3.3 x86_64 basesystem 0 8.0 5.1.1.el5.centos noarch chkconfig 0 1.3.30.2 2.el5 x86_64 myresource 0 1.2.3.4 5.el4 noarch mysummaryless 0 1.2.3.4 5.el4 noarch RPM_OUTPUT end let(:resource_name) { 'myresource' } let(:resource) do Puppet::Type.type(:package).new( :name => resource_name, :ensure => :installed, :provider => 'rpm' ) end let(:provider) do provider = provider_class.new provider.resource = resource provider end let(:nevra_format) { %Q{%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\\n} } let(:execute_options) do {:failonfail => true, :combine => true, :custom_environment => {}} end let(:rpm_version) { "RPM version 5.0.0\n" } before(:each) do Puppet::Util.stubs(:which).with("rpm").returns("/bin/rpm") provider_class.stubs(:which).with("rpm").returns("/bin/rpm") provider_class.instance_variable_set("@current_version", nil) Puppet::Type::Package::ProviderRpm.expects(:execute).with(["/bin/rpm", "--version"]).returns(rpm_version).at_most_once Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "--version"], execute_options).returns(rpm_version).at_most_once end describe 'provider features' do it { should be_versionable } it { should be_install_options } it { should be_uninstall_options } it { should be_virtual_packages } end describe "self.instances" do describe "with a modern version of RPM" do it "includes all the modern flags" do Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = provider_class.instances end end describe "with a version of RPM < 4.1" do let(:rpm_version) { "RPM version 4.0.2\n" } it "excludes the --nosignature flag" do Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = provider_class.instances end end describe "with a version of RPM < 4.0.2" do let(:rpm_version) { "RPM version 3.0.5\n" } it "excludes the --nodigest flag" do Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --qf '#{nevra_format}'").yields(packages) installed_packages = provider_class.instances end end it "returns an array of packages" do Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = provider_class.instances expect(installed_packages[0].properties).to eq( { :provider => :rpm, :name => "cracklib-dicts", :epoch => "0", :version => "2.8.9", :release => "3.3", :arch => "x86_64", :ensure => "2.8.9-3.3", } ) expect(installed_packages[1].properties).to eq( { :provider => :rpm, :name => "basesystem", :epoch => "0", :version => "8.0", :release => "5.1.1.el5.centos", :arch => "noarch", :ensure => "8.0-5.1.1.el5.centos", } ) expect(installed_packages[2].properties).to eq( { :provider => :rpm, :name => "chkconfig", :epoch => "0", :version => "1.3.30.2", :release => "2.el5", :arch => "x86_64", :ensure => "1.3.30.2-2.el5", } ) expect(installed_packages.last.properties).to eq( { :provider => :rpm, :name => "mysummaryless", :epoch => "0", :version => "1.2.3.4", :release => "5.el4", :arch => "noarch", :ensure => "1.2.3.4-5.el4", } ) end end describe "#install" do let(:resource) do Puppet::Type.type(:package).new( :name => 'myresource', :ensure => :installed, :source => '/path/to/package' ) end describe "when not already installed" do it "only includes the '-i' flag" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-i"], '/path/to/package'], execute_options) provider.install end end describe "when installed with options" do let(:resource) do Puppet::Type.type(:package).new( :name => resource_name, :ensure => :installed, :provider => 'rpm', :source => '/path/to/package', :install_options => ['-D', {'--test' => 'value'}, '-Q'] ) end it "includes the options" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-i", "-D", "--test=value", "-Q"], '/path/to/package'], execute_options) provider.install end end describe "when an older version is installed" do before(:each) do # Force the provider to think a version of the package is already installed # This is real hacky. I'm sorry. --jeffweiss 25 Jan 2013 provider.instance_variable_get('@property_hash')[:ensure] = '1.2.3.3' end it "includes the '-U --oldpackage' flags" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-U", "--oldpackage"], '/path/to/package'], execute_options) provider.install end end end describe "#latest" do it "retrieves version string after querying rpm for version from source file" do resource.expects(:[]).with(:source).returns('source-string') Puppet::Util::Execution.expects(:execfail).with(["/bin/rpm", "-q", "--qf", nevra_format, "-p", "source-string"], Puppet::Error).returns("myresource 0 1.2.3.4 5.el4 noarch\n") expect(provider.latest).to eq("1.2.3.4-5.el4") end end describe "#uninstall" do let(:resource) do Puppet::Type.type(:package).new( :name => 'myresource', :ensure => :installed ) end describe "on a modern RPM" do before(:each) do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "myresource", '--nosignature', '--nodigest', "--qf", nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n") end let(:rpm_version) { "RPM version 4.10.0\n" } it "includes the architecture in the package name" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-e"], 'myresource-1.2.3.4-5.el4.noarch'], execute_options).returns('').at_most_once provider.uninstall end end describe "on an ancient RPM" do before(:each) do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "myresource", '', '', '--qf', nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n") end let(:rpm_version) { "RPM version 3.0.6\n" } it "excludes the architecture from the package name" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-e"], 'myresource-1.2.3.4-5.el4'], execute_options).returns('').at_most_once provider.uninstall end end describe "when uninstalled with options" do before(:each) do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "myresource", '--nosignature', '--nodigest', "--qf", nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n") end let(:resource) do Puppet::Type.type(:package).new( :name => resource_name, :ensure => :absent, :provider => 'rpm', :uninstall_options => ['--nodeps'] ) end it "includes the options" do Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-e", "--nodeps"], 'myresource-1.2.3.4-5.el4.noarch'], execute_options) provider.uninstall end end end describe "parsing" do def parser_test(rpm_output_string, gold_hash, number_of_debug_logs = 0) Puppet.expects(:debug).times(number_of_debug_logs) Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).returns(rpm_output_string) expect(provider.query).to eq(gold_hash) end let(:resource_name) { 'name' } let('delimiter') { ':DESC:' } let(:package_hash) do { :name => 'name', :epoch => 'epoch', :version => 'version', :release => 'release', :arch => 'arch', :provider => :rpm, :ensure => 'version-release', } end let(:line) { 'name epoch version release arch' } ['name', 'epoch', 'version', 'release', 'arch'].each do |field| it "still parses if #{field} is replaced by delimiter" do parser_test( line.gsub(field, delimiter), package_hash.merge( field.to_sym => delimiter, :ensure => 'version-release'.gsub(field, delimiter) ) ) end end it "does not fail if line is unparseable, but issues a debug log" do parser_test('bad data', {}, 1) end it "does not log or fail if rpm returns package not found" do Puppet.expects(:debug).never expected_args = ["/bin/rpm", "-q", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format] Puppet::Util::Execution.expects(:execute).with(expected_args, execute_options).raises Puppet::ExecutionFailure.new("package #{resource_name} is not installed") expect(provider.query).to be_nil end it "parses virtual package" do provider.resource[:allow_virtual] = true expected_args = ["/bin/rpm", "-q", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format] Puppet::Util::Execution.expects(:execute).with(expected_args, execute_options).raises Puppet::ExecutionFailure.new("package #{resource_name} is not installed") Puppet::Util::Execution.expects(:execute).with(expected_args + ["--whatprovides"], execute_options).returns "myresource 0 1.2.3.4 5.el4 noarch\n" expect(provider.query).to eq({ :name => "myresource", :epoch => "0", :version => "1.2.3.4", :release => "5.el4", :arch => "noarch", :provider => :rpm, :ensure => "1.2.3.4-5.el4" }) end end describe "#install_options" do it "returns nil by default" do expect(provider.install_options).to eq(nil) end it "returns install_options when set" do provider.resource[:install_options] = ['-n'] expect(provider.install_options).to eq(['-n']) end it "returns multiple install_options when set" do provider.resource[:install_options] = ['-L', '/opt/puppet'] expect(provider.install_options).to eq(['-L', '/opt/puppet']) end it 'returns install_options when set as hash' do provider.resource[:install_options] = [{ '-Darch' => 'vax' }] expect(provider.install_options).to eq(['-Darch=vax']) end it 'returns install_options when an array with hashes' do provider.resource[:install_options] = [ '-L', { '-Darch' => 'vax' }] expect(provider.install_options).to eq(['-L', '-Darch=vax']) end end describe "#uninstall_options" do it "returns nil by default" do expect(provider.uninstall_options).to eq(nil) end it "returns uninstall_options when set" do provider.resource[:uninstall_options] = ['-n'] expect(provider.uninstall_options).to eq(['-n']) end it "returns multiple uninstall_options when set" do provider.resource[:uninstall_options] = ['-L', '/opt/puppet'] expect(provider.uninstall_options).to eq(['-L', '/opt/puppet']) end it 'returns uninstall_options when set as hash' do provider.resource[:uninstall_options] = [{ '-Darch' => 'vax' }] expect(provider.uninstall_options).to eq(['-Darch=vax']) end it 'returns uninstall_options when an array with hashes' do provider.resource[:uninstall_options] = [ '-L', { '-Darch' => 'vax' }] expect(provider.uninstall_options).to eq(['-L', '-Darch=vax']) end end describe ".nodigest" do { '4.0' => nil, '4.0.1' => nil, '4.0.2' => '--nodigest', '4.0.3' => '--nodigest', '4.1' => '--nodigest', '5' => '--nodigest', }.each do |version, expected| describe "when current version is #{version}" do it "returns #{expected.inspect}" do provider_class.stubs(:current_version).returns(version) expect(provider_class.nodigest).to eq(expected) end end end end describe ".nosignature" do { '4.0.3' => nil, '4.1' => '--nosignature', '4.1.1' => '--nosignature', '4.2' => '--nosignature', '5' => '--nosignature', }.each do |version, expected| describe "when current version is #{version}" do it "returns #{expected.inspect}" do provider_class.stubs(:current_version).returns(version) expect(provider_class.nosignature).to eq(expected) end end end end + + describe 'version comparison' do + + # test cases munged directly from rpm's own + # tests/rpmvercmp.at + it { provider.rpmvercmp("1.0", "1.0").should == 0 } + it { provider.rpmvercmp("1.0", "2.0").should == -1 } + it { provider.rpmvercmp("2.0", "1.0").should == 1 } + it { provider.rpmvercmp("2.0.1", "2.0.1").should == 0 } + it { provider.rpmvercmp("2.0", "2.0.1").should == -1 } + it { provider.rpmvercmp("2.0.1", "2.0").should == 1 } + it { provider.rpmvercmp("2.0.1a", "2.0.1a").should == 0 } + it { provider.rpmvercmp("2.0.1a", "2.0.1").should == 1 } + it { provider.rpmvercmp("2.0.1", "2.0.1a").should == -1 } + it { provider.rpmvercmp("5.5p1", "5.5p1").should == 0 } + it { provider.rpmvercmp("5.5p1", "5.5p2").should == -1 } + it { provider.rpmvercmp("5.5p2", "5.5p1").should == 1 } + it { provider.rpmvercmp("5.5p10", "5.5p10").should == 0 } + it { provider.rpmvercmp("5.5p1", "5.5p10").should == -1 } + it { provider.rpmvercmp("5.5p10", "5.5p1").should == 1 } + it { provider.rpmvercmp("10xyz", "10.1xyz").should == -1 } + it { provider.rpmvercmp("10.1xyz", "10xyz").should == 1 } + it { provider.rpmvercmp("xyz10", "xyz10").should == 0 } + it { provider.rpmvercmp("xyz10", "xyz10.1").should == -1 } + it { provider.rpmvercmp("xyz10.1", "xyz10").should == 1 } + it { provider.rpmvercmp("xyz.4", "xyz.4").should == 0 } + it { provider.rpmvercmp("xyz.4", "8").should == -1 } + it { provider.rpmvercmp("8", "xyz.4").should == 1 } + it { provider.rpmvercmp("xyz.4", "2").should == -1 } + it { provider.rpmvercmp("2", "xyz.4").should == 1 } + it { provider.rpmvercmp("5.5p2", "5.6p1").should == -1 } + it { provider.rpmvercmp("5.6p1", "5.5p2").should == 1 } + it { provider.rpmvercmp("5.6p1", "6.5p1").should == -1 } + it { provider.rpmvercmp("6.5p1", "5.6p1").should == 1 } + it { provider.rpmvercmp("6.0.rc1", "6.0").should == 1 } + it { provider.rpmvercmp("6.0", "6.0.rc1").should == -1 } + it { provider.rpmvercmp("10b2", "10a1").should == 1 } + it { provider.rpmvercmp("10a2", "10b2").should == -1 } + it { provider.rpmvercmp("1.0aa", "1.0aa").should == 0 } + it { provider.rpmvercmp("1.0a", "1.0aa").should == -1 } + it { provider.rpmvercmp("1.0aa", "1.0a").should == 1 } + it { provider.rpmvercmp("10.0001", "10.0001").should == 0 } + it { provider.rpmvercmp("10.0001", "10.1").should == 0 } + it { provider.rpmvercmp("10.1", "10.0001").should == 0 } + it { provider.rpmvercmp("10.0001", "10.0039").should == -1 } + it { provider.rpmvercmp("10.0039", "10.0001").should == 1 } + it { provider.rpmvercmp("4.999.9", "5.0").should == -1 } + it { provider.rpmvercmp("5.0", "4.999.9").should == 1 } + it { provider.rpmvercmp("20101121", "20101121").should == 0 } + it { provider.rpmvercmp("20101121", "20101122").should == -1 } + it { provider.rpmvercmp("20101122", "20101121").should == 1 } + it { provider.rpmvercmp("2_0", "2_0").should == 0 } + it { provider.rpmvercmp("2.0", "2_0").should == 0 } + it { provider.rpmvercmp("2_0", "2.0").should == 0 } + it { provider.rpmvercmp("a", "a").should == 0 } + it { provider.rpmvercmp("a+", "a+").should == 0 } + it { provider.rpmvercmp("a+", "a_").should == 0 } + it { provider.rpmvercmp("a_", "a+").should == 0 } + it { provider.rpmvercmp("+a", "+a").should == 0 } + it { provider.rpmvercmp("+a", "_a").should == 0 } + it { provider.rpmvercmp("_a", "+a").should == 0 } + it { provider.rpmvercmp("+_", "+_").should == 0 } + it { provider.rpmvercmp("_+", "+_").should == 0 } + it { provider.rpmvercmp("_+", "_+").should == 0 } + it { provider.rpmvercmp("+", "_").should == 0 } + it { provider.rpmvercmp("_", "+").should == 0 } + it { provider.rpmvercmp("1.0~rc1", "1.0~rc1").should == 0 } + it { provider.rpmvercmp("1.0~rc1", "1.0").should == -1 } + it { provider.rpmvercmp("1.0", "1.0~rc1").should == 1 } + it { provider.rpmvercmp("1.0~rc1", "1.0~rc2").should == -1 } + it { provider.rpmvercmp("1.0~rc2", "1.0~rc1").should == 1 } + it { provider.rpmvercmp("1.0~rc1~git123", "1.0~rc1~git123").should == 0 } + it { provider.rpmvercmp("1.0~rc1~git123", "1.0~rc1").should == -1 } + it { provider.rpmvercmp("1.0~rc1", "1.0~rc1~git123").should == 1 } + it { provider.rpmvercmp("1.0~rc1", "1.0arc1").should == -1 } + + # non-upstream test cases + it { provider.rpmvercmp("405", "406").should == -1 } + it { provider.rpmvercmp("1", "0").should == 1 } + end + end diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index 20a523be9..0e88270ed 100755 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -1,308 +1,449 @@ #! /usr/bin/env ruby require 'spec_helper' provider_class = Puppet::Type.type(:package).provider(:yum) describe provider_class do let(:name) { 'mypackage' } let(:resource) do Puppet::Type.type(:package).new( :name => name, :ensure => :installed, :provider => 'yum' ) end let(:provider) do provider = provider_class.new provider.resource = resource provider end before do provider.stubs(:yum).returns 'yum' provider.stubs(:rpm).returns 'rpm' provider.stubs(:get).with(:version).returns '1' provider.stubs(:get).with(:release).returns '1' provider.stubs(:get).with(:arch).returns 'i386' end describe 'provider features' do it { should be_versionable } it { should be_install_options } it { should be_virtual_packages } end # provider should repond to the following methods [:install, :latest, :update, :purge, :install_options].each do |method| it "should have a(n) #{method}" do provider.should respond_to(method) end end + describe 'package evr parsing' do + + it 'should parse full simple evr' do + v = provider.yum_parse_evr('0:1.2.3-4.el5') + v[:epoch].should == '0' + v[:version].should == '1.2.3' + v[:release].should == '4.el5' + end + + it 'should parse version only' do + v = provider.yum_parse_evr('1.2.3') + v[:epoch].should == '0' + v[:version].should == '1.2.3' + v[:release].should == nil + end + + it 'should parse version-release' do + v = provider.yum_parse_evr('1.2.3-4.5.el6') + v[:epoch].should == '0' + v[:version].should == '1.2.3' + v[:release].should == '4.5.el6' + end + + it 'should parse release with git hash' do + v = provider.yum_parse_evr('1.2.3-4.1234aefd') + v[:epoch].should == '0' + v[:version].should == '1.2.3' + v[:release].should == '4.1234aefd' + end + + it 'should parse single integer versions' do + v = provider.yum_parse_evr('12345') + v[:epoch].should == '0' + v[:version].should == '12345' + v[:release].should == nil + end + + it 'should parse text in the epoch to 0' do + v = provider.yum_parse_evr('foo0:1.2.3-4') + v[:epoch].should == '0' + v[:version].should == '1.2.3' + v[:release].should == '4' + end + + it 'should parse revisions with text' do + v = provider.yum_parse_evr('1.2.3-SNAPSHOT20140107') + v[:epoch].should == '0' + v[:version].should == '1.2.3' + v[:release].should == 'SNAPSHOT20140107' + end + + # test cases for PUP-682 + it 'should parse revisions with text and numbers' do + v = provider.yum_parse_evr('2.2-SNAPSHOT20121119105647') + v[:epoch].should == '0' + v[:version].should == '2.2' + v[:release].should == 'SNAPSHOT20121119105647' + end + + end + + describe 'yum evr comparison' do + + # currently passing tests + it 'should evaluate identical version-release as equal' do + v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => '1.el5'}, + {:epoch => '0', :version => '1.2.3', :release => '1.el5'}) + v.should == 0 + end + + it 'should evaluate identical version as equal' do + v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => nil}, + {:epoch => '0', :version => '1.2.3', :release => nil}) + v.should == 0 + end + + it 'should evaluate identical version but older release as less' do + v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => '1.el5'}, + {:epoch => '0', :version => '1.2.3', :release => '2.el5'}) + v.should == -1 + end + + it 'should evaluate identical version but newer release as greater' do + v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => '3.el5'}, + {:epoch => '0', :version => '1.2.3', :release => '2.el5'}) + v.should == 1 + end + + it 'should evaluate a newer epoch as greater' do + v = provider.yum_compareEVR({:epoch => '1', :version => '1.2.3', :release => '4.5'}, + {:epoch => '0', :version => '1.2.3', :release => '4.5'}) + v.should == 1 + end + + # these tests describe PUP-1244 logic yet to be implemented + it 'should evaluate any version as equal to the same version followed by release' do + v = provider.yum_compareEVR({:epoch => '0', :version => '1.2.3', :release => nil}, + {:epoch => '0', :version => '1.2.3', :release => '2.el5'}) + v.should == 0 + end + + # test cases for PUP-682 + it 'should evaluate same-length numeric revisions numerically' do + provider.yum_compareEVR({:epoch => '0', :version => '2.2', :release => '405'}, + {:epoch => '0', :version => '2.2', :release => '406'}).should == -1 + end + + end + + describe 'yum version segment comparison' do + + it 'should treat two nil values as equal' do + v = provider.compare_values(nil, nil) + v.should == 0 + end + + it 'should treat a nil value as less than a non-nil value' do + v = provider.compare_values(nil, '0') + v.should == -1 + end + + it 'should treat a non-nil value as greater than a nil value' do + v = provider.compare_values('0', nil) + v.should == 1 + end + + it 'should pass two non-nil values on to rpmvercmp' do + provider.stubs(:rpmvercmp) { 0 } + provider.expects(:rpmvercmp).with('s1', 's2') + provider.compare_values('s1', 's2') + end + + end + describe 'when installing' do before(:each) do Puppet::Util.stubs(:which).with("rpm").returns("/bin/rpm") provider.stubs(:which).with("rpm").returns("/bin/rpm") Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "--version"], {:combine => true, :custom_environment => {}, :failonfail => true}).returns("4.10.1\n").at_most_once end it 'should call yum install for :installed' do resource.stubs(:should).with(:ensure).returns :installed provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :list, name) provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, name) provider.install end it 'should use :install to update' do provider.expects(:install) provider.update end it 'should be able to set version' do version = '1.2' resource[:ensure] = version provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :list, name) provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, "#{name}-#{version}") provider.stubs(:query).returns :ensure => version provider.install end + it 'should handle partial versions specified' do + version = '1.3.4' + resource[:ensure] = version + provider.stubs(:query).returns :ensure => '1.3.4-1.el6' + provider.install + end + it 'should be able to downgrade' do current_version = '1.2' version = '1.0' resource[:ensure] = '1.0' provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :downgrade, "#{name}-#{version}") provider.stubs(:query).returns(:ensure => current_version).then.returns(:ensure => version) provider.install end it 'should accept install options' do resource[:ensure] = :installed resource[:install_options] = ['-t', {'-x' => 'expackage'}] provider.expects(:yum).with('-d', '0', '-e', '0', '-y', ['-t', '-x=expackage'], :list, name) provider.expects(:yum).with('-d', '0', '-e', '0', '-y', ['-t', '-x=expackage'], :install, name) provider.install end it 'allow virtual packages' do resource[:ensure] = :installed resource[:allow_virtual] = true provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :list, name).never provider.expects(:yum).with('-d', '0', '-e', '0', '-y', :install, name) provider.install end end describe 'when uninstalling' do it 'should use erase to purge' do provider.expects(:yum).with('-y', :erase, name) provider.purge end end it 'should be versionable' do provider.should be_versionable end describe 'determining the latest version available for a package' do it "passes the value of enablerepo install_options when querying" do resource[:install_options] = [ {'--enablerepo' => 'contrib'}, {'--enablerepo' => 'centosplus'}, ] provider.stubs(:properties).returns({:ensure => '3.4.5'}) described_class.expects(:latest_package_version).with(name, ['contrib', 'centosplus'], []) provider.latest end it "passes the value of disablerepo install_options when querying" do resource[:install_options] = [ {'--disablerepo' => 'updates'}, {'--disablerepo' => 'centosplus'}, ] provider.stubs(:properties).returns({:ensure => '3.4.5'}) described_class.expects(:latest_package_version).with(name, [], ['updates', 'centosplus']) provider.latest end describe 'and a newer version is not available' do before :each do described_class.stubs(:latest_package_version).with(name, [], []).returns nil end it 'raises an error the package is not installed' do provider.stubs(:properties).returns({:ensure => :absent}) expect { provider.latest }.to raise_error(Puppet::DevError, 'Tried to get latest on a missing package') end it 'returns version of the currently installed package' do provider.stubs(:properties).returns({:ensure => '3.4.5'}) provider.latest.should == '3.4.5' end end describe 'and a newer version is available' do let(:latest_version) do { :name => name, :epoch => '1', :version => '2.3.4', :release => '5', :arch => 'i686', } end it 'includes the epoch in the version string' do described_class.stubs(:latest_package_version).with(name, [], []).returns(latest_version) provider.latest.should == '1:2.3.4-5' end end end describe "lazy loading of latest package versions" do before { described_class.clear } after { described_class.clear } let(:mypackage_version) do { :name => name, :epoch => '1', :version => '2.3.4', :release => '5', :arch => 'i686', } end let(:mypackage_newerversion) do { :name => name, :epoch => '1', :version => '4.5.6', :release => '7', :arch => 'i686', } end let(:latest_versions) { {name => [mypackage_version]} } let(:enabled_versions) { {name => [mypackage_newerversion]} } it "returns the version hash if the package was found" do described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) version = described_class.latest_package_version(name, [], []) expect(version).to eq(mypackage_version) end it "is nil if the package was not found in the query" do described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) version = described_class.latest_package_version('nopackage', [], []) expect(version).to be_nil end it "caches the package list and reuses that for subsequent queries" do described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) 2.times { version = described_class.latest_package_version(name, [], []) expect(version).to eq mypackage_version } end it "caches separate lists for each combination of 'enablerepo' and 'disablerepo'" do described_class.expects(:fetch_latest_versions).with([], []).once.returns(latest_versions) described_class.expects(:fetch_latest_versions).with(['enabled'], ['disabled']).once.returns(enabled_versions) 2.times { version = described_class.latest_package_version(name, [], []) expect(version).to eq mypackage_version } 2.times { version = described_class.latest_package_version(name, ['enabled'], ['disabled']) expect(version).to eq(mypackage_newerversion) } end end describe "querying for the latest version of all packages" do let(:yumhelper_single_arch) do <<-YUMHELPER_OUTPUT * base: centos.tcpdiag.net * extras: centos.mirrors.hoobly.com * updates: mirrors.arsc.edu _pkg nss-tools 0 3.14.3 4.el6_4 x86_64 _pkg pixman 0 0.26.2 5.el6_4 x86_64 _pkg myresource 0 1.2.3.4 5.el4 noarch _pkg mysummaryless 0 1.2.3.4 5.el4 noarch YUMHELPER_OUTPUT end let(:yumhelper_multi_arch) do yumhelper_single_arch + <<-YUMHELPER_OUTPUT _pkg nss-tools 0 3.14.3 4.el6_4 i386 _pkg pixman 0 0.26.2 5.el6_4 i386 YUMHELPER_OUTPUT end it "creates an entry for each line that's prefixed with '_pkg'" do described_class.expects(:python).with([described_class::YUMHELPER]).returns(yumhelper_single_arch) entries = described_class.fetch_latest_versions([], []) expect(entries.keys).to include 'nss-tools' expect(entries.keys).to include 'pixman' expect(entries.keys).to include 'myresource' expect(entries.keys).to include 'mysummaryless' end it "creates an entry for each package name and architecture" do described_class.expects(:python).with([described_class::YUMHELPER]).returns(yumhelper_single_arch) entries = described_class.fetch_latest_versions([], []) expect(entries.keys).to include 'nss-tools.x86_64' expect(entries.keys).to include 'pixman.x86_64' expect(entries.keys).to include 'myresource.noarch' expect(entries.keys).to include 'mysummaryless.noarch' end it "stores multiple entries if a package is build for multiple architectures" do described_class.expects(:python).with([described_class::YUMHELPER]).returns(yumhelper_multi_arch) entries = described_class.fetch_latest_versions([], []) expect(entries.keys).to include 'nss-tools.x86_64' expect(entries.keys).to include 'pixman.x86_64' expect(entries.keys).to include 'nss-tools.i386' expect(entries.keys).to include 'pixman.i386' expect(entries['nss-tools']).to have(2).items expect(entries['pixman']).to have(2).items end it "passes the repos to enable to the helper" do described_class.expects(:python).with do |script, *args| expect(script).to eq described_class::YUMHELPER expect(args).to eq %w[-e updates -e centosplus] end.returns('') described_class.fetch_latest_versions(['updates', 'centosplus'], []) end it "passes the repos to disable to the helper" do described_class.expects(:python).with do |script, *args| expect(script).to eq described_class::YUMHELPER expect(args).to eq %w[-d updates -d centosplus] end.returns('') described_class.fetch_latest_versions([], ['updates', 'centosplus']) end it 'passes a combination of repos to the helper' do described_class.expects(:python).with do |script, *args| expect(script).to eq described_class::YUMHELPER expect(args).to eq %w[-e os -e contrib -d updates -d centosplus] end.returns('') described_class.fetch_latest_versions(['os', 'contrib'], ['updates', 'centosplus']) end end end diff --git a/spec/unit/provider/service/openbsd_spec.rb b/spec/unit/provider/service/openbsd_spec.rb index ad1e50d18..b2467a5e7 100755 --- a/spec/unit/provider/service/openbsd_spec.rb +++ b/spec/unit/provider/service/openbsd_spec.rb @@ -1,256 +1,178 @@ #!/usr/bin/env ruby # # Unit testing for the OpenBSD service provider require 'spec_helper' provider_class = Puppet::Type.type(:service).provider(:openbsd) describe provider_class do before :each do Puppet::Type.type(:service).stubs(:defaultprovider).returns described_class Facter.stubs(:value).with(:operatingsystem).returns :openbsd - end - - let :rcscripts do - [ - '/etc/rc.d/apmd', - '/etc/rc.d/aucat', - '/etc/rc.d/cron', - '/etc/rc.d/puppetd' - ] + FileTest.stubs(:file?).with('/usr/sbin/rcctl').returns true + FileTest.stubs(:executable?).with('/usr/sbin/rcctl').returns true end describe "#instances" do it "should have an instances method" do described_class.should respond_to :instances end it "should list all available services" do - File.expects(:directory?).with('/etc/rc.d').returns true - Dir.expects(:glob).with('/etc/rc.d/*').returns rcscripts - - rcscripts.each do |script| - File.expects(:executable?).with(script).returns true - end - + described_class.stubs(:execpipe).with(['/usr/sbin/rcctl', :status]).yields File.read(my_fixture('rcctl_status')) described_class.instances.map(&:name).should == [ - 'apmd', - 'aucat', - 'cron', - 'puppetd' + 'accounting', 'pf', 'postgresql', 'tftpd', 'wsmoused', 'xdm', ] end end describe "#start" do it "should use the supplied start command if specified" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :start => '/bin/foo')) provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) provider.start end it "should start the service otherwise" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) - provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :start], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) - provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd') + provider.expects(:texecute).with(:start, ['/usr/sbin/rcctl', '-f', :start, 'sshd'], true) provider.start end end describe "#stop" do it "should use the supplied stop command if specified" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :stop => '/bin/foo')) provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) provider.stop end it "should stop the service otherwise" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) - provider.expects(:execute).with(['/etc/rc.d/sshd', :stop], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) - provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd') + provider.expects(:texecute).with(:stop, ['/usr/sbin/rcctl', :stop, 'sshd'], true) provider.stop end end describe "#status" do it "should use the status command from the resource" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :status => '/bin/foo')) - provider.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never + provider.expects(:execute).with(['/usr/sbin/rcctl', :status, 'sshd'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true) provider.status end it "should return :stopped when status command returns with a non-zero exitcode" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :status => '/bin/foo')) - provider.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never + provider.expects(:execute).with(['/usr/sbin/rcctl', :status, 'sshd'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true) $CHILD_STATUS.stubs(:exitstatus).returns 3 provider.status.should == :stopped end it "should return :running when status command returns with a zero exitcode" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :status => '/bin/foo')) - provider.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never + provider.expects(:execute).with(['/usr/sbin/rcctl', :status, 'sshd'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true) $CHILD_STATUS.stubs(:exitstatus).returns 0 provider.status.should == :running end end describe "#restart" do it "should use the supplied restart command if specified" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :restart => '/bin/foo')) - provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never + provider.expects(:execute).with(['/usr/sbin/rcctl', '-f', :restart, 'sshd'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) provider.restart end - it "should restart the service with rc-service restart if hasrestart is true" do + it "should restart the service with rcctl restart if hasrestart is true" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasrestart => true)) - provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) - provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd') + provider.expects(:texecute).with(:restart, ['/usr/sbin/rcctl', '-f', :restart, 'sshd'], true) provider.restart end - it "should restart the service with rc-service stop/start if hasrestart is false" do + it "should restart the service with rcctl stop/start if hasrestart is false" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasrestart => false)) - provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never - provider.expects(:execute).with(['/etc/rc.d/sshd', :stop], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) - provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :start], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) - provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd') + provider.expects(:texecute).with(:restart, ['/usr/sbin/rcctl', '-f', :restart, 'sshd'], true).never + provider.expects(:texecute).with(:stop, ['/usr/sbin/rcctl', :stop, 'sshd'], true) + provider.expects(:texecute).with(:start, ['/usr/sbin/rcctl', '-f', :start, 'sshd'], true) provider.restart end end - describe "#parse_rc_line" do - it "can parse a flag line with a known value" do - output = described_class.parse_rc_line('daemon_flags=') - output.should eq('') - end - - it "can parse a flag line with a flag is wrapped in single quotes" do - output = described_class.parse_rc_line('daemon_flags=\'\'') - output.should eq('\'\'') - end - - it "can parse a flag line with a flag is wrapped in double quotes" do - output = described_class.parse_rc_line('daemon_flags=""') - output.should eq('') - end - - it "can parse a flag line with a trailing comment" do - output = described_class.parse_rc_line('daemon_flags="-d" # bees') - output.should eq('-d') - end - - it "can parse a flag line with a bare word" do - output = described_class.parse_rc_line('daemon_flags=YES') - output.should eq('YES') - end - - it "can parse a flag line with a flag that contains an equals" do - output = described_class.parse_rc_line('daemon_flags="-Dbla -tmpdir=foo"') - output.should eq('-Dbla -tmpdir=foo') - end - end - - describe "#pkg_scripts" do - it "can retrieve the package_scripts array from rc.conf.local" do - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.expects(:load_rcconf_local_array).returns ['pkg_scripts="dbus_daemon cupsd"'] - expect(provider.pkg_scripts).to match_array(['dbus_daemon', 'cupsd']) + describe "#enabled?" do + it "should return :true if the service is enabled" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) + described_class.stubs(:rcctl).with('status', 'sshd').returns('-6') + provider.expects(:execute).with(['/usr/sbin/rcctl', 'status', 'sshd'], :failonfail => false, :combine => false, :squelch => false).returns('-6') + provider.enabled?.should == :true end - it "returns an empty array when no pkg_scripts line is found" do - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.expects(:load_rcconf_local_array).returns ["#\n#\n#"] - expect(provider.pkg_scripts).to match_array([]) + it "should return :false if the service is disabled" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) + described_class.stubs(:rcctl).with('status', 'sshd').returns('NO') + provider.expects(:execute).with(['/usr/sbin/rcctl', 'status', 'sshd'], :failonfail => false, :combine => false, :squelch => false).returns('NO') + provider.enabled?.should == :false end end - describe "#pkg_scripts_append" do - it "can append to the package_scripts array and return the result" do - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.expects(:load_rcconf_local_array).returns ['pkg_scripts="dbus_daemon"'] - provider.pkg_scripts_append.should === ['dbus_daemon', 'cupsd'] + describe "#enable" do + it "should run rcctl enable to enable the service" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) + described_class.stubs(:rcctl).with(:enable, 'sshd').returns('') + provider.expects(:rcctl).with(:enable, 'sshd') + provider.enable end - it "should not duplicate the script name" do - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.expects(:load_rcconf_local_array).returns ['pkg_scripts="cupsd dbus_daemon"'] - provider.pkg_scripts_append.should === ['cupsd', 'dbus_daemon'] + it "should run rcctl enable with flags if provided" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :flags => '-6')) + described_class.stubs(:rcctl).with(:enable, 'sshd', :flags, '-6').returns('') + provider.expects(:rcctl).with(:enable, 'sshd', :flags, '-6') + provider.enable end end - describe "#pkg_scripts_remove" do - it "can append to the package_scripts array and return the result" do - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.expects(:load_rcconf_local_array).returns ['pkg_scripts="dbus_daemon cupsd"'] - expect(provider.pkg_scripts_remove).to match_array(['dbus_daemon']) - end - - it "should not remove the script from the array unless its needed" do - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.expects(:load_rcconf_local_array).returns ['pkg_scripts="dbus_daemon"'] - expect(provider.pkg_scripts_remove).to match_array(['dbus_daemon']) + describe "#disable" do + it "should run rcctl disable to disable the service" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) + described_class.stubs(:rcctl).with(:disable, 'sshd').returns('') + provider.expects(:rcctl).with(:disable, 'sshd') + provider.disable end end - describe "#set_content_flags" do - it "can create the necessary content where none is provided" do - content = [] - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.set_content_flags(content,'-d').should match_array(['cupsd_flags="-d"']) - end - - it "can modify the existing content" do - content = ['cupsd_flags="-f"'] - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - output = provider.set_content_flags(content,"-d") - output.should match_array(['cupsd_flags="-d"']) - end - - it "does not set empty flags for package scripts" do - content = [] - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.expects(:in_base?).returns(false) - output = provider.set_content_flags(content,'') - output.should match_array([nil]) - end - - it "does set empty flags for base scripts" do - content = [] - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'ntpd')) - provider.expects(:in_base?).returns(true) - output = provider.set_content_flags(content,'') - output.should match_array(['ntpd_flags=""']) + describe "#flags" do + it "should return flags when set" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :flags => '-6')) + described_class.stubs(:rcctl).with(:status, 'sshd').returns('-6') + provider.expects(:execute).with(['/usr/sbin/rcctl', 'status', 'sshd'], :failonfail => false, :combine => false, :squelch => false).returns('-6') + provider.flags end - end - describe "#remove_content_flags" do - it "can remove the flags line from the requested content" do - content = ['cupsd_flags="-d"'] - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - output = provider.remove_content_flags(content) - output.should_not match_array(['cupsd_flags="-d"']) + it "should return empty flags" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) + described_class.stubs(:rcctl).with(:status, 'sshd').returns('') + provider.expects(:execute).with(['/usr/sbin/rcctl', 'status', 'sshd'], :failonfail => false, :combine => false, :squelch => false).returns('') + provider.flags end - end - describe "#set_content_scripts" do - it "should append to the list of scripts" do - content = ['pkg_scripts="dbus_daemon"'] - scripts = ['dbus_daemon','cupsd'] - provider = described_class.new(Puppet::Type.type(:service).new(:name => 'cupsd')) - provider.set_content_scripts(content,scripts).should match_array(['pkg_scripts="dbus_daemon cupsd"']) + it "should return flags for special services" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'pf')) + described_class.stubs(:rcctl).with(:status, 'pf').returns('YES') + provider.expects(:execute).with(['/usr/sbin/rcctl', 'status', 'pf'], :failonfail => false, :combine => false, :squelch => false).returns('YES') + provider.flags end end - describe "#in_base?" do - it "should true if in base" do - File.stubs(:readlines).with('/etc/rc.conf').returns(['sshd_flags=""']) + describe "#flags=" do + it "should run rcctl to set flags" do provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) - provider.in_base?.should be_true + described_class.stubs(:rcctl).with(:enable, 'sshd', :flags, '-4').returns('') + provider.expects(:rcctl).with(:enable, 'sshd', :flags, '-4') + provider.flags = '-4' end end end diff --git a/spec/unit/provider/service/windows_spec.rb b/spec/unit/provider/service/windows_spec.rb index bb883bdf5..3ecff6aa3 100755 --- a/spec/unit/provider/service/windows_spec.rb +++ b/spec/unit/provider/service/windows_spec.rb @@ -1,183 +1,231 @@ #! /usr/bin/env ruby # # Unit testing for the Windows service Provider # require 'spec_helper' require 'win32/service' if Puppet.features.microsoft_windows? describe Puppet::Type.type(:service).provider(:windows), :if => Puppet.features.microsoft_windows? do let(:name) { 'nonexistentservice' } let(:resource) { Puppet::Type.type(:service).new(:name => name, :provider => :windows) } let(:provider) { resource.provider } let(:config) { Struct::ServiceConfigInfo.new } let(:status) { Struct::ServiceStatus.new } before :each do # make sure we never actually execute anything (there are two execute methods) provider.class.expects(:execute).never provider.expects(:execute).never Win32::Service.stubs(:config_info).with(name).returns(config) Win32::Service.stubs(:status).with(name).returns(status) end describe ".instances" do it "should enumerate all services" do list_of_services = ['snmptrap', 'svchost', 'sshd'].map { |s| stub('service', :service_name => s) } Win32::Service.expects(:services).returns(list_of_services) described_class.instances.map(&:name).should =~ ['snmptrap', 'svchost', 'sshd'] end end describe "#start" do before :each do config.start_type = Win32::Service.get_start_type(Win32::Service::SERVICE_AUTO_START) end it "should start the service" do provider.expects(:net).with(:start, name) provider.start end it "should raise an error if the start command fails" do provider.expects(:net).with(:start, name).raises(Puppet::ExecutionFailure, "The service name is invalid.") expect { provider.start }.to raise_error(Puppet::Error, /Cannot start #{name}, error was: The service name is invalid./) end + it "raises an error if the service doesn't exist" do + Win32::Service.expects(:config_info).with(name).raises(SystemCallError, 'OpenService') + + expect { + provider.start + }.to raise_error(Puppet::Error, /Cannot get start type for #{name}/) + end + describe "when the service is disabled" do before :each do config.start_type = Win32::Service.get_start_type(Win32::Service::SERVICE_DISABLED) end it "should refuse to start if not managing enable" do expect { provider.start }.to raise_error(Puppet::Error, /Will not start disabled service/) end it "should enable if managing enable and enable is true" do resource[:enable] = :true provider.expects(:net).with(:start, name) Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_AUTO_START).returns(Win32::Service) provider.start end it "should manual start if managing enable and enable is false" do resource[:enable] = :false provider.expects(:net).with(:start, name) Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_DEMAND_START).returns(Win32::Service) provider.start end end end describe "#stop" do it "should stop a running service" do provider.expects(:net).with(:stop, name) provider.stop end it "should raise an error if the stop command fails" do provider.expects(:net).with(:stop, name).raises(Puppet::ExecutionFailure, 'The service name is invalid.') expect { provider.stop }.to raise_error(Puppet::Error, /Cannot stop #{name}, error was: The service name is invalid./) end end describe "#status" do ['stopped', 'paused', 'stop pending', 'pause pending'].each do |state| it "should report a #{state} service as stopped" do status.current_state = state provider.status.should == :stopped end end ["running", "continue pending", "start pending" ].each do |state| it "should report a #{state} service as running" do status.current_state = state provider.status.should == :running end end + + it "raises an error if the service doesn't exist" do + Win32::Service.expects(:status).with(name).raises(SystemCallError, 'OpenService') + + expect { + provider.status + }.to raise_error(Puppet::Error, /Cannot get status of #{name}/) + end end describe "#restart" do it "should use the supplied restart command if specified" do resource[:restart] = 'c:/bin/foo' provider.expects(:execute).never provider.expects(:execute).with(['c:/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true) provider.restart end it "should restart the service" do seq = sequence("restarting") provider.expects(:stop).in_sequence(seq) provider.expects(:start).in_sequence(seq) provider.restart end end describe "#enabled?" do it "should report a service with a startup type of manual as manual" do config.start_type = Win32::Service.get_start_type(Win32::Service::SERVICE_DEMAND_START) provider.enabled?.should == :manual end it "should report a service with a startup type of disabled as false" do config.start_type = Win32::Service.get_start_type(Win32::Service::SERVICE_DISABLED) provider.enabled?.should == :false end + it "raises an error if the service doesn't exist" do + Win32::Service.expects(:config_info).with(name).raises(SystemCallError, 'OpenService') + + expect { + provider.enabled? + }.to raise_error(Puppet::Error, /Cannot get start type for #{name}/) + end + # We need to guard this section explicitly since rspec will always # construct all examples, even if it isn't going to run them. if Puppet.features.microsoft_windows? [Win32::Service::SERVICE_AUTO_START, Win32::Service::SERVICE_BOOT_START, Win32::Service::SERVICE_SYSTEM_START].each do |start_type_const| start_type = Win32::Service.get_start_type(start_type_const) it "should report a service with a startup type of '#{start_type}' as true" do config.start_type = start_type provider.enabled?.should == :true end end end end describe "#enable" do it "should set service start type to Service_Auto_Start when enabled" do Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_AUTO_START).returns(Win32::Service) provider.enable end + + it "raises an error if the service doesn't exist" do + Win32::Service.expects(:configure).with(has_entry('service_name' => name)).raises(SystemCallError, 'OpenService') + + expect { + provider.enable + }.to raise_error(Puppet::Error, /Cannot enable #{name}/) + end end describe "#disable" do it "should set service start type to Service_Disabled when disabled" do Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_DISABLED).returns(Win32::Service) provider.disable end + + it "raises an error if the service doesn't exist" do + Win32::Service.expects(:configure).with(has_entry('service_name' => name)).raises(SystemCallError, 'OpenService') + + expect { + provider.disable + }.to raise_error(Puppet::Error, /Cannot disable #{name}/) + end end describe "#manual_start" do it "should set service start type to Service_Demand_Start (manual) when manual" do Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_DEMAND_START).returns(Win32::Service) provider.manual_start end + + it "raises an error if the service doesn't exist" do + Win32::Service.expects(:configure).with(has_entry('service_name' => name)).raises(SystemCallError, 'OpenService') + + expect { + provider.manual_start + }.to raise_error(Puppet::Error, /Cannot enable #{name}/) + end end end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 91be16ae6..e60b29cb2 100755 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -1,985 +1,1005 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/resource' describe Puppet::Resource do include PuppetSpec::Files let(:basepath) { make_absolute("/somepath") } let(:environment) { Puppet::Node::Environment.create(:testing, []) } [:catalog, :file, :line].each do |attr| it "should have an #{attr} attribute" do resource = Puppet::Resource.new("file", "/my/file") resource.should respond_to(attr) resource.should respond_to(attr.to_s + "=") end end it "should have a :title attribute" do Puppet::Resource.new(:user, "foo").title.should == "foo" end it "should require the type and title" do expect { Puppet::Resource.new }.to raise_error(ArgumentError) end it "should canonize types to capitalized strings" do Puppet::Resource.new(:user, "foo").type.should == "User" end it "should canonize qualified types so all strings are capitalized" do Puppet::Resource.new("foo::bar", "foo").type.should == "Foo::Bar" end it "should tag itself with its type" do Puppet::Resource.new("file", "/f").should be_tagged("file") end it "should tag itself with its title if the title is a valid tag" do Puppet::Resource.new("user", "bar").should be_tagged("bar") end it "should not tag itself with its title if the title is a not valid tag" do Puppet::Resource.new("file", "/bar").should_not be_tagged("/bar") end it "should allow setting of attributes" do Puppet::Resource.new("file", "/bar", :file => "/foo").file.should == "/foo" Puppet::Resource.new("file", "/bar", :exported => true).should be_exported end it "should set its type to 'Class' and its title to the passed title if the passed type is :component and the title has no square brackets in it" do ref = Puppet::Resource.new(:component, "foo") ref.type.should == "Class" ref.title.should == "Foo" end it "should interpret the title as a reference and assign appropriately if the type is :component and the title contains square brackets" do ref = Puppet::Resource.new(:component, "foo::bar[yay]") ref.type.should == "Foo::Bar" ref.title.should == "yay" end it "should set the type to 'Class' if it is nil and the title contains no square brackets" do ref = Puppet::Resource.new(nil, "yay") ref.type.should == "Class" ref.title.should == "Yay" end it "should interpret the title as a reference and assign appropriately if the type is nil and the title contains square brackets" do ref = Puppet::Resource.new(nil, "foo::bar[yay]") ref.type.should == "Foo::Bar" ref.title.should == "yay" end it "should interpret the title as a reference and assign appropriately if the type is nil and the title contains nested square brackets" do ref = Puppet::Resource.new(nil, "foo::bar[baz[yay]]") ref.type.should == "Foo::Bar" ref.title.should =="baz[yay]" end it "should interpret the type as a reference and assign appropriately if the title is nil and the type contains square brackets" do ref = Puppet::Resource.new("foo::bar[baz]") ref.type.should == "Foo::Bar" ref.title.should =="baz" end it "should not interpret the title as a reference if the type is a non component or whit reference" do ref = Puppet::Resource.new("Notify", "foo::bar[baz]") ref.type.should == "Notify" ref.title.should =="foo::bar[baz]" end it "should be able to extract its information from a Puppet::Type instance" do ral = Puppet::Type.type(:file).new :path => basepath+"/foo" ref = Puppet::Resource.new(ral) ref.type.should == "File" ref.title.should == basepath+"/foo" end it "should fail if the title is nil and the type is not a valid resource reference string" do expect { Puppet::Resource.new("resource-spec-foo") }.to raise_error(ArgumentError) end it 'should fail if strict is set and type does not exist' do expect { Puppet::Resource.new('resource-spec-foo', 'title', {:strict=>true}) }.to raise_error(ArgumentError, 'Invalid resource type resource-spec-foo') end it 'should fail if strict is set and class does not exist' do expect { Puppet::Resource.new('Class', 'resource-spec-foo', {:strict=>true}) }.to raise_error(ArgumentError, 'Could not find declared class resource-spec-foo') end it "should fail if the title is a hash and the type is not a valid resource reference string" do expect { Puppet::Resource.new({:type => "resource-spec-foo", :title => "bar"}) }. to raise_error ArgumentError, /Puppet::Resource.new does not take a hash/ end it "should be taggable" do Puppet::Resource.ancestors.should be_include(Puppet::Util::Tagging) end it "should have an 'exported' attribute" do resource = Puppet::Resource.new("file", "/f") resource.exported = true resource.exported.should == true resource.should be_exported end describe "and munging its type and title" do describe "when modeling a builtin resource" do it "should be able to find the resource type" do Puppet::Resource.new("file", "/my/file").resource_type.should equal(Puppet::Type.type(:file)) end it "should set its type to the capitalized type name" do Puppet::Resource.new("file", "/my/file").type.should == "File" end end describe "when modeling a defined resource" do describe "that exists" do before do @type = Puppet::Resource::Type.new(:definition, "foo::bar") environment.known_resource_types.add @type end it "should set its type to the capitalized type name" do Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).type.should == "Foo::Bar" end it "should be able to find the resource type" do Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).resource_type.should equal(@type) end it "should set its title to the provided title" do Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).title.should == "/my/file" end end describe "that does not exist" do it "should set its resource type to the capitalized resource type name" do Puppet::Resource.new("foo::bar", "/my/file").type.should == "Foo::Bar" end end end describe "when modeling a node" do # Life's easier with nodes, because they can't be qualified. it "should set its type to 'Node' and its title to the provided title" do node = Puppet::Resource.new("node", "foo") node.type.should == "Node" node.title.should == "foo" end end describe "when modeling a class" do it "should set its type to 'Class'" do Puppet::Resource.new("class", "foo").type.should == "Class" end describe "that exists" do before do @type = Puppet::Resource::Type.new(:hostclass, "foo::bar") environment.known_resource_types.add @type end it "should set its title to the capitalized, fully qualified resource type" do Puppet::Resource.new("class", "foo::bar", :environment => environment).title.should == "Foo::Bar" end it "should be able to find the resource type" do Puppet::Resource.new("class", "foo::bar", :environment => environment).resource_type.should equal(@type) end end describe "that does not exist" do it "should set its type to 'Class' and its title to the capitalized provided name" do klass = Puppet::Resource.new("class", "foo::bar") klass.type.should == "Class" klass.title.should == "Foo::Bar" end end describe "and its name is set to the empty string" do it "should set its title to :main" do Puppet::Resource.new("class", "").title.should == :main end describe "and a class exists whose name is the empty string" do # this was a bit tough to track down it "should set its title to :main" do @type = Puppet::Resource::Type.new(:hostclass, "") environment.known_resource_types.add @type Puppet::Resource.new("class", "", :environment => environment).title.should == :main end end end describe "and its name is set to :main" do it "should set its title to :main" do Puppet::Resource.new("class", :main).title.should == :main end describe "and a class exists whose name is the empty string" do # this was a bit tough to track down it "should set its title to :main" do @type = Puppet::Resource::Type.new(:hostclass, "") environment.known_resource_types.add @type Puppet::Resource.new("class", :main, :environment => environment).title.should == :main end end end end end it "should return nil when looking up resource types that don't exist" do Puppet::Resource.new("foobar", "bar").resource_type.should be_nil end it "should not fail when an invalid parameter is used and strict mode is disabled" do type = Puppet::Resource::Type.new(:definition, "foobar") environment.known_resource_types.add type resource = Puppet::Resource.new("foobar", "/my/file", :environment => environment) resource[:yay] = true end it "should be considered equivalent to another resource if their type and title match and no parameters are set" do Puppet::Resource.new("file", "/f").should == Puppet::Resource.new("file", "/f") end it "should be considered equivalent to another resource if their type, title, and parameters are equal" do Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}).should == Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}) end it "should not be considered equivalent to another resource if their type and title match but parameters are different" do Puppet::Resource.new("file", "/f", :parameters => {:fee => "baz"}).should_not == Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}) end it "should not be considered equivalent to a non-resource" do Puppet::Resource.new("file", "/f").should_not == "foo" end it "should not be considered equivalent to another resource if their types do not match" do Puppet::Resource.new("file", "/f").should_not == Puppet::Resource.new("exec", "/f") end it "should not be considered equivalent to another resource if their titles do not match" do Puppet::Resource.new("file", "/foo").should_not == Puppet::Resource.new("file", "/f") end describe "when setting default parameters" do let(:foo_node) { Puppet::Node.new('foo', :environment => environment) } let(:compiler) { Puppet::Parser::Compiler.new(foo_node) } let(:scope) { Puppet::Parser::Scope.new(compiler) } def ast_string(value) Puppet::Parser::AST::String.new({:value => value}) end it "should fail when asked to set default values and it is not a parser resource" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("default")}) ) resource = Puppet::Resource.new("default_param", "name", :environment => environment) lambda { resource.set_default_parameters(scope) }.should raise_error(Puppet::DevError) end it "should evaluate and set any default values when no value is provided" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("a_default_value")}) ) resource = Puppet::Parser::Resource.new("default_param", "name", :scope => scope) resource.set_default_parameters(scope) resource["a"].should == "a_default_value" end it "should skip attributes with no default value" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "no_default_param", :arguments => {"a" => ast_string("a_default_value")}) ) resource = Puppet::Parser::Resource.new("no_default_param", "name", :scope => scope) lambda { resource.set_default_parameters(scope) }.should_not raise_error end it "should return the list of default parameters set" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("a_default_value")}) ) resource = Puppet::Parser::Resource.new("default_param", "name", :scope => scope) resource.set_default_parameters(scope).should == ["a"] end describe "when the resource type is :hostclass" do let(:environment_name) { "testing env" } let(:fact_values) { { :a => 1 } } let(:port) { Puppet::Parser::AST::String.new(:value => '80') } let(:apache) { Puppet::Resource::Type.new(:hostclass, 'apache', :arguments => { 'port' => port }) } before do environment.known_resource_types.add(apache) scope.stubs(:host).returns('host') scope.stubs(:environment).returns(environment) scope.stubs(:facts).returns(Puppet::Node::Facts.new("facts", fact_values)) end context "when no value is provided" do before(:each) do Puppet[:binder] = true end let(:resource) do Puppet::Parser::Resource.new("class", "apache", :scope => scope) end it "should query the data_binding terminus using a namespaced key" do Puppet::DataBinding.indirection.expects(:find).with( 'apache::port', all_of(has_key(:environment), has_key(:variables))) resource.set_default_parameters(scope) end it "should use the value from the data_binding terminus" do Puppet::DataBinding.indirection.expects(:find).returns('443') resource.set_default_parameters(scope) resource[:port].should == '443' end it "should use the default value if the data_binding terminus returns nil" do Puppet::DataBinding.indirection.expects(:find).returns(nil) resource.set_default_parameters(scope) resource[:port].should == '80' end it "should fail with error message about data binding on a hiera failure" do Puppet::DataBinding.indirection.expects(:find).raises(Puppet::DataBinding::LookupError, 'Forgettabotit') expect { resource.set_default_parameters(scope) }.to raise_error(Puppet::Error, /Error from DataBinding 'hiera' while looking up 'apache::port':.*Forgettabotit/) end end context "when a value is provided" do let(:port_parameter) do Puppet::Parser::Resource::Param.new( { :name => 'port', :value => '8080' } ) end let(:resource) do Puppet::Parser::Resource.new("class", "apache", :scope => scope, :parameters => [port_parameter]) end it "should not query the data_binding terminus" do Puppet::DataBinding.indirection.expects(:find).never resource.set_default_parameters(scope) end it "should not query the injector" do # enable the injector Puppet[:binder] = true compiler.injector.expects(:find).never resource.set_default_parameters(scope) end it "should use the value provided" do Puppet::DataBinding.indirection.expects(:find).never resource.set_default_parameters(scope).should == [] resource[:port].should == '8080' end end end end describe "when validating all required parameters are present" do it "should be able to validate that all required parameters are present" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "required_param", :arguments => {"a" => nil}) ) lambda { Puppet::Resource.new("required_param", "name", :environment => environment).validate_complete }.should raise_error(Puppet::ParseError) end it "should not fail when all required parameters are present" do environment.known_resource_types.add( Puppet::Resource::Type.new(:definition, "no_required_param") ) resource = Puppet::Resource.new("no_required_param", "name", :environment => environment) resource["a"] = "meh" lambda { resource.validate_complete }.should_not raise_error end it "should not validate against builtin types" do lambda { Puppet::Resource.new("file", "/bar").validate_complete }.should_not raise_error end end describe "when referring to a resource with name canonicalization" do it "should canonicalize its own name" do res = Puppet::Resource.new("file", "/path/") res.uniqueness_key.should == ["/path"] res.ref.should == "File[/path/]" end end describe "when running in strict mode" do it "should be strict" do Puppet::Resource.new("file", "/path", :strict => true).should be_strict end it "should fail if invalid parameters are used" do expect { Puppet::Resource.new("file", "/path", :strict => true, :parameters => {:nosuchparam => "bar"}) }.to raise_error end it "should fail if the resource type cannot be resolved" do expect { Puppet::Resource.new("nosuchtype", "/path", :strict => true) }.to raise_error end end describe "when managing parameters" do before do @resource = Puppet::Resource.new("file", "/my/file") end it "should correctly detect when provided parameters are not valid for builtin types" do Puppet::Resource.new("file", "/my/file").should_not be_valid_parameter("foobar") end it "should correctly detect when provided parameters are valid for builtin types" do Puppet::Resource.new("file", "/my/file").should be_valid_parameter("mode") end it "should correctly detect when provided parameters are not valid for defined resource types" do type = Puppet::Resource::Type.new(:definition, "foobar") environment.known_resource_types.add type Puppet::Resource.new("foobar", "/my/file", :environment => environment).should_not be_valid_parameter("myparam") end it "should correctly detect when provided parameters are valid for defined resource types" do type = Puppet::Resource::Type.new(:definition, "foobar", :arguments => {"myparam" => nil}) environment.known_resource_types.add type Puppet::Resource.new("foobar", "/my/file", :environment => environment).should be_valid_parameter("myparam") end it "should allow setting and retrieving of parameters" do @resource[:foo] = "bar" @resource[:foo].should == "bar" end it "should allow setting of parameters at initialization" do Puppet::Resource.new("file", "/my/file", :parameters => {:foo => "bar"})[:foo].should == "bar" end it "should canonicalize retrieved parameter names to treat symbols and strings equivalently" do @resource[:foo] = "bar" @resource["foo"].should == "bar" end it "should canonicalize set parameter names to treat symbols and strings equivalently" do @resource["foo"] = "bar" @resource[:foo].should == "bar" end it "should set the namevar when asked to set the name" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource[:name] = "bob" resource[:myvar].should == "bob" end it "should return the namevar when asked to return the name" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource[:myvar] = "test" resource[:name].should == "test" end it "should be able to set the name for non-builtin types" do resource = Puppet::Resource.new(:foo, "bar") resource[:name] = "eh" expect { resource[:name] = "eh" }.to_not raise_error end it "should be able to return the name for non-builtin types" do resource = Puppet::Resource.new(:foo, "bar") resource[:name] = "eh" resource[:name].should == "eh" end it "should be able to iterate over parameters" do @resource[:foo] = "bar" @resource[:fee] = "bare" params = {} @resource.each do |key, value| params[key] = value end params.should == {:foo => "bar", :fee => "bare"} end it "should include Enumerable" do @resource.class.ancestors.should be_include(Enumerable) end it "should have a method for testing whether a parameter is included" do @resource[:foo] = "bar" @resource.should be_has_key(:foo) @resource.should_not be_has_key(:eh) end it "should have a method for providing the list of parameters" do @resource[:foo] = "bar" @resource[:bar] = "foo" keys = @resource.keys keys.should be_include(:foo) keys.should be_include(:bar) end it "should have a method for providing the number of parameters" do @resource[:foo] = "bar" @resource.length.should == 1 end it "should have a method for deleting parameters" do @resource[:foo] = "bar" @resource.delete(:foo) @resource[:foo].should be_nil end it "should have a method for testing whether the parameter list is empty" do @resource.should be_empty @resource[:foo] = "bar" @resource.should_not be_empty end it "should be able to produce a hash of all existing parameters" do @resource[:foo] = "bar" @resource[:fee] = "yay" hash = @resource.to_hash hash[:foo].should == "bar" hash[:fee].should == "yay" end it "should not provide direct access to the internal parameters hash when producing a hash" do hash = @resource.to_hash hash[:foo] = "bar" @resource[:foo].should be_nil end it "should use the title as the namevar to the hash if no namevar is present" do resource = Puppet::Resource.new("user", "bob") Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar] resource.to_hash[:myvar].should == "bob" end it "should set :name to the title if :name is not present for non-builtin types" do krt = Puppet::Resource::TypeCollection.new("myenv") krt.add Puppet::Resource::Type.new(:definition, :foo) resource = Puppet::Resource.new :foo, "bar" resource.stubs(:known_resource_types).returns krt resource.to_hash[:name].should == "bar" end end describe "when serializing a native type" do before do @resource = Puppet::Resource.new("file", "/my/file") @resource["one"] = "test" @resource["two"] = "other" end it "should produce an equivalent yaml object" do text = @resource.render('yaml') newresource = Puppet::Resource.convert_from('yaml', text) newresource.should equal_resource_attributes_of @resource end end describe "when serializing a defined type" do before do type = Puppet::Resource::Type.new(:definition, "foo::bar") environment.known_resource_types.add type @resource = Puppet::Resource.new('foo::bar', 'xyzzy', :environment => environment) @resource['one'] = 'test' @resource['two'] = 'other' @resource.resource_type end it "doesn't include transient instance variables (#4506)" do expect(@resource.to_yaml_properties).to_not include :@rstype end it "produces an equivalent yaml object" do text = @resource.render('yaml') newresource = Puppet::Resource.convert_from('yaml', text) newresource.should equal_resource_attributes_of @resource end end describe "when converting to a RAL resource" do it "should use the resource type's :new method to create the resource if the resource is of a builtin type" do resource = Puppet::Resource.new("file", basepath+"/my/file") result = resource.to_ral result.must be_instance_of(Puppet::Type.type(:file)) result[:path].should == basepath+"/my/file" end it "should convert to a component instance if the resource type is not of a builtin type" do resource = Puppet::Resource.new("foobar", "somename") result = resource.to_ral result.must be_instance_of(Puppet::Type.type(:component)) result.title.should == "Foobar[somename]" end end describe "when converting to puppet code" do before do @resource = Puppet::Resource.new("one::two", "/my/file", :parameters => { :noop => true, :foo => %w{one two}, :ensure => 'present', } ) end it "should align, sort and add trailing commas to attributes with ensure first" do @resource.to_manifest.should == <<-HEREDOC.gsub(/^\s{8}/, '').gsub(/\n$/, '') one::two { '/my/file': ensure => 'present', foo => ['one', 'two'], noop => 'true', } HEREDOC end end + describe "when converting to Yaml for Hiera" do + before do + @resource = Puppet::Resource.new("one::two", "/my/file", + :parameters => { + :noop => true, + :foo => %w{one two}, + :ensure => 'present', + } + ) + end + + it "should align and sort to attributes with ensure first" do + @resource.to_hierayaml.should == <<-HEREDOC.gsub(/^\s{8}/, '') + /my/file: + ensure: 'present' + foo : ['one', 'two'] + noop : 'true' + HEREDOC + end + end describe "when converting to pson" do def pson_output_should @resource.class.expects(:pson_create).with { |hash| yield hash } end it "should include the pson util module" do Puppet::Resource.singleton_class.ancestors.should be_include(Puppet::Util::Pson) end # LAK:NOTE For all of these tests, we convert back to the resource so we can # trap the actual data structure then. it "should set its type to the provided type" do Puppet::Resource.from_data_hash(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).type.should == "File" end it "should set its title to the provided title" do Puppet::Resource.from_data_hash(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).title.should == "/foo" end it "should include all tags from the resource" do resource = Puppet::Resource.new("File", "/foo") resource.tag("yay") Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).tags.should == resource.tags end it "should include the file if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.file = "/my/file" Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).file.should == "/my/file" end it "should include the line if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.line = 50 Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).line.should == 50 end it "should include the 'exported' value if one is set" do resource = Puppet::Resource.new("File", "/foo") resource.exported = true Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).exported?.should be_true end it "should set 'exported' to false if no value is set" do resource = Puppet::Resource.new("File", "/foo") Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).exported?.should be_false end it "should set all of its parameters as the 'parameters' entry" do resource = Puppet::Resource.new("File", "/foo") resource[:foo] = %w{bar eh} resource[:fee] = %w{baz} result = Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)) result["foo"].should == %w{bar eh} result["fee"].should == %w{baz} end it "should serialize relationships as reference strings" do resource = Puppet::Resource.new("File", "/foo") resource[:requires] = Puppet::Resource.new("File", "/bar") result = Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)) result[:requires].should == "File[/bar]" end it "should serialize multiple relationships as arrays of reference strings" do resource = Puppet::Resource.new("File", "/foo") resource[:requires] = [Puppet::Resource.new("File", "/bar"), Puppet::Resource.new("File", "/baz")] result = Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)) result[:requires].should == [ "File[/bar]", "File[/baz]" ] end end describe "when converting from pson" do def pson_result_should Puppet::Resource.expects(:new).with { |hash| yield hash } end before do @data = { 'type' => "file", 'title' => basepath+"/yay", } end it "should set its type to the provided type" do Puppet::Resource.from_data_hash(@data).type.should == "File" end it "should set its title to the provided title" do Puppet::Resource.from_data_hash(@data).title.should == basepath+"/yay" end it "should tag the resource with any provided tags" do @data['tags'] = %w{foo bar} resource = Puppet::Resource.from_data_hash(@data) resource.tags.should be_include("foo") resource.tags.should be_include("bar") end it "should set its file to the provided file" do @data['file'] = "/foo/bar" Puppet::Resource.from_data_hash(@data).file.should == "/foo/bar" end it "should set its line to the provided line" do @data['line'] = 50 Puppet::Resource.from_data_hash(@data).line.should == 50 end it "should 'exported' to true if set in the pson data" do @data['exported'] = true Puppet::Resource.from_data_hash(@data).exported.should be_true end it "should 'exported' to false if not set in the pson data" do Puppet::Resource.from_data_hash(@data).exported.should be_false end it "should fail if no title is provided" do @data.delete('title') expect { Puppet::Resource.from_data_hash(@data) }.to raise_error(ArgumentError) end it "should fail if no type is provided" do @data.delete('type') expect { Puppet::Resource.from_data_hash(@data) }.to raise_error(ArgumentError) end it "should set each of the provided parameters" do @data['parameters'] = {'foo' => %w{one two}, 'fee' => %w{three four}} resource = Puppet::Resource.from_data_hash(@data) resource['foo'].should == %w{one two} resource['fee'].should == %w{three four} end it "should convert single-value array parameters to normal values" do @data['parameters'] = {'foo' => %w{one}} resource = Puppet::Resource.from_data_hash(@data) resource['foo'].should == %w{one} end end it "implements copy_as_resource" do resource = Puppet::Resource.new("file", "/my/file") resource.copy_as_resource.should == resource end describe "because it is an indirector model" do it "should include Puppet::Indirector" do Puppet::Resource.should be_is_a(Puppet::Indirector) end it "should have a default terminus" do Puppet::Resource.indirection.terminus_class.should be end it "should have a name" do Puppet::Resource.new("file", "/my/file").name.should == "File//my/file" end end describe "when resolving resources with a catalog" do it "should resolve all resources using the catalog" do catalog = mock 'catalog' resource = Puppet::Resource.new("foo::bar", "yay") resource.catalog = catalog catalog.expects(:resource).with("Foo::Bar[yay]").returns(:myresource) resource.resolve.should == :myresource end end describe "when generating the uniqueness key" do it "should include all of the key_attributes in alphabetical order by attribute name" do Puppet::Type.type(:file).stubs(:key_attributes).returns [:myvar, :owner, :path] Puppet::Type.type(:file).stubs(:title_patterns).returns( [ [ /(.*)/, [ [:path, lambda{|x| x} ] ] ] ] ) res = Puppet::Resource.new("file", "/my/file", :parameters => {:owner => 'root', :content => 'hello'}) res.uniqueness_key.should == [ nil, 'root', '/my/file'] end end describe '#parse_title' do describe 'with a composite namevar' do before do Puppet::Type.newtype(:composite) do newparam(:name) newparam(:value) # Configure two title patterns to match a title that is either # separated with a colon or exclamation point. The first capture # will be used for the :name param, and the second capture will be # used for the :value param. def self.title_patterns identity = lambda {|x| x } reverse = lambda {|x| x.reverse } [ [ /^(.*?):(.*?)$/, [ [:name, identity], [:value, identity], ] ], [ /^(.*?)!(.*?)$/, [ [:name, reverse], [:value, reverse], ] ], ] end end end describe "with no matching title patterns" do subject { Puppet::Resource.new(:composite, 'unmatching title')} it "should raise an exception if no title patterns match" do expect do subject.to_hash end.to raise_error(Puppet::Error, /No set of title patterns matched/) end end describe "with a matching title pattern" do subject { Puppet::Resource.new(:composite, 'matching:title') } it "should not raise an exception if there was a match" do expect do subject.to_hash end.to_not raise_error end it "should set the resource parameters from the parsed title values" do h = subject.to_hash h[:name].should == 'matching' h[:value].should == 'title' end end describe "and multiple title patterns" do subject { Puppet::Resource.new(:composite, 'matching!title') } it "should use the first title pattern that matches" do h = subject.to_hash h[:name].should == 'gnihctam' h[:value].should == 'eltit' end end end end describe "#prune_parameters" do before do Puppet.newtype('blond') do newproperty(:ensure) newproperty(:height) newproperty(:weight) newproperty(:sign) newproperty(:friends) newparam(:admits_to_dying_hair) newparam(:admits_to_age) newparam(:name) end end it "should strip all parameters and strip properties that are nil, empty or absent except for ensure" do resource = Puppet::Resource.new("blond", "Bambi", :parameters => { :ensure => 'absent', :height => '', :weight => 'absent', :friends => [], :admits_to_age => true, :admits_to_dying_hair => false }) pruned_resource = resource.prune_parameters pruned_resource.should == Puppet::Resource.new("blond", "Bambi", :parameters => {:ensure => 'absent'}) end it "should leave parameters alone if in parameters_to_include" do resource = Puppet::Resource.new("blond", "Bambi", :parameters => { :admits_to_age => true, :admits_to_dying_hair => false }) pruned_resource = resource.prune_parameters(:parameters_to_include => [:admits_to_dying_hair]) pruned_resource.should == Puppet::Resource.new("blond", "Bambi", :parameters => {:admits_to_dying_hair => false}) end it "should leave properties if not nil, absent or empty" do resource = Puppet::Resource.new("blond", "Bambi", :parameters => { :ensure => 'silly', :height => '7 ft 5 in', :friends => ['Oprah'], }) pruned_resource = resource.prune_parameters pruned_resource.should == resource = Puppet::Resource.new("blond", "Bambi", :parameters => { :ensure => 'silly', :height => '7 ft 5 in', :friends => ['Oprah'], }) end end end diff --git a/spec/unit/settings_spec.rb b/spec/unit/settings_spec.rb index 0a1cee2bd..c40a01bbd 100755 --- a/spec/unit/settings_spec.rb +++ b/spec/unit/settings_spec.rb @@ -1,1883 +1,1903 @@ #! /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 describe "setbycli" do it "should generate a deprecation warning" do Puppet.expects(:deprecation_warning).at_least(1) @settings.setting(:myval).setbycli = true end it "should set the value" do @settings[:myval] = "blah" @settings.setting(:myval).setbycli = true @settings.set_by_cli?(:myval).should be_true end it "should raise error if trying to unset value" do @settings.handlearg("--myval", "blah") expect do @settings.setting(:myval).setbycli = nil end.to raise_error(ArgumentError, /unset/) end 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 describe "call_on_define" do [true, false].each do |val| describe "to #{val}" do it "should generate a deprecation warning" do Puppet.expects(:deprecation_warning) values = [] @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_on_define => val, :hook => lambda { |v| values << v }}) end it "should should set call_hook" do values = [] name = "hooker_#{val}".to_sym @settings.define_settings(:section, name => {:default => "yay", :desc => "boo", :call_on_define => val, :hook => lambda { |v| values << v }}) @settings.setting(name).call_hook.should == :on_define_and_write if val @settings.setting(name).call_hook.should == :on_write_only unless val end 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 describe "call_on_define" do it "should generate a deprecation warning" do Puppet.expects(:deprecation_warning) @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| hook_values << v }}) @settings.setting(:hooker).call_on_define end Puppet::Settings::StringSetting.available_call_hook_values.each do |val| it "should match value for call_hook => :#{val}" do hook_values = [] @settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val, :hook => lambda { |v| hook_values << v }}) @settings.setting(:hooker).call_on_define.should == @settings.setting(:hooker).call_hook_on_define? end end 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 describe "setbycli" do it "should generate a deprecation warning" do @settings.handlearg("--one", "blah") Puppet.expects(:deprecation_warning) @settings.setting(:one).setbycli end it "should be true" do @settings.handlearg("--one", "blah") @settings.setting(:one).setbycli.should be_true end 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.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 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") 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 => { :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') completely_deprecated_settings.parse_config(<<-CONF) manifestdir='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') args = ["--manifestdir", "/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') end end context "partial" do let(:partially_deprecated_settings) do settings.define_settings(:main, { :modulepath => { :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') partially_deprecated_settings.parse_config(<<-CONF) modulepath='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"] 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') 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/ssl/validator_spec.rb b/spec/unit/ssl/validator_spec.rb index ade1575dc..9faec6813 100644 --- a/spec/unit/ssl/validator_spec.rb +++ b/spec/unit/ssl/validator_spec.rb @@ -1,353 +1,419 @@ require 'spec_helper' require 'puppet/ssl' describe Puppet::SSL::Validator::DefaultValidator do let(:ssl_context) do mock('OpenSSL::X509::StoreContext') end let(:ssl_configuration) do Puppet::SSL::Configuration.new( Puppet[:localcacert], :ca_chain_file => Puppet[:ssl_client_ca_chain], :ca_auth_file => Puppet[:ssl_client_ca_auth]) end let(:ssl_host) do stub('ssl_host', :ssl_store => nil, :certificate => stub('cert', :content => nil), :key => stub('key', :content => nil)) end subject do described_class.new(ssl_configuration, ssl_host) end before :each do ssl_configuration.stubs(:read_file). with(Puppet[:localcacert]). returns(root_ca) end describe '#call' do before :each do ssl_context.stubs(:current_cert).returns(*cert_chain_in_callback_order) ssl_context.stubs(:chain).returns(cert_chain) end context 'When pre-verification is not OK' do context 'and the ssl_context is in an error state' do - before :each do - ssl_context.stubs(:error_string).returns("Something went wrong.") + let(:root_subject) { OpenSSL::X509::Certificate.new(root_ca).subject.to_s } + let(:code) { OpenSSL::X509::V_ERR_INVALID_CA } + + it 'rejects the connection' do + ssl_context.stubs(:error_string).returns("Something went wrong") + ssl_context.stubs(:error).returns(code) + + expect(subject.call(false, ssl_context)).to eq(false) end it 'makes the error available via #verify_errors' do + ssl_context.stubs(:error_string).returns("Something went wrong") + ssl_context.stubs(:error).returns(code) + + subject.call(false, ssl_context) + expect(subject.verify_errors).to eq(["Something went wrong for #{root_subject}"]) + end + + it 'uses a generic message if error_string is nil' do + ssl_context.stubs(:error_string).returns(nil) + ssl_context.stubs(:error).returns(code) + subject.call(false, ssl_context) - msg_suffix = OpenSSL::X509::Certificate.new(root_ca).subject - subject.verify_errors.should == ["Something went wrong. for #{msg_suffix}"] + expect(subject.verify_errors).to eq(["OpenSSL error #{code} for #{root_subject}"]) + end + + it 'uses 0 for nil error codes' do + ssl_context.stubs(:error_string).returns("Something went wrong") + ssl_context.stubs(:error).returns(nil) + + subject.call(false, ssl_context) + expect(subject.verify_errors).to eq(["Something went wrong for #{root_subject}"]) + end + + context "when CRL is not yet valid" do + before :each do + ssl_context.stubs(:error_string).returns("CRL is not yet valid") + ssl_context.stubs(:error).returns(OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID) + end + + it 'rejects nil CRL' do + ssl_context.stubs(:current_crl).returns(nil) + + expect(subject.call(false, ssl_context)).to eq(false) + expect(subject.verify_errors).to eq(["CRL is not yet valid"]) + end + + it 'includes the CRL issuer in the verify error message' do + crl = OpenSSL::X509::CRL.new + crl.issuer = OpenSSL::X509::Name.new([['CN','Puppet CA: puppetmaster.example.com']]) + crl.last_update = Time.now + 24 * 60 * 60 + ssl_context.stubs(:current_crl).returns(crl) + + subject.call(false, ssl_context) + expect(subject.verify_errors).to eq(["CRL is not yet valid for /CN=Puppet CA: puppetmaster.example.com"]) + end + + it 'rejects CRLs whose last_update time is more than 5 minutes in the future' do + crl = OpenSSL::X509::CRL.new + crl.issuer = OpenSSL::X509::Name.new([['CN','Puppet CA: puppetmaster.example.com']]) + crl.last_update = Time.now + 24 * 60 * 60 + ssl_context.stubs(:current_crl).returns(crl) + + expect(subject.call(false, ssl_context)).to eq(false) + end + + it 'accepts CRLs whose last_update time is 10 seconds in the future' do + crl = OpenSSL::X509::CRL.new + crl.issuer = OpenSSL::X509::Name.new([['CN','Puppet CA: puppetmaster.example.com']]) + crl.last_update = Time.now + 10 + ssl_context.stubs(:current_crl).returns(crl) + + expect(subject.call(false, ssl_context)).to eq(true) + end end end end context 'When pre-verification is OK' do context 'and the ssl_context is in an error state' do before :each do - ssl_context.stubs(:error_string).returns("Something went wrong.") + ssl_context.stubs(:error_string).returns("Something went wrong") end it 'does not make the error available via #verify_errors' do subject.call(true, ssl_context) subject.verify_errors.should == [] end end context 'and the chain is valid' do it 'is true for each CA certificate in the chain' do (cert_chain.length - 1).times do subject.call(true, ssl_context).should be_true end end it 'is true for the SSL certificate ending the chain' do (cert_chain.length - 1).times do subject.call(true, ssl_context) end subject.call(true, ssl_context).should be_true end end context 'and the chain is invalid' do before :each do ssl_configuration.stubs(:read_file). with(Puppet[:localcacert]). returns(agent_ca) end it 'is true for each CA certificate in the chain' do (cert_chain.length - 1).times do subject.call(true, ssl_context).should be_true end end it 'is false for the SSL certificate ending the chain' do (cert_chain.length - 1).times do subject.call(true, ssl_context) end subject.call(true, ssl_context).should be_false end end context 'an error is raised inside of #call' do before :each do ssl_context.expects(:current_cert).raises(StandardError, "BOOM!") end it 'is false' do subject.call(true, ssl_context).should be_false end it 'makes the error available through #verify_errors' do subject.call(true, ssl_context) subject.verify_errors.should == ["BOOM!"] end end end end describe '#setup_connection' do it 'updates the connection for verification' do subject.stubs(:ssl_certificates_are_present?).returns(true) connection = mock('Net::HTTP') connection.expects(:cert_store=).with(ssl_host.ssl_store) connection.expects(:ca_file=).with(ssl_configuration.ca_auth_file) connection.expects(:cert=).with(ssl_host.certificate.content) connection.expects(:key=).with(ssl_host.key.content) connection.expects(:verify_callback=).with(subject) connection.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) subject.setup_connection(connection) end it 'does not perform verification if certificate files are missing' do subject.stubs(:ssl_certificates_are_present?).returns(false) connection = mock('Net::HTTP') connection.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) subject.setup_connection(connection) end end describe '#valid_peer?' do before :each do peer_certs = cert_chain_in_callback_order.map do |c| Puppet::SSL::Certificate.from_instance(c) end subject.instance_variable_set(:@peer_certs, peer_certs) end context 'when the peer presents a valid chain' do before :each do subject.stubs(:has_authz_peer_cert).returns(true) end it 'is true' do subject.valid_peer?.should be_true end end context 'when the peer presents an invalid chain' do before :each do subject.stubs(:has_authz_peer_cert).returns(false) end it 'is false' do subject.valid_peer?.should be_false end it 'makes a helpful error message available via #verify_errors' do subject.valid_peer? subject.verify_errors.should == [expected_authz_error_msg] end end end describe '#has_authz_peer_cert' do context 'when the Root CA is listed as authorized' do it 'returns true when the SSL cert is issued by the Master CA' do subject.has_authz_peer_cert(cert_chain, [root_ca_cert]).should be_true end it 'returns true when the SSL cert is issued by the Agent CA' do subject.has_authz_peer_cert(cert_chain_agent_ca, [root_ca_cert]).should be_true end end context 'when the Master CA is listed as authorized' do it 'returns false when the SSL cert is issued by the Master CA' do subject.has_authz_peer_cert(cert_chain, [master_ca_cert]).should be_true end it 'returns true when the SSL cert is issued by the Agent CA' do subject.has_authz_peer_cert(cert_chain_agent_ca, [master_ca_cert]).should be_false end end context 'when the Agent CA is listed as authorized' do it 'returns true when the SSL cert is issued by the Master CA' do subject.has_authz_peer_cert(cert_chain, [agent_ca_cert]).should be_false end it 'returns true when the SSL cert is issued by the Agent CA' do subject.has_authz_peer_cert(cert_chain_agent_ca, [agent_ca_cert]).should be_true end end end def root_ca <<-ROOT_CA -----BEGIN CERTIFICATE----- MIICYDCCAcmgAwIBAgIJALf2Pk2HvtBzMA0GCSqGSIb3DQEBBQUAMEkxEDAOBgNV BAMMB1Jvb3QgQ0ExGjAYBgNVBAsMEVNlcnZlciBPcGVyYXRpb25zMRkwFwYDVQQK DBBFeGFtcGxlIE9yZywgTExDMB4XDTEzMDMzMDA1NTA0OFoXDTMzMDMyNTA1NTA0 OFowSTEQMA4GA1UEAwwHUm9vdCBDQTEaMBgGA1UECwwRU2VydmVyIE9wZXJhdGlv bnMxGTAXBgNVBAoMEEV4YW1wbGUgT3JnLCBMTEMwgZ8wDQYJKoZIhvcNAQEBBQAD gY0AMIGJAoGBAMGSpafR4lboYOPfPJC1wVHHl0gD49ZVRjOlJ9jidEUjBdFXK6SA S1tecDv2G4tM1ANmfMKjZl0m+KaZ8O2oq0g6kxkq1Mg0eSNvlnEyehjmTLRzHC2i a0biH2wMtCLzfAoXDKy4GPlciBPE9mup5I8Kien5s91t92tc7K8AJ8oBAgMBAAGj UDBOMB0GA1UdDgQWBBQwTdZqjjXOIFK2hOM0bcOrnhQw2jAfBgNVHSMEGDAWgBQw TdZqjjXOIFK2hOM0bcOrnhQw2jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUA A4GBACs8EZRrzgzAlcKC1Tz8GYlNHQg0XhpbEDm+p2mOV//PuDD190O+UBpWxo9Q rrkkx8En0wXQZJf6iH3hwewwHLOq5yXZKbJN+SmvJvRNL95Yhyy08Y9N65tJveE7 rPsNU/Tx19jHC87oXlmAePLI4IaUHXrWb7CRbY9TEcPdmj1R -----END CERTIFICATE----- ROOT_CA end def master_ca <<-MASTER_CA -----BEGIN CERTIFICATE----- MIICljCCAf+gAwIBAgIBAjANBgkqhkiG9w0BAQUFADBJMRAwDgYDVQQDDAdSb290 IENBMRowGAYDVQQLDBFTZXJ2ZXIgT3BlcmF0aW9uczEZMBcGA1UECgwQRXhhbXBs ZSBPcmcsIExMQzAeFw0xMzAzMzAwNTUwNDhaFw0zMzAzMjUwNTUwNDhaMH4xJDAi BgNVBAMTG0ludGVybWVkaWF0ZSBDQSAobWFzdGVyLWNhKTEfMB0GCSqGSIb3DQEJ ARYQdGVzdEBleGFtcGxlLm9yZzEZMBcGA1UEChMQRXhhbXBsZSBPcmcsIExMQzEa MBgGA1UECxMRU2VydmVyIE9wZXJhdGlvbnMwXDANBgkqhkiG9w0BAQEFAANLADBI AkEAvo/az3oR69SP92jGnUHMJLEyyD1Ui1BZ/rUABJcQTRQqn3RqtlfYePWZnUaZ srKbXRS4q0w5Vqf1kx5w3q5tIwIDAQABo4GcMIGZMHkGA1UdIwRyMHCAFDBN1mqO Nc4gUraE4zRtw6ueFDDaoU2kSzBJMRAwDgYDVQQDDAdSb290IENBMRowGAYDVQQL DBFTZXJ2ZXIgT3BlcmF0aW9uczEZMBcGA1UECgwQRXhhbXBsZSBPcmcsIExMQ4IJ ALf2Pk2HvtBzMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMA0GCSqGSIb3 DQEBBQUAA4GBACRfa1YPS7RQUuhYovGgV0VYqxuATC7WwdIRihVh5FceSXKgSIbz BKmOBAy/KixEhpnHTbkpaJ0d9ITkvjMTmj3M5YMahKaQA5niVPckQPecMMd6jg9U l1k75xLLIcrlsDYo3999KOSSchH2K7bLT7TuQ2okdP6FHWmeWmudewlu -----END CERTIFICATE----- MASTER_CA end def agent_ca <<-AGENT_CA -----BEGIN CERTIFICATE----- MIIClTCCAf6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBJMRAwDgYDVQQDDAdSb290 IENBMRowGAYDVQQLDBFTZXJ2ZXIgT3BlcmF0aW9uczEZMBcGA1UECgwQRXhhbXBs ZSBPcmcsIExMQzAeFw0xMzAzMzAwNTUwNDhaFw0zMzAzMjUwNTUwNDhaMH0xIzAh BgNVBAMTGkludGVybWVkaWF0ZSBDQSAoYWdlbnQtY2EpMR8wHQYJKoZIhvcNAQkB FhB0ZXN0QGV4YW1wbGUub3JnMRkwFwYDVQQKExBFeGFtcGxlIE9yZywgTExDMRow GAYDVQQLExFTZXJ2ZXIgT3BlcmF0aW9uczBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC QQDkEj/Msmi4hJImxP5+ocixMTHuYC1M1E2p4QcuzOkZYrfHf+5hJMcahfYhLiXU jHBredOXhgSisHh6CLSb/rKzAgMBAAGjgZwwgZkweQYDVR0jBHIwcIAUME3Wao41 ziBStoTjNG3Dq54UMNqhTaRLMEkxEDAOBgNVBAMMB1Jvb3QgQ0ExGjAYBgNVBAsM EVNlcnZlciBPcGVyYXRpb25zMRkwFwYDVQQKDBBFeGFtcGxlIE9yZywgTExDggkA t/Y+TYe+0HMwDwYDVR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCAQYwDQYJKoZIhvcN AQEFBQADgYEAujSj9rxIxJHEuuYXb15L30yxs9Tdvy4OCLiKdjvs9Z7gG8Pbutls ooCwyYAkmzKVs/8cYjZJnvJrPEW1gFwqX7Xknp85Cfrl+/pQEPYq5sZVa5BIm9tI 0EvlDax/Hd28jI6Bgq5fsTECNl9GDGknCy7vwRZem0h+hI56lzR3pYE= -----END CERTIFICATE----- AGENT_CA end # Signed by the master CA (Good) def master_issued_by_master_ca <<-GOOD_SSL_CERT -----BEGIN CERTIFICATE----- MIICZzCCAhGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB+MSQwIgYDVQQDExtJbnRl cm1lZGlhdGUgQ0EgKG1hc3Rlci1jYSkxHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhh bXBsZS5vcmcxGTAXBgNVBAoTEEV4YW1wbGUgT3JnLCBMTEMxGjAYBgNVBAsTEVNl cnZlciBPcGVyYXRpb25zMB4XDTEzMDMzMDA1NTA0OFoXDTMzMDMyNTA1NTA0OFow HjEcMBoGA1UEAwwTbWFzdGVyMS5leGFtcGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUA A0sAMEgCQQDACW8fryVZH0dC7vYUASonVBKYcILnKN2O9QX7RenZGN1TWek9LQxr yQFDyp7WJ8jUw6nENGniLU8J+QSSxryjAgMBAAGjgdkwgdYwWwYDVR0jBFQwUqFN pEswSTEQMA4GA1UEAwwHUm9vdCBDQTEaMBgGA1UECwwRU2VydmVyIE9wZXJhdGlv bnMxGTAXBgNVBAoMEEV4YW1wbGUgT3JnLCBMTEOCAQIwDAYDVR0TAQH/BAIwADAL BgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMD0GA1Ud EQQ2MDSCE21hc3RlcjEuZXhhbXBsZS5vcmeCB21hc3RlcjGCBnB1cHBldIIMcHVw cGV0bWFzdGVyMA0GCSqGSIb3DQEBBQUAA0EAo8PvgLrah6jQVs6YCBxOTn13PDip fVbcRsFd0dtIr00N61bCqr6Fa0aRwy424gh6bVJTNmk2zoaH7r025dZRhw== -----END CERTIFICATE----- GOOD_SSL_CERT end # Signed by the agent CA, not the master CA (Rogue) def master_issued_by_agent_ca <<-BAD_SSL_CERT -----BEGIN CERTIFICATE----- MIICZjCCAhCgAwIBAgIBBDANBgkqhkiG9w0BAQUFADB9MSMwIQYDVQQDExpJbnRl cm1lZGlhdGUgQ0EgKGFnZW50LWNhKTEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFt cGxlLm9yZzEZMBcGA1UEChMQRXhhbXBsZSBPcmcsIExMQzEaMBgGA1UECxMRU2Vy dmVyIE9wZXJhdGlvbnMwHhcNMTMwMzMwMDU1MDQ4WhcNMzMwMzI1MDU1MDQ4WjAe MRwwGgYDVQQDDBNtYXN0ZXIxLmV4YW1wbGUub3JnMFwwDQYJKoZIhvcNAQEBBQAD SwAwSAJBAPnCDnryLLXWepGLqsdBWlytfeakE/yijM8GlE/yT0SbpJInIhJR1N1A 0RskriHrxTU5qQEhd0RIja7K5o4NYksCAwEAAaOB2TCB1jBbBgNVHSMEVDBSoU2k SzBJMRAwDgYDVQQDDAdSb290IENBMRowGAYDVQQLDBFTZXJ2ZXIgT3BlcmF0aW9u czEZMBcGA1UECgwQRXhhbXBsZSBPcmcsIExMQ4IBATAMBgNVHRMBAf8EAjAAMAsG A1UdDwQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwPQYDVR0R BDYwNIITbWFzdGVyMS5leGFtcGxlLm9yZ4IHbWFzdGVyMYIGcHVwcGV0ggxwdXBw ZXRtYXN0ZXIwDQYJKoZIhvcNAQEFBQADQQA841IzHLlnn4RIJ0/BOZ/16iWC1dNr jV9bELC5OxeMNSsVXbFNeTHwbHEYjDg5dQ6eUkxPdBSMWBeQwe2Mw+xG -----END CERTIFICATE----- BAD_SSL_CERT end def cert_chain [ master_issued_by_master_ca, master_ca, root_ca ].map do |pem| OpenSSL::X509::Certificate.new(pem) end end def cert_chain_agent_ca [ master_issued_by_agent_ca, agent_ca, root_ca ].map do |pem| OpenSSL::X509::Certificate.new(pem) end end def cert_chain_in_callback_order cert_chain.reverse end let :authz_error_prefix do "The server presented a SSL certificate chain which does not include a CA listed in the ssl_client_ca_auth file. " end let :expected_authz_error_msg do authz_ca_certs = ssl_configuration.ca_auth_certificates msg = authz_error_prefix msg << "Authorized Issuers: #{authz_ca_certs.collect {|c| c.subject}.join(', ')} " msg << "Peer Chain: #{cert_chain.collect {|c| c.subject}.join(' => ')}" msg end let :root_ca_cert do OpenSSL::X509::Certificate.new(root_ca) end let :master_ca_cert do OpenSSL::X509::Certificate.new(master_ca) end let :agent_ca_cert do OpenSSL::X509::Certificate.new(agent_ca) end end