diff --git a/acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb b/acceptance/pending/pluginsync/7316_faces_should_be_available_via_pluginsync.rb similarity index 90% copy from acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb copy to acceptance/pending/pluginsync/7316_faces_should_be_available_via_pluginsync.rb index 616942f13..281c68fc4 100644 --- a/acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb +++ b/acceptance/pending/pluginsync/7316_faces_should_be_available_via_pluginsync.rb @@ -1,262 +1,242 @@ test_name "the pluginsync functionality should sync app definitions, and they should be runnable afterwards" # -# This test is intended to ensure that pluginsync syncs app and face definitions to the agents. -# Further, the apps and faces should be runnable on the agent after the sync has occurred. +# This test is intended to ensure that pluginsync syncs face definitions to the agents. +# Further, the face should be runnable on the agent after the sync has occurred. # # (NOTE: When this test is passing, it should resolve both #7316 re: verifying that apps/faces can # be run on the agent node after a plugin sync, and #6753 re: being able to run a face without # having a placeholder stub file in the "applications" directory.) # ############################################################################### # BEGIN UTILITY METHODS - ideally this stuff would live somewhere besides in # the actual test. ############################################################################### # Create a file on the host. # Parameters: # [host] the host to create the file on # [file_path] the path to the file to be created # [file_content] a string containing the contents to be written to the file # [options] a hash containing additional behavior options. Currently supported: # * :mkdirs (default false) if true, attempt to create the parent directories on the remote host before writing # the file # * :owner (default 'root') the username of the user that the file should be owned by # * :group (default 'puppet') the name of the group that the file should be owned by # * :mode (default '644') the mode (file permissions) that the file should be created with def create_test_file(host, file_rel_path, file_content, options) # set default options options[:mkdirs] ||= false options[:owner] ||= "root" options[:group] ||= "puppet" options[:mode] ||= "755" file_path = get_test_file_path(host, file_rel_path) mkdirs(host, File.dirname(file_path)) if (options[:mkdirs] == true) create_remote_file(host, file_path, file_content) # # NOTE: we need these chown/chmod calls because the acceptance framework connects to the nodes as "root", but # puppet 'master' runs as user 'puppet'. Therefore, in order for puppet master to be able to read any files # that we've created, we have to carefully set their permissions # chown(host, options[:owner], options[:group], file_path) chmod(host, options[:mode], file_path) end # Given a relative path, returns an absolute path for a test file. Basically, this just prepends the # a unique temp dir path (specific to the current test execution) to your relative path. def get_test_file_path(host, file_rel_path) File.join(@host_test_tmp_dirs[host.name], file_rel_path) end # Check for the existence of a temp file for the current test; basically, this just calls file_exists?(), # but prepends the path to the current test's temp dir onto the file_rel_path parameter. This allows # tests to be written using only a relative path to specify file locations, while still taking advantage # of automatic temp file cleanup at test completion. def test_file_exists?(host, file_rel_path) - # I don't think we can easily use "test -f" here, because our "execute" commands are all built around reading - # stdout as opposed to reading process exit codes - result = host.execute("ruby -e \"print File.exists?('#{get_test_file_path(host, file_rel_path)}')\"") - # get a boolean return value - result == "true" + host.execute("test -f \"#{get_test_file_path(host, file_rel_path)}\"", + :acceptable_exit_codes => [0, 1]) do |result| + return result.exit_code == 0 + end end def tmpdir(host, basename) host_tmpdir = host.tmpdir(basename) # we need to make sure that the puppet user can traverse this directory... chmod(host, "755", host_tmpdir) host_tmpdir end def mkdirs(host, dir_path) on(host, "mkdir -p #{dir_path}") end def chown(host, owner, group, path) on(host, "chown #{owner}:#{group} #{path}") end def chmod(host, mode, path) on(host, "chmod #{mode} #{path}") end # pluck this out of the test case environment; not sure if there is a better way cur_test_file = @path cur_test_file_shortname = File.basename(cur_test_file, File.extname(cur_test_file)) # we need one list of all of the hosts, to assist in managing temp dirs. It's possible # that the master is also an agent, so this will consolidate them into a unique set all_hosts = Set[master, *agents] # now we can create a hash of temp dirs--one per host, and unique to this test--without worrying about # doing it twice on any individual host @host_test_tmp_dirs = Hash[all_hosts.map do |host| [host.name, tmpdir(host, cur_test_file_shortname)] end ] # a silly variable for keeping track of whether or not all of the tests passed... all_tests_passed = false ############################################################################### # END UTILITY METHODS ############################################################################### ############################################################################### # BEGIN TEST LOGIC ############################################################################### # create some vars to point to the directories that we're going to point the master/agents at master_module_dir = "master_modules" agent_lib_dir = "agent_lib" app_name = "superbogus" app_desc = "a simple %1$s for testing %1$s delivery via plugin sync" app_output = "Hello from the #{app_name} %s" master_module_file_content = {} -master_module_file_content["application"] = <<-HERE -require 'puppet/application' - -class Puppet::Application::#{app_name.capitalize} < Puppet::Application - - def help - <<-HELP - -puppet-#{app_name}(8) -- #{app_desc % "application"} -======== - HELP - end - - def main() - puts("#{app_output % "application"}") - end -end -HERE - master_module_file_content["face"] = <<-HERE Puppet::Face.define(:#{app_name}, '0.0.1') do copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" summary "#{app_desc % "face"}" action(:foo) do summary "a test action defined in the test face in the main puppet lib dir" default when_invoked do |*args| puts "#{app_output % "face"}" end end end HERE # this begin block is here for handling temp file cleanup via an "ensure" block at the very end of the # test. begin - modes = ["application", "face"] + modes = ["face"] modes.each do |mode| # here we create a custom app, which basically doesn't do anything except for print a hello-world message agent_module_app_file = "#{agent_lib_dir}/puppet/#{mode}/#{app_name}.rb" master_module_app_file = "#{master_module_dir}/#{app_name}/lib/puppet/#{mode}/#{app_name}.rb" # copy all the files to the master step "write our simple module out to the master" do create_test_file(master, master_module_app_file, master_module_file_content[mode], :mkdirs => true) end step "verify that the app file exists on the master" do unless test_file_exists?(master, master_module_app_file) then fail_test("Failed to create app file '#{get_test_file_path(master, master_module_app_file)}' on master") end end step "start the master" do with_master_running_on(master, "--modulepath=\"#{get_test_file_path(master, master_module_dir)}\"") do # the module files shouldn't exist on the agent yet because they haven't been synced step "verify that the module files don't exist on the agent path" do agents.each do |agent| if test_file_exists?(agent, agent_module_app_file) then fail_test("app file already exists on agent: '#{get_test_file_path(agent, agent_module_app_file)}'") end end end step "run the agent" do agents.each do |agent| run_agent_on(agent, "--trace --libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\" " + "--no-daemonize --verbose --onetime --test --server #{master}") end end end end step "verify that the module files were synced down to the agent" do agents.each do |agent| unless test_file_exists?(agent, agent_module_app_file) then fail_test("Expected app file not synced to agent: '#{get_test_file_path(agent, agent_module_app_file)}'") end end end step "verify that the application shows up in help" do agents.each do |agent| on(agent, PuppetCommand.new(:help, "--libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\"")) do assert_match(/^\s+#{app_name}\s+#{app_desc % mode}$/, result.stdout) end end end step "verify that we can run the application" do agents.each do |agent| on(agent, PuppetCommand.new(:"#{app_name}", "--libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\"")) do assert_match(/^#{app_output % mode}$/, result.stdout) end end end step "clear out the libdir on the agents in preparation for the next test" do agents.each do |agent| on(agent, "rm -rf #{get_test_file_path(agent, agent_module_app_file)}/*") end end end all_tests_passed = true ensure ########################################################################################## # Clean up all of the temp files created by this test. It would be nice if this logic # could be handled outside of the test itself; I envision a stanza like this one appearing # in a very large number of the tests going forward unless it is handled by the framework. ########################################################################################## if all_tests_passed then all_hosts.each do |host| on(host, "rm -rf #{@host_test_tmp_dirs[host.name]}") end end end \ No newline at end of file diff --git a/acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb b/acceptance/tests/pluginsync/7316_apps_should_be_available_via_pluginsync.rb similarity index 87% copy from acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb copy to acceptance/tests/pluginsync/7316_apps_should_be_available_via_pluginsync.rb index 616942f13..01768109c 100644 --- a/acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb +++ b/acceptance/tests/pluginsync/7316_apps_should_be_available_via_pluginsync.rb @@ -1,262 +1,237 @@ test_name "the pluginsync functionality should sync app definitions, and they should be runnable afterwards" # -# This test is intended to ensure that pluginsync syncs app and face definitions to the agents. -# Further, the apps and faces should be runnable on the agent after the sync has occurred. -# -# (NOTE: When this test is passing, it should resolve both #7316 re: verifying that apps/faces can -# be run on the agent node after a plugin sync, and #6753 re: being able to run a face without -# having a placeholder stub file in the "applications" directory.) +# This test is intended to ensure that pluginsync syncs app definitions to the agents. +# Further, the apps should be runnable on the agent after the sync has occurred. # ############################################################################### # BEGIN UTILITY METHODS - ideally this stuff would live somewhere besides in # the actual test. ############################################################################### # Create a file on the host. # Parameters: # [host] the host to create the file on # [file_path] the path to the file to be created # [file_content] a string containing the contents to be written to the file # [options] a hash containing additional behavior options. Currently supported: # * :mkdirs (default false) if true, attempt to create the parent directories on the remote host before writing # the file # * :owner (default 'root') the username of the user that the file should be owned by # * :group (default 'puppet') the name of the group that the file should be owned by # * :mode (default '644') the mode (file permissions) that the file should be created with def create_test_file(host, file_rel_path, file_content, options) # set default options options[:mkdirs] ||= false options[:owner] ||= "root" options[:group] ||= "puppet" options[:mode] ||= "755" file_path = get_test_file_path(host, file_rel_path) mkdirs(host, File.dirname(file_path)) if (options[:mkdirs] == true) create_remote_file(host, file_path, file_content) # # NOTE: we need these chown/chmod calls because the acceptance framework connects to the nodes as "root", but # puppet 'master' runs as user 'puppet'. Therefore, in order for puppet master to be able to read any files # that we've created, we have to carefully set their permissions # chown(host, options[:owner], options[:group], file_path) chmod(host, options[:mode], file_path) end # Given a relative path, returns an absolute path for a test file. Basically, this just prepends the # a unique temp dir path (specific to the current test execution) to your relative path. def get_test_file_path(host, file_rel_path) File.join(@host_test_tmp_dirs[host.name], file_rel_path) end # Check for the existence of a temp file for the current test; basically, this just calls file_exists?(), # but prepends the path to the current test's temp dir onto the file_rel_path parameter. This allows # tests to be written using only a relative path to specify file locations, while still taking advantage # of automatic temp file cleanup at test completion. def test_file_exists?(host, file_rel_path) - # I don't think we can easily use "test -f" here, because our "execute" commands are all built around reading - # stdout as opposed to reading process exit codes - result = host.execute("ruby -e \"print File.exists?('#{get_test_file_path(host, file_rel_path)}')\"") - # get a boolean return value - result == "true" + host.execute("test -f \"#{get_test_file_path(host, file_rel_path)}\"", + :acceptable_exit_codes => [0, 1]) do |result| + return result.exit_code == 0 + end end def tmpdir(host, basename) host_tmpdir = host.tmpdir(basename) # we need to make sure that the puppet user can traverse this directory... chmod(host, "755", host_tmpdir) host_tmpdir end def mkdirs(host, dir_path) on(host, "mkdir -p #{dir_path}") end def chown(host, owner, group, path) on(host, "chown #{owner}:#{group} #{path}") end def chmod(host, mode, path) on(host, "chmod #{mode} #{path}") end # pluck this out of the test case environment; not sure if there is a better way cur_test_file = @path cur_test_file_shortname = File.basename(cur_test_file, File.extname(cur_test_file)) # we need one list of all of the hosts, to assist in managing temp dirs. It's possible # that the master is also an agent, so this will consolidate them into a unique set all_hosts = Set[master, *agents] # now we can create a hash of temp dirs--one per host, and unique to this test--without worrying about # doing it twice on any individual host @host_test_tmp_dirs = Hash[all_hosts.map do |host| [host.name, tmpdir(host, cur_test_file_shortname)] end ] # a silly variable for keeping track of whether or not all of the tests passed... all_tests_passed = false ############################################################################### # END UTILITY METHODS ############################################################################### ############################################################################### # BEGIN TEST LOGIC ############################################################################### # create some vars to point to the directories that we're going to point the master/agents at master_module_dir = "master_modules" agent_lib_dir = "agent_lib" app_name = "superbogus" app_desc = "a simple %1$s for testing %1$s delivery via plugin sync" app_output = "Hello from the #{app_name} %s" master_module_file_content = {} master_module_file_content["application"] = <<-HERE require 'puppet/application' class Puppet::Application::#{app_name.capitalize} < Puppet::Application def help <<-HELP puppet-#{app_name}(8) -- #{app_desc % "application"} ======== HELP end def main() puts("#{app_output % "application"}") end end HERE -master_module_file_content["face"] = <<-HERE -Puppet::Face.define(:#{app_name}, '0.0.1') do - copyright "Puppet Labs", 2011 - license "Apache 2 license; see COPYING" - - summary "#{app_desc % "face"}" - - action(:foo) do - summary "a test action defined in the test face in the main puppet lib dir" - - default - when_invoked do |*args| - puts "#{app_output % "face"}" - end - end - -end -HERE - - - # this begin block is here for handling temp file cleanup via an "ensure" block at the very end of the # test. begin - modes = ["application", "face"] + modes = ["application"] modes.each do |mode| # here we create a custom app, which basically doesn't do anything except for print a hello-world message agent_module_app_file = "#{agent_lib_dir}/puppet/#{mode}/#{app_name}.rb" master_module_app_file = "#{master_module_dir}/#{app_name}/lib/puppet/#{mode}/#{app_name}.rb" # copy all the files to the master step "write our simple module out to the master" do create_test_file(master, master_module_app_file, master_module_file_content[mode], :mkdirs => true) end step "verify that the app file exists on the master" do unless test_file_exists?(master, master_module_app_file) then fail_test("Failed to create app file '#{get_test_file_path(master, master_module_app_file)}' on master") end end step "start the master" do with_master_running_on(master, - "--modulepath=\"#{get_test_file_path(master, master_module_dir)}\"") do + "--modulepath=\"#{get_test_file_path(master, master_module_dir)}\" " + + "--autosign true") do # the module files shouldn't exist on the agent yet because they haven't been synced step "verify that the module files don't exist on the agent path" do agents.each do |agent| if test_file_exists?(agent, agent_module_app_file) then fail_test("app file already exists on agent: '#{get_test_file_path(agent, agent_module_app_file)}'") end end end step "run the agent" do agents.each do |agent| run_agent_on(agent, "--trace --libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\" " + "--no-daemonize --verbose --onetime --test --server #{master}") end end end end step "verify that the module files were synced down to the agent" do agents.each do |agent| unless test_file_exists?(agent, agent_module_app_file) then fail_test("Expected app file not synced to agent: '#{get_test_file_path(agent, agent_module_app_file)}'") end end end step "verify that the application shows up in help" do agents.each do |agent| on(agent, PuppetCommand.new(:help, "--libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\"")) do assert_match(/^\s+#{app_name}\s+#{app_desc % mode}$/, result.stdout) end end end step "verify that we can run the application" do agents.each do |agent| on(agent, PuppetCommand.new(:"#{app_name}", "--libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\"")) do assert_match(/^#{app_output % mode}$/, result.stdout) end end end step "clear out the libdir on the agents in preparation for the next test" do agents.each do |agent| on(agent, "rm -rf #{get_test_file_path(agent, agent_module_app_file)}/*") end end end all_tests_passed = true ensure ########################################################################################## # Clean up all of the temp files created by this test. It would be nice if this logic # could be handled outside of the test itself; I envision a stanza like this one appearing # in a very large number of the tests going forward unless it is handled by the framework. ########################################################################################## if all_tests_passed then all_hosts.each do |host| on(host, "rm -rf #{@host_test_tmp_dirs[host.name]}") end end end \ No newline at end of file diff --git a/acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb b/acceptance/tests/pluginsync/7316_faces_with_app_stubs_should_be_available_via_pluginsync.rb similarity index 60% rename from acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb rename to acceptance/tests/pluginsync/7316_faces_with_app_stubs_should_be_available_via_pluginsync.rb index 616942f13..f0f7769de 100644 --- a/acceptance/pending/pluginsync/faces/7316_apps_and_faces_should_be_available_via_pluginsync.rb +++ b/acceptance/tests/pluginsync/7316_faces_with_app_stubs_should_be_available_via_pluginsync.rb @@ -1,262 +1,259 @@ test_name "the pluginsync functionality should sync app definitions, and they should be runnable afterwards" # -# This test is intended to ensure that pluginsync syncs app and face definitions to the agents. -# Further, the apps and faces should be runnable on the agent after the sync has occurred. +# This test is intended to ensure that pluginsync syncs face definitions to the agents. +# Further, the face should be runnable on the agent after the sync has occurred. # # (NOTE: When this test is passing, it should resolve both #7316 re: verifying that apps/faces can # be run on the agent node after a plugin sync, and #6753 re: being able to run a face without # having a placeholder stub file in the "applications" directory.) # ############################################################################### # BEGIN UTILITY METHODS - ideally this stuff would live somewhere besides in # the actual test. ############################################################################### # Create a file on the host. # Parameters: # [host] the host to create the file on # [file_path] the path to the file to be created # [file_content] a string containing the contents to be written to the file # [options] a hash containing additional behavior options. Currently supported: # * :mkdirs (default false) if true, attempt to create the parent directories on the remote host before writing # the file # * :owner (default 'root') the username of the user that the file should be owned by # * :group (default 'puppet') the name of the group that the file should be owned by # * :mode (default '644') the mode (file permissions) that the file should be created with def create_test_file(host, file_rel_path, file_content, options) # set default options options[:mkdirs] ||= false options[:owner] ||= "root" options[:group] ||= "puppet" options[:mode] ||= "755" file_path = get_test_file_path(host, file_rel_path) mkdirs(host, File.dirname(file_path)) if (options[:mkdirs] == true) create_remote_file(host, file_path, file_content) # # NOTE: we need these chown/chmod calls because the acceptance framework connects to the nodes as "root", but # puppet 'master' runs as user 'puppet'. Therefore, in order for puppet master to be able to read any files # that we've created, we have to carefully set their permissions # chown(host, options[:owner], options[:group], file_path) chmod(host, options[:mode], file_path) end # Given a relative path, returns an absolute path for a test file. Basically, this just prepends the # a unique temp dir path (specific to the current test execution) to your relative path. def get_test_file_path(host, file_rel_path) File.join(@host_test_tmp_dirs[host.name], file_rel_path) end # Check for the existence of a temp file for the current test; basically, this just calls file_exists?(), # but prepends the path to the current test's temp dir onto the file_rel_path parameter. This allows # tests to be written using only a relative path to specify file locations, while still taking advantage # of automatic temp file cleanup at test completion. def test_file_exists?(host, file_rel_path) - # I don't think we can easily use "test -f" here, because our "execute" commands are all built around reading - # stdout as opposed to reading process exit codes - result = host.execute("ruby -e \"print File.exists?('#{get_test_file_path(host, file_rel_path)}')\"") - # get a boolean return value - result == "true" + host.execute("test -f \"#{get_test_file_path(host, file_rel_path)}\"", + :acceptable_exit_codes => [0, 1]) do |result| + return result.exit_code == 0 + end end def tmpdir(host, basename) host_tmpdir = host.tmpdir(basename) # we need to make sure that the puppet user can traverse this directory... chmod(host, "755", host_tmpdir) host_tmpdir end def mkdirs(host, dir_path) on(host, "mkdir -p #{dir_path}") end def chown(host, owner, group, path) on(host, "chown #{owner}:#{group} #{path}") end def chmod(host, mode, path) on(host, "chmod #{mode} #{path}") end # pluck this out of the test case environment; not sure if there is a better way cur_test_file = @path cur_test_file_shortname = File.basename(cur_test_file, File.extname(cur_test_file)) # we need one list of all of the hosts, to assist in managing temp dirs. It's possible # that the master is also an agent, so this will consolidate them into a unique set all_hosts = Set[master, *agents] # now we can create a hash of temp dirs--one per host, and unique to this test--without worrying about # doing it twice on any individual host @host_test_tmp_dirs = Hash[all_hosts.map do |host| [host.name, tmpdir(host, cur_test_file_shortname)] end ] # a silly variable for keeping track of whether or not all of the tests passed... all_tests_passed = false ############################################################################### # END UTILITY METHODS ############################################################################### ############################################################################### # BEGIN TEST LOGIC ############################################################################### # create some vars to point to the directories that we're going to point the master/agents at master_module_dir = "master_modules" agent_lib_dir = "agent_lib" app_name = "superbogus" app_desc = "a simple %1$s for testing %1$s delivery via plugin sync" app_output = "Hello from the #{app_name} %s" master_module_file_content = {} -master_module_file_content["application"] = <<-HERE -require 'puppet/application' - -class Puppet::Application::#{app_name.capitalize} < Puppet::Application - - def help - <<-HELP - -puppet-#{app_name}(8) -- #{app_desc % "application"} -======== - HELP - end - - def main() - puts("#{app_output % "application"}") - end -end -HERE - -master_module_file_content["face"] = <<-HERE +master_module_face_content = <<-HERE Puppet::Face.define(:#{app_name}, '0.0.1') do copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" summary "#{app_desc % "face"}" action(:foo) do summary "a test action defined in the test face in the main puppet lib dir" default when_invoked do |*args| puts "#{app_output % "face"}" end end end HERE +master_module_app_content = <<-HERE +require 'puppet/application/face_base' + +class Puppet::Application::#{app_name.capitalize} < Puppet::Application::FaceBase +end + +HERE + # this begin block is here for handling temp file cleanup via an "ensure" block at the very end of the # test. begin - modes = ["application", "face"] + # here we create a custom app, which basically doesn't do anything except for print a hello-world message + agent_module_face_file = "#{agent_lib_dir}/puppet/face/#{app_name}.rb" + master_module_face_file = "#{master_module_dir}/#{app_name}/lib/puppet/face/#{app_name}.rb" - modes.each do |mode| + agent_module_app_file = "#{agent_lib_dir}/puppet/application/#{app_name}.rb" + master_module_app_file = "#{master_module_dir}/#{app_name}/lib/puppet/application/#{app_name}.rb" - # here we create a custom app, which basically doesn't do anything except for print a hello-world message - agent_module_app_file = "#{agent_lib_dir}/puppet/#{mode}/#{app_name}.rb" - master_module_app_file = "#{master_module_dir}/#{app_name}/lib/puppet/#{mode}/#{app_name}.rb" + # copy all the files to the master + step "write our simple module out to the master" do + create_test_file(master, master_module_app_file, master_module_app_content, :mkdirs => true) + create_test_file(master, master_module_face_file, master_module_face_content, :mkdirs => true) + end - # copy all the files to the master - step "write our simple module out to the master" do - create_test_file(master, master_module_app_file, master_module_file_content[mode], :mkdirs => true) + step "verify that the app file exists on the master" do + unless test_file_exists?(master, master_module_app_file) then + fail_test("Failed to create app file '#{get_test_file_path(master, master_module_app_file)}' on master") end - - step "verify that the app file exists on the master" do - unless test_file_exists?(master, master_module_app_file) then - fail_test("Failed to create app file '#{get_test_file_path(master, master_module_app_file)}' on master") - end + unless test_file_exists?(master, master_module_face_file) then + fail_test("Failed to create face file '#{get_test_file_path(master, master_module_face_file)}' on master") end + end - step "start the master" do - with_master_running_on(master, - "--modulepath=\"#{get_test_file_path(master, master_module_dir)}\"") do - - # the module files shouldn't exist on the agent yet because they haven't been synced - step "verify that the module files don't exist on the agent path" do - agents.each do |agent| - if test_file_exists?(agent, agent_module_app_file) then - fail_test("app file already exists on agent: '#{get_test_file_path(agent, agent_module_app_file)}'") - end - end - end - - step "run the agent" do - agents.each do |agent| - run_agent_on(agent, "--trace --libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\" " + - "--no-daemonize --verbose --onetime --test --server #{master}") - end + step "start the master" do + with_master_running_on(master, + "--modulepath=\"#{get_test_file_path(master, master_module_dir)}\" " + + "--autosign true") do + + # the module files shouldn't exist on the agent yet because they haven't been synced + step "verify that the module files don't exist on the agent path" do + agents.each do |agent| + if test_file_exists?(agent, agent_module_app_file) then + fail_test("app file already exists on agent: '#{get_test_file_path(agent, agent_module_app_file)}'") + end + if test_file_exists?(agent, agent_module_app_file) then + fail_test("face file already exists on agent: '#{get_test_file_path(agent, agent_module_face_file)}'") + end end - end - end - step "verify that the module files were synced down to the agent" do - agents.each do |agent| - unless test_file_exists?(agent, agent_module_app_file) then - fail_test("Expected app file not synced to agent: '#{get_test_file_path(agent, agent_module_app_file)}'") + step "run the agent" do + agents.each do |agent| + run_agent_on(agent, "--trace --libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\" " + + "--no-daemonize --verbose --onetime --test --server #{master}") end end + end + end - step "verify that the application shows up in help" do - agents.each do |agent| - on(agent, PuppetCommand.new(:help, "--libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\"")) do - assert_match(/^\s+#{app_name}\s+#{app_desc % mode}$/, result.stdout) - end + step "verify that the module files were synced down to the agent" do + agents.each do |agent| + unless test_file_exists?(agent, agent_module_app_file) then + fail_test("Expected app file not synced to agent: '#{get_test_file_path(agent, agent_module_app_file)}'") + end + unless test_file_exists?(agent, agent_module_face_file) then + fail_test("Expected face file not synced to agent: '#{get_test_file_path(agent, agent_module_face_file)}'") end end + end - step "verify that we can run the application" do - agents.each do |agent| - on(agent, PuppetCommand.new(:"#{app_name}", "--libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\"")) do - assert_match(/^#{app_output % mode}$/, result.stdout) - end + step "verify that the application shows up in help" do + agents.each do |agent| + on(agent, PuppetCommand.new(:help, "--libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\"")) do + assert_match(/^\s+#{app_name}\s+#{app_desc % "face"}$/, result.stdout) end end + end - step "clear out the libdir on the agents in preparation for the next test" do - agents.each do |agent| - on(agent, "rm -rf #{get_test_file_path(agent, agent_module_app_file)}/*") + step "verify that we can run the application" do + agents.each do |agent| + on(agent, PuppetCommand.new(:"#{app_name}", "--libdir=\"#{get_test_file_path(agent, agent_lib_dir)}\"")) do + assert_match(/^#{app_output % "face"}$/, result.stdout) end end + end + step "clear out the libdir on the agents in preparation for the next test" do + agents.each do |agent| + on(agent, "rm -rf #{get_test_file_path(agent, agent_module_app_file)}/*") + on(agent, "rm -rf #{get_test_file_path(agent, agent_module_face_file)}/*") + end end all_tests_passed = true ensure ########################################################################################## # Clean up all of the temp files created by this test. It would be nice if this logic # could be handled outside of the test itself; I envision a stanza like this one appearing # in a very large number of the tests going forward unless it is handled by the framework. ########################################################################################## if all_tests_passed then all_hosts.each do |host| on(host, "rm -rf #{@host_test_tmp_dirs[host.name]}") end end end \ No newline at end of file diff --git a/acceptance/tests/pluginsync/feature/pluginsync_should_sync_features.rb b/acceptance/tests/pluginsync/feature/pluginsync_should_sync_features.rb index e48039388..80d1c381a 100644 --- a/acceptance/tests/pluginsync/feature/pluginsync_should_sync_features.rb +++ b/acceptance/tests/pluginsync/feature/pluginsync_should_sync_features.rb @@ -1,287 +1,286 @@ test_name "the pluginsync functionality should sync feature definitions" # # This test is intended to ensure that pluginsync syncs feature definitions to the agents. It checks the feature # twice; once to make sure that it gets loaded successfully during the run in which it was synced, and once to # ensure that it still gets loaded successfully during the subsequent run (in which it should not be synced because # the files haven't changed.) # ############################################################################### # BEGIN UTILITY METHODS - ideally this stuff would live somewhere besides in # the actual test. ############################################################################### # Create a file on the host. # Parameters: # [host] the host to create the file on # [file_path] the path to the file to be created # [file_content] a string containing the contents to be written to the file # [options] a hash containing additional behavior options. Currently supported: # * :mkdirs (default false) if true, attempt to create the parent directories on the remote host before writing # the file # * :owner (default 'root') the username of the user that the file should be owned by # * :group (default 'puppet') the name of the group that the file should be owned by # * :mode (default '644') the mode (file permissions) that the file should be created with def create_test_file(host, file_rel_path, file_content, options) # set default options options[:mkdirs] ||= false options[:owner] ||= "root" options[:group] ||= "puppet" options[:mode] ||= "755" file_path = get_test_file_path(host, file_rel_path) mkdirs(host, File.dirname(file_path)) if (options[:mkdirs] == true) create_remote_file(host, file_path, file_content) # # NOTE: we need these chown/chmod calls because the acceptance framework connects to the nodes as "root", but # puppet 'master' runs as user 'puppet'. Therefore, in order for puppet master to be able to read any files # that we've created, we have to carefully set their permissions # chown(host, options[:owner], options[:group], file_path) chmod(host, options[:mode], file_path) end # Given a relative path, returns an absolute path for a test file. Basically, this just prepends the # a unique temp dir path (specific to the current test execution) to your relative path. def get_test_file_path(host, file_rel_path) File.join(@host_test_tmp_dirs[host.name], file_rel_path) end # Check for the existence of a temp file for the current test; basically, this just calls file_exists?(), # but prepends the path to the current test's temp dir onto the file_rel_path parameter. This allows # tests to be written using only a relative path to specify file locations, while still taking advantage # of automatic temp file cleanup at test completion. def test_file_exists?(host, file_rel_path) - # I don't think we can easily use "test -f" here, because our "execute" commands are all built around reading - # stdout as opposed to reading process exit codes - result = host.execute("ruby -e \"print File.exists?('#{get_test_file_path(host, file_rel_path)}')\"") - # get a boolean return value - result == "true" + host.execute("test -f \"#{get_test_file_path(host, file_rel_path)}\"", + :acceptable_exit_codes => [0, 1]) do |result| + return result.exit_code == 0 + end end def tmpdir(host, basename) host_tmpdir = host.tmpdir(basename) # we need to make sure that the puppet user can traverse this directory... chmod(host, "755", host_tmpdir) host_tmpdir end def mkdirs(host, dir_path) on(host, "mkdir -p #{dir_path}") end def chown(host, owner, group, path) on(host, "chown #{owner}:#{group} #{path}") end def chmod(host, mode, path) on(host, "chmod #{mode} #{path}") end # pluck this out of the test case environment; not sure if there is a better way cur_test_file = @path cur_test_file_shortname = File.basename(cur_test_file, File.extname(cur_test_file)) # we need one list of all of the hosts, to assist in managing temp dirs. It's possible # that the master is also an agent, so this will consolidate them into a unique set all_hosts = Set[master, *agents] # now we can create a hash of temp dirs--one per host, and unique to this test--without worrying about # doing it twice on any individual host @host_test_tmp_dirs = Hash[all_hosts.map do |host| [host.name, tmpdir(host, cur_test_file_shortname)] end ] # a silly variable for keeping track of whether or not all of the tests passed... all_tests_passed = false ############################################################################### # END UTILITY METHODS ############################################################################### ############################################################################### # BEGIN TEST LOGIC ############################################################################### # create some vars to point to the directories that we're going to point the master/agents at test_identifier = "pluginsync_should_sync_features" master_module_dir = "master_modules" agent_lib_dir = "agent_lib" module_name = "superbogus" # here we create a custom type, which basically doesn't do anything except for test the value of # our custom feature and write the result to a file agent_module_type_file = "#{agent_lib_dir}/puppet/type/#{module_name}.rb" master_module_type_file = "#{master_module_dir}/#{module_name}/lib/puppet/type/#{module_name}.rb" master_module_type_content = < "Hi. I'm setting the testfeature property of #{module_name} here in site.pp", } HERE # for convenience we build up a list of all of the files we are expecting to deploy on the master all_master_files = [ [master_module_feature_file, 'feature'], [master_module_type_file, 'type'], [master_manifest_file, 'manifest'] ] # for convenience we build up a list of all of the files we are expecting to deploy on the agents all_agent_files = [ [agent_module_feature_file, 'feature'], [agent_module_type_file, 'type'] ] # the command line args we'll pass to the agent each time we call it agent_args = "--trace --libdir=\"%s\" --pluginsync --no-daemonize --verbose " + "--onetime --test --server #{master}" # legal exit codes whenever we run the agent # we need to allow exit code 2, which means "changes were applied" on the agent agent_exit_codes = [0, 2] # this begin block is here for handling temp file cleanup via an "ensure" block at the very end of the # test. begin # copy all the files to the master step "write our simple module out to the master" do create_test_file(master, master_module_type_file, master_module_type_content, :mkdirs => true) create_test_file(master, master_module_feature_file, master_module_feature_content, :mkdirs => true) create_test_file(master, master_manifest_file, master_manifest_content, :mkdirs => true) end step "verify that the module and manifest files exist on the master" do all_master_files.each do |file_path, desc| unless test_file_exists?(master, file_path) then fail_test("Failed to create #{desc} file '#{get_test_file_path(master, file_path)}' on master") end end end step "start the master" do with_master_running_on(master, "--manifest=\"#{get_test_file_path(master, master_manifest_file)}\" " + "--modulepath=\"#{get_test_file_path(master, master_module_dir)}\" " + "--autosign true --pluginsync") do # the module files shouldn't exist on the agent yet because they haven't been synced step "verify that the module files don't exist on the agent path" do agents.each do |agent| all_agent_files.each do |file_path, desc| if test_file_exists?(agent, file_path) then fail_test("#{desc} file already exists on agent: '#{get_test_file_path(agent, file_path)}'") end end end end step "run the agent and verify that it loaded the feature" do agents.each do |agent| run_agent_on(agent, agent_args % get_test_file_path(agent, agent_lib_dir), :acceptable_exit_codes => agent_exit_codes) do assert_match(/The value of the #{module_name} feature is: true/, result.stdout, "Expected agent stdout to include confirmation that the feature was 'true'") end end end step "verify that the module files were synced down to the agent" do agents.each do |agent| all_agent_files.each do |file_path, desc| unless test_file_exists?(agent, file_path) then fail_test("Expected #{desc} file not synced to agent: '#{get_test_file_path(agent, file_path)}'") end end end end step "run the agent again" do agents.each do |agent| run_agent_on(agent, agent_args % get_test_file_path(agent, agent_lib_dir), :acceptable_exit_codes => agent_exit_codes) do assert_match(/The value of the #{module_name} feature is: true/, result.stdout, "Expected agent stdout to include confirmation that the feature was 'true'") end end end #TODO: was thinking about putting in a check for the timestamps on the files (maybe add a method for that to # the framework?) to verify that they didn't get re-synced, but it seems like more trouble than it's worth # at the moment. #step "verify that the module files were not re-synced" do # fail_test("NOT YET IMPLEMENTED: verify that the module files were not re-synced") #end end all_tests_passed = true end ensure ########################################################################################## # Clean up all of the temp files created by this test. It would be nice if this logic # could be handled outside of the test itself; I envision a stanza like this one appearing # in a very large number of the tests going forward unless it is handled by the framework. ########################################################################################## if all_tests_passed then all_hosts.each do |host| on(host, "rm -rf #{@host_test_tmp_dirs[host.name]}") end end end diff --git a/ext/upload_facts.rb b/ext/upload_facts.rb index 3a9903378..786c921d8 100755 --- a/ext/upload_facts.rb +++ b/ext/upload_facts.rb @@ -1,120 +1,119 @@ #!/usr/bin/env ruby require 'net/https' require 'openssl' require 'openssl/x509' require 'optparse' require 'pathname' require 'yaml' require 'puppet' require 'puppet/network/http_pool' class Puppet::Application::UploadFacts < Puppet::Application - should_parse_config run_mode :master option('--debug', '-d') option('--verbose', '-v') option('--logdest DEST', '-l DEST') do |arg| Puppet::Util::Log.newdestination(arg) options[:setdest] = true end option('--minutes MINUTES', '-m MINUTES') do |minutes| options[:time_limit] = 60 * minutes.to_i end def help print <] [-l|--logdest syslog||console] = Description This command will read YAML facts from the puppet master's YAML directory, and save them to the configured facts_terminus. It is intended to be used with the facts_terminus set to inventory_service, in order to ensure facts which have been cached locally due to a temporary failure are still uploaded to the inventory service. = Usage Notes upload_facts is intended to be run from cron, with the facts_terminus set to inventory_service. The +--minutes+ argument should be set to the length of time between upload_facts runs. This will ensure that only new YAML files are uploaded. = Options Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'server' is a valid configuration parameter, so you can specify '--server ' 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 agent with '--genconfig'. minutes:: Limit the upload only to YAML files which have been added within the last n minutes. HELP exit end def setup # Handle the logging settings. if options[:debug] or options[:verbose] if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end Puppet::Util::Log.newdestination(:console) unless options[:setdest] end exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? end def main dir = Pathname.new(Puppet[:yamldir]) + 'facts' cutoff = options[:time_limit] ? Time.now - options[:time_limit] : Time.at(0) files = dir.children.select do |file| file.extname == '.yaml' && file.mtime > cutoff end failed = false terminus = Puppet::Node::Facts.indirection.terminus files.each do |file| facts = YAML.load_file(file) request = Puppet::Indirector::Request.new(:facts, :save, facts) # The terminus warns for us if we fail. if terminus.save(request) Puppet.info "Uploaded facts for #{facts.name} to inventory service" else failed = true end end exit !failed end end Puppet::Application::UploadFacts.new.run diff --git a/lib/puppet.rb b/lib/puppet.rb index 4d6f8f112..995f06be7 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -1,129 +1,140 @@ # Try to load rubygems. Hey rubygems, I hate you. begin require 'rubygems' rescue LoadError end # see the bottom of the file for further inclusions require 'singleton' require 'facter' require 'puppet/error' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/util/settings' require 'puppet/util/feature' require 'puppet/util/suidmanager' require 'puppet/util/run_mode' #------------------------------------------------------------ # the top-level module # # all this really does is dictate how the whole system behaves, through # preferences for things like debugging # # it's also a place to find top-level commands like 'debug' module Puppet PUPPETVERSION = '2.7.12' def Puppet.version PUPPETVERSION end class << self include Puppet::Util attr_reader :features attr_writer :name end # the hash that determines how our system behaves @@settings = Puppet::Util::Settings.new # The services running in this process. @services ||= [] require 'puppet/util/logging' extend Puppet::Util::Logging # The feature collection @features = Puppet::Util::Feature.new('puppet/feature') # Load the base features. require 'puppet/feature/base' # Store a new default value. - def self.setdefaults(section, hash) - @@settings.setdefaults(section, hash) + def self.define_settings(section, hash) + @@settings.define_settings(section, hash) end # configuration parameter access and stuff def self.[](param) if param == :debug return Puppet::Util::Log.level == :debug else return @@settings[param] end end # configuration parameter access and stuff def self.[]=(param,value) @@settings[param] = value end def self.clear @@settings.clear end def self.debug=(value) if value Puppet::Util::Log.level=(:debug) else Puppet::Util::Log.level=(:notice) end end def self.settings @@settings end - def self.run_mode - $puppet_application_mode || Puppet::Util::RunMode[:user] - end - def self.application_name - $puppet_application_name ||= "apply" + def self.run_mode + # This sucks (the existence of this method); there are a lot of places in our code that branch based the value of + # "run mode", but there used to be some really confusing code paths that made it almost impossible to determine + # when during the lifecycle of a puppet application run the value would be set properly. A lot of the lifecycle + # stuff has been cleaned up now, but it still seems frightening that we rely so heavily on this value. + # + # I'd like to see about getting rid of the concept of "run_mode" entirely, but there are just too many places in + # the code that call this method at the moment... so I've settled for isolating it inside of the Settings class + # (rather than using a global variable, as we did previously...). Would be good to revisit this at some point. + # + # --cprice 2012-03-16 + Puppet::Util::RunMode[@@settings.run_mode] end # Load all of the configuration parameters. require 'puppet/defaults' def self.genmanifest if Puppet[:genmanifest] puts Puppet.settings.to_manifest exit(0) end end # Parse the config file for this process. def self.parse_config Puppet.settings.parse end # Create a new type. Just proxy to the Type class. The mirroring query # code was deprecated in 2008, but this is still in heavy use. I suppose # this can count as a soft deprecation for the next dev. --daniel 2011-04-12 def self.newtype(name, options = {}, &block) Puppet::Type.newtype(name, options, &block) end end +# This feels weird to me; I would really like for us to get to a state where there is never a "require" statement +# anywhere besides the very top of a file. That would not be possible at the moment without a great deal of +# effort, but I think we should strive for it and revisit this at some point. --cprice 2012-03-16 + require 'puppet/type' require 'puppet/parser' require 'puppet/resource' require 'puppet/network' require 'puppet/ssl' require 'puppet/module' require 'puppet/util/storage' require 'puppet/status' require 'puppet/file_bucket/file' diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb index 51a309454..ad92aa11f 100644 --- a/lib/puppet/application.rb +++ b/lib/puppet/application.rb @@ -1,420 +1,415 @@ require 'optparse' require 'puppet/util/plugins' +require 'puppet/error' # This class handles all the aspects of a Puppet application/executable # * setting up options # * setting up logs # * choosing what to run # * representing execution status # # === Usage # An application is a subclass of Puppet::Application. # # For legacy compatibility, # Puppet::Application[:example].run # is equivalent to # Puppet::Application::Example.new.run # # # class Puppet::Application::Example << Puppet::Application # # def preinit # # perform some pre initialization # @all = false # end # # # run_command is called to actually run the specified command # def run_command # send Puppet::Util::CommandLine.new.args.shift # end # # # option uses metaprogramming to create a method # # and also tells the option parser how to invoke that method # option("--arg ARGUMENT") do |v| # @args << v # end # # option("--debug", "-d") do |v| # @debug = v # end # # option("--all", "-a:) do |v| # @all = v # end # # def handle_unknown(opt,arg) # # last chance to manage an option # ... # # let's say to the framework we finally handle this option # true # end # # def read # # read action # end # # def write # # writeaction # end # # end # # === Preinit # The preinit block is the first code to be called in your application, before option parsing, # setup or command execution. # # === Options # Puppet::Application uses +OptionParser+ to manage the application options. # Options are defined with the +option+ method to which are passed various # arguments, including the long option, the short option, a description... # Refer to +OptionParser+ documentation for the exact format. # * If the option method is given a block, this one will be called whenever # the option is encountered in the command-line argument. # * If the option method has no block, a default functionnality will be used, that # stores the argument (or true/false if the option doesn't require an argument) in # the global (to the application) options array. # * If a given option was not defined by a the +option+ method, but it exists as a Puppet settings: # * if +unknown+ was used with a block, it will be called with the option name and argument # * if +unknown+ wasn't used, then the option/argument is handed to Puppet.settings.handlearg for # a default behavior # # --help is managed directly by the Puppet::Application class, but can be overriden. # # === Setup # Applications can use the setup block to perform any initialization. # The defaul +setup+ behaviour is to: read Puppet configuration and manage log level and destination # # === What and how to run # If the +dispatch+ block is defined it is called. This block should return the name of the registered command # to be run. # If it doesn't exist, it defaults to execute the +main+ command if defined. # # === Execution state # The class attributes/methods of Puppet::Application serve as a global place to set and query the execution # status of the application: stopping, restarting, etc. The setting of the application status does not directly # aftect its running status; it's assumed that the various components within the application will consult these # settings appropriately and affect their own processing accordingly. Control operations (signal handlers and # the like) should set the status appropriately to indicate to the overall system that it's the process of # stopping or restarting (or just running as usual). # # So, if something in your application needs to stop the process, for some reason, you might consider: # # def stop_me! # # indicate that we're stopping # Puppet::Application.stop! # # ...do stuff... # end # # And, if you have some component that involves a long-running process, you might want to consider: # # def my_long_process(giant_list_to_munge) # giant_list_to_munge.collect do |member| # # bail if we're stopping # return if Puppet::Application.stop_requested? # process_member(member) # end # end module Puppet class Application require 'puppet/util' include Puppet::Util DOCPATTERN = ::File.expand_path(::File.dirname(__FILE__) + "/util/command_line/*" ) + class << self include Puppet::Util attr_accessor :run_status def clear! self.run_status = nil end def stop! self.run_status = :stop_requested end def restart! self.run_status = :restart_requested end # Indicates that Puppet::Application.restart! has been invoked and components should # do what is necessary to facilitate a restart. def restart_requested? :restart_requested == run_status end # Indicates that Puppet::Application.stop! has been invoked and components should do what is necessary # for a clean stop. def stop_requested? :stop_requested == run_status end # Indicates that one of stop! or start! was invoked on Puppet::Application, and some kind of process # shutdown/short-circuit may be necessary. def interrupted? [:restart_requested, :stop_requested].include? run_status end # Indicates that Puppet::Application believes that it's in usual running run_mode (no stop/restart request # currently active). def clear? run_status.nil? end # Only executes the given block if the run status of Puppet::Application is clear (no restarts, stops, # etc. requested). # Upon block execution, checks the run status again; if a restart has been requested during the block's # execution, then controlled_run will send a new HUP signal to the current process. # Thus, long-running background processes can potentially finish their work before a restart. def controlled_run(&block) return unless clear? result = block.call Process.kill(:HUP, $PID) if restart_requested? result end + SHOULD_PARSE_CONFIG_DEPRECATION_MSG = "is no longer supported; config file parsing " + + "is now controlled by the puppet engine, rather than by individual applications. This " + + "method will be removed in a future version of puppet." + def should_parse_config - @parse_config = true + Puppet.deprecation_warning("should_parse_config " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) end def should_not_parse_config - @parse_config = false + Puppet.deprecation_warning("should_not_parse_config " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) end def should_parse_config? - @parse_config = true if ! defined?(@parse_config) - @parse_config + Puppet.deprecation_warning("should_parse_config? " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG) + true end # used to declare code that handle an option def option(*options, &block) long = options.find { |opt| opt =~ /^--/ }.gsub(/^--(?:\[no-\])?([^ =]+).*$/, '\1' ).gsub('-','_') fname = symbolize("handle_#{long}") if (block_given?) define_method(fname, &block) else define_method(fname) do |value| self.options["#{long}".to_sym] = value end end self.option_parser_commands << [options, fname] end def banner(banner = nil) @banner ||= banner end def option_parser_commands @option_parser_commands ||= ( superclass.respond_to?(:option_parser_commands) ? superclass.option_parser_commands.dup : [] ) @option_parser_commands end def find(name) klass = name.to_s.capitalize begin require ::File.join('puppet', 'application', name.to_s.downcase) rescue LoadError => e puts "Unable to find application '#{name}'. #{e}" Kernel::exit(1) end self.const_get(klass) end def [](name) find(name).new end + # + # I think that it would be nice to look into changing this into two methods (getter/setter); however, + # it sounds like this is a desirable feature of our ruby DSL. --cprice 2012-03-06 + # + # Sets or gets the run_mode name. Sets the run_mode name if a mode_name is # passed. Otherwise, gets the run_mode or a default run_mode # def run_mode( mode_name = nil) return @run_mode if @run_mode and not mode_name require 'puppet/util/run_mode' @run_mode = Puppet::Util::RunMode[ mode_name || :user ] end end attr_reader :options, :command_line # Every app responds to --version option("--version", "-V") do |arg| puts "#{Puppet.version}" exit end # Every app responds to --help option("--help", "-h") do |v| puts help exit end - def should_parse_config? - self.class.should_parse_config? + def app_defaults() + { + :name => name, + :run_mode => self.class.run_mode.name, + :confdir => self.class.run_mode.conf_dir, + :vardir => self.class.run_mode.var_dir, + :rundir => self.class.run_mode.run_dir, + :logdir => self.class.run_mode.log_dir, + } + end + + def initialize_app_defaults() + Puppet.settings.initialize_app_defaults(app_defaults) end # override to execute code before running anything else def preinit end def initialize(command_line = nil) require 'puppet/util/command_line' @command_line = command_line || Puppet::Util::CommandLine.new - set_run_mode self.class.run_mode @options = {} - require 'puppet' - require 'puppet/util/instrumentation' - Puppet::Util::Instrumentation.init - end - - # WARNING: This is a totally scary, frightening, and nasty internal API. We - # strongly advise that you do not use this, and if you insist, we will - # politely allow you to keep both pieces of your broken code. - # - # We plan to provide a supported, long-term API to deliver this in a way - # that you can use. Please make sure that you let us know if you do require - # this, and this message is still present in the code. --daniel 2011-02-03 - def set_run_mode(mode) - @run_mode = mode - $puppet_application_mode = @run_mode - $puppet_application_name = name - - if Puppet.respond_to? :settings - # This is to reduce the amount of confusion in rspec - # because it might have loaded defaults.rb before the globals were set - # and thus have the wrong defaults for the current application - Puppet.settings.set_value(:confdir, Puppet.run_mode.conf_dir, :mutable_defaults) - Puppet.settings.set_value(:vardir, Puppet.run_mode.var_dir, :mutable_defaults) - Puppet.settings.set_value(:name, Puppet.application_name.to_s, :mutable_defaults) - Puppet.settings.set_value(:logdir, Puppet.run_mode.logopts, :mutable_defaults) - Puppet.settings.set_value(:rundir, Puppet.run_mode.run_dir, :mutable_defaults) - Puppet.settings.set_value(:run_mode, Puppet.run_mode.name.to_s, :mutable_defaults) - end end # This is the main application entry point def run - exit_on_fail("initialize") { hook('preinit') { preinit } } - exit_on_fail("parse options") { hook('parse_options') { parse_options } } - exit_on_fail("parse configuration file") { Puppet.settings.parse } if should_parse_config? - exit_on_fail("prepare for execution") { hook('setup') { setup } } + + # I don't really like the names of these lifecycle phases. It would be nice to change them to some more meaningful + # names, and make deprecated aliases. Also, Daniel suggests that we can probably get rid of this "plugin_hook" + # pattern, but we need to check with PE and the community first. --cprice 2012-03-16 + # + + exit_on_fail("get application-specific default settings") do + plugin_hook('initialize_app_defaults') { initialize_app_defaults } + end + + require 'puppet' + require 'puppet/util/instrumentation' + Puppet::Util::Instrumentation.init + + exit_on_fail("initialize") { plugin_hook('preinit') { preinit } } + exit_on_fail("parse application options") { plugin_hook('parse_options') { parse_options } } + exit_on_fail("prepare for execution") { plugin_hook('setup') { setup } } exit_on_fail("configure routes from #{Puppet[:route_file]}") { configure_indirector_routes } - exit_on_fail("run") { hook('run_command') { run_command } } + exit_on_fail("run") { plugin_hook('run_command') { run_command } } end def main raise NotImplementedError, "No valid command or main" end def run_command main end def setup setup_logs end def setup_logs if options[:debug] or options[:verbose] Puppet::Util::Log.newdestination(:console) if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end end Puppet::Util::Log.setup_default unless options[:setdest] end def configure_indirector_routes route_file = Puppet[:route_file] if ::File.exists?(route_file) routes = YAML.load_file(route_file) application_routes = routes[name.to_s] Puppet::Indirector.configure_routes(application_routes) if application_routes end end def parse_options # Create an option parser option_parser = OptionParser.new(self.class.banner) + # He're we're building up all of the options that the application may need to handle. The main + # puppet settings defined in "defaults.rb" have already been parsed once (in command_line.rb) by + # the time we get here; however, our app may wish to handle some of them specially, so we need to + # make the parser aware of them again. We might be able to make this a bit more efficient by + # re-using the parser object that gets built up in command_line.rb. --cprice 2012-03-16 + # Add all global options to it. Puppet.settings.optparse_addargs([]).each do |option| option_parser.on(*option) do |arg| handlearg(option[0], arg) end end # Add options that are local to this application, which were # created using the "option()" metaprogramming method. If there # are any conflicts, this application's options will be favored. self.class.option_parser_commands.each do |options, fname| option_parser.on(*options) do |value| # Call the method that "option()" created. self.send(fname, value) end end # Scan command line. We just hand any exceptions to our upper levels, # rather than printing help and exiting, so that we can meaningfully # respond with context-sensitive help if we want to. --daniel 2011-04-12 option_parser.parse!(self.command_line.args) end - def handlearg(opt, arg) - # rewrite --[no-]option to --no-option if that's what was given - if opt =~ /\[no-\]/ and !arg - opt = opt.gsub(/\[no-\]/,'no-') - end - # otherwise remove the [no-] prefix to not confuse everybody - opt = opt.gsub(/\[no-\]/, '') - unless respond_to?(:handle_unknown) and send(:handle_unknown, opt, arg) - # Puppet.settings.handlearg doesn't handle direct true/false :-) - if arg.is_a?(FalseClass) - arg = "false" - elsif arg.is_a?(TrueClass) - arg = "true" - end - Puppet.settings.handlearg(opt, arg) - end + + + def handlearg(opt, val) + opt, val = Puppet::Util::CommandLine.clean_opt(opt, val) + send(:handle_unknown, opt, val) if respond_to?(:handle_unknown) end # this is used for testing def self.exit(code) exit(code) end def name self.class.to_s.sub(/.*::/,"").downcase.to_sym end def help "No help available for puppet #{name}" end - private - def exit_on_fail(message, code = 1) - yield - rescue ArgumentError, RuntimeError, NotImplementedError => detail - Puppet.log_exception(detail, "Could not #{message}: #{detail}") - exit(code) - end - def hook(step,&block) + def plugin_hook(step,&block) Puppet::Plugins.send("before_application_#{step}",:application_object => self) x = yield Puppet::Plugins.send("after_application_#{step}",:application_object => self, :return_value => x) x end + private :plugin_hook end end diff --git a/lib/puppet/application/agent.rb b/lib/puppet/application/agent.rb index 3a9eb4a43..90c608be0 100644 --- a/lib/puppet/application/agent.rb +++ b/lib/puppet/application/agent.rb @@ -1,461 +1,460 @@ require 'puppet/application' class Puppet::Application::Agent < Puppet::Application - should_parse_config run_mode :agent attr_accessor :args, :agent, :daemon, :host def preinit # Do an initial trap, so that cancels don't get a stack trace. Signal.trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end { :waitforcert => nil, :detailed_exitcodes => false, :verbose => false, :debug => false, :centrallogs => false, :setdest => false, :enable => false, :disable => false, :client => true, :fqdn => nil, :serve => [], :digest => :MD5, :graph => true, :fingerprint => false, }.each do |opt,val| options[opt] = val end @args = {} require 'puppet/daemon' @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup end option("--centrallogging") option("--disable") option("--enable") option("--debug","-d") option("--fqdn FQDN","-f") option("--test","-t") option("--verbose","-v") option("--fingerprint") option("--digest DIGEST") option("--no-client") do |arg| options[:client] = false end option("--detailed-exitcodes") do |arg| options[:detailed_exitcodes] = true end option("--logdest DEST", "-l DEST") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail Puppet.log_exception(detail) end end option("--waitforcert WAITFORCERT", "-w") do |arg| options[:waitforcert] = arg.to_i end option("--port PORT","-p") do |arg| @args[:Port] = arg end def help <<-HELP puppet-agent(8) -- The puppet agent daemon ======== SYNOPSIS -------- Retrieves the client configuration from the puppet master and applies it to the local host. This service may be run as a daemon, run periodically using cron (or something similar), or run interactively for testing purposes. USAGE ----- puppet agent [--certname ] [-D|--daemonize|--no-daemonize] [-d|--debug] [--detailed-exitcodes] [--digest ] [--disable] [--enable] [--fingerprint] [-h|--help] [-l|--logdest syslog||console] [--no-client] [--noop] [-o|--onetime] [-t|--test] [-v|--verbose] [-V|--version] [-w|--waitforcert ] DESCRIPTION ----------- This is the main puppet client. Its job is to retrieve the local machine's configuration from a remote server and apply it. In order to successfully communicate with the remote server, the client must have a certificate signed by a certificate authority that the server trusts; the recommended method for this, at the moment, is to run a certificate authority as part of the puppet server (which is the default). The client will connect and request a signed certificate, and will continue connecting until it receives one. Once the client has a signed certificate, it will retrieve its configuration and apply it. USAGE NOTES ----------- 'puppet agent' does its best to find a compromise between interactive use and daemon use. Run with no arguments and no configuration, it will go into the background, attempt to get a signed certificate, and retrieve and apply its configuration every 30 minutes. Some flags are meant specifically for interactive use -- in particular, 'test', 'tags' or 'fingerprint' are useful. 'test' enables verbose logging, causes the daemon to stay in the foreground, exits if the server's configuration is invalid (this happens if, for instance, you've left a syntax error on the server), and exits after running the configuration once (rather than hanging around as a long-running process). 'tags' allows you to specify what portions of a configuration you want to apply. Puppet elements are tagged with all of the class or definition names that contain them, and you can use the 'tags' flag to specify one of these names, causing only configuration elements contained within that class or definition to be applied. This is very useful when you are testing new configurations -- for instance, if you are just starting to manage 'ntpd', you would put all of the new elements into an 'ntpd' class, and call puppet with '--tags ntpd', which would only apply that small portion of the configuration during your testing, rather than applying the whole thing. 'fingerprint' is a one-time flag. In this mode 'puppet agent' will run once and display on the console (and in the log) the current certificate (or certificate request) fingerprint. Providing the '--digest' option allows to use a different digest algorithm to generate the fingerprint. The main use is to verify that before signing a certificate request on the master, the certificate request the master received is the same as the one the client sent (to prevent against man-in-the-middle attacks when signing certificates). OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'server' is a valid configuration parameter, so you can specify '--server ' 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 agent with '--genconfig'. * --certname: Set the certname (unique ID) of the client. The master reads this unique identifying string, which is usually set to the node's fully-qualified domain name, to determine which configurations the node will receive. Use this option to debug setup problems or implement unusual node identification schemes. * --daemonize: Send the process into the background. This is the default. * --no-daemonize: Do not send the process into the background. * --debug: Enable full debugging. * --detailed-exitcodes: Provide transaction information via exit codes. If this is enabled, an exit code of '2' means there were changes, an exit code of '4' means there were failures during the transaction, and an exit code of '6' means there were both changes and failures. * --digest: Change the certificate fingerprinting digest algorithm. The default is MD5. Valid values depends on the version of OpenSSL installed, but should always at least contain MD5, MD2, SHA1 and SHA256. * --disable: Disable working on the local system. This puts a lock file in place, causing 'puppet agent' not to work on the system until the lock file is removed. This is useful if you are testing a configuration and do not want the central configuration to override the local state until everything is tested and committed. 'puppet agent' uses the same lock file while it is running, so no more than one 'puppet agent' process is working at a time. 'puppet agent' exits after executing this. * --enable: Enable working on the local system. This removes any lock file, causing 'puppet agent' to start managing the local system again (although it will continue to use its normal scheduling, so it might not start for another half hour). 'puppet agent' exits after executing this. * --fingerprint: Display the current certificate or certificate signing request fingerprint and then exit. Use the '--digest' option to change the digest algorithm used. * --help: Print this help message * --logdest: Where to send messages. Choose between syslog, the console, and a log file. Defaults to sending messages to syslog, or the console if debugging or verbosity is enabled. * --no-client: Do not create a config client. This will cause the daemon to run without ever checking for its configuration automatically, and only makes sense when puppet agent is being run with listen = true in puppet.conf or was started with the `--listen` option. * --noop: Use 'noop' mode where the daemon runs in a no-op or dry-run mode. This is useful for seeing what changes Puppet will make without actually executing the changes. * --onetime: Run the configuration once. Runs a single (normally daemonized) Puppet run. Useful for interactively running puppet agent when used in conjunction with the --no-daemonize option. * --test: Enable the most common options used for testing. These are 'onetime', 'verbose', 'ignorecache', 'no-daemonize', 'no-usecacheonfailure', 'detailed-exit-codes', 'no-splay', and 'show_diff'. * --verbose: Turn on verbose reporting. * --version: Print the puppet version number and exit. * --waitforcert: This option only matters for daemons that do not yet have certificates and it is enabled by default, with a value of 120 (seconds). This causes 'puppet agent' to connect to the server every 2 minutes and ask it to sign a certificate request. This is useful for the initial setup of a puppet client. You can turn off waiting for certificates by specifying a time of 0. EXAMPLE ------- $ puppet agent --server puppet.domain.com DIAGNOSTICS ----------- Puppet agent accepts the following signals: * SIGHUP: Restart the puppet agent daemon. * SIGINT and SIGTERM: Shut down the puppet agent daemon. * SIGUSR1: Immediately retrieve and apply configurations from the puppet master. AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def run_command return fingerprint if options[:fingerprint] return onetime if Puppet[:onetime] main end def fingerprint unless cert = host.certificate || host.certificate_request $stderr.puts "Fingerprint asked but no certificate nor certificate request have yet been issued" exit(1) return end unless fingerprint = cert.fingerprint(options[:digest]) raise ArgumentError, "Could not get fingerprint for digest '#{options[:digest]}'" end puts fingerprint end def onetime unless options[:client] $stderr.puts "onetime is specified but there is no client" exit(43) return end @daemon.set_signal_traps begin @agent.should_fork = false exitstatus = @agent.run rescue => detail Puppet.log_exception(detail) end @daemon.stop(:exit => false) if not exitstatus exit(1) elsif options[:detailed_exitcodes] then exit(exitstatus) else exit(0) end end def main Puppet.notice "Starting Puppet client version #{Puppet.version}" @daemon.start end # Enable all of the most common test options. def setup_test Puppet.settings.handlearg("--ignorecache") Puppet.settings.handlearg("--no-usecacheonfailure") Puppet.settings.handlearg("--no-splay") Puppet.settings.handlearg("--show_diff") Puppet.settings.handlearg("--no-daemonize") options[:verbose] = true Puppet[:onetime] = true options[:detailed_exitcodes] = true end def enable_disable_client(agent) if options[:enable] agent.enable elsif options[:disable] agent.disable end exit(0) end def setup_listen unless FileTest.exists?(Puppet[:rest_authconfig]) Puppet.err "Will not start without authorization file #{Puppet[:rest_authconfig]}" exit(14) end require 'puppet/network/server' # No REST handlers yet. server = Puppet::Network::Server.new(:port => Puppet[:puppetport]) @daemon.server = server end def setup_host @host = Puppet::SSL::Host.new waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : Puppet[:waitforcert]) cert = @host.wait_for_cert(waitforcert) unless options[:fingerprint] end def setup_agent # We need tomake the client either way, we just don't start it # if --no-client is set. require 'puppet/agent' require 'puppet/configurer' @agent = Puppet::Agent.new(Puppet::Configurer) enable_disable_client(@agent) if options[:enable] or options[:disable] @daemon.agent = agent if options[:client] # It'd be nice to daemonize later, but we have to daemonize before the # waitforcert happens. @daemon.daemonize if Puppet[:daemonize] setup_host @objects = [] # This has to go after the certs are dealt with. if Puppet[:listen] unless Puppet[:onetime] setup_listen else Puppet.notice "Ignoring --listen on onetime run" end end end def setup setup_test if options[:test] setup_logs exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? args[:Server] = Puppet[:server] if options[:fqdn] args[:FQDN] = options[:fqdn] Puppet[:certname] = options[:fqdn] end if options[:centrallogs] logdest = args[:Server] logdest += ":" + args[:Port] if args.include?(:Port) Puppet::Util::Log.newdestination(logdest) end Puppet.settings.use :main, :agent, :ssl # Always ignoreimport for agent. It really shouldn't even try to import, # but this is just a temporary band-aid. Puppet[:ignoreimport] = true # We need to specify a ca location for all of the SSL-related i # indirected classes to work; in fingerprint mode we just need # access to the local files and we don't need a ca. Puppet::SSL::Host.ca_location = options[:fingerprint] ? :none : :remote Puppet::Transaction::Report.indirection.terminus_class = :rest # we want the last report to be persisted locally Puppet::Transaction::Report.indirection.cache_class = :yaml # Override the default; puppetd needs this, usually. # You can still override this on the command-line with, e.g., :compiler. Puppet[:catalog_terminus] = :rest # Override the default. Puppet[:facts_terminus] = :facter Puppet::Resource::Catalog.indirection.cache_class = :yaml unless options[:fingerprint] setup_agent else setup_host end end end diff --git a/lib/puppet/application/apply.rb b/lib/puppet/application/apply.rb index 739452d49..fe37f3ab8 100644 --- a/lib/puppet/application/apply.rb +++ b/lib/puppet/application/apply.rb @@ -1,279 +1,277 @@ require 'puppet/application' class Puppet::Application::Apply < Puppet::Application - should_parse_config - option("--debug","-d") option("--execute EXECUTE","-e") do |arg| options[:code] = arg end option("--loadclasses","-L") option("--verbose","-v") option("--use-nodes") option("--detailed-exitcodes") option("--apply catalog", "-a catalog") do |arg| Puppet.deprecation_warning <'. EOM options[:catalog] = arg end option("--catalog catalog", "-c catalog") do |arg| options[:catalog] = arg end option("--logdest LOGDEST", "-l") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:logset] = true rescue => detail $stderr.puts detail.to_s end end option("--parseonly") do puts "--parseonly has been removed. Please use 'puppet parser validate '" exit 1 end def help <<-HELP puppet-apply(8) -- Apply Puppet manifests locally ======== SYNOPSIS -------- Applies a standalone Puppet manifest to the local system. USAGE ----- puppet apply [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] [-e|--execute] [--detailed-exitcodes] [-l|--logdest ] [--noop] [--apply ] [--catalog ] DESCRIPTION ----------- This is the standalone puppet execution tool; use it to apply individual manifests. When provided with a modulepath, via command line or config file, puppet apply can effectively mimic the catalog that would be served by puppet master with access to the same modules, although there are some subtle differences. When combined with scheduling and an automated system for pushing manifests, this can be used to implement a serverless Puppet site. Most users should use 'puppet agent' and 'puppet master' for site-wide manifests. OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'tags' is a valid configuration parameter, so you can specify '--tags ,' as an argument. See the configuration file documentation at http://docs.puppetlabs.com/references/stable/configuration.html for the full list of acceptable parameters. A commented list of all configuration options can also be generated by running puppet with '--genconfig'. * --debug: Enable full debugging. * --detailed-exitcodes: Provide transaction information via exit codes. If this is enabled, an exit code of '2' means there were changes, an exit code of '4' means there were failures during the transaction, and an exit code of '6' means there were both changes and failures. * --help: Print this help message * --loadclasses: Load any stored classes. 'puppet agent' caches configured classes (usually at /etc/puppet/classes.txt), and setting this option causes all of those classes to be set in your puppet manifest. * --logdest: Where to send messages. Choose between syslog, the console, and a log file. Defaults to sending messages to the console. * --noop: Use 'noop' mode where Puppet runs in a no-op or dry-run mode. This is useful for seeing what changes Puppet will make without actually executing the changes. * --execute: Execute a specific piece of Puppet code * --verbose: Print extra information. * --apply: Apply a JSON catalog (such as one generated with 'puppet master --compile'). You can either specify a JSON file or pipe in JSON from standard input. Deprecated, please use --catalog instead. * --catalog: Apply a JSON catalog (such as one generated with 'puppet master --compile'). You can either specify a JSON file or pipe in JSON from standard input. EXAMPLE ------- $ puppet apply -l /tmp/manifest.log manifest.pp $ puppet apply --modulepath=/root/dev/modules -e "include ntpd::server" $ puppet apply --catalog catalog.json AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def run_command if options[:catalog] apply else main end end def apply if options[:catalog] == "-" text = $stdin.read else text = ::File.read(options[:catalog]) end catalog = read_catalog(text) apply_catalog(catalog) end def main # Set our code or file to use. if options[:code] or command_line.args.length == 0 Puppet[:code] = options[:code] || STDIN.read else manifest = command_line.args.shift raise "Could not find file #{manifest}" unless ::File.exist?(manifest) Puppet.warning("Only one file can be applied per run. Skipping #{command_line.args.join(', ')}") if command_line.args.size > 0 Puppet[:manifest] = manifest end unless Puppet[:node_name_fact].empty? # Collect our facts. unless facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value]) raise "Could not find facts for #{Puppet[:node_name_value]}" end Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]] facts.name = Puppet[:node_name_value] end # Find our Node unless node = Puppet::Node.indirection.find(Puppet[:node_name_value]) raise "Could not find node #{Puppet[:node_name_value]}" end # Merge in the facts. node.merge(facts.values) if facts # Allow users to load the classes that puppet agent creates. if options[:loadclasses] file = Puppet[:classfile] if FileTest.exists?(file) unless FileTest.readable?(file) $stderr.puts "#{file} is not readable" exit(63) end node.classes = ::File.read(file).split(/[\s\n]+/) end end begin # Compile our catalog starttime = Time.now catalog = Puppet::Resource::Catalog.indirection.find(node.name, :use_node => node) # Translate it to a RAL catalog catalog = catalog.to_ral catalog.finalize catalog.retrieval_duration = Time.now - starttime exit_status = apply_catalog(catalog) if not exit_status exit(1) elsif options[:detailed_exitcodes] then exit(exit_status) else exit(0) end rescue => detail Puppet.log_exception(detail) exit(1) end end def setup exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? Puppet::Util::Log.newdestination(:console) unless options[:logset] client = nil server = nil Signal.trap(:INT) do $stderr.puts "Exiting" exit(1) end # we want the last report to be persisted locally Puppet::Transaction::Report.indirection.cache_class = :yaml if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end # Make pluginsync local Puppet[:pluginsource] = 'puppet:///plugins' end private def read_catalog(text) begin catalog = Puppet::Resource::Catalog.convert_from(Puppet::Resource::Catalog.default_format,text) catalog = Puppet::Resource::Catalog.pson_create(catalog) unless catalog.is_a?(Puppet::Resource::Catalog) rescue => detail raise Puppet::Error, "Could not deserialize catalog from pson: #{detail}" end catalog.to_ral end def apply_catalog(catalog) require 'puppet/configurer' configurer = Puppet::Configurer.new configurer.run(:catalog => catalog) end end diff --git a/lib/puppet/application/cert.rb b/lib/puppet/application/cert.rb index a21033440..0cd749fdc 100644 --- a/lib/puppet/application/cert.rb +++ b/lib/puppet/application/cert.rb @@ -1,239 +1,238 @@ require 'puppet/application' class Puppet::Application::Cert < Puppet::Application - should_parse_config run_mode :master attr_accessor :all, :ca, :digest, :signed def subcommand @subcommand end def subcommand=(name) # Handle the nasty, legacy mapping of "clean" to "destroy". sub = name.to_sym @subcommand = (sub == :clean ? :destroy : sub) end option("--clean", "-c") do |arg| self.subcommand = "destroy" end option("--all", "-a") do |arg| @all = true end option("--digest DIGEST") do |arg| @digest = arg end option("--signed", "-s") do |arg| @signed = true end option("--debug", "-d") do |arg| Puppet::Util::Log.level = :debug end require 'puppet/ssl/certificate_authority/interface' Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS.reject {|m| m == :destroy }.each do |method| option("--#{method.to_s.gsub('_','-')}", "-#{method.to_s[0,1]}") do |arg| self.subcommand = method end end option("--[no-]allow-dns-alt-names") do |value| options[:allow_dns_alt_names] = value end option("--verbose", "-v") do |arg| Puppet::Util::Log.level = :info end def help <<-HELP puppet-cert(8) -- Manage certificates and requests ======== SYNOPSIS -------- Standalone certificate authority. Capable of generating certificates, but mostly used for signing certificate requests from puppet clients. USAGE ----- puppet cert [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] [--digest ] [] DESCRIPTION ----------- Because the puppet master service defaults to not signing client certificate requests, this script is available for signing outstanding requests. It can be used to list outstanding requests and then either sign them individually or sign all of them. ACTIONS ------- Every action except 'list' and 'generate' requires a hostname to act on, unless the '--all' option is set. * clean: Revoke a host's certificate (if applicable) and remove all files related to that host from puppet cert's storage. This is useful when rebuilding hosts, since new certificate signing requests will only be honored if puppet cert does not have a copy of a signed certificate for that host. If '--all' is specified then all host certificates, both signed and unsigned, will be removed. * fingerprint: Print the DIGEST (defaults to md5) fingerprint of a host's certificate. * generate: Generate a certificate for a named client. A certificate/keypair will be generated for each client named on the command line. * list: List outstanding certificate requests. If '--all' is specified, signed certificates are also listed, prefixed by '+', and revoked or invalid certificates are prefixed by '-' (the verification outcome is printed in parenthesis). * print: Print the full-text version of a host's certificate. * revoke: Revoke the certificate of a client. The certificate can be specified either by its serial number (given as a decimal number or a hexadecimal number prefixed by '0x') or by its hostname. The certificate is revoked by adding it to the Certificate Revocation List given by the 'cacrl' configuration option. Note that the puppet master needs to be restarted after revoking certificates. * sign: Sign an outstanding certificate request. * verify: Verify the named certificate against the local CA certificate. OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'ssldir' is a valid configuration parameter, 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 cert with '--genconfig'. * --all: Operate on all items. Currently only makes sense with the 'sign', 'clean', 'list', and 'fingerprint' actions. * --digest: Set the digest for fingerprinting (defaults to md5). Valid values depends on your openssl and openssl ruby extension version, but should contain at least md5, sha1, md2, sha256. * --debug: Enable full debugging. * --help: Print this help message * --verbose: Enable verbosity. * --version: Print the puppet version number and exit. EXAMPLE ------- $ puppet cert list culain.madstop.com $ puppet cert sign culain.madstop.com AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def main if @all hosts = :all elsif @signed hosts = :signed else hosts = command_line.args.collect { |h| h.downcase } end begin @ca.apply(:revoke, options.merge(:to => hosts)) if subcommand == :destroy @ca.apply(subcommand, options.merge(:to => hosts, :digest => @digest)) rescue => detail Puppet.log_exception(detail) exit(24) end end def setup require 'puppet/ssl/certificate_authority' exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? Puppet::Util::Log.newdestination :console if [:generate, :destroy].include? subcommand Puppet::SSL::Host.ca_location = :local else Puppet::SSL::Host.ca_location = :only end # If we are generating, and the option came from the CLI, it gets added to # the data. This will do the right thing for non-local certificates, in # that the command line but *NOT* the config file option will apply. if subcommand == :generate if Puppet.settings.setting(:dns_alt_names).setbycli options[:dns_alt_names] = Puppet[:dns_alt_names] end end begin @ca = Puppet::SSL::CertificateAuthority.new rescue => detail Puppet.log_exception(detail) exit(23) end end def parse_options # handle the bareword subcommand pattern. result = super unless self.subcommand then if sub = self.command_line.args.shift then self.subcommand = sub else puts help exit end end result end end diff --git a/lib/puppet/application/certificate.rb b/lib/puppet/application/certificate.rb index de5b2c499..b080b1300 100644 --- a/lib/puppet/application/certificate.rb +++ b/lib/puppet/application/certificate.rb @@ -1,13 +1,18 @@ require 'puppet/application/indirection_base' class Puppet::Application::Certificate < Puppet::Application::IndirectionBase def setup location = Puppet::SSL::Host.ca_location if location == :local && !Puppet::SSL::CertificateAuthority.ca? - self.class.run_mode("master") - self.set_run_mode self.class.run_mode + # I'd prefer if this could be dealt with differently; ideally, run_mode should be set as + # part of a class definition, and should not be modifiable beyond that. This is one of + # the cases where that isn't currently possible. + # Perhaps a separate issue, but related, is that the run_mode probably shouldn't be a + # normal 'setting' like the rest of the config stuff; I left some notes in settings.rb + # and defaults.rb discussing this. --cprice 2012-03-22 + Puppet.settings.set_value(:run_mode, :master, :application_defaults) end super end end diff --git a/lib/puppet/application/describe.rb b/lib/puppet/application/describe.rb index 8ce20b652..50a6153de 100644 --- a/lib/puppet/application/describe.rb +++ b/lib/puppet/application/describe.rb @@ -1,256 +1,254 @@ require 'puppet/application' class Formatter def initialize(width) @width = width end def wrap(txt, opts) return "" unless txt && !txt.empty? work = (opts[:scrub] ? scrub(txt) : txt) indent = (opts[:indent] ? opts[:indent] : 0) textLen = @width - indent patt = Regexp.new("^(.{0,#{textLen}})[ \n]") prefix = " " * indent res = [] while work.length > textLen if work =~ patt res << $1 work.slice!(0, $MATCH.length) else res << work.slice!(0, textLen) end end res << work if work.length.nonzero? prefix + res.join("\n#{prefix}") end def header(txt, sep = "-") "\n#{txt}\n" + sep * txt.size end private def scrub(text) # For text with no carriage returns, there's nothing to do. return text if text !~ /\n/ indent = nil # If we can match an indentation, then just remove that same level of # indent from every line. if text =~ /^(\s+)/ indent = $1 return text.gsub(/^#{indent}/,'') else return text end end end class TypeDoc def initialize @format = Formatter.new(76) @types = {} Puppet::Type.loadall Puppet::Type.eachtype { |type| next if type.name == :component @types[type.name] = type } end def list_types puts "These are the types known to puppet:\n" @types.keys.sort { |a, b| a.to_s <=> b.to_s }.each do |name| type = @types[name] s = type.doc.gsub(/\s+/, " ") n = s.index(".") if n.nil? s = ".. no documentation .." elsif n > 45 s = s[0, 45] + " ..." else s = s[0, n] end printf "%-15s - %s\n", name, s end end def format_type(name, opts) name = name.to_sym unless @types.has_key?(name) puts "Unknown type #{name}" return end type = @types[name] puts @format.header(name.to_s, "=") puts @format.wrap(type.doc, :indent => 0, :scrub => true) + "\n\n" puts @format.header("Parameters") if opts[:parameters] format_attrs(type, [:property, :param]) else list_attrs(type, [:property, :param]) end if opts[:meta] puts @format.header("Meta Parameters") if opts[:parameters] format_attrs(type, [:meta]) else list_attrs(type, [:meta]) end end if type.providers.size > 0 puts @format.header("Providers") if opts[:providers] format_providers(type) else list_providers(type) end end end # List details about attributes def format_attrs(type, attrs) docs = {} type.allattrs.each do |name| kind = type.attrtype(name) docs[name] = type.attrclass(name).doc if attrs.include?(kind) && name != :provider end docs.sort { |a,b| a[0].to_s <=> b[0].to_s }.each { |name, doc| print "\n- **#{name}**" if type.key_attributes.include?(name) and name != :name puts " (*namevar*)" else puts "" end puts @format.wrap(doc, :indent => 4, :scrub => true) } end # List the names of attributes def list_attrs(type, attrs) params = [] type.allattrs.each do |name| kind = type.attrtype(name) params << name.to_s if attrs.include?(kind) && name != :provider end puts @format.wrap(params.sort.join(", "), :indent => 4) end def format_providers(type) type.providers.sort { |a,b| a.to_s <=> b.to_s }.each { |prov| puts "\n- **#{prov}**" puts @format.wrap(type.provider(prov).doc, :indent => 4, :scrub => true) } end def list_providers(type) list = type.providers.sort { |a,b| a.to_s <=> b.to_s }.join(", ") puts @format.wrap(list, :indent => 4) end end class Puppet::Application::Describe < Puppet::Application banner "puppet describe [options] [type]" - should_not_parse_config - option("--short", "-s", "Only list parameters without detail") do |arg| options[:parameters] = false end option("--providers","-p") option("--list", "-l") option("--meta","-m") def help <<-HELP puppet-describe(8) -- Display help about resource types ======== SYNOPSIS -------- Prints help about Puppet resource types, providers, and metaparameters. USAGE ----- puppet describe [-h|--help] [-s|--short] [-p|--providers] [-l|--list] [-m|--meta] OPTIONS ------- * --help: Print this help text * --providers: Describe providers in detail for each type * --list: List all types * --meta: List all metaparameters * --short: List only parameters without detail EXAMPLE ------- $ puppet describe --list $ puppet describe file --providers $ puppet describe user -s -m AUTHOR ------ David Lutterkort COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def preinit options[:parameters] = true end def main doc = TypeDoc.new if options[:list] doc.list_types else options[:types].each { |name| doc.format_type(name, options) } end end def setup options[:types] = command_line.args.dup handle_help(nil) unless options[:list] || options[:types].size > 0 $stderr.puts "Warning: ignoring types when listing all types" if options[:list] && options[:types].size > 0 end end diff --git a/lib/puppet/application/device.rb b/lib/puppet/application/device.rb index 0e0829cb2..f49f5320a 100644 --- a/lib/puppet/application/device.rb +++ b/lib/puppet/application/device.rb @@ -1,240 +1,239 @@ require 'puppet/application' require 'puppet/util/network_device' class Puppet::Application::Device < Puppet::Application - should_parse_config run_mode :agent attr_accessor :args, :agent, :host def preinit # Do an initial trap, so that cancels don't get a stack trace. Signal.trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end { :waitforcert => nil, :detailed_exitcodes => false, :verbose => false, :debug => false, :centrallogs => false, :setdest => false, }.each do |opt,val| options[opt] = val end @args = {} end option("--centrallogging") option("--debug","-d") option("--verbose","-v") option("--detailed-exitcodes") do |arg| options[:detailed_exitcodes] = true end option("--logdest DEST", "-l DEST") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail Puppet.log_exception(detail) end end option("--waitforcert WAITFORCERT", "-w") do |arg| options[:waitforcert] = arg.to_i end option("--port PORT","-p") do |arg| @args[:Port] = arg end def help <<-HELP puppet-device(8) -- Manage remote network devices ======== SYNOPSIS -------- Retrieves all configurations from the puppet master and apply them to the remote devices configured in /etc/puppet/device.conf. Currently must be run out periodically, using cron or something similar. USAGE ----- puppet device [-d|--debug] [--detailed-exitcodes] [-V|--version] [-h|--help] [-l|--logdest syslog||console] [-v|--verbose] [-w|--waitforcert ] DESCRIPTION ----------- Once the client has a signed certificate for a given remote device, it will retrieve its configuration and apply it. USAGE NOTES ----------- One need a /etc/puppet/device.conf file with the following content: [remote.device.fqdn] type url where: * type: the current device type (the only value at this time is cisco) * url: an url allowing to connect to the device Supported url must conforms to: scheme://user:password@hostname/?query with: * scheme: either ssh or telnet * user: username, can be omitted depending on the switch/router configuration * password: the connection password * query: this is device specific. Cisco devices supports an enable parameter whose value would be the enable password. OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'server' is a valid configuration parameter, so you can specify '--server ' as an argument. * --debug: Enable full debugging. * --detailed-exitcodes: Provide transaction information via exit codes. If this is enabled, an exit code of '2' means there were changes, an exit code of '4' means there were failures during the transaction, and an exit code of '6' means there were both changes and failures. * --help: Print this help message * --logdest: Where to send messages. Choose between syslog, the console, and a log file. Defaults to sending messages to syslog, or the console if debugging or verbosity is enabled. * --verbose: Turn on verbose reporting. * --waitforcert: This option only matters for daemons that do not yet have certificates and it is enabled by default, with a value of 120 (seconds). This causes +puppet agent+ to connect to the server every 2 minutes and ask it to sign a certificate request. This is useful for the initial setup of a puppet client. You can turn off waiting for certificates by specifying a time of 0. EXAMPLE ------- $ puppet device --server puppet.domain.com AUTHOR ------ Brice Figureau COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def main vardir = Puppet[:vardir] confdir = Puppet[:confdir] certname = Puppet[:certname] # find device list require 'puppet/util/network_device/config' devices = Puppet::Util::NetworkDevice::Config.devices if devices.empty? Puppet.err "No device found in #{Puppet[:deviceconfig]}" exit(1) end devices.each_value do |device| begin Puppet.info "starting applying configuration to #{device.name} at #{device.url}" # override local $vardir and $certname Puppet.settings.set_value(:confdir, ::File.join(Puppet[:devicedir], device.name), :cli) Puppet.settings.set_value(:vardir, ::File.join(Puppet[:devicedir], device.name), :cli) Puppet.settings.set_value(:certname, device.name, :cli) # this will reload and recompute default settings and create the devices sub vardir, or we hope so :-) Puppet.settings.use :main, :agent, :ssl # this init the device singleton, so that the facts terminus # and the various network_device provider can use it Puppet::Util::NetworkDevice.init(device) # ask for a ssl cert if needed, but at least # setup the ssl system for this device. setup_host require 'puppet/configurer' configurer = Puppet::Configurer.new report = configurer.run(:network_device => true) rescue => detail Puppet.log_exception(detail) ensure Puppet.settings.set_value(:vardir, vardir, :cli) Puppet.settings.set_value(:confdir, confdir, :cli) Puppet.settings.set_value(:certname, certname, :cli) Puppet::SSL::Host.reset end end end def setup_host @host = Puppet::SSL::Host.new waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : Puppet[:waitforcert]) cert = @host.wait_for_cert(waitforcert) end def setup setup_logs args[:Server] = Puppet[:server] if options[:centrallogs] logdest = args[:Server] logdest += ":" + args[:Port] if args.include?(:Port) Puppet::Util::Log.newdestination(logdest) end Puppet.settings.use :main, :agent, :device, :ssl # Always ignoreimport for agent. It really shouldn't even try to import, # but this is just a temporary band-aid. Puppet[:ignoreimport] = true # We need to specify a ca location for all of the SSL-related i # indirected classes to work; in fingerprint mode we just need # access to the local files and we don't need a ca. Puppet::SSL::Host.ca_location = :remote Puppet::Transaction::Report.indirection.terminus_class = :rest # Override the default; puppetd needs this, usually. # You can still override this on the command-line with, e.g., :compiler. Puppet[:catalog_terminus] = :rest Puppet[:facts_terminus] = :network_device Puppet::Resource::Catalog.indirection.cache_class = :yaml end end diff --git a/lib/puppet/application/doc.rb b/lib/puppet/application/doc.rb index 39e744e06..1a80823dd 100644 --- a/lib/puppet/application/doc.rb +++ b/lib/puppet/application/doc.rb @@ -1,273 +1,272 @@ require 'puppet/application' class Puppet::Application::Doc < Puppet::Application - should_not_parse_config run_mode :master attr_accessor :unknown_args, :manifest def preinit {:references => [], :mode => :text, :format => :to_markdown }.each do |name,value| options[name] = value end @unknown_args = [] @manifest = false end option("--all","-a") option("--outputdir OUTPUTDIR","-o") option("--verbose","-v") option("--debug","-d") option("--charset CHARSET") option("--format FORMAT", "-f") do |arg| method = "to_#{arg}" require 'puppet/util/reference' if Puppet::Util::Reference.method_defined?(method) options[:format] = method else raise "Invalid output format #{arg}" end end option("--mode MODE", "-m") do |arg| require 'puppet/util/reference' if Puppet::Util::Reference.modes.include?(arg) or arg.intern==:rdoc options[:mode] = arg.intern else raise "Invalid output mode #{arg}" end end option("--list", "-l") do |arg| require 'puppet/util/reference' puts Puppet::Util::Reference.references.collect { |r| Puppet::Util::Reference.reference(r).doc }.join("\n") exit(0) end option("--reference REFERENCE", "-r") do |arg| options[:references] << arg.intern end def help <<-HELP puppet-doc(8) -- Generate Puppet documentation and references ======== SYNOPSIS -------- Generates a reference for all Puppet types. Largely meant for internal Puppet Labs use. USAGE ----- puppet doc [-a|--all] [-h|--help] [-o|--outputdir ] [-m|--mode text|pdf|rdoc] [-r|--reference ] [--charset ] [] DESCRIPTION ----------- If mode is not 'rdoc', then this command generates a Markdown document describing all installed Puppet types or all allowable arguments to puppet executables. It is largely meant for internal use and is used to generate the reference document available on the Puppet Labs web site. In 'rdoc' mode, this command generates an html RDoc hierarchy describing the manifests that are in 'manifestdir' and 'modulepath' configuration directives. The generated documentation directory is doc by default but can be changed with the 'outputdir' option. If the command is run with the name of a manifest file as an argument, puppet doc will output a single manifest's documentation on stdout. OPTIONS ------- * --all: Output the docs for all of the reference types. In 'rdoc' mode, this also outputs documentation for all resources. * --help: Print this help message * --outputdir: Used only in 'rdoc' mode. The directory to which the rdoc output should be written. * --mode: Determine the output mode. Valid modes are 'text', 'pdf' and 'rdoc'. The 'pdf' mode creates PDF formatted files in the /tmp directory. The default mode is 'text'. In 'rdoc' mode you must provide 'manifests-path' * --reference: Build a particular reference. Get a list of references by running 'puppet doc --list'. * --charset: Used only in 'rdoc' mode. It sets the charset used in the html files produced. * --manifestdir: Used only in 'rdoc' mode. The directory to scan for stand-alone manifests. If not supplied, puppet doc will use the manifestdir from puppet.conf. * --modulepath: Used only in 'rdoc' mode. The directory or directories to scan for modules. If not supplied, puppet doc will use the modulepath from puppet.conf. * --environment: Used only in 'rdoc' mode. The configuration environment from which to read the modulepath and manifestdir settings, when reading said settings from puppet.conf. Due to a known bug, this option is not currently effective. EXAMPLE ------- $ puppet doc -r type > /tmp/type_reference.markdown or $ puppet doc --outputdir /tmp/rdoc --mode rdoc /path/to/manifests or $ puppet doc /etc/puppet/manifests/site.pp or $ puppet doc -m pdf -r configuration AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def handle_unknown( opt, arg ) @unknown_args << {:opt => opt, :arg => arg } true end def run_command return[:rdoc].include?(options[:mode]) ? send(options[:mode]) : other end def rdoc exit_code = 0 files = [] unless @manifest env = Puppet::Node::Environment.new files += env.modulepath files << ::File.dirname(env[:manifest]) end files += command_line.args Puppet.info "scanning: #{files.inspect}" Puppet.settings[:document_all] = options[:all] || false begin require 'puppet/util/rdoc' if @manifest Puppet::Util::RDoc.manifestdoc(files) else options[:outputdir] = "doc" unless options[:outputdir] Puppet::Util::RDoc.rdoc(options[:outputdir], files, options[:charset]) end rescue => detail Puppet.log_exception(detail, "Could not generate documentation: #{detail}") exit_code = 1 end exit exit_code end def other text = "" with_contents = options[:references].length <= 1 exit_code = 0 require 'puppet/util/reference' options[:references].sort { |a,b| a.to_s <=> b.to_s }.each do |name| raise "Could not find reference #{name}" unless section = Puppet::Util::Reference.reference(name) begin # Add the per-section text, but with no ToC text += section.send(options[:format], with_contents) rescue => detail Puppet.log_exception(detail, "Could not generate reference #{name}: #{detail}") exit_code = 1 next end end text += Puppet::Util::Reference.footer unless with_contents # We've only got one reference if options[:mode] == :pdf Puppet::Util::Reference.pdf(text) else puts text end exit exit_code end def setup # sole manifest documentation if command_line.args.size > 0 options[:mode] = :rdoc @manifest = true end if options[:mode] == :rdoc setup_rdoc else setup_reference end end def setup_reference if options[:all] # Don't add dynamic references to the "all" list. require 'puppet/util/reference' options[:references] = Puppet::Util::Reference.references.reject do |ref| Puppet::Util::Reference.reference(ref).dynamic? end end options[:references] << :type if options[:references].empty? end def setup_rdoc(dummy_argument=:work_arround_for_ruby_GC_bug) # consume the unknown options # and feed them as settings if @unknown_args.size > 0 @unknown_args.each do |option| # force absolute path for modulepath when passed on commandline if option[:opt]=="--modulepath" or option[:opt] == "--manifestdir" option[:arg] = option[:arg].split(::File::PATH_SEPARATOR).collect { |p| ::File.expand_path(p) }.join(::File::PATH_SEPARATOR) end Puppet.settings.handlearg(option[:opt], option[:arg]) end end # Now parse the config Puppet.parse_config # Handle the logging settings. if options[:debug] or options[:verbose] if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end Puppet::Util::Log.newdestination(:console) end end end diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index 6df52b922..a80ca5437 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -1,263 +1,265 @@ require 'puppet/application' require 'puppet/face' require 'optparse' require 'pp' class Puppet::Application::FaceBase < Puppet::Application - should_parse_config run_mode :agent option("--debug", "-d") do |arg| Puppet::Util::Log.level = :debug end option("--verbose", "-v") do Puppet::Util::Log.level = :info end option("--render-as FORMAT") do |format| self.render_as = format.to_sym end + # This seems like a bad thing; it seems like--in an ideal world--a given app/face should have one constant run mode. + # This isn't currently possible because of issues relating to the certificate authority, but I've left some notes + # about "run_mode" in settings.rb and defaults.rb, and if we are able to tighten up the behavior / implementation + # of that setting, we might want to revisit this. --cprice 2012-03-16 option("--mode RUNMODE", "-r") do |arg| raise "Invalid run mode #{arg}; supported modes are user, agent, master" unless %w{user agent master}.include?(arg) self.class.run_mode(arg.to_sym) - set_run_mode self.class.run_mode end attr_accessor :face, :action, :type, :arguments, :render_as def render_as=(format) if format == :json then @render_as = Puppet::Network::FormatHandler.format(:pson) else @render_as = Puppet::Network::FormatHandler.format(format) end @render_as or raise ArgumentError, "I don't know how to render '#{format}'" end def render(result, args_and_options) hook = action.when_rendering(render_as.name) if hook # when defining when_rendering on your action you can optionally # include arguments and options if hook.arity > 1 result = hook.call(result, *args_and_options) else result = hook.call(result) end end render_as.render(result) end def preinit super Signal.trap(:INT) do $stderr.puts "Cancelling Face" exit(0) end end def parse_options # We need to parse enough of the command line out early, to identify what # the action is, so that we can obtain the full set of options to parse. # REVISIT: These should be configurable versions, through a global # '--version' option, but we don't implement that yet... --daniel 2011-03-29 @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym @face = Puppet::Face[@type, :current] # Now, walk the command line and identify the action. We skip over # arguments based on introspecting the action and all, and find the first # non-option word to use as the action. action_name = nil index = -1 until action_name or (index += 1) >= command_line.args.length do item = command_line.args[index] if item =~ /^-/ then option = @face.options.find do |name| item =~ /^-+#{name.to_s.gsub(/[-_]/, '[-_]')}(?:[ =].*)?$/ end if option then option = @face.get_option(option) # If we have an inline argument, just carry on. We don't need to # care about optional vs mandatory in that case because we do a real # parse later, and that will totally take care of raising the error # when we get there. --daniel 2011-04-04 if option.takes_argument? and !item.index('=') then index += 1 unless (option.optional_argument? and command_line.args[index + 1] =~ /^-/) end elsif option = find_global_settings_argument(item) then unless Puppet.settings.boolean? option.name then # As far as I can tell, we treat non-bool options as always having # a mandatory argument. --daniel 2011-04-05 index += 1 # ...so skip the argument. end elsif option = find_application_argument(item) then index += 1 if (option[:argument] and not option[:optional]) else raise OptionParser::InvalidOption.new(item.sub(/=.*$/, '')) end else # Stash away the requested action name for later, and try to fetch the # action object it represents; if this is an invalid action name that # will be nil, and handled later. action_name = item.to_sym @action = Puppet::Face.find_action(@face.name, action_name) @face = @action.face if @action end end if @action.nil? if @action = @face.get_default_action() then @is_default_action = true else # REVISIT: ...and this horror thanks to our log setup, which doesn't # initialize destinations until the setup method, which we will never # reach. We could also just print here, but that is actually a little # uglier and nastier in the long term, in which we should do log setup # earlier if at all possible. --daniel 2011-05-31 Puppet::Util::Log.newdestination(:console) face = @face.name action = action_name.nil? ? 'default' : "'#{action_name}'" msg = "'#{face}' has no #{action} action. See `puppet help #{face}`." Puppet.err(msg) exit false end end # Now we can interact with the default option code to build behaviour # around the full set of options we now know we support. @action.options.each do |option| option = @action.get_option(option) # make it the object. self.class.option(*option.optparse) # ...and make the CLI parse it. end # ...and invoke our parent to parse all the command line options. super end def find_global_settings_argument(item) Puppet.settings.each do |name, object| object.optparse_args.each do |arg| next unless arg =~ /^-/ # sadly, we have to emulate some of optparse here... pattern = /^#{arg.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ pattern.match item and return object end end return nil # nothing found. end def find_application_argument(item) self.class.option_parser_commands.each do |options, function| options.each do |option| next unless option =~ /^-/ pattern = /^#{option.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ next unless pattern.match(item) return { :argument => option =~ /[ =]/, :optional => option =~ /[ =]\[/ } end end return nil # not found end def setup Puppet::Util::Log.newdestination :console @arguments = command_line.args # Note: because of our definition of where the action is set, we end up # with it *always* being the first word of the remaining set of command # line arguments. So, strip that off when we construct the arguments to # pass down to the face action. --daniel 2011-04-04 # Of course, now that we have default actions, we should leave the # "action" name on if we didn't actually consume it when we found our # action. @arguments.delete_at(0) unless @is_default_action # We copy all of the app options to the end of the call; This allows each # action to read in the options. This replaces the older model where we # would invoke the action with options set as global state in the # interface object. --daniel 2011-03-28 @arguments << options # If we don't have a rendering format, set one early. self.render_as ||= (@action.render_as || :console) end def main status = false # Call the method associated with the provided action (e.g., 'find'). unless @action puts Puppet::Face[:help, :current].help(@face.name) raise "#{face} does not respond to action #{arguments.first}" end # We need to do arity checking here because this is generic code # calling generic methods – that have argument defaulting. We need to # make sure we don't accidentally pass the options as the first # argument to a method that takes one argument. eg: # # puppet facts find # => options => {} # @arguments => [{}] # => @face.send :bar, {} # # def face.bar(argument, options = {}) # => bar({}, {}) # oops! we thought the options were the # # positional argument!! # # We could also fix this by making it mandatory to pass the options on # every call, but that would make the Ruby API much more annoying to # work with; having the defaulting is a much nicer convention to have. # # We could also pass the arguments implicitly, by having a magic # 'options' method that was visible in the scope of the action, which # returned the right stuff. # # That sounds attractive, but adds complications to all sorts of # things, especially when you think about how to pass options when you # are writing Ruby code that calls multiple faces. Especially if # faces are involved in that. ;) # # --daniel 2011-04-27 if (arity = @action.positional_arg_count) > 0 unless (count = arguments.length) == arity then s = arity == 2 ? '' : 's' raise ArgumentError, "puppet #{@face.name} #{@action.name} takes #{arity-1} argument#{s}, but you gave #{count-1}" end end result = @face.send(@action.name, *arguments) puts render(result, arguments) unless result.nil? status = true # We need an easy way for the action to set a specific exit code, so we # rescue SystemExit here; This allows each action to set the desired exit # code by simply calling Kernel::exit. eg: # # exit(2) # # --kelsey 2012-02-14 rescue SystemExit => detail status = detail.status rescue Exception => detail Puppet.log_exception(detail) Puppet.err "Try 'puppet help #{@face.name} #{@action.name}' for usage" ensure exit status end end diff --git a/lib/puppet/application/filebucket.rb b/lib/puppet/application/filebucket.rb index 472e62c85..c58d8a23f 100644 --- a/lib/puppet/application/filebucket.rb +++ b/lib/puppet/application/filebucket.rb @@ -1,190 +1,188 @@ require 'puppet/application' class Puppet::Application::Filebucket < Puppet::Application - should_not_parse_config - option("--bucket BUCKET","-b") option("--debug","-d") option("--local","-l") option("--remote","-r") option("--verbose","-v") attr :args def help <<-HELP puppet-filebucket(8) -- Store and retrieve files in a filebucket ======== SYNOPSIS -------- A stand-alone Puppet filebucket client. USAGE ----- puppet filebucket [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] [-l|--local] [-r|--remote] [-s|--server ] [-b|--bucket ] ... Puppet filebucket can operate in three modes, with only one mode per call: backup: Send one or more files to the specified file bucket. Each sent file is printed with its resulting md5 sum. get: Return the text associated with an md5 sum. The text is printed to stdout, and only one file can be retrieved at a time. restore: Given a file path and an md5 sum, store the content associated with the sum into the specified file path. You can specify an entirely new path to this argument; you are not restricted to restoring the content to its original location. DESCRIPTION ----------- This is a stand-alone filebucket client for sending files to a local or central filebucket. Note that 'filebucket' defaults to using a network-based filebucket available on the server named 'puppet'. To use this, you'll have to be running as a user with valid Puppet certificates. Alternatively, you can use your local file bucket by specifying '--local'. OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'ssldir' is a valid configuration parameter, 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. * --help: Print this help message * --local: Use the local filebucket. This will use the default configuration information. * --remote: Use a remote filebucket. This will use the default configuration information. * --server: The server to send the file to, instead of locally. * --verbose: Print extra information. * --version: Print version information. EXAMPLE ------- $ puppet filebucket backup /etc/passwd /etc/passwd: 429b225650b912a2ee067b0a4cf1e949 $ puppet filebucket restore /tmp/passwd 429b225650b912a2ee067b0a4cf1e949 AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def run_command @args = command_line.args command = args.shift return send(command) if %w{get backup restore}.include? command help end def get md5 = args.shift out = @client.getfile(md5) print out end def backup raise "You must specify a file to back up" unless args.length > 0 args.each do |file| unless FileTest.exists?(file) $stderr.puts "#{file}: no such file" next end unless FileTest.readable?(file) $stderr.puts "#{file}: cannot read file" next end md5 = @client.backup(file) puts "#{file}: #{md5}" end end def restore file = args.shift md5 = args.shift @client.restore(file, md5) end def setup Puppet::Log.newdestination(:console) @client = nil @server = nil Signal.trap(:INT) do $stderr.puts "Cancelling" exit(1) end if options[:debug] Puppet::Log.level = :debug elsif options[:verbose] Puppet::Log.level = :info end # Now parse the config Puppet.parse_config exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? require 'puppet/file_bucket/dipper' begin if options[:local] or options[:bucket] path = options[:bucket] || Puppet[:bucketdir] @client = Puppet::FileBucket::Dipper.new(:Path => path) else @client = Puppet::FileBucket::Dipper.new(:Server => Puppet[:server]) end rescue => detail Puppet.log_exception(detail) exit(1) end end end diff --git a/lib/puppet/application/inspect.rb b/lib/puppet/application/inspect.rb index a0f61e296..80d10ac5d 100644 --- a/lib/puppet/application/inspect.rb +++ b/lib/puppet/application/inspect.rb @@ -1,189 +1,188 @@ require 'puppet/application' class Puppet::Application::Inspect < Puppet::Application - should_parse_config run_mode :agent option("--debug","-d") option("--verbose","-v") option("--logdest LOGDEST", "-l") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:logset] = true rescue => detail $stderr.puts detail.to_s end end def help <<-HELP puppet-inspect(8) -- Send an inspection report ======== SYNOPSIS -------- Prepares and submits an inspection report to the puppet master. USAGE ----- puppet inspect [--archive_files] [--archive_file_server] DESCRIPTION ----------- This command uses the cached catalog from the previous run of 'puppet agent' to determine which attributes of which resources have been marked as auditable with the 'audit' metaparameter. It then examines the current state of the system, writes the state of the specified resource attributes to a report, and submits the report to the puppet master. Puppet inspect does not run as a daemon, and must be run manually or from cron. OPTIONS ------- Any configuration setting which is valid in the configuration file is also a valid long argument, e.g. '--server=master.domain.com'. See the configuration file documentation at http://docs.puppetlabs.com/references/latest/configuration.html for the full list of acceptable settings. * --archive_files: During an inspect run, whether to archive files whose contents are audited to a file bucket. * --archive_file_server: During an inspect run, the file bucket server to archive files to if archive_files is set. The default value is '$server'. AUTHOR ------ Puppet Labs COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def setup exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? raise "Inspect requires reporting to be enabled. Set report=true in puppet.conf to enable reporting." unless Puppet[:report] @report = Puppet::Transaction::Report.new("inspect") Puppet::Util::Log.newdestination(@report) Puppet::Util::Log.newdestination(:console) unless options[:logset] Signal.trap(:INT) do $stderr.puts "Exiting" exit(1) end if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end Puppet::Transaction::Report.indirection.terminus_class = :rest Puppet::Resource::Catalog.indirection.terminus_class = :yaml end def preinit require 'puppet' require 'puppet/file_bucket/dipper' end def run_command benchmark(:notice, "Finished inspection") do retrieval_starttime = Time.now unless catalog = Puppet::Resource::Catalog.indirection.find(Puppet[:certname]) raise "Could not find catalog for #{Puppet[:certname]}" end @report.configuration_version = catalog.version @report.environment = Puppet[:environment] inspect_starttime = Time.now @report.add_times("config_retrieval", inspect_starttime - retrieval_starttime) if Puppet[:archive_files] dipper = Puppet::FileBucket::Dipper.new(:Server => Puppet[:archive_file_server]) end catalog.to_ral.resources.each do |ral_resource| audited_attributes = ral_resource[:audit] next unless audited_attributes status = Puppet::Resource::Status.new(ral_resource) begin audited_resource = ral_resource.to_resource rescue StandardError => detail ral_resource.log_exception(detail, "Could not inspect #{ral_resource}; skipping: #{detail}") audited_attributes.each do |name| event = ral_resource.event( :property => name, :status => "failure", :audited => true, :message => "failed to inspect #{name}" ) status.add_event(event) end else audited_attributes.each do |name| next if audited_resource[name].nil? # Skip :absent properties of :absent resources. Really, it would be nicer if the RAL returned nil for those, but it doesn't. ~JW if name == :ensure or audited_resource[:ensure] != :absent or audited_resource[name] != :absent event = ral_resource.event( :previous_value => audited_resource[name], :property => name, :status => "audit", :audited => true, :message => "inspected value is #{audited_resource[name].inspect}" ) status.add_event(event) end end end if Puppet[:archive_files] and ral_resource.type == :file and audited_attributes.include?(:content) path = ral_resource[:path] if ::File.readable?(path) begin dipper.backup(path) rescue StandardError => detail Puppet.warning detail end end end @report.add_resource_status(status) end finishtime = Time.now @report.add_times("inspect", finishtime - inspect_starttime) @report.finalize_report begin Puppet::Transaction::Report.indirection.save(@report) rescue => detail Puppet.log_exception(detail, "Could not send report: #{detail}") end end end end diff --git a/lib/puppet/application/kick.rb b/lib/puppet/application/kick.rb index 8e9fac97c..72c9bf963 100644 --- a/lib/puppet/application/kick.rb +++ b/lib/puppet/application/kick.rb @@ -1,350 +1,348 @@ require 'puppet/application' class Puppet::Application::Kick < Puppet::Application - should_not_parse_config - attr_accessor :hosts, :tags, :classes option("--all","-a") option("--foreground","-f") option("--debug","-d") option("--ping","-P") option("--test") option("--host HOST") do |arg| @hosts << arg end option("--tag TAG", "-t") do |arg| @tags << arg end option("--class CLASS", "-c") do |arg| @classes << arg end option("--no-fqdn", "-n") do |arg| options[:fqdn] = false end option("--parallel PARALLEL", "-p") do |arg| begin options[:parallel] = Integer(arg) rescue $stderr.puts "Could not convert #{arg.inspect} to an integer" exit(23) end end def help <<-HELP puppet-kick(8) -- Remotely control puppet agent ======== SYNOPSIS -------- Trigger a puppet agent run on a set of hosts. USAGE ----- puppet kick [-a|--all] [-c|--class ] [-d|--debug] [-f|--foreground] [-h|--help] [--host ] [--no-fqdn] [--ignoreschedules] [-t|--tag ] [--test] [-p|--ping] [ [...]] DESCRIPTION ----------- This script can be used to connect to a set of machines running 'puppet agent' and trigger them to run their configurations. The most common usage would be to specify a class of hosts and a set of tags, and 'puppet kick' would look up in LDAP all of the hosts matching that class, then connect to each host and trigger a run of all of the objects with the specified tags. If you are not storing your host configurations in LDAP, you can specify hosts manually. You will most likely have to run 'puppet kick' as root to get access to the SSL certificates. 'puppet kick' reads 'puppet master''s configuration file, so that it can copy things like LDAP settings. USAGE NOTES ----------- Puppet kick needs the puppet agent on the target machine to be running as a daemon, be configured to listen for incoming network connections, and have an appropriate security configuration. The specific changes required are: * Set `listen = true` in the agent's `puppet.conf` file (or `--listen` on the command line) * Configure the node's firewall to allow incoming connections on port 8139 * Insert the following stanza at the top of the node's `auth.conf` file: # Allow puppet kick access path /run method save auth any allow workstation.example.com This example would allow the machine `workstation.example.com` to trigger a Puppet run; adjust the "allow" directive to suit your site. You may also use `allow *` to allow anyone to trigger a Puppet run, but that makes it possible to interfere with your site by triggering excessive Puppet runs. See `http://docs.puppetlabs.com/guides/rest_auth_conf.html` for more details about security settings. OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'ssldir' is a valid configuration parameter, so you can specify '--ssldir ' as an argument. See the configuration file documentation at http://docs.puppetlabs.com/references/latest/configuration.html for the full list of acceptable parameters. A commented list of all configuration options can also be generated by running puppet master with '--genconfig'. * --all: Connect to all available hosts. Requires LDAP support at this point. * --class: Specify a class of machines to which to connect. This only works if you have LDAP configured, at the moment. * --debug: Enable full debugging. * --foreground: Run each configuration in the foreground; that is, when connecting to a host, do not return until the host has finished its run. The default is false. * --help: Print this help message * --host: A specific host to which to connect. This flag can be specified more than once. * --ignoreschedules: Whether the client should ignore schedules when running its configuration. This can be used to force the client to perform work it would not normally perform so soon. The default is false. * --parallel: How parallel to make the connections. Parallelization is provided by forking for each client to which to connect. The default is 1, meaning serial execution. * --tag: Specify a tag for selecting the objects to apply. Does not work with the --test option. * --test: Print the hosts you would connect to but do not actually connect. This option requires LDAP support at this point. * --ping: Do a ICMP echo against the target host. Skip hosts that don't respond to ping. EXAMPLE ------- $ sudo puppet kick -p 10 -t remotefile -t webserver host1 host2 AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def run_command @hosts += command_line.args options[:test] ? test : main end def test puts "Skipping execution in test mode" exit(0) end def main Puppet.warning "Failed to load ruby LDAP library. LDAP functionality will not be available" unless Puppet.features.ldap? require 'puppet/util/ldap/connection' todo = @hosts.dup failures = [] # Now do the actual work go = true while go # If we don't have enough children in process and we still have hosts left to # do, then do the next host. if @children.length < options[:parallel] and ! todo.empty? host = todo.shift pid = fork do run_for_host(host) end @children[pid] = host else # Else, see if we can reap a process. begin pid = Process.wait if host = @children[pid] # Remove our host from the list of children, so the parallelization # continues working. @children.delete(pid) failures << host if $CHILD_STATUS.exitstatus != 0 print "#{host} finished with exit code #{$CHILD_STATUS.exitstatus}\n" else $stderr.puts "Could not find host for PID #{pid} with status #{$CHILD_STATUS.exitstatus}" end rescue Errno::ECHILD # There are no children left, so just exit unless there are still # children left to do. next unless todo.empty? if failures.empty? puts "Finished" exit(0) else puts "Failed: #{failures.join(", ")}" exit(3) end end end end end def run_for_host(host) if options[:ping] out = %x{ping -c 1 #{host}} unless $CHILD_STATUS == 0 $stderr.print "Could not contact #{host}\n" exit($CHILD_STATUS) end end require 'puppet/run' Puppet::Run.indirection.terminus_class = :rest port = Puppet[:puppetport] url = ["https://#{host}:#{port}", "production", "run", host].join('/') print "Triggering #{host}\n" begin run_options = { :tags => @tags, :background => ! options[:foreground], :ignoreschedules => options[:ignoreschedules] } run = Puppet::Run.indirection.save(Puppet::Run.new( run_options ), url) puts "Getting status" result = run.status puts "status is #{result}" rescue => detail Puppet.log_exception(detail, "Host #{host} failed: #{detail}\n") exit(2) end case result when "success"; exit(0) when "running" $stderr.puts "Host #{host} is already running" exit(3) else $stderr.puts "Host #{host} returned unknown answer '#{result}'" exit(12) end end def initialize(*args) super @hosts = [] @classes = [] @tags = [] end def preinit [:INT, :TERM].each do |signal| Signal.trap(signal) do $stderr.puts "Cancelling" exit(1) end end options[:parallel] = 1 options[:verbose] = true options[:fqdn] = true options[:ignoreschedules] = false options[:foreground] = false end def setup raise Puppet::Error.new("Puppet kick is not supported on Microsoft Windows") if Puppet.features.microsoft_windows? if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end # Now parse the config Puppet.parse_config if Puppet[:node_terminus] == "ldap" and (options[:all] or @classes) if options[:all] @hosts = Puppet::Node.indirection.search("whatever", :fqdn => options[:fqdn]).collect { |node| node.name } puts "all: #{@hosts.join(", ")}" else @hosts = [] @classes.each do |klass| list = Puppet::Node.indirection.search("whatever", :fqdn => options[:fqdn], :class => klass).collect { |node| node.name } puts "#{klass}: #{list.join(", ")}" @hosts += list end end elsif ! @classes.empty? $stderr.puts "You must be using LDAP to specify host classes" exit(24) end @children = {} # If we get a signal, then kill all of our children and get out. [:INT, :TERM].each do |signal| Signal.trap(signal) do Puppet.notice "Caught #{signal}; shutting down" @children.each do |pid, host| Process.kill("INT", pid) end waitall exit(1) end end end end diff --git a/lib/puppet/application/master.rb b/lib/puppet/application/master.rb index 2b309de3e..6d3f21dc3 100644 --- a/lib/puppet/application/master.rb +++ b/lib/puppet/application/master.rb @@ -1,237 +1,240 @@ require 'puppet/application' class Puppet::Application::Master < Puppet::Application - should_parse_config run_mode :master option("--debug", "-d") option("--verbose", "-v") # internal option, only to be used by ext/rack/config.ru option("--rack") option("--compile host", "-c host") do |arg| options[:node] = arg end option("--logdest DEST", "-l DEST") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail Puppet.log_exception(detail) end end option("--parseonly") do puts "--parseonly has been removed. Please use 'puppet parser validate '" exit 1 end def help <<-HELP puppet-master(8) -- The puppet master daemon ======== SYNOPSIS -------- The central puppet server. Functions as a certificate authority by default. USAGE ----- puppet master [-D|--daemonize|--no-daemonize] [-d|--debug] [-h|--help] [-l|--logdest |console|syslog] [-v|--verbose] [-V|--version] [--compile ] DESCRIPTION ----------- This command starts an instance of puppet master, running as a daemon and using Ruby's built-in Webrick webserver. Puppet master can also be managed by other application servers; when this is the case, this executable is not used. OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'ssldir' is a valid configuration parameter, 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 master with '--genconfig'. * --daemonize: Send the process into the background. This is the default. * --no-daemonize: Do not send the process into the background. * --debug: Enable full debugging. * --help: Print this help message. * --logdest: Where to send messages. Choose between syslog, the console, and a log file. Defaults to sending messages to syslog, or the console if debugging or verbosity is enabled. * --verbose: Enable verbosity. * --version: Print the puppet version number and exit. * --compile: Compile a catalogue and output it in JSON from the puppet master. Uses facts contained in the $vardir/yaml/ directory to compile the catalog. EXAMPLE ------- puppet master DIAGNOSTICS ----------- When running as a standalone daemon, puppet master accepts the following signals: * SIGHUP: Restart the puppet master server. * SIGINT and SIGTERM: Shut down the puppet master server. AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end + def app_defaults() + super.merge :facts_terminus => 'yaml' + end + def preinit Signal.trap(:INT) do $stderr.puts "Cancelling startup" exit(0) end # Create this first-off, so we have ARGV require 'puppet/daemon' @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup end def run_command if options[:node] compile else main end end def compile Puppet::Util::Log.newdestination :console raise ArgumentError, "Cannot render compiled catalogs without pson support" unless Puppet.features.pson? begin unless catalog = Puppet::Resource::Catalog.indirection.find(options[:node]) raise "Could not compile catalog for #{options[:node]}" end jj catalog.to_resource rescue => detail $stderr.puts detail exit(30) end exit(0) end def main require 'etc' require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' # Make sure we've got a localhost ssl cert Puppet::SSL::Host.localhost # And now configure our server to *only* hit the CA for data, because that's # all it will have write access to. Puppet::SSL::Host.ca_location = :only if Puppet::SSL::CertificateAuthority.ca? if Puppet.features.root? begin Puppet::Util.chuser rescue => detail Puppet.log_exception(detail, "Could not change user to #{Puppet[:user]}: #{detail}") exit(39) end end unless options[:rack] require 'puppet/network/server' @daemon.server = Puppet::Network::Server.new() @daemon.daemonize if Puppet[:daemonize] else require 'puppet/network/http/rack' @app = Puppet::Network::HTTP::Rack.new() end Puppet.notice "Starting Puppet master version #{Puppet.version}" unless options[:rack] @daemon.start else return @app end end def setup raise Puppet::Error.new("Puppet master is not supported on Microsoft Windows") if Puppet.features.microsoft_windows? # Handle the logging settings. if options[:debug] or options[:verbose] if options[:debug] Puppet::Util::Log.level = :debug else Puppet::Util::Log.level = :info end unless Puppet[:daemonize] or options[:rack] Puppet::Util::Log.newdestination(:console) options[:setdest] = true end end Puppet::Util::Log.newdestination(:syslog) unless options[:setdest] exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? Puppet.settings.use :main, :master, :ssl, :metrics # Cache our nodes in yaml. Currently not configurable. Puppet::Node.indirection.cache_class = :yaml # Configure all of the SSL stuff. if Puppet::SSL::CertificateAuthority.ca? Puppet::SSL::Host.ca_location = :local Puppet.settings.use :ca Puppet::SSL::CertificateAuthority.instance else Puppet::SSL::Host.ca_location = :none end end end diff --git a/lib/puppet/application/queue.rb b/lib/puppet/application/queue.rb index ab48d6c88..22bdfe005 100644 --- a/lib/puppet/application/queue.rb +++ b/lib/puppet/application/queue.rb @@ -1,159 +1,158 @@ require 'puppet/application' require 'puppet/util' class Puppet::Application::Queue < Puppet::Application - should_parse_config attr_accessor :daemon def preinit require 'puppet/daemon' @daemon = Puppet::Daemon.new @daemon.argv = ARGV.dup # Do an initial trap, so that cancels don't get a stack trace. # This exits with exit code 1 Signal.trap(:INT) do $stderr.puts "Caught SIGINT; shutting down" exit(1) end # This is a normal shutdown, so code 0 Signal.trap(:TERM) do $stderr.puts "Caught SIGTERM; shutting down" exit(0) end { :verbose => false, :debug => false }.each do |opt,val| options[opt] = val end end option("--debug","-d") option("--verbose","-v") def help <<-HELP puppet-queue(8) -- Queuing daemon for asynchronous storeconfigs ======== SYNOPSIS -------- Retrieves serialized storeconfigs records from a queue and processes them in order. USAGE ----- puppet queue [-d|--debug] [-v|--verbose] DESCRIPTION ----------- This application runs as a daemon and processes storeconfigs data, retrieving the data from a stomp server message queue and writing it to a database. For more information, including instructions for properly setting up your puppet master and message queue, see the documentation on setting up asynchronous storeconfigs at: http://projects.puppetlabs.com/projects/1/wiki/Using_Stored_Configuration OPTIONS ------- Note that any configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'server' is a valid configuration parameter, so you can specify '--server ' 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 queue with '--genconfig'. * --debug: Enable full debugging. * --help: Print this help message * --verbose: Turn on verbose reporting. * --version: Print the puppet version number and exit. EXAMPLE ------- $ puppet queue AUTHOR ------ Luke Kanies COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end option("--logdest DEST", "-l DEST") do |arg| begin Puppet::Util::Log.newdestination(arg) options[:setdest] = true rescue => detail Puppet.log_exception(detail) end end def main require 'puppet/indirector/catalog/queue' # provides Puppet::Indirector::Queue.subscribe Puppet.notice "Starting puppetqd #{Puppet.version}" Puppet::Resource::Catalog::Queue.subscribe do |catalog| # Once you have a Puppet::Resource::Catalog instance, passing it to save should suffice # to put it through to the database via its active_record indirector (which is determined # by the terminus_class = :active_record setting above) Puppet::Util.benchmark(:notice, "Processing queued catalog for #{catalog.name}") do begin Puppet::Resource::Catalog.indirection.save(catalog) rescue => detail Puppet.log_exception(detail, "Could not save queued catalog for #{catalog.name}: #{detail}") end end end Thread.list.each { |thread| thread.join } end def setup unless Puppet.features.stomp? raise ArgumentError, "Could not load the 'stomp' library, which must be present for queueing to work. You must install the required library." end setup_logs exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? require 'puppet/resource/catalog' Puppet::Resource::Catalog.indirection.terminus_class = :store_configs daemon.daemonize if Puppet[:daemonize] # We want to make sure that we don't have a cache # class set up, because if storeconfigs is enabled, # we'll get a loop of continually caching the catalog # for storage again. Puppet::Resource::Catalog.indirection.cache_class = nil end end diff --git a/lib/puppet/application/resource.rb b/lib/puppet/application/resource.rb index c9a84f390..a11b03f91 100644 --- a/lib/puppet/application/resource.rb +++ b/lib/puppet/application/resource.rb @@ -1,237 +1,235 @@ require 'puppet/application' class Puppet::Application::Resource < Puppet::Application - should_not_parse_config - attr_accessor :host, :extra_params def preinit @extra_params = [] Facter.loadfacts end option("--debug","-d") option("--verbose","-v") option("--edit","-e") option("--host HOST","-H") do |arg| @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] [] [= ...] 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 configuration parameter that's valid in the configuration file is also a valid long argument. For example, 'ssldir' is a valid configuration parameter, 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. 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 resources = find_or_save_resources(type, name, params) text = resources. map { |resource| resource.prune_parameters(:parameters_to_include => @extra_params).to_manifest }. join("\n") options[:edit] ? handle_editing(text) : (puts text) end def setup Puppet::Util::Log.newdestination(:console) Puppet.parse_config if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end 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 7025fc21f..6aac56aa7 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -1,953 +1,1482 @@ # The majority of Puppet's configuration settings are set in this file. module Puppet - setdefaults(:main, - :confdir => [Puppet.run_mode.conf_dir, "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 => [Puppet.run_mode.var_dir, "Where Puppet stores dynamic and growing data. The default for this setting is calculated specially, like `confdir`_."], - :name => [Puppet.application_name.to_s, "The name of the application, if we are running as one. The - default is essentially $0 without the path or `.rb`."], - :run_mode => [Puppet.run_mode.name.to_s, "The effective 'run mode' of the application: master, agent, or user."] + + ################################################################################################################# + # NOTE: For information about the available values for the ":type" property of settings, see the docs for + # Settings.define_settings. + ################################################################################################################# + + 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\n" + + "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,\n" + + "it defaults to being in the user's home directory.", + }, + :vardir => { + :default => nil, + :type => :directory, + :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\n" + + "default is essentially $0 without the path or `.rb`.", + }, + + ## This setting needs to go away. As a first step, we could just make it a first-class property of the Settings + ## class, instead of treating it as a normal setting. There are places where the Settings class tries to use + ## the value of run_mode to help in resolving other values, and that is no good for nobody. It would cause + ## infinite recursion and stack overflows without some chicanery... so, it needs to be cleaned up. + ## + ## As a longer term goal I think we should be looking into getting rid of run_mode altogether, but that is going + ## to be a larger undertaking, as it is being branched on in a lot of places in the current code. + ## + ## --cprice 2012-03-16 + :run_mode => { + :default => nil, + :desc => "The effective 'run mode' of the application: master, agent, or user.", + } ) - setdefaults(:main, :logdir => Puppet.run_mode.logopts) + define_settings(:main, + :logdir => { + :default => nil, + :type => :directory, + :mode => 0750, + :desc => "The directory in which to store log files", + } + ) - setdefaults(:main, - :trace => [false, "Whether to print stack traces on some errors"], + define_settings(:main, + :trace => { + :default => false, + :type => :boolean, + :desc => "Whether to print stack traces on some errors", + }, :autoflush => { :default => false, + :type => :boolean, :desc => "Whether log files should always flush to disk.", :hook => proc { |value| Log.autoflush = value } }, - :syslogfacility => ["daemon", "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", - :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)." + :syslogfacility => { + :default => "daemon", + :desc => "What syslog facility to use when logging to\n" + + "syslog. Syslog has a fixed list of valid facilities, and you must\n" + + "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 => Puppet.run_mode.run_dir, - :mode => 01777, - :desc => "Where Puppet PID files are kept." - }, - :genconfig => [false, - "Whether to just print a configuration to stdout and exit. Only makes - sense when used interactively. Takes into account arguments specified - on the CLI."], - :genmanifest => [false, - "Whether to just print a manifest to stdout and exit. Only makes - sense when used interactively. Takes into account arguments specified - on the CLI."], - :configprint => ["", - "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'."], + :default => nil, + :type => :directory, + :mode => 01777, + :desc => "Where Puppet PID files are kept." + }, + :genconfig => { + :default => false, + :type => :boolean, + :desc => "Whether to just print a configuration to stdout and exit. Only makes\n" + + "sense when used interactively. Takes into account arguments specified\n" + + "on the CLI.", + }, + :genmanifest => { + :default => false, + :type => :boolean, + :desc => "Whether to just print a manifest to stdout and exit. Only makes\n" + + "sense when used interactively. Takes into account arguments specified\n" + + "on the CLI.", + }, + :configprint => { + :default => "", + :desc => "Print the value of a specific configuration setting. If the name of a\n" + + "setting is provided for this, then the value is printed and puppet\n" + + "exits. Comma-separate multiple values. For a list of all values,\n" + + "specify 'all'.", + }, :color => { :default => (Puppet.features.microsoft_windows? ? "false" : "ansi"), - :type => :setting, - :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 => [false, - "Whether to create the necessary user and group that puppet agent will - run as."], - :manage_internal_file_permissions => [true, - "Whether Puppet should manage the owner, group, and mode of files - it uses internally" - ], - :onetime => {:default => false, - :desc => "Run the configuration once, rather than as a long-running - daemon. This is useful for interactively running puppetd.", - :short => 'o' - }, - :path => {:default => "none", - :desc => "The shell search path. Defaults to whatever is inherited - from the parent process.", - :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. - :hook => proc do |value| - ENV["PATH"] = "" if ENV["PATH"].nil? - ENV["PATH"] = value unless value == "none" - paths = ENV["PATH"].split(File::PATH_SEPARATOR) - %w{/usr/sbin /sbin}.each do |path| - ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path) + :type => :string, + :desc => "Whether to use colors when logging to the console. Valid values are\n" + + "`ansi` (equivalent to `true`), `html`, and `false`, which produces no color.\n" + + "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 => "Run the configuration once, rather than as a long-running\n" + + "daemon. This is useful for interactively running puppetd.", + :short => 'o', + }, + :path => { + :default => "none", + :desc => "The shell search path. Defaults to whatever is inherited\n" + + "from the parent process.", + :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. + :hook => proc do |value| + ENV["PATH"] = "" if ENV["PATH"].nil? + ENV["PATH"] = value unless value == "none" + paths = ENV["PATH"].split(File::PATH_SEPARATOR) + %w{/usr/sbin /sbin}.each do |path| + ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path) + end + value end - value - end }, - :libdir => {: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", - :call_on_define => true, # Call our hook with the default value, so we always get the libdir set. - :hook => proc do |value| + :libdir => { + :type => :directory, + :default => "$vardir/lib", + :desc => "An extra search path for Puppet. This is only useful\n" + + "for those files that Puppet will load on demand, and is only\n" + + "guaranteed to work for those cases. In fact, the autoload\n" + + "mechanism is responsible for making sure this directory\n" + + "is in Ruby's search path\n", + #:call_on_define => true, # Call our hook with the default value, so we always get the libdir set. + :hook => proc do |value| $LOAD_PATH.delete(@oldlibdir) if defined?(@oldlibdir) and $LOAD_PATH.include?(@oldlibdir) @oldlibdir = value $LOAD_PATH << value end }, - :ignoreimport => [false, "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."], - :authconfig => [ "$confdir/namespaceauth.conf", - "The configuration file that defines the rights to the different - namespaces and methods. This can be used as a coarse-grained - authorization system for both `puppet agent` and `puppet master`." - ], - :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." - }, - :diff_args => ["-u", "Which arguments to pass to the diff command when printing differences between - files. The command to use can be chosen with the `diff` setting."], + :ignoreimport => { + :default => false, + :type => :boolean, + :desc => "If true, allows the parser to continue without requiring\n" + + "all files referenced with `import` statements to exist. This setting was primarily\n" + + "designed for use with commit hooks for parse-checking.", + }, + :authconfig => { + :default => "$confdir/namespaceauth.conf", + :desc => "The configuration file that defines the rights to the different\n" + + "namespaces and methods. This can be used as a coarse-grained\n" + + "authorization system for both `puppet agent` and `puppet master`.", + }, + :environment => { + :default => "production", + :desc => "The environment Puppet is running in. For clients\n" + + "(e.g., `puppet agent`) this determines the environment itself, which\n" + + "is used to find modules and much more. For servers (i.e., `puppet master`)\n" + + "this provides the default environment for nodes we know nothing about." + }, + :diff_args => { + :default => "-u", + :desc => "Which arguments to pass to the diff command when printing differences between\n" + + "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 => [false, "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."], + :desc => "Which diff command to use when printing differences between files. This setting\n" + + "has no default value on Windows, as standard `diff` is not available, but Puppet can use many\n" + + "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\n" + + "partial file contents to pass through Puppet's normal logging and reporting system, so this setting\n" + + "should be used with caution if you are sending Puppet's reports to an insecure destination.\n" + + "This feature currently requires the `diff/lcs` Ruby library.", + }, :daemonize => { - :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 + :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 => [4294967290, "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 => ["$confdir/routes.yaml", "The YAML file containing indirector route configuration."], - :node_terminus => ["plain", "Where to find information about nodes."], - :catalog_terminus => ["compiler", "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."], + :maximum_uid => { + :default => 4294967290, + :desc => "The maximum allowed UID. Some platforms use negative UIDs\n" + + "but then ship with tools that do not know how to handle signed ints, so the UIDs show up as\n" + + "huge numbers that can then not be fed back into the system. This is a hackish way to fail in a\n" + + "slightly more useful way when that happens.", + }, + :route_file => { + :default => "$confdir/routes.yaml", + :desc => "The YAML file containing indirector route configuration.", + }, + :node_terminus => { + :default => "plain", + :desc => "Where to find information about nodes.", + }, + :catalog_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.", + }, :facts_terminus => { - :default => Puppet.application_name.to_s == "master" ? 'yaml' : 'facter', + :default => 'facter', :desc => "The node facts terminus.", :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::Node::Facts.indirection.cache_class = :yaml end end }, - :inventory_terminus => [ "$facts_terminus", "Should usually be the same as the facts terminus" ], - :httplog => { :default => "$logdir/http.log", - :owner => "root", - :mode => 0640, - :desc => "Where the puppet agent web server logs." - }, - :http_proxy_host => ["none", - "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."], - :http_proxy_port => [3128, "The HTTP proxy port to use for outgoing connections"], - :filetimeout => [ 15, - "The minimum time to wait (in seconds) between checking for updates in + :inventory_terminus => { + :default => "$facts_terminus", + :desc => "Should usually be the same as the facts terminus", + }, + :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.", + }, + :http_proxy_port => { + :default => 3128, + :desc => "The HTTP proxy port to use for outgoing connections", + }, + :filetimeout => { + :default => 15, + :desc => "The minimum time to wait (in seconds) 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." - ], - :queue_type => ["stomp", "Which type of queue to use for asynchronous processing."], - :queue_type => ["stomp", "Which type of queue to use for asynchronous processing."], - :queue_source => ["stomp://localhost:61613/", "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, :desc => "Whether to use a queueing system to provide asynchronous database integration. + a file (such as manifests or templates) has changed on disk.", + }, + :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 `puppetqd` be running and that 'PSON' support for ruby be installed.", - :hook => proc do |value| - if value - # This reconfigures the terminii for Node, Facts, and Catalog - Puppet.settings[:storeconfigs] = true + :hook => proc do |value| + if value + # This reconfigures the terminii for Node, Facts, and Catalog + Puppet.settings[:storeconfigs] = true - # But then we modify the configuration - Puppet::Resource::Catalog.indirection.cache_class = :queue - else - raise "Cannot disable asynchronous storeconfigs in a running process" + # But then we modify the configuration + Puppet::Resource::Catalog.indirection.cache_class = :queue + else + raise "Cannot disable asynchronous storeconfigs in a running process" + end end - end }, - :thin_storeconfigs => {:default => false, :desc => - "Boolean; whether storeconfigs store in the database only the facts and exported resources. - If true, then storeconfigs performance will be higher and still allow exported/collected - resources, but other usage external to Puppet might not work", + :thin_storeconfigs => { + :default => false, + :type => :boolean, + :desc => + "Boolean; whether storeconfigs store in the database only the facts and exported resources. + If true, then storeconfigs performance will be higher and still allow exported/collected + resources, but other usage external to Puppet might not work", :hook => proc do |value| Puppet.settings[:storeconfigs] = true if value end }, - :config_version => ["", "How to determine the configuration version. By default, it will be the + :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."], - :zlib => [true, - "Boolean; whether to use the zlib library", - ], - :prerun_command => ["", "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 => ["", "A command to run after every agent run. If this command returns a non-zero + reports, allowing you to correlate changes on your hosts to the source version on the server.", + }, + :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 => [false, "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."] + 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\n" + + "essentially means that you can't have any code outside of a node, class, or definition other\n" + + "than in the site manifest.", + } ) - Puppet.setdefaults(:module_tool, - :module_repository => ['http://forge.puppetlabs.com', "The module repository"], - :module_working_dir => ['$vardir/puppet-module', "The directory into which module tool data is stored"] + Puppet.define_settings(:module_tool, + :module_repository => { + :default => 'http://forge.puppetlabs.com', + :desc => "The module repository", + }, + :module_working_dir => { + :default => '$vardir/puppet-module', + :desc => "The directory into which module tool data is stored", + } ) hostname = Facter["hostname"].value domain = Facter["domain"].value if domain and domain != "" fqdn = [hostname, domain].join(".") else fqdn = hostname end - Puppet.setdefaults( + 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 => fqdn.downcase, :desc => "The name to use when handling certificates. Defaults + :certname => { + :default => fqdn.downcase, :desc => "The name to use when handling certificates. Defaults to the fully qualified domain name.", :call_on_define => true, # Call our hook with the default value, so we're always downcased :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 => "$ssldir/certs", + :type => :directory, :owner => "service", :desc => "The certificate directory." }, :ssldir => { :default => "$confdir/ssl", + :type => :directory, :mode => 0771, :owner => "service", :desc => "Where SSL certificates are kept." }, :publickeydir => { :default => "$ssldir/public_keys", + :type => :directory, :owner => "service", :desc => "The public key directory." }, :requestdir => { :default => "$ssldir/certificate_requests", + :type => :directory, + :type => :directory, :owner => "service", :desc => "Where host certificate requests are stored." }, - :privatekeydir => { :default => "$ssldir/private_keys", + :privatekeydir => { + :default => "$ssldir/private_keys", + :type => :directory, :mode => 0750, :owner => "service", :desc => "The private key directory." }, - :privatedir => { :default => "$ssldir/private", + :privatedir => { + :default => "$ssldir/private", + :type => :directory, :mode => 0750, :owner => "service", :desc => "Where the client stores private certificate information." }, - :passfile => { :default => "$privatedir/password", + :passfile => { + :default => "$privatedir/password", + :type => :file, :mode => 0640, :owner => "service", :desc => "Where puppet agent stores the password for its private key. Generally unused." }, - :hostcsr => { :default => "$ssldir/csr_$certname.pem", + :hostcsr => { + :default => "$ssldir/csr_$certname.pem", + :type => :file, :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their certificate requests." }, - :hostcert => { :default => "$certdir/$certname.pem", + :hostcert => { + :default => "$certdir/$certname.pem", + :type => :file, :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their certificates." }, - :hostprivkey => { :default => "$privatekeydir/$certname.pem", + :hostprivkey => { + :default => "$privatekeydir/$certname.pem", + :type => :file, :mode => 0600, :owner => "service", :desc => "Where individual hosts store and look for their private key." }, - :hostpubkey => { :default => "$publickeydir/$certname.pem", + :hostpubkey => { + :default => "$publickeydir/$certname.pem", + :type => :file, :mode => 0644, :owner => "service", :desc => "Where individual hosts store and look for their public key." }, - :localcacert => { :default => "$certdir/ca.pem", + :localcacert => { + :default => "$certdir/ca.pem", + :type => :file, :mode => 0644, :owner => "service", :desc => "Where each client stores the CA certificate." }, - :hostcrl => { :default => "$ssldir/crl.pem", + :hostcrl => { + :default => "$ssldir/crl.pem", + :type => :file, :mode => 0644, :owner => "service", :desc => "Where the host's certificate revocation list can be found. This is distinct from the certificate authority's CRL." }, - :certificate_revocation => [true, "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_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.", + } ) - setdefaults( + define_settings( :ca, - :ca_name => ["Puppet CA: $certname", "The name to use the Certificate Authority certificate."], - :cadir => { :default => "$ssldir/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 => 0770, :desc => "The root directory for the certificate authority." }, - :cacert => { :default => "$cadir/ca_crt.pem", + :cacert => { + :default => "$cadir/ca_crt.pem", + :type => :file, :owner => "service", :group => "service", :mode => 0660, :desc => "The CA certificate." }, - :cakey => { :default => "$cadir/ca_key.pem", + :cakey => { + :default => "$cadir/ca_key.pem", + :type => :file, :owner => "service", :group => "service", :mode => 0660, :desc => "The CA private key." }, - :capub => { :default => "$cadir/ca_pub.pem", + :capub => { + :default => "$cadir/ca_pub.pem", + :type => :file, :owner => "service", :group => "service", :desc => "The CA public key." }, - :cacrl => { :default => "$cadir/ca_crl.pem", + :cacrl => { + :default => "$cadir/ca_crl.pem", + :type => :file, :owner => "service", :group => "service", :mode => 0664, :desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.", :hook => proc do |value| if value == 'false' Puppet.deprecation_warning "Setting the :cacrl to 'false' is deprecated; Puppet will just ignore the crl if yours is missing" end end }, - :caprivatedir => { :default => "$cadir/private", + :caprivatedir => { + :default => "$cadir/private", + :type => :directory, :owner => "service", :group => "service", :mode => 0770, :desc => "Where the CA stores private certificate information." }, - :csrdir => { :default => "$cadir/requests", + :csrdir => { + :default => "$cadir/requests", + :type => :directory, :owner => "service", :group => "service", :desc => "Where the CA stores certificate requests" }, - :signeddir => { :default => "$cadir/signed", + :signeddir => { + :default => "$cadir/signed", + :type => :directory, :owner => "service", :group => "service", :mode => 0770, :desc => "Where the CA stores signed certificates." }, - :capass => { :default => "$caprivatedir/ca.pass", + :capass => { + :default => "$caprivatedir/ca.pass", + :type => :file, :owner => "service", :group => "service", :mode => 0660, :desc => "Where the CA stores the password for the private key" }, - :serial => { :default => "$cadir/serial", + :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", + :autosign => { + :default => "$confdir/autosign.conf", + :type => :file, :mode => 0644, :desc => "Whether to enable autosign. Valid values are true (which autosigns any key request, and is a very bad idea), false (which never autosigns any key request), and the path to a file, which uses that configuration file to determine which keys to sign."}, - :allow_duplicate_certs => [false, "Whether to allow a new certificate - request to overwrite an existing certificate."], - :ca_days => ["", "How long a certificate should be valid, in days. - This setting is deprecated; use `ca_ttl` instead"], - :ca_ttl => ["5y", "The default TTL for new certificates; valid values + :allow_duplicate_certs => { + :default => false, + :type => :boolean, + :desc => "Whether to allow a new certificate + request to overwrite an existing certificate.", + }, + :ca_days => { + :default => "", + :desc => "How long a certificate should be valid, in days. + This setting is deprecated; use `ca_ttl` instead", + }, + :ca_ttl => { + :default => "5y", + :desc => "The default TTL for new certificates; valid values must be an integer, optionally followed by one of the units 'y' (years of 365 days), 'd' (days), 'h' (hours), or 's' (seconds). The unit defaults to seconds. If this setting is set, ca_days is ignored. Examples are '3600' (one hour) - and '1825d', which is the same as '5y' (5 years) "], - :ca_md => ["md5", "The type of hash used in certificates."], - :req_bits => [4096, "The bit length of the certificates."], - :keylength => [4096, "The bit length of keys."], + and '1825d', which is the same as '5y' (5 years) ", + }, + :ca_md => { + :default => "md5", + :desc => "The type of hash used in certificates.", + }, + :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 => "A Complete listing of all certificates" } ) # Define the config default. - setdefaults( - Puppet.settings[:name], - :config => ["$confdir/puppet.conf", - "The configuration file for #{Puppet[:name]}."], - :pidfile => ["$rundir/$name.pid", "The pid file"], - :bindaddress => ["", "The address a listening server should bind to. Mongrel servers - default to 127.0.0.1 and WEBrick defaults to 0.0.0.0."], - :servertype => {:default => "webrick", :desc => "The type of server to use. Currently supported - options are webrick and mongrel. If you use mongrel, you will need - a proxy in front of the process or processes, since Mongrel cannot - speak SSL.", - - :call_on_define => true, # Call our hook with the default value, so we always get the correct bind address set. - :hook => proc { |value| value == "webrick" ? Puppet.settings[:bindaddress] = "0.0.0.0" : Puppet.settings[:bindaddress] = "127.0.0.1" if Puppet.settings[:bindaddress] == "" } - } + define_settings(:application, + :config_file_name => { + :type => :string, + :default => Puppet::Util::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/$name.pid", + :desc => "The pid file", + }, + :bindaddress => { + :default => "", + :desc => "The address a listening server should bind to. Mongrel servers + default to 127.0.0.1 and WEBrick defaults to 0.0.0.0.", + }, + :servertype => { + :default => "webrick", :desc => "The type of server to use. Currently supported + options are webrick and mongrel. If you use mongrel, you will need + a proxy in front of the process or processes, since Mongrel cannot + speak SSL.", + + :call_on_define => true, # Call our hook with the default value, so we always get the correct bind address set. + :hook => proc { |value| value == "webrick" ? Puppet.settings[:bindaddress] = "0.0.0.0" : Puppet.settings[:bindaddress] = "127.0.0.1" if Puppet.settings[:bindaddress] == "" } + } ) - setdefaults(:master, - :user => ["puppet", "The user puppet master should run as."], - :group => ["puppet", "The group puppet master should run as."], - :manifestdir => ["$confdir/manifests", "Where puppet master looks for its manifests."], - :manifest => ["$manifestdir/site.pp", "The entry-point manifest for puppet master."], - :code => ["", "Code to parse directly. This is essentially only used + 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 => "Where puppet master looks for its manifests.", + }, + :manifest => { + :default => "$manifestdir/site.pp", + :type => :file, + :desc => "The entry-point manifest for puppet master.", + }, + :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"], - :masterlog => { :default => "$logdir/puppetmaster.log", + executable", + }, + :masterlog => { + :default => "$logdir/puppetmaster.log", + :type => :file, :owner => "service", :group => "service", :mode => 0660, :desc => "Where puppet master logs. This is generally not used, since syslog is the default log destination." }, - :masterhttplog => { :default => "$logdir/masterhttp.log", + :masterhttplog => { + :default => "$logdir/masterhttp.log", + :type => :file, :owner => "service", :group => "service", :mode => 0660, :create => true, :desc => "Where the puppet master web server logs." }, - :masterport => [8140, "Which port puppet master listens on."], - :node_name => ["cert", "How the puppet master determines the client's identity + :masterport => { + :default => 8140, + :desc => "Which port puppet master listens on.", + }, + :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)"], + reported in its facts)", + }, :bucketdir => { :default => "$vardir/bucket", + :type => :directory, :mode => 0750, :owner => "service", :group => "service", :desc => "Where FileBucket files are stored." }, - :rest_authconfig => [ "$confdir/auth.conf", - "The configuration file that defines the rights to the different + :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 => [true, "Wether the master should function as a certificate authority."], + authorization system for `puppet master`.", + }, + :ca => { + :default => true, + :type => :boolean, + :desc => "Whether the master should function as a certificate authority.", + }, :modulepath => { :default => "$confdir/modules#{File::PATH_SEPARATOR}/usr/share/puppet/modules", - :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 ';'.)", - :type => :setting # We don't want this to be considered a file, since it's multiple files. + :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 ';'.)", }, - :ssl_client_header => ["HTTP_X_CLIENT_DN", "The header containing an authenticated + :ssl_client_header => { + :default => "HTTP_X_CLIENT_DN", + :desc => "The header containing an authenticated client's SSL DN. Only used with Mongrel. This header must be set by the proxy to the authenticated client's SSL DN (e.g., `/CN=puppet.puppetlabs.com`). - See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information."], - :ssl_client_verify_header => ["HTTP_X_CLIENT_VERIFY", "The header containing the status + See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information.", + }, + :ssl_client_verify_header => { + :default => "HTTP_X_CLIENT_VERIFY", + :desc => "The header containing the status message of the client verification. Only used with Mongrel. This header must be set by the proxy to 'SUCCESS' if the client successfully authenticated, and anything else otherwise. - See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information."], + See http://projects.puppetlabs.com/projects/puppet/wiki/Using_Mongrel for more information.", + }, # 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", :owner => "service", :group => "service", :mode => "750", + :yamldir => { + :default => "$vardir/yaml", + :type => :directory, + :owner => "service", + :group => "service", + :mode => "750", :desc => "The directory in which YAML data is stored, usually in a subdirectory."}, - :server_datadir => {:default => "$vardir/server_data", :owner => "service", :group => "service", :mode => "750", + :server_datadir => { + :default => "$vardir/server_data", + :type => :directory, + :owner => "service", + :group => "service", + :mode => "750", :desc => "The directory in which serialized data is stored, usually in a subdirectory."}, - :reports => ["store", - "The list of reports to generate. All reports are looked for - in `puppet/reports/name.rb`, and multiple report names should be - comma-separated (whitespace is okay)." - ], - :reportdir => {:default => "$vardir/reports", + :reports => { + :default => "store", + :desc => "The list of reports to generate. All reports are looked for + in `puppet/reports/name.rb`, and multiple report names should be + comma-separated (whitespace is okay).", + }, + :reportdir => { + :default => "$vardir/reports", + :type => :directory, :mode => 0750, :owner => "service", :group => "service", :desc => "The directory in which to store reports received from the client. Each client gets a separate subdirectory."}, - :reporturl => ["http://localhost:3000/reports/upload", - "The URL used by the http reports processor to send reports"], - :fileserverconfig => ["$confdir/fileserver.conf", "Where the fileserver configuration is stored."], - :strict_hostname_checking => [false, "Whether to only search for the complete - hostname as it is in the certificate when searching for node information - in the catalogs."] + :reporturl => { + :default => "http://localhost:3000/reports/upload", + :desc => "The URL used by the http reports processor to send reports", + }, + :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.", + } ) - setdefaults(:metrics, - :rrddir => {:default => "$vardir/rrd", - :mode => 0750, - :owner => "service", - :group => "service", - :desc => "The directory where RRD database files are stored. + 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 => ["$runinterval", "How often RRD should expect data. - This should match how often the hosts report back to the server."] + :rrdinterval => { + :default => "$runinterval", + :desc => "How often RRD should expect data. + This should match how often the hosts report back to the server.", + } ) - setdefaults(:device, - :devicedir => {:default => "$vardir/devices", :mode => "750", :desc => "The root directory of devices' $vardir"}, - :deviceconfig => ["$confdir/device.conf","Path to the device config file for puppet device"] + define_settings(:device, + :devicedir => { + :default => "$vardir/devices", + :type => :directory, + :mode => "750", + :desc => "The root directory of devices' $vardir", + }, + :deviceconfig => { + :default => "$confdir/device.conf", + :desc => "Path to the device config file for puppet device", + } ) - setdefaults(:agent, - :node_name_value => { :default => "$certname", + 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 => "", + :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", + :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", + :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", :mode => "750", :desc => "The directory in which client-side YAML data is stored."}, - :client_datadir => {:default => "$vardir/client_data", :mode => "750", :desc => "The directory in which serialized data is stored on the client."}, - :classfile => { :default => "$statedir/classes.txt", + :clientyamldir => { + :default => "$vardir/client_yaml", + :type => :directory, + :mode => "750", + :desc => "The directory in which client-side YAML data is stored." + }, + :client_datadir => { + :default => "$vardir/client_data", + :type => :directory, + :mode => "750", + :desc => "The directory in which serialized data is stored on the client." + }, + :classfile => { + :default => "$statedir/classes.txt", + :type => :file, :owner => "root", :mode => 0644, :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", + :resourcefile => { + :default => "$statedir/resources.txt", + :type => :file, :owner => "root", :mode => 0644, :desc => "The file in which puppet agent stores a list of the resources associated with the retrieved configuration." }, - :puppetdlog => { :default => "$logdir/puppetd.log", + :puppetdlog => { + :default => "$logdir/puppetd.log", + :type => :file, :owner => "root", :mode => 0640, :desc => "The log file for puppet agent. This is generally not used." }, :server => { :default => "puppet", :desc => "The server to which the puppet agent should connect" }, - :use_srv_records => [true, "Whether the server will search for SRV records in DNS for the current domain."], - :srv_domain => [ "#{domain}", "The domain which will be queried to find the SRV records of servers to use."], - :ignoreschedules => [false, - "Boolean; whether puppet agent should ignore schedules. This is useful - for initial puppet agent runs."], - :puppetport => [8139, "Which port puppet agent listens on."], - :noop => [false, "Whether puppet agent should be run in noop mode."], - :runinterval => [1800, # 30 minutes - "How often puppet agent applies the client configuration; in seconds. - 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."], - :listen => [false, "Whether puppet agent should listen for + :use_srv_records => { + :default => true, + :type => :boolean, + :desc => "Whether the server will search for SRV records in DNS for the current domain.", + }, + :srv_domain => { + :default => "#{domain}", + :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.", + }, + :puppetport => { + :default => 8139, + :desc => "Which port puppet agent listens on.", + }, + :noop => { + :default => false, + :type => :boolean, + :desc => "Whether puppet agent should be run in noop mode.", + }, + :runinterval => { + :default => 1800, # 30 minutes + :desc => "How often puppet agent applies the client configuration; in seconds. + 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.", + }, + :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 => ["$server", "The server to use for certificate + 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 => ["$masterport", "The port to use for the certificate authority."], + 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[:preferred_serialization_format] = value end } }, - :preferred_serialization_format => ["pson", "The preferred means of serializing + :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."], - :puppetdlockfile => [ "$statedir/puppetdlock", "A lock file to temporarily stop puppet agent from doing anything."], - :usecacheonfailure => [true, - "Whether to use the cached configuration when the remote + classes that support it.", + }, + :puppetdlockfile => { + :default => "$statedir/puppetdlock", + :type => :file, + :desc => "A lock file to temporarily stop puppet agent from doing anything.", + }, + :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 => [false, - "Whether to only use the cached catalog rather than compiling a new catalog + 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."], - :ignorecache => [false, - "Ignore cache and always recompile the configuration. This is + disabled when a recompile is desired.", + }, + :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." - ], - :downcasefacts => [false, "Whether facts should be made all lowercase when sent to the server."], - :dynamicfacts => ["memorysize,memoryfree,swapsize,swapfree", - "Facts that are dynamic; these facts will be ignored when deciding whether + change or if the server changes.", + }, + :downcasefacts => { + :default => false, + :type => :boolean, + :desc => "Whether facts should be made all lowercase when sent to the server.", + }, + :dynamicfacts => { + :default => "memorysize,memoryfree,swapsize,swapfree", + :desc => "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."], - :splaylimit => ["$runinterval", - "The maximum time to delay before runs. Defaults to being the same as the - run interval."], - :splay => [false, - "Whether to sleep for a pseudo-random (but consistent) amount of time before - a run."], + comma-separated.", + }, + :splaylimit => { + :default => "$runinterval", + :desc => "The maximum time to delay before runs. Defaults to being the same as the + run interval.", + }, + :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", - :mode => 0750, - :desc => "Where FileBucket files are stored locally." + :default => "$vardir/clientbucket", + :type => :directory, + :mode => 0750, + :desc => "Where FileBucket files are stored locally." }, - :configtimeout => [120, - "How long the client should wait for the configuration to be retrieved + :configtimeout => { + :default => 120, + :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." - ], + many clients contact the server at one time.", + }, :reportserver => { :default => "$server", :call_on_define => false, :desc => "(Deprecated for 'report_server') The server to which to send transaction reports.", :hook => proc do |value| Puppet.settings[:report_server] = value if value end }, - :report_server => ["$server", - "The server to send transaction reports to." - ], - :report_port => ["$masterport", - "The port to communicate with the report_server." - ], - :inventory_server => ["$server", - "The server to send facts to." - ], - :inventory_port => ["$masterport", - "The port to communicate with the inventory_server." - ], - :report => [true, - "Whether to send reports after every transaction." - ], - :lastrunfile => { :default => "$statedir/last_run_summary.yaml", - :mode => 0644, - :desc => "Where puppet agent stores the last run report summary in yaml format." + :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", - :mode => 0644, - :desc => "Where puppet agent stores the last run report in yaml format." + :lastrunreport => { + :default => "$statedir/last_run_report.yaml", + :type => :file, + :mode => 0644, + :desc => "Where puppet agent stores the last run report in yaml format." }, - :graph => [false, "Whether to create dot graph files for the different + :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 => ["$statedir/graphs", "Where to store dot-outputted graphs."], - :http_compression => [false, "Allow http compression in REST communication with the master. + 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 => [120, # 2 minutes - "The time interval, specified in seconds, 'puppet agent' should connect to the server - and ask it to sign a certificate request. This is useful for the initial setup of a - puppet client. You can turn off waiting for certificates by specifying a time of 0." - ] + compression, but if it supports it, this setting might reduce performance on high-speed LANs.", + }, + :waitforcert => { + :default => 120, # 2 minutes + :desc => "The time interval, specified in seconds, 'puppet agent' should connect to the server + and ask it to sign a certificate request. This is useful for the initial setup of a + puppet client. You can turn off waiting for certificates by specifying a time of 0.", + } ) - setdefaults(:inspect, - :archive_files => [false, "During an inspect run, whether to archive files whose contents are audited to a file bucket."], - :archive_file_server => ["$server", "During an inspect run, the file bucket server to archive files to if archive_files is set."] + 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. - setdefaults( + define_settings( :main, - :plugindest => ["$libdir", - "Where Puppet should store plugins that it pulls down from the central - server."], - :pluginsource => ["puppet://$server/plugins", - "From where to retrieve plugins. The standard Puppet `file` type + :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."], - :pluginsync => [true, "Whether plugins should be synced with the central server."], + be used here.", + }, + :pluginsync => { + :default => true, + :type => :boolean, + :desc => "Whether plugins should be synced with the central server.", + }, - :pluginsignore => [".svn CVS .git", "What files to ignore when pulling down plugins."] + :pluginsignore => { + :default => ".svn CVS .git", + :desc => "What files to ignore when pulling down plugins.", + } ) # Central fact information. - setdefaults( + define_settings( :main, - :factpath => {:default => "$vardir/lib/facter#{File::PATH_SEPARATOR}$vardir/facts", - :desc => "Where Puppet should look for facts. Multiple directories should + :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_on_define => true, # Call our hook with the default value, so we always get the value added to facter. - :type => :setting, # Don't consider it a file, because it could be multiple colon-separated files + #:call_on_define => true, # Call our hook with the default value, so we always get the value added to facter. :hook => proc { |value| Facter.search(value) if Facter.respond_to?(:search) }} ) - setdefaults( + define_settings( :tagmail, - :tagmap => ["$confdir/tagmail.conf", "The mapping between reporting tags and email addresses."], - :sendmail => [which('sendmail') || '', "Where to find the sendmail binary with which to send email."], + :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 => ["report@" + [Facter["hostname"].value, Facter["domain"].value].join("."), "The 'from' email address for the reports."], - :smtpserver => ["none", "The server through which to send email reports."] + :reportfrom => { + :default => "report@" + [Facter["hostname"].value,Facter["domain"].value].join("."), + :desc => "The 'from' email address for the reports.", + }, + + :smtpserver => { + :default => "none", + :desc => "The server through which to send email reports.", + } ) - setdefaults( + define_settings( :rails, - :dblocation => { :default => "$statedir/clientconfigs.sqlite3", - :mode => 0660, - :owner => "service", - :group => "service", - :desc => "The database cache for client configurations. Used for + :dblocation => { + :default => "$statedir/clientconfigs.sqlite3", + :type => :file, + :mode => 0660, + :owner => "service", + :group => "service", + :desc => "The database cache for client configurations. Used for querying within the language." }, - :dbadapter => [ "sqlite3", "The type of database to use." ], - :dbmigrate => [ false, "Whether to automatically migrate the database." ], - :dbname => [ "puppet", "The name of the database to use." ], - :dbserver => [ "localhost", "The database server for caching. Only - used when networked databases are used."], - :dbport => [ "", "The database password for caching. Only - used when networked databases are used."], - :dbuser => [ "puppet", "The database user for caching. Only - used when networked databases are used."], - :dbpassword => [ "puppet", "The database password for caching. Only - used when networked databases are used."], - :dbconnections => [ '', "The number of database connections for networked - databases. Will be ignored unless the value is a positive integer."], - :dbsocket => [ "", "The database socket location. Only used when networked - databases are used. Will be ignored if the value is an empty string."], - :railslog => {:default => "$logdir/rails.log", - :mode => 0600, - :owner => "service", - :group => "service", - :desc => "Where Rails-specific logs are sent" + :dbadapter => { + :default => "sqlite3", + :desc => "The type of database to use.", + }, + :dbmigrate => { + :default => false, + :type => :boolean, + :desc => "Whether to automatically migrate the database.", + }, + :dbname => { + :default => "puppet", + :desc => "The name of the database to use.", + }, + :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.", + }, + :dbuser => { + :default => "puppet", + :desc => "The database user for caching. Only + used when networked databases are used.", + }, + :dbpassword => { + :default => "puppet", + :desc => "The database password for caching. Only + used when networked databases are used.", + }, + :dbconnections => { + :default => '', + :desc => "The number of database connections for networked + databases. Will be ignored unless the value is a positive integer.", + }, + :dbsocket => { + :default => "", + :desc => "The database socket location. Only used when networked + databases are used. Will be ignored if the value is an empty string.", + }, + :railslog => { + :default => "$logdir/rails.log", + :type => :file, + :mode => 0600, + :owner => "service", + :group => "service", + :desc => "Where Rails-specific logs are sent" }, - :rails_loglevel => ["info", "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`."] + :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`.", + } ) - setdefaults( + define_settings( :couchdb, - :couchdb_url => ["http://127.0.0.1:5984/puppet", "The url where the puppet couchdb database will be created"] + :couchdb_url => { + :default => "http://127.0.0.1:5984/puppet", + :desc => "The url where the puppet couchdb database will be created", + } ) - setdefaults( + define_settings( :transaction, - :tags => ["", "Tags to use to find resources. If this is set, then + :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 => [false, "Whether each resource should log when it is + 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 => [false, - - "Whether to print a transaction summary." - ] + what is being done.", + }, + :summarize => { + :default => false, + :type => :boolean, + :desc => "Whether to print a transaction summary.", + } ) - setdefaults( + define_settings( :main, - :external_nodes => ["none", - - "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. + :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."]) + This command makes it straightforward to store your node mapping + information in other data sources like databases.", + } + ) - setdefaults( + define_settings( :ldap, - :ldapnodes => [false, - "Whether to search for node configurations in LDAP. See - http://projects.puppetlabs.com/projects/puppet/wiki/LDAP_Nodes for more information."], - :ldapssl => [false, - "Whether SSL should be used when searching for nodes. + :ldapnodes => { + :default => false, + :type => :boolean, + :desc => "Whether to search for node configurations in LDAP. See + http://projects.puppetlabs.com/projects/puppet/wiki/LDAP_Nodes for more information.", + }, + :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 => [false, - "Whether TLS should be used when searching for nodes. + 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 => ["ldap", - "The LDAP server. Only used if `ldapnodes` is enabled."], - :ldapport => [389, - "The LDAP port. Only used if `ldapnodes` is enabled."], - - :ldapstring => ["(&(objectclass=puppetClient)(cn=%s))", - "The search string used to find an LDAP node."], - :ldapclassattrs => ["puppetclass", - "The LDAP attributes to use to define Puppet classes. Values - should be comma-separated."], - :ldapstackedattrs => ["puppetvar", - "The LDAP attributes that should be stacked to arrays by adding + to be set up on the client side.", + }, + :ldapserver => { + :default => "ldap", + :desc => "The LDAP server. Only used if `ldapnodes` is enabled.", + }, + :ldapport => { + :default => 389, + :desc => "The LDAP port. Only used if `ldapnodes` is enabled.", + }, + + :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 => ["all", - "The LDAP attributes to include when querying LDAP for nodes. All + 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 => ["parentnode", - "The attribute to use to define the parent node."], - :ldapuser => ["", - "The user to use to connect to LDAP. Must be specified as a - full DN."], - :ldappassword => ["", "The password to use to connect to LDAP."], - :ldapbase => ["", - "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."] + 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.", + } ) - setdefaults(:master, + 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_on_define => true, + #:call_on_define => true, :hook => proc do |value| require 'puppet/node' require 'puppet/node/facts' if value Puppet.settings[:async_storeconfigs] or Puppet::Resource::Catalog.indirection.cache_class = :store_configs Puppet::Node::Facts.indirection.cache_class = :store_configs Puppet::Node.indirection.cache_class = :store_configs Puppet::Resource.indirection.terminus_class = :store_configs end end }, :storeconfigs_backend => { :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." } ) # This doesn't actually work right now. - setdefaults( + define_settings( :parser, - :lexical => [false, "Whether to use lexical scoping (vs. dynamic)."], - :templatedir => ["$vardir/templates", - "Where Puppet looks for template files. Can be a list of colon-separated - directories." - ] + :lexical => { + :default => false, + :type => :boolean, + :desc => "Whether to use lexical scoping (vs. dynamic).", + }, + :templatedir => { + :default => "$vardir/templates", + :type => :directory, + :desc => "Where Puppet looks for template files. Can be a list of colon-separated + directories.", + } ) - setdefaults( + define_settings( :puppetdoc, - :document_all => [false, "Document all resources"] + :document_all => { + :default => false, + :type => :boolean, + :desc => "Document all resources", + } ) end diff --git a/lib/puppet/face/node/clean.rb b/lib/puppet/face/node/clean.rb index a4df1bfaf..e9ea6e713 100644 --- a/lib/puppet/face/node/clean.rb +++ b/lib/puppet/face/node/clean.rb @@ -1,151 +1,154 @@ Puppet::Face.define(:node, '0.0.1') do action(:clean) do option "--[no-]unexport" do summary "Unexport exported resources" end summary "Clean up everything a puppetmaster knows about a node" arguments " [ ...]" description <<-EOT This includes * Signed certificates ($vardir/ssl/ca/signed/node.domain.pem) * Cached facts ($vardir/yaml/facts/node.domain.yaml) * Cached node stuff ($vardir/yaml/node/node.domain.yaml) * Reports ($vardir/reports/node.domain) * Stored configs: it can either remove all data from an host in your storedconfig database, or with --unexport turn every exported resource supporting ensure to absent so that any other host checking out their config can remove those exported configurations. This will unexport exported resources of a host, so that consumers of these resources can remove the exported resources and we will safely remove the node from our infrastructure. EOT when_invoked do |*args| nodes = args[0..-2] options = args.last raise "At least one node should be passed" if nodes.empty? || nodes == options - # TODO: this is a hack and should be removed if faces provide the proper - # infrastructure to set the run mode. - require 'puppet/util/run_mode' - $puppet_application_mode = Puppet::Util::RunMode[:master] + + + # This seems really bad; run_mode should be set as part of a class definition, and should + # not be modifiable beyond that. This is one of the only places left in the code that + # tries to manipulate it. I would like to get rid of it but I'm not entirely familiar + # with what we are trying to do here, so I'm postponing for now... --cprice 2012-03-16 + Puppet.settings.set_value(:run_mode, :master, :application_defaults) if Puppet::SSL::CertificateAuthority.ca? Puppet::SSL::Host.ca_location = :local else Puppet::SSL::Host.ca_location = :none end Puppet::Node::Facts.indirection.terminus_class = :yaml Puppet::Node::Facts.indirection.cache_class = :yaml Puppet::Node.indirection.terminus_class = :yaml Puppet::Node.indirection.cache_class = :yaml nodes.each { |node| cleanup(node.downcase, options[:unexport]) } end end def cleanup(node, unexport) clean_cert(node) clean_cached_facts(node) clean_cached_node(node) clean_reports(node) clean_storeconfigs(node, unexport) end # clean signed cert for +host+ def clean_cert(node) if Puppet::SSL::CertificateAuthority.ca? Puppet::Face[:ca, :current].revoke(node) Puppet::Face[:ca, :current].destroy(node) Puppet.info "#{node} certificates removed from ca" else Puppet.info "Not managing #{node} certs as this host is not a CA" end end # clean facts for +host+ def clean_cached_facts(node) Puppet::Node::Facts.indirection.destroy(node) Puppet.info "#{node}'s facts removed" end # clean cached node +host+ def clean_cached_node(node) Puppet::Node.indirection.destroy(node) Puppet.info "#{node}'s cached node removed" end # clean node reports for +host+ def clean_reports(node) Puppet::Transaction::Report.indirection.destroy(node) Puppet.info "#{node}'s reports removed" end # clean storeconfig for +node+ def clean_storeconfigs(node, do_unexport=false) return unless Puppet[:storeconfigs] && Puppet.features.rails? require 'puppet/rails' Puppet::Rails.connect unless rails_node = Puppet::Rails::Host.find_by_name(node) Puppet.notice "No entries found for #{node} in storedconfigs." return end if do_unexport unexport(rails_node) Puppet.notice "Force #{node}'s exported resources to absent" Puppet.warning "Please wait until all other hosts have checked out their configuration before finishing the cleanup with:" Puppet.warning "$ puppet node clean #{node}" else rails_node.destroy Puppet.notice "#{node} storeconfigs removed" end end def unexport(node) # fetch all exported resource query = {:include => {:param_values => :param_name}} query[:conditions] = [ "exported=? AND host_id=?", true, node.id ] Puppet::Rails::Resource.find(:all, query).each do |resource| if type_is_ensurable(resource) line = 0 param_name = Puppet::Rails::ParamName.find_or_create_by_name("ensure") if ensure_param = resource.param_values.find( :first, :conditions => [ 'param_name_id = ?', param_name.id ] ) line = ensure_param.line.to_i Puppet::Rails::ParamValue.delete(ensure_param.id); end # force ensure parameter to "absent" resource.param_values.create( :value => "absent", :line => line, :param_name => param_name ) Puppet.info("#{resource.name} has been marked as \"absent\"") end end end def environment @environment ||= Puppet::Node::Environment.new end def type_is_ensurable(resource) if (type = Puppet::Type.type(resource.restype)) && type.validattr?(:ensure) return true else type = environment.known_resource_types.find_definition('', resource.restype) return true if type && type.arguments.keys.include?('ensure') end return false end end diff --git a/lib/puppet/file_serving/indirection_hooks.rb b/lib/puppet/file_serving/indirection_hooks.rb index 4aafcb9fd..735c1dd9c 100644 --- a/lib/puppet/file_serving/indirection_hooks.rb +++ b/lib/puppet/file_serving/indirection_hooks.rb @@ -1,31 +1,38 @@ require 'uri' require 'puppet/file_serving' require 'puppet/util' # This module is used to pick the appropriate terminus # in file-serving indirections. This is necessary because # the terminus varies based on the URI asked for. module Puppet::FileServing::IndirectionHooks PROTOCOL_MAP = {"puppet" => :rest, "file" => :file} # Pick an appropriate terminus based on the protocol. def select_terminus(request) # We rely on the request's parsing of the URI. # Short-circuit to :file if it's a fully-qualified path or specifies a 'file' protocol. return PROTOCOL_MAP["file"] if Puppet::Util.absolute_path?(request.key) return PROTOCOL_MAP["file"] if request.protocol == "file" + # TODO: this seems like an incredibly fragile way to determine our protocol. (In fact, I broke it pretty nicely + # during my changes relating to settings/defaults.) Nick said he's going to fix it. :) + # --cprice 2012-03-14 + # TODO: we are special-casing both "puppet" and "apply" here... this shows up in tests as well + # (file_serving.rb in shared_behaviors). I don't think we need to special-case ":puppet" any longer, + # as I am not aware of any code path where the Puppet.settings[:name] could end up having that as a value. + # We're heading over the wire the protocol is 'puppet' and we've got a server name or we're not named 'apply' or 'puppet' - if request.protocol == "puppet" and (request.server or !["puppet","apply"].include?(Puppet.settings[:name])) + if request.protocol == "puppet" and (request.server or ![:puppet, :apply].include?(Puppet.settings[:name])) return PROTOCOL_MAP["puppet"] end if request.protocol and PROTOCOL_MAP[request.protocol].nil? raise(ArgumentError, "URI protocol '#{request.protocol}' is not currently supported for file serving") end # If we're still here, we're using the file_server or modules. :file_server end end diff --git a/lib/puppet/network/http/webrick.rb b/lib/puppet/network/http/webrick.rb index f51a8bfe6..b69813a25 100644 --- a/lib/puppet/network/http/webrick.rb +++ b/lib/puppet/network/http/webrick.rb @@ -1,109 +1,109 @@ require 'webrick' require 'webrick/https' require 'puppet/network/http/webrick/rest' require 'thread' require 'puppet/ssl/certificate' require 'puppet/ssl/certificate_revocation_list' class Puppet::Network::HTTP::WEBrick def initialize(args = {}) @listening = false @mutex = Mutex.new end def listen(args = {}) raise ArgumentError, ":address must be specified." unless args[:address] raise ArgumentError, ":port must be specified." unless args[:port] arguments = {:BindAddress => args[:address], :Port => args[:port]} arguments.merge!(setup_logger) arguments.merge!(setup_ssl) @server = WEBrick::HTTPServer.new(arguments) @server.listeners.each { |l| l.start_immediately = false } @server.mount('/', Puppet::Network::HTTP::WEBrickREST, :this_value_is_apparently_necessary_but_unused) @mutex.synchronize do raise "WEBrick server is already listening" if @listening @listening = true @thread = Thread.new { @server.start { |sock| raise "Client disconnected before connection could be established" unless IO.select([sock],nil,nil,6.2) sock.accept @server.run(sock) } } sleep 0.1 until @server.status == :Running end end def unlisten @mutex.synchronize do raise "WEBrick server is not listening" unless @listening @server.shutdown @thread.join @server = nil @listening = false end end def listening? @mutex.synchronize do @listening end end # Configure our http log file. def setup_logger # Make sure the settings are all ready for us. - Puppet.settings.use(:main, :ssl, Puppet[:name]) + Puppet.settings.use(:main, :ssl, :application) if Puppet.run_mode.master? file = Puppet[:masterhttplog] else file = Puppet[:httplog] end # open the log manually to prevent file descriptor leak file_io = ::File.open(file, "a+") file_io.sync = true if defined?(Fcntl::FD_CLOEXEC) file_io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) end args = [file_io] args << WEBrick::Log::DEBUG if Puppet::Util::Log.level == :debug logger = WEBrick::Log.new(*args) return :Logger => logger, :AccessLog => [ [logger, WEBrick::AccessLog::COMMON_LOG_FORMAT ], [logger, WEBrick::AccessLog::REFERER_LOG_FORMAT ] ] end # Add all of the ssl cert information. def setup_ssl results = {} # Get the cached copy. We know it's been generated, too. host = Puppet::SSL::Host.localhost raise Puppet::Error, "Could not retrieve certificate for #{host.name} and not running on a valid certificate authority" unless host.certificate results[:SSLPrivateKey] = host.key.content results[:SSLCertificate] = host.certificate.content results[:SSLStartImmediately] = true results[:SSLEnable] = true raise Puppet::Error, "Could not find CA certificate" unless Puppet::SSL::Certificate.indirection.find(Puppet::SSL::CA_NAME) results[:SSLCACertificateFile] = Puppet[:localcacert] results[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER results[:SSLCertificateStore] = host.ssl_store results end end diff --git a/lib/puppet/network/server.rb b/lib/puppet/network/server.rb index 3e79f4ded..f92d600a5 100644 --- a/lib/puppet/network/server.rb +++ b/lib/puppet/network/server.rb @@ -1,138 +1,139 @@ require 'puppet/network/http' require 'puppet/util/pidlock' class Puppet::Network::Server attr_reader :server_type, :address, :port # Put the daemon into the background. def daemonize if pid = fork Process.detach(pid) exit(0) end # Get rid of console logging Puppet::Util::Log.close(:console) Process.setsid Dir.chdir("/") begin $stdin.reopen "/dev/null" $stdout.reopen "/dev/null", "a" $stderr.reopen $stdout Puppet::Util::Log.reopen rescue => detail Puppet::Util.replace_file("/tmp/daemonout", 0644) { |f| f.puts "Could not start #{Puppet[:name]}: #{detail}" } raise "Could not start #{Puppet[:name]}: #{detail}" end end # Create a pidfile for our daemon, so we can be stopped and others # don't try to start. def create_pidfile Puppet::Util.synchronize_on(Puppet[:name],Sync::EX) do raise "Could not create PID file: #{pidfile}" unless Puppet::Util::Pidlock.new(pidfile).lock end end # Remove the pid file for our daemon. def remove_pidfile Puppet::Util.synchronize_on(Puppet[:name],Sync::EX) do Puppet::Util::Pidlock.new(pidfile).unlock end end # Provide the path to our pidfile. def pidfile Puppet[:pidfile] end def initialize(args = {}) valid_args = [:handlers, :port] bad_args = args.keys.find_all { |p| ! valid_args.include?(p) }.collect { |p| p.to_s }.join(",") raise ArgumentError, "Invalid argument(s) #{bad_args}" unless bad_args == "" @server_type = Puppet[:servertype] or raise "No servertype configuration found." # e.g., WEBrick, Mongrel, etc. http_server_class || raise(ArgumentError, "Could not determine HTTP Server class for server type [#{@server_type}]") @port = args[:port] || Puppet[:masterport] || raise(ArgumentError, "Must specify :port or configure Puppet :masterport") @address = determine_bind_address @listening = false @routes = {} self.register(args[:handlers]) if args[:handlers] # Make sure we have all of the directories we need to function. - Puppet.settings.use(:main, :ssl, Puppet[:name]) + #Puppet.settings.use(:main, :ssl, Puppet[:name]) + Puppet.settings.use(:main, :ssl, :application) end # Register handlers for REST networking, based on the Indirector. def register(*indirections) raise ArgumentError, "Indirection names are required." if indirections.empty? indirections.flatten.each do |name| Puppet::Indirector::Indirection.model(name) || raise(ArgumentError, "Cannot locate indirection '#{name}'.") @routes[name.to_sym] = true end end # Unregister Indirector handlers. def unregister(*indirections) raise "Cannot unregister indirections while server is listening." if listening? indirections = @routes.keys if indirections.empty? indirections.flatten.each do |i| raise(ArgumentError, "Indirection [#{i}] is unknown.") unless @routes[i.to_sym] end indirections.flatten.each do |i| @routes.delete(i.to_sym) end end def listening? @listening end def listen raise "Cannot listen -- already listening." if listening? @listening = true http_server.listen(:address => address, :port => port, :handlers => @routes.keys) end def unlisten raise "Cannot unlisten -- not currently listening." unless listening? http_server.unlisten @listening = false end def http_server_class http_server_class_by_type(@server_type) end def start create_pidfile listen end def stop unlisten remove_pidfile end private def http_server @http_server ||= http_server_class.new end def http_server_class_by_type(kind) Puppet::Network::HTTP.server_class_by_type(kind) end def determine_bind_address tmp = Puppet[:bindaddress] return tmp if tmp != "" server_type.to_s == "webrick" ? "0.0.0.0" : "127.0.0.1" end end diff --git a/lib/puppet/parser/ast/resource_defaults.rb b/lib/puppet/parser/ast/resource_defaults.rb index 812b979e9..9dfed28a0 100644 --- a/lib/puppet/parser/ast/resource_defaults.rb +++ b/lib/puppet/parser/ast/resource_defaults.rb @@ -1,24 +1,24 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # A statement syntactically similar to an ResourceDef, but uses a # capitalized object type and cannot have a name. class ResourceDefaults < AST::Branch attr_accessor :type, :parameters associates_doc # As opposed to ResourceDef, this stores each default for the given # object type. def evaluate(scope) # Use a resource reference to canonize the type ref = Puppet::Resource.new(@type, "whatever") type = ref.type params = @parameters.safeevaluate(scope) parsewrap do - scope.setdefaults(type, params) + scope.define_settings(type, params) end end end end diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index cbc933660..84a569bb5 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,476 +1,480 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'puppet/parser/parser' require 'puppet/parser/templatewrapper' require 'strscan' require 'puppet/resource/type_collection_helper' class Puppet::Parser::Scope include Puppet::Resource::TypeCollectionHelper require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) include Enumerable include Puppet::Util::Errors attr_accessor :source, :resource attr_accessor :base, :keyword attr_accessor :top, :translated, :compiler attr_accessor :parent, :dynamic attr_reader :namespaces # thin wrapper around an ephemeral # symbol table. # when a symbol class Ephemeral def initialize(parent=nil) @symbols = {} @parent = parent end [:include?, :delete, :[]=].each do |m| define_method(m) do |*args| @symbols.send(m, *args) end end def [](name) unless @symbols.include?(name) or @parent.nil? @parent[name] else @symbols[name] end end end def [](name, options = {}) table = ephemeral?(name) ? @ephemeral.last : @symtable # If the variable is qualified, then find the specified scope and look the variable up there instead. if name =~ /^(.*)::(.+)$/ begin qualified_scope($1)[$2,options] rescue RuntimeError => e location = (options[:file] && options[:line]) ? " at #{options[:file]}:#{options[:line]}" : '' warning "Could not look up qualified variable '#{name}'; #{e.message}#{location}" nil end elsif ephemeral_include?(name) or table.include?(name) # We can't use "if table[name]" here because the value might be false if options[:dynamic] and self != compiler.topscope location = (options[:file] && options[:line]) ? " at #{options[:file]}:#{options[:line]}" : '' Puppet.deprecation_warning "Dynamic lookup of $#{name}#{location} is deprecated. Support will be removed in Puppet 2.8. Use a fully-qualified variable name (e.g., $classname::variable) or parameterized classes." end table[name] elsif parent parent[name,options.merge(:dynamic => (dynamic || options[:dynamic]))] else nil end end def []=(var, value) setvar(var, value) end # A demeterific shortcut to the catalog. def catalog compiler.catalog end def each to_hash.each { |name, value| yield(name, value) } end # Proxy accessors def host @compiler.node.name end def include?(name) ! self[name].nil? end # Is the value true? This allows us to control the definition of truth # in one place. def self.true?(value) (value != false and value != "" and value != :undef) end # Is the value a number?, return the correct object or nil if not a number def self.number?(value) return nil unless value.is_a?(Fixnum) or value.is_a?(Bignum) or value.is_a?(Float) or value.is_a?(String) if value.is_a?(String) if value =~ /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/ return value.to_f elsif value =~ /^0x[0-9a-f]+$/i return value.to_i(16) elsif value =~ /^0[0-7]+$/ return value.to_i(8) elsif value =~ /^-?\d+$/ return value.to_i else return nil end end # it is one of Fixnum,Bignum or Float value end # Add to our list of namespaces. def add_namespace(ns) return false if @namespaces.include?(ns) if @namespaces == [""] @namespaces = [ns] else @namespaces << ns end end # Remove this when rebasing def environment compiler ? compiler.environment : Puppet::Node::Environment.new end def find_hostclass(name) known_resource_types.find_hostclass(namespaces, name) end def find_definition(name) known_resource_types.find_definition(namespaces, name) end def findresource(string, name = nil) compiler.findresource(string, name) end # Initialize our new scope. Defaults to having no parent. def initialize(hash = {}) if hash.include?(:namespace) if n = hash[:namespace] @namespaces = [n] end hash.delete(:namespace) else @namespaces = [""] end hash.each { |name, val| method = name.to_s + "=" if self.respond_to? method self.send(method, val) else raise Puppet::DevError, "Invalid scope argument #{name}" end } extend_with_functions_module @tags = [] # The symbol table for this scope. This is where we store variables. @symtable = {} # the ephemeral symbol tables # those should not persist long, and are used for the moment only # for $0..$xy capture variables of regexes # this is actually implemented as a stack, with each ephemeral scope # shadowing the previous one @ephemeral = [ Ephemeral.new ] # All of the defaults set for types. It's a hash of hashes, # with the first key being the type, then the second key being # the parameter. @defaults = Hash.new { |dhash,type| dhash[type] = {} } # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} end # Store the fact that we've evaluated a class, and store a reference to # the scope in which it was evaluated, so that we can look it up later. def class_set(name, scope) return parent.class_set(name,scope) if parent @class_scopes[name] = scope end # Return the scope associated with a class. This is just here so # that subclasses can set their parent scopes to be the scope of # their parent class, and it's also used when looking up qualified # variables. def class_scope(klass) # They might pass in either the class or class name k = klass.respond_to?(:name) ? klass.name : klass @class_scopes[k] || (parent && parent.class_scope(k)) end # Collect all of the defaults set at any higher scopes. # This is a different type of lookup because it's additive -- # it collects all of the defaults, with defaults in closer scopes # overriding those in later scopes. def lookupdefaults(type) values = {} # first collect the values from the parents unless parent.nil? parent.lookupdefaults(type).each { |var,value| values[var] = value } end # then override them with any current values # this should probably be done differently if @defaults.include?(type) @defaults[type].each { |var,value| values[var] = value } end #Puppet.debug "Got defaults for %s: %s" % # [type,values.inspect] values end # Look up a defined type. def lookuptype(name) find_definition(name) || find_hostclass(name) end def undef_as(x,v) if v.nil? or v == :undef x else v end end def qualified_scope(classname) raise "class #{classname} could not be found" unless klass = find_hostclass(classname) raise "class #{classname} has not been evaluated" unless kscope = class_scope(klass) kscope end private :qualified_scope # Look up a variable. The simplest value search we do. # This method is effectively deprecated - use self[] instead. def lookupvar(name, options = {}) self[name, options] end # Return a hash containing our variables and their values, optionally (and # by default) including the values defined in our parent. Local values # shadow parent values. def to_hash(recursive = true) target = parent.to_hash(recursive) if recursive and parent target ||= Hash.new @symtable.keys.each { |name| value = @symtable[name] if value == :undef target.delete(name) else target[name] = value end } target end def namespaces @namespaces.dup end # Create a new scope and set these options. def newscope(options = {}) compiler.newscope(self, options) end def parent_module_name return nil unless @parent return nil unless @parent.source @parent.source.module_name end # Return the list of scopes up to the top scope, ordered with our own first. # This is used for looking up variables and defaults. def scope_path if parent [self, parent.scope_path].flatten.compact else [self] end end # Set defaults for a type. The typename should already be downcased, # so that the syntax is isolated. We don't do any kind of type-checking # here; instead we let the resource do it when the defaults are used. - def setdefaults(type, params) + def define_settings(type, params) table = @defaults[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) params.each { |param| #Puppet.debug "Default for %s is %s => %s" % # [type,ary[0].inspect,ary[1].inspect] if table.include?(param.name) raise Puppet::ParseError.new("Default already defined for #{type} { #{param.name} }; cannot redefine", param.line, param.file) end table[param.name] = param } end # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope # to be reassigned. # It's preferred that you use self[]= instead of this; only use this # when you need to set options. def setvar(name,value, options = {}) table = options[:ephemeral] ? @ephemeral.last : @symtable if table.include?(name) unless options[:append] error = Puppet::ParseError.new("Cannot reassign variable #{name}") else error = Puppet::ParseError.new("Cannot append, variable #{name} is defined in this scope") end error.file = options[:file] if options[:file] error.line = options[:line] if options[:line] raise error end unless options[:append] table[name] = value else # append case # lookup the value in the scope if it exists and insert the var table[name] = undef_as('',self[name]) # concatenate if string, append if array, nothing for other types case value when Array table[name] += value when Hash raise ArgumentError, "Trying to append to a hash with something which is not a hash is unsupported" unless value.is_a?(Hash) table[name].merge!(value) else table[name] << value end end end # Return the tags associated with this scope. It's basically # just our parents' tags, plus our type. We don't cache this value # because our parent tags might change between calls. def tags resource.tags end # Used mainly for logging def to_s "Scope(#{@resource})" end # Undefine a variable; only used for testing. def unsetvar(var) table = ephemeral?(var) ? @ephemeral.last : @symtable table.delete(var) if table.include?(var) end # remove ephemeral scope up to level def unset_ephemeral_var(level=:all) if level == :all @ephemeral = [ Ephemeral.new ] else (@ephemeral.size - level).times do @ephemeral.pop end end end # check if name exists in one of the ephemeral scope. def ephemeral_include?(name) @ephemeral.reverse.each do |eph| return true if eph.include?(name) end false end # is name an ephemeral variable? def ephemeral?(name) name =~ /^\d+$/ end def ephemeral_level @ephemeral.size end def new_ephemeral @ephemeral.push(Ephemeral.new(@ephemeral.last)) end def ephemeral_from(match, file = nil, line = nil) raise(ArgumentError,"Invalid regex match data") unless match.is_a?(MatchData) new_ephemeral setvar("0", match[0], :file => file, :line => line, :ephemeral => true) match.captures.each_with_index do |m,i| setvar("#{i+1}", m, :file => file, :line => line, :ephemeral => true) end end def find_resource_type(type) # It still works fine without the type == 'class' short-cut, but it is a lot slower. return nil if ["class", "node"].include? type.to_s.downcase find_builtin_resource_type(type) || find_defined_resource_type(type) end def find_builtin_resource_type(type) Puppet::Type.type(type.to_s.downcase.to_sym) end def find_defined_resource_type(type) environment.known_resource_types.find_definition(namespaces, type.to_s.downcase) end def method_missing(method, *args, &block) method.to_s =~ /^function_(.*)$/ super unless $1 super unless Puppet::Parser::Functions.function($1) # Calling .function(name) adds "function_#{name}" as a callable method on # self if it's found, so now we can just send it + # NOTE: I have watched this method end up in an infinite recursion / stack overflow. It seems + # to me that we ought to be checkign with "respond_to?" before calling send, and throwing an + # exception if we get a "false" back. However, I tried this, and it broke 1 test in scope_spec... + # and I don't have time to debug it further right now. --cprice 2012-03-15 send(method, *args) end def resolve_type_and_titles(type, titles) raise ArgumentError, "titles must be an array" unless titles.is_a?(Array) case type.downcase when "class" # resolve the titles titles = titles.collect do |a_title| hostclass = find_hostclass(a_title) hostclass ? hostclass.name : a_title end when "node" # no-op else # resolve the type resource_type = find_resource_type(type) type = resource_type.name if resource_type end return [type, titles] end private def extend_with_functions_module extend Puppet::Parser::Functions.environment_module(Puppet::Node::Environment.root) extend Puppet::Parser::Functions.environment_module(environment) if environment != Puppet::Node::Environment.root end end diff --git a/lib/puppet/property.rb b/lib/puppet/property.rb index 602fa0337..f0d62f044 100644 --- a/lib/puppet/property.rb +++ b/lib/puppet/property.rb @@ -1,372 +1,372 @@ # The virtual base class for properties, which are the self-contained building # blocks for actually doing work on the system. require 'puppet' require 'puppet/parameter' class Puppet::Property < Puppet::Parameter require 'puppet/property/ensure' # Because 'should' uses an array, we have a special method for handling # it. We also want to keep copies of the original values, so that # they can be retrieved and compared later when merging. attr_reader :shouldorig attr_writer :noop class << self attr_accessor :unmanaged attr_reader :name # Return array matching info, defaulting to just matching # the first value. def array_matching @array_matching ||= :first end # Set whether properties should match all values or just the first one. def array_matching=(value) value = value.intern if value.is_a?(String) raise ArgumentError, "Supported values for Property#array_matching are 'first' and 'all'" unless [:first, :all].include?(value) @array_matching = value end end # Look up a value's name, so we can find options and such. def self.value_name(name) if value = value_collection.match?(name) value.name end end # Retrieve an option set when a value was defined. def self.value_option(name, option) if value = value_collection.value(name) value.send(option) end end # Define a new valid value for a property. You must provide the value itself, # usually as a symbol, or a regex to match the value. # # The first argument to the method is either the value itself or a regex. # The second argument is an option hash; valid options are: # * :method: The name of the method to define. Defaults to 'set_'. # * :required_features: A list of features this value requires. # * :event: The event that should be returned when this value is set. # * :call: When to call any associated block. The default value # is `instead`, which means to call the value instead of calling the # provider. You can also specify `before` or `after`, which will # call both the block and the provider, according to the order you specify # (the `first` refers to when the block is called, not the provider). def self.newvalue(name, options = {}, &block) value = value_collection.newvalue(name, options, &block) define_method(value.method, &value.block) if value.method and value.block value end # Call the provider method. def call_provider(value) provider.send(self.class.name.to_s + "=", value) rescue NoMethodError self.fail "The #{provider.class.name} provider can not handle attribute #{self.class.name}" end # Call the dynamically-created method associated with our value, if # there is one. def call_valuemethod(name, value) if method = self.class.value_option(name, :method) and self.respond_to?(method) begin event = self.send(method) rescue Puppet::Error raise rescue => detail - error = Puppet::Error.new("Could not set '#{value} on #{self.class.name}: #{detail}", @resource.line, @resource.file) + error = Puppet::Error.new("Could not set '#{value}' on #{self.class.name}: #{detail}", @resource.line, @resource.file) error.set_backtrace detail.backtrace Puppet.log_exception(detail, error.message) raise error end elsif block = self.class.value_option(name, :block) # FIXME It'd be better here to define a method, so that # the blocks could return values. self.instance_eval(&block) else devfail "Could not find method for value '#{name}'" end end # How should a property change be printed as a string? def change_to_s(current_value, newvalue) begin if current_value == :absent return "defined '#{name}' as #{self.class.format_value_for_display should_to_s(newvalue)}" elsif newvalue == :absent or newvalue == [:absent] return "undefined '#{name}' from #{self.class.format_value_for_display is_to_s(current_value)}" else return "#{name} changed #{self.class.format_value_for_display is_to_s(current_value)} to #{self.class.format_value_for_display should_to_s(newvalue)}" end rescue Puppet::Error, Puppet::DevError raise rescue => detail message = "Could not convert change '#{name}' to string: #{detail}" Puppet.log_exception(detail, message) raise Puppet::DevError, message end end # Figure out which event to return. def event_name value = self.should event_name = self.class.value_option(value, :event) and return event_name name == :ensure or return (name.to_s + "_changed").to_sym return (resource.type.to_s + case value when :present; "_created" when :absent; "_removed" else "_changed" end).to_sym end # Return a modified form of the resource event. def event resource.event :name => event_name, :desired_value => should, :property => self, :source_description => path end attr_reader :shadow # initialize our property def initialize(hash = {}) super if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name) setup_shadow(klass) end end # Determine whether the property is in-sync or not. If @should is # not defined or is set to a non-true value, then we do not have # a valid value for it and thus consider the property to be in-sync # since we cannot fix it. Otherwise, we expect our should value # to be an array, and if @is matches any of those values, then # we consider it to be in-sync. # # Don't override this method. def safe_insync?(is) # If there is no @should value, consider the property to be in sync. return true unless @should # Otherwise delegate to the (possibly derived) insync? method. insync?(is) end def self.method_added(sym) raise "Puppet::Property#safe_insync? shouldn't be overridden; please override insync? instead" if sym == :safe_insync? end # This method may be overridden by derived classes if necessary # to provide extra logic to determine whether the property is in # sync. In most cases, however, only `property_matches?` needs to be # overridden to give the correct outcome - without reproducing all the array # matching logic, etc, found here. def insync?(is) self.devfail "#{self.class.name}'s should is not array" unless @should.is_a?(Array) # an empty array is analogous to no should values return true if @should.empty? # Look for a matching value, either for all the @should values, or any of # them, depending on the configuration of this property. if match_all? then # Emulate Array#== using our own comparison function. # A non-array was not equal to an array, which @should always is. return false unless is.is_a? Array # If they were different lengths, they are not equal. return false unless is.length == @should.length # Finally, are all the elements equal? In order to preserve the # behaviour of previous 2.7.x releases, we need to impose some fun rules # on "equality" here. # # Specifically, we need to implement *this* comparison: the two arrays # are identical if the is values are == the should values, or if the is # values are == the should values, stringified. # # This does mean that property equality is not commutative, and will not # work unless the `is` value is carefully arranged to match the should. return (is == @should or is == @should.map(&:to_s)) # When we stop being idiots about this, and actually have meaningful # semantics, this version is the thing we actually want to do. # # return is.zip(@should).all? {|a, b| property_matches?(a, b) } else return @should.any? {|want| property_matches?(is, want) } end end # Compare the current and desired value of a property in a property-specific # way. Invoked by `insync?`; this should be overridden if your property # has a different comparison type but does not actually differentiate the # overall insync? logic. def property_matches?(current, desired) # This preserves the older Puppet behaviour of doing raw and string # equality comparisons for all equality. I am not clear this is globally # desirable, but at least it is not a breaking change. --daniel 2011-11-11 current == desired or current == desired.to_s end # because the @should and @is vars might be in weird formats, # we need to set up a mechanism for pretty printing of the values # default to just the values, but this way individual properties can # override these methods def is_to_s(currentvalue) currentvalue end # Send a log message. def log(msg) Puppet::Util::Log.create( :level => resource[:loglevel], :message => msg, :source => self ) end # Should we match all values, or just the first? def match_all? self.class.array_matching == :all end # Execute our shadow's munge code, too, if we have one. def munge(value) self.shadow.munge(value) if self.shadow super end # each property class must define the name method, and property instances # do not change that name # this implicitly means that a given object can only have one property # instance of a given property class def name self.class.name end # for testing whether we should actually do anything def noop # This is only here to make testing easier. if @resource.respond_to?(:noop?) @resource.noop? else if defined?(@noop) @noop else Puppet[:noop] end end end # By default, call the method associated with the property name on our # provider. In other words, if the property name is 'gid', we'll call # 'provider.gid' to retrieve the current value. def retrieve provider.send(self.class.name) end # Set our value, using the provider, an associated block, or both. def set(value) # Set a name for looking up associated options like the event. name = self.class.value_name(value) call = self.class.value_option(name, :call) || :none if call == :instead call_valuemethod(name, value) elsif call == :none # They haven't provided a block, and our parent does not have # a provider, so we have no idea how to handle this. self.fail "#{self.class.name} cannot handle values of type #{value.inspect}" unless @resource.provider call_provider(value) else # LAK:NOTE 20081031 This is a change in behaviour -- you could # previously specify :call => [;before|:after], which would call # the setter *in addition to* the block. I'm convinced this # was never used, and it makes things unecessarily complicated. # If you want to specify a block and still call the setter, then # do so in the block. devfail "Cannot use obsolete :call value '#{call}' for property '#{self.class.name}'" end end # If there's a shadowing metaparam, instantiate it now. # This allows us to create a property or parameter with the # same name as a metaparameter, and the metaparam will only be # stored as a shadow. def setup_shadow(klass) @shadow = klass.new(:resource => self.resource) end # Only return the first value def should return nil unless defined?(@should) self.devfail "should for #{self.class.name} on #{resource.name} is not an array" unless @should.is_a?(Array) if match_all? return @should.collect { |val| self.unmunge(val) } else return self.unmunge(@should[0]) end end # Set the should value. def should=(values) values = [values] unless values.is_a?(Array) @shouldorig = values values.each { |val| validate(val) } @should = values.collect { |val| self.munge(val) } end def should_to_s(newvalue) [newvalue].flatten.join(" ") end def sync devfail "Got a nil value for should" unless should set(should) end # Verify that the passed value is valid. # If the developer uses a 'validate' hook, this method will get overridden. def unsafe_validate(value) super validate_features_per_value(value) end # Make sure that we've got all of the required features for a given value. def validate_features_per_value(value) if features = self.class.value_option(self.class.value_name(value), :required_features) features = Array(features) needed_features = features.collect { |f| f.to_s }.join(", ") raise ArgumentError, "Provider must have features '#{needed_features}' to set '#{self.class.name}' to '#{value}'" unless provider.satisfies?(features) end end # Just return any should value we might have. def value self.should end # Match the Parameter interface, but we really just use 'should' internally. # Note that the should= method does all of the validation and such. def value=(value) self.should = value end end diff --git a/lib/puppet/reports/rrdgraph.rb b/lib/puppet/reports/rrdgraph.rb index 517fa8f03..17cffc5e0 100644 --- a/lib/puppet/reports/rrdgraph.rb +++ b/lib/puppet/reports/rrdgraph.rb @@ -1,128 +1,128 @@ Puppet::Reports.register_report(:rrdgraph) do desc "Graph all available data about hosts using the RRD library. You must have the Ruby RRDtool library installed to use this report, which you can get from [the RubyRRDTool RubyForge page](http://rubyforge.org/projects/rubyrrdtool/). This package may also be available as `ruby-rrd` or `rrdtool-ruby` in your distribution's package management system. The library and/or package will both require the binary `rrdtool` package from your distribution to be installed. This report will create, manage, and graph RRD database files for each of the metrics generated during transactions, and it will create a few simple html files to display the reporting host's graphs. At this point, it will not create a common index file to display links to all hosts. All RRD files and graphs get created in the `rrddir` directory. If you want to serve these publicly, you should be able to just alias that directory in a web server. If you really know what you're doing, you can tune the `rrdinterval`, which defaults to the `runinterval`." def hostdir @hostdir ||= File.join(Puppet[:rrddir], self.host) end def htmlfile(type, graphs, field) file = File.join(hostdir, "#{type}.html") File.open(file, "w") do |of| of.puts "#{type.capitalize} graphs for #{host}" graphs.each do |graph| if field == :first name = graph.sub(/-\w+.png/, '').capitalize else name = graph.sub(/\w+-/, '').sub(".png", '').capitalize end of.puts "
" end of.puts "" end file end def mkhtml images = Dir.entries(hostdir).find_all { |d| d =~ /\.png/ } periodorder = %w{daily weekly monthly yearly} periods = {} types = {} images.each do |n| type, period = n.sub(".png", '').split("-") periods[period] ||= [] types[type] ||= [] periods[period] << n types[type] << n end files = [] # Make the period html files periodorder.each do |period| unless ary = periods[period] raise Puppet::Error, "Could not find graphs for #{period}" end files << htmlfile(period, ary, :first) end # make the type html files types.sort { |a,b| a[0] <=> b[0] }.each do |type, ary| newary = [] periodorder.each do |period| if graph = ary.find { |g| g.include?("-#{period}.png") } newary << graph else raise "Could not find #{type}-#{period} graph" end end files << htmlfile(type, newary, :second) end File.open(File.join(hostdir, "index.html"), "w") do |of| of.puts "Report graphs for #{host}" files.each do |file| of.puts "#{File.basename(file).sub(".html",'').capitalize}
" end of.puts "" end end def process(time = nil) time ||= Time.now.to_i unless File.directory?(hostdir) and FileTest.writable?(hostdir) # Some hackishness to create the dir with all of the right modes and ownership config = Puppet::Util::Settings.new - config.setdefaults(:reports, :hostdir => {:default => hostdir, :owner => 'service', :mode => 0755, :group => 'service', :desc => "eh"}) + config.define_settings(:reports, :hostdir => {:type => :directory, :default => hostdir, :owner => 'service', :mode => 0755, :group => 'service', :desc => "eh"}) # This creates the dir. config.use(:reports) end self.metrics.each do |name, metric| metric.basedir = hostdir if name == "time" timeclean(metric) end metric.store(time) metric.graph end mkhtml unless FileTest.exists?(File.join(hostdir, "index.html")) end # Unfortunately, RRD does not deal well with changing lists of values, # so we have to pick a list of values and stick with it. In this case, # that means we record the total time, the config time, and that's about # it. We should probably send each type's time as a separate metric. def timeclean(metric) metric.values = metric.values.find_all { |name, label, value| ['total', 'config_retrieval'].include?(name.to_s) } end end diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index e6f36bc97..3358b2018 100755 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -1,238 +1,242 @@ require 'net/http' require 'uri' require 'tempfile' require 'puppet/util/checksums' require 'puppet/network/http/api/v1' require 'puppet/network/http/compression' module Puppet Puppet::Type.type(:file).newproperty(:content) do include Puppet::Util::Diff include Puppet::Util::Checksums include Puppet::Network::HTTP::API::V1 include Puppet::Network::HTTP::Compression.module attr_reader :actual_content desc <<-EOT The desired contents of a file, as a string. This attribute is mutually exclusive with `source` and `target`. Newlines and tabs can be specified in double-quoted strings using standard escaped syntax --- \n for a newline, and \t for a tab. With very small files, you can construct content strings directly in the manifest... define resolve(nameserver1, nameserver2, domain, search) { $str = "search $search domain $domain nameserver $nameserver1 nameserver $nameserver2 " file { "/etc/resolv.conf": content => "$str", } } ...but for larger files, this attribute is more useful when combined with the [template](http://docs.puppetlabs.com/references/latest/function.html#template) function. EOT # Store a checksum as the value, rather than the actual content. # Simplifies everything. munge do |value| if value == :absent value elsif checksum?(value) # XXX This is potentially dangerous because it means users can't write a file whose # entire contents are a plain checksum value else @actual_content = value resource.parameter(:checksum).sum(value) end end # Checksums need to invert how changes are printed. def change_to_s(currentvalue, newvalue) # Our "new" checksum value is provided by the source. if source = resource.parameter(:source) and tmp = source.checksum newvalue = tmp end if currentvalue == :absent return "defined content as '#{newvalue}'" elsif newvalue == :absent return "undefined content from '#{currentvalue}'" else return "content changed '#{currentvalue}' to '#{newvalue}'" end end def checksum_type if source = resource.parameter(:source) result = source.checksum else checksum = resource.parameter(:checksum) result = resource[:checksum] end if result =~ /^\{(\w+)\}.+/ return $1.to_sym else return result end end def length (actual_content and actual_content.length) || 0 end def content self.should end # Override this method to provide diffs if asked for. # Also, fix #872: when content is used, and replace is true, the file # should be insync when it exists def insync?(is) if resource.should_be_file? return false if is == :absent else return true end return true if ! @resource.replace? result = super if ! result and Puppet[:show_diff] write_temporarily do |path| notice "\n" + diff(@resource[:path], path) end end result end def retrieve return :absent unless stat = @resource.stat ftype = stat.ftype # Don't even try to manage the content on directories or links return nil if ["directory","link"].include?(ftype) begin resource.parameter(:checksum).sum_file(resource[:path]) rescue => detail raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}" end end # Make sure we're also managing the checksum property. def should=(value) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end # Just write our content out to disk. def sync return_event = @resource.stat ? :file_changed : :file_created # We're safe not testing for the 'source' if there's no 'should' # because we wouldn't have gotten this far if there weren't at least # one valid value somewhere. @resource.write(:content) return_event end def write_temporarily tempfile = Tempfile.new("puppet-file") tempfile.open write(tempfile) tempfile.close yield tempfile.path tempfile.delete end def write(file) resource.parameter(:checksum).sum_stream { |sum| each_chunk_from(actual_content || resource.parameter(:source)) { |chunk| sum << chunk file.print chunk } } end + # TODO: this is another terrible, fragile means of determining whether or not to + # make a web request... it makes me tempted to get rid of the ":name" setting + # entirely... --cprice 2012-03-14 + def self.standalone? - Puppet.settings[:name] == "apply" + Puppet.settings[:name] == :apply end # the content is munged so if it's a checksum source_or_content is nil # unless the checksum indirectly comes from source def each_chunk_from(source_or_content) if source_or_content.is_a?(String) yield source_or_content elsif content_is_really_a_checksum? && source_or_content.nil? yield read_file_from_filebucket elsif source_or_content.nil? yield '' elsif self.class.standalone? yield source_or_content.content elsif source_or_content.local? chunk_file_from_disk(source_or_content) { |chunk| yield chunk } else chunk_file_from_source(source_or_content) { |chunk| yield chunk } end end private def content_is_really_a_checksum? checksum?(should) end def chunk_file_from_disk(source_or_content) File.open(source_or_content.full_path, "rb") do |src| while chunk = src.read(8192) yield chunk end end end def get_from_source(source_or_content, &block) request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path.sub(/^\//,'')) request.do_request(:fileserver) do |req| connection = Puppet::Network::HttpPool.http_instance(req.server, req.port) connection.request_get(indirection2uri(req), add_accept_encoding({"Accept" => "raw"}), &block) end end def chunk_file_from_source(source_or_content) get_from_source(source_or_content) do |response| case response.code when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } } else # Raise the http error if we didn't get a 'success' of some kind. message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}" raise Net::HTTPError.new(message, response) end end end def read_file_from_filebucket raise "Could not get filebucket from file" unless dipper = resource.bucket sum = should.sub(/\{\w+\}/, '') dipper.getfile(sum) rescue => detail fail "Could not retrieve content for #{should} from filebucket: #{detail}" end end end diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index c4207faee..736c1a96b 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,492 +1,508 @@ # A module to collect utility functions. require 'English' require 'puppet/util/monkey_patches' require 'puppet/external/lock' require 'puppet/error' require 'puppet/util/execution_stub' require 'uri' require 'sync' require 'monitor' require 'tempfile' require 'pathname' module Puppet module Util require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX @@sync_objects = {}.extend MonitorMixin def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end # Run some code with a specific environment. Resets the environment back to # what it was at the end of the code. def self.withenv(hash) saved = ENV.to_hash hash.each do |name, val| ENV[name.to_s] = val end yield ensure ENV.clear saved.each do |name, val| ENV[name] = val end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| begin dest = File.expand_path(File.join(dir, bin)) rescue ArgumentError => e # if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get # an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error # was thrown. raise e unless ((dir =~ /~/) && ((ENV['HOME'].nil? || ENV['HOME'] == ""))) # if we get here they have a tilde in their PATH. We'll issue a single warning about this and then # ignore this path element and carry on with our lives. Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.") next end if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. def absolute_path?(path, platform=nil) # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' name = '[^\\\\/]+' regexes = { :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, :posix => %r!^/!, } require 'puppet' platform ||= Puppet.features.microsoft_windows? ? :windows : :posix !! (path =~ regexes[platform]) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}" end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_path # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark # Because IO#binread is only available in 1.9 def binread(file) File.open(file, 'rb') { |f| f.read } end module_function :binread # utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors # will recognize as links to the line numbers in the trace) def self.pretty_backtrace(backtrace = caller(1)) backtrace.collect do |line| file_path, line_num = line.split(":") file_path = expand_symlinks(File.expand_path(file_path)) file_path + ":" + line_num end .join("\n") end # utility method that takes a path as input, checks each component of the path to see if it is a symlink, and expands # it if it is. returns the expanded path. def self.expand_symlinks(file_path) file_path.split("/").inject do |full_path, next_dir| next_path = full_path + "/" + next_dir if File.symlink?(next_path) then link = File.readlink(next_path) next_path = case link when /^\// then link else File.expand_path(full_path + "/" + link) end end next_path end end # Replace a file, securely. This takes a block, and passes it the file # handle of a file open for writing. Write the replacement content inside # the block and it will safely replace the target file. # # This method will make no changes to the target file until the content is # successfully written and the block returns without raising an error. # # As far as possible the state of the existing file, such as mode, is # preserved. This works hard to avoid loss of any metadata, but will result # in an inode change for the file. # # Arguments: `filename`, `default_mode` # # The filename is the file we are going to replace. # # The default_mode is the mode to use when the target file doesn't already # exist; if the file is present we copy the existing mode/owner/group values # across. def replace_file(file, default_mode, &block) raise Puppet::DevError, "replace_file requires a block" unless block_given? raise Puppet::DevError, "replace_file is non-functional on Windows" if Puppet.features.microsoft_windows? file = Pathname(file) tempfile = Tempfile.new(file.basename.to_s, file.dirname.to_s) file_exists = file.exist? # If the file exists, use its current mode/owner/group. If it doesn't, use # the supplied mode, and default to current user/group. if file_exists stat = file.lstat # We only care about the four lowest-order octets. Higher octets are # filesystem-specific. mode = stat.mode & 07777 uid = stat.uid gid = stat.gid else mode = default_mode uid = Process.euid gid = Process.egid end # Set properties of the temporary file before we write the content, because # Tempfile doesn't promise to be safe from reading by other people, just # that it avoids races around creating the file. tempfile.chmod(mode) tempfile.chown(uid, gid) # OK, now allow the caller to write the content of the file. yield tempfile # Now, make sure the data (which includes the mode) is safe on disk. tempfile.flush begin tempfile.fsync rescue NotImplementedError # fsync may not be implemented by Ruby on all platforms, but # there is absolutely no recovery path if we detect that. So, we just # ignore the return code. # # However, don't be fooled: that is accepting that we are running in # an unsafe fashion. If you are porting to a new platform don't stub # that out. end tempfile.close File.rename(tempfile.path, file) # Ideally, we would now fsync the directory as well, but Ruby doesn't # have support for that, and it doesn't matter /that/ much... # Return something true, and possibly useful. file end module_function :replace_file + + # Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to + # exit if the block throws an exception. + # + # @param [String] message a message to log if the block fails + # @param [Integer] code the exit code that the ruby interpreter should return if the block fails + # @yield + def exit_on_fail(message, code = 1) + yield + rescue ArgumentError, RuntimeError, NotImplementedError => detail + Puppet.log_exception(detail, "Could not #{message}: #{detail}") + exit(code) + end + module_function :exit_on_fail + + ####################################################################################################### # Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution ####################################################################################################### def execpipe(command, failonfail = true, &block) Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe") Puppet::Util::Execution.execpipe(command, failonfail, &block) end module_function :execpipe def execfail(command, exception) Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail") Puppet::Util::Execution.execfail(command, exception) end module_function :execfail def execute(command, arguments = {}) Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute") Puppet::Util::Execution.execute(command, arguments) end module_function :execute end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/autoload.rb b/lib/puppet/util/autoload.rb index 20b137905..0658b2a03 100644 --- a/lib/puppet/util/autoload.rb +++ b/lib/puppet/util/autoload.rb @@ -1,167 +1,199 @@ require 'pathname' require 'puppet/util/warnings' # Autoload paths, either based on names or all at once. class Puppet::Util::Autoload @autoloaders = {} @loaded = {} class << self attr_reader :autoloaders attr_accessor :loaded private :autoloaders, :loaded # List all loaded files. def list_loaded loaded.keys.sort { |a,b| a[0] <=> b[0] }.collect do |path, hash| "#{path}: #{hash[:file]}" end end # Has a given path been loaded? This is used for testing whether a # changed file should be loaded or just ignored. This is only # used in network/client/master, when downloading plugins, to # see if a given plugin is currently loaded and thus should be # reloaded. def loaded?(path) path = cleanpath(path).chomp('.rb') loaded.include?(path) end # Save the fact that a given path has been loaded. This is so # we can load downloaded plugins if they've already been loaded # into memory. def mark_loaded(name, file) name = cleanpath(name) $LOADED_FEATURES << name + ".rb" unless $LOADED_FEATURES.include?(name) loaded[name] = [file, File.mtime(file)] end def changed?(name) name = cleanpath(name) return true unless loaded.include?(name) file, old_mtime = loaded[name] return true unless file == get_file(name) begin old_mtime != File.mtime(file) rescue Errno::ENOENT true end end # Load a single plugin by name. We use 'load' here so we can reload a # given plugin. def load_file(name, env=nil) file = get_file(name.to_s, env) return false unless file begin mark_loaded(name, file) Kernel.load file, @wrap return true rescue SystemExit,NoMemoryError raise rescue Exception => detail message = "Could not autoload #{name}: #{detail}" Puppet.log_exception(detail, message) raise Puppet::Error, message end end def loadall(path) # Load every instance of everything we can find. files_to_load(path).each do |file| name = file.chomp(".rb") load_file(name) unless loaded?(name) end end def reload_changed loaded.keys.each { |file| load_file(file) if changed?(file) } end # Get the correct file to load for a given path # returns nil if no file is found def get_file(name, env=nil) name = name + '.rb' unless name =~ /\.rb$/ dirname, base = File.split(name) path = search_directories(env).find { |dir| File.exist?(File.join(dir, name)) } path and File.join(path, name) end def files_to_load(path) search_directories.map {|dir| files_in_dir(dir, path) }.flatten.uniq end def files_in_dir(dir, path) dir = Pathname.new(dir) Dir.glob(File.join(dir, path, "*.rb")).collect do |file| Pathname.new(file).relative_path_from(dir).to_s end end def module_directories(env=nil) # We have to require this late in the process because otherwise we might have # load order issues. require 'puppet/node/environment' real_env = Puppet::Node::Environment.new(env) # We're using a per-thread cache of module directories so that we don't # scan the filesystem each time we try to load something. This is reset # at the beginning of compilation and at the end of an agent run. Thread.current[:env_module_directories] ||= {} - Thread.current[:env_module_directories][real_env] ||= real_env.modulepath.collect do |dir| + + + # This is a little bit of a hack. Basically, the autoloader is being called indirectly during application + # bootstrapping when we do things such as check "features". However, during bootstrapping, we haven't + # yet parsed all of the command line parameters nor the config files, and thus we don't yet know with certainty + # what the module path is. This should be irrelevant during bootstrapping, because anything that we are attempting + # to load during bootstrapping should be something that we ship with puppet, and thus the module path is irrelevant. + # + # In the long term, I think the way that we want to handle this is to have the autoloader ignore the module path + # in all cases where it is not specifically requested (e.g., by a constructor param or something)... because there + # are very few cases where we should actually be loading code from the module path. However, until that happens, + # we at least need a way to prevent the autoloader from attempting to access the module path before it is + # initialized. For now we are accomplishing that by calling the "app_defaults_initialized?" method on the + # main puppet Settings object. --cprice 2012-03-16 + if Puppet.settings.app_defaults_initialized? + # if the app defaults have been initialized then it should be safe to access the module path setting. + Thread.current[:env_module_directories][real_env] ||= real_env.modulepath.collect do |dir| Dir.entries(dir).reject { |f| f =~ /^\./ }.collect { |f| File.join(dir, f) } end.flatten.collect { |d| [File.join(d, "plugins"), File.join(d, "lib")] }.flatten.find_all do |d| FileTest.directory?(d) end + else + # if we get here, the app defaults have not been initialized, so we basically use an empty module path. + Thread.current[:env_module_directories][real_env] = [] + end + + + end + + def libdirs() + # See the comments in #module_directories above. Basically, we need to be careful not to try to access the + # libdir before we know for sure that all of the settings have been initialized (e.g., during bootstrapping). + if (Puppet.settings.app_defaults_initialized?) + Puppet[:libdir].split(File::PATH_SEPARATOR) + else + [] + end end def search_directories(env=nil) - [module_directories(env), Puppet[:libdir].split(File::PATH_SEPARATOR), $LOAD_PATH].flatten + [module_directories(env), libdirs(), $LOAD_PATH].flatten end # Normalize a path. This converts ALT_SEPARATOR to SEPARATOR on Windows # and eliminates unnecessary parts of a path. def cleanpath(path) Pathname.new(path).cleanpath.to_s end end # Send [] and []= to the @autoloaders hash Puppet::Util.classproxy self, :autoloaders, "[]", "[]=" attr_accessor :object, :path, :objwarn, :wrap def initialize(obj, path, options = {}) @path = path.to_s raise ArgumentError, "Autoload paths cannot be fully qualified" if @path !~ /^\w/ @object = obj self.class[obj] = self options.each do |opt, value| begin self.send(opt.to_s + "=", value) rescue NoMethodError raise ArgumentError, "#{opt} is not a valid option" end end @wrap = true unless defined?(@wrap) end def load(name, env=nil) self.class.load_file(File.join(@path, name.to_s), env) end # Load all instances that we can. This uses require, rather than load, # so that already-loaded files don't get reloaded unnecessarily. def loadall self.class.loadall(@path) end def files_to_load self.class.files_to_load(@path) end end diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb index 409a08e26..4200f7a34 100644 --- a/lib/puppet/util/command_line.rb +++ b/lib/puppet/util/command_line.rb @@ -1,185 +1,184 @@ +require 'puppet' require "puppet/util/plugins" +require 'puppet/util/command_line/puppet_option_parser' module Puppet module Util class CommandLine - LegacyName = Hash.new{|h,k| k}.update( - 'agent' => 'puppetd', - 'cert' => 'puppetca', - 'doc' => 'puppetdoc', - 'filebucket' => 'filebucket', - 'apply' => 'puppet', - 'describe' => 'pi', - 'queue' => 'puppetqd', - 'resource' => 'ralsh', - 'kick' => 'puppetrun', - 'master' => 'puppetmasterd', - 'device' => 'puppetdevice' - ) - def initialize(zero = $0, argv = ARGV, stdin = STDIN) @zero = zero @argv = argv.dup @stdin = stdin @subcommand_name, @args = subcommand_and_args(@zero, @argv, @stdin) Puppet::Plugins.on_commandline_initialization(:command_line_object => self) end attr :subcommand_name attr :args def appdir File.join('puppet', 'application') end + # This method is called during application bootstrapping. It is responsible for parsing all of the + # command line options and initializing the values in Puppet.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 + # Create an option parser + option_parser = PuppetOptionParser.new + option_parser.ignore_invalid_options = true + + # Add all global options to it. + Puppet.settings.optparse_addargs([]).each do |option| + option_parser.on(*option) do |arg| + handlearg(option[0], arg) + + end + end + + option_parser.parse(args) + + end + private :parse_global_options + + + # Private utility method; this is the callback that the OptionParser will use when it finds + # an option that was defined in Puppet.settings. All that this method does is a little bit + # of clanup to get the option into the exact format that Puppet.settings expects it to be in, + # and then passes it along to Puppet.settings. + # + # @param [String] opt the command-line option that was matched + # @param [String, TrueClass, FalseClass] the value for the setting (as determined by the OptionParser) + def handlearg(opt, val) + opt, val = self.class.clean_opt(opt, val) + Puppet.settings.handlearg(opt, val) + end + private :handlearg + + # 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] 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 self.available_subcommands + # Eventually we probably want to replace this with a call to the autoloader. however, at the moment + # the autoloader considers the module path when loading, and we don't want to allow apps / faces to load + # from there. Once that is resolved, this should be replaced. --cprice 2012-03-06 absolute_appdirs = $LOAD_PATH.collect do |x| File.join(x,'puppet','application') end.select{ |x| File.directory?(x) } absolute_appdirs.inject([]) do |commands, dir| commands + Dir[File.join(dir, '*.rb')].map{|fn| File.basename(fn, '.rb')} end.uniq end # available_subcommands was previously an instance method, not a class # method, and we have an unknown number of user-implemented applications # that depend on that behaviour. Forwarding allows us to preserve a # backward compatible API. --daniel 2011-04-11 def available_subcommands self.class.available_subcommands end def require_application(application) require File.join(appdir, application) end + # This is the main entry point for all puppet applications / faces; it is basically where the bootstrapping + # process / lifecycle of an app begins. def execute + # The first two phases of the lifecycle of a puppet application are: + # 1) To parse the command line options and handle any of them that are registered, defined "global" puppet + # settings (mostly from defaults.rb).) + # 2) To parse the puppet config file(s). + # + # These 2 steps are being handled explicitly here. If there ever arises a situation where they need to be + # triggered from outside of this class, without triggering the rest of the lifecycle--we might want to move them + # out into a separate method that we call from here. However, this seems to be sufficient for now. + # --cprice 2012-03-16 + + # Here's step 1. + Puppet::Util.exit_on_fail("parse global options") { parse_global_options } + + # Here's step 2. NOTE: this is a change in behavior where we are now parsing the config file on every run; + # before, there were several apps that specifically registered themselves as not requiring anything from + # the config file. The fact that we're always parsing it now might be a small performance hit, but it was + # necessary in order to make sure that we can resolve the libdir before we look for the available applications. + Puppet::Util.exit_on_fail("parse configuration file") { Puppet.settings.parse } + + # OK, now that we've processed the command line options and the config files, we should be able to say that + # we definitively know where the libdir is... which means that we can now look for our available + # applications / subcommands / faces. + if subcommand_name and available_subcommands.include?(subcommand_name) then require_application subcommand_name + # This will need to be cleaned up to do something that is not so application-specific + # (i.e.. so that we can load faces). Longer-term, use the autoloader. See comments in + # #available_subcommands method above. --cprice 2012-03-06 app = Puppet::Application.find(subcommand_name).new(self) Puppet::Plugins.on_application_initialization(:appliation_object => self) - # See the note in 'warn_later' down below. --daniel 2011-06-01 - if $delayed_deprecation_warning_for_p_u_cl.is_a? String then - Puppet.deprecation_warning($delayed_deprecation_warning_for_p_u_cl) - $delayed_deprecation_warning_for_p_u_cl = true - end - app.run elsif ! execute_external_subcommand then unless subcommand_name.nil? then puts "Error: Unknown Puppet subcommand '#{subcommand_name}'" end puts "See 'puppet help' for help on available puppet subcommands" end end def execute_external_subcommand external_command = "puppet-#{subcommand_name}" require 'puppet/util' path_to_subcommand = Puppet::Util.which(external_command) return false unless path_to_subcommand exec(path_to_subcommand, *args) end def legacy_executable_name - LegacyName[ subcommand_name ] + name = CommandLine::LegacyCommandLine::LEGACY_NAMES[ subcommand_name.intern ] + return name unless name.nil? + return subcommand_name.intern end private def subcommand_and_args(zero, argv, stdin) zero = File.basename(zero, '.rb') if zero == 'puppet' case argv.first - when nil then - if stdin.tty? then - [nil, argv] # ttys get usage info + # if they didn't pass a command, or passed a help flag, we will fall back to showing a usage message. + # we no longer default to 'apply' + when nil, "--help", "-h", /^-|\.pp$|\.rb$/ + [nil, argv] else - # Killed for 2.7.0 --daniel 2011-06-01 - warn_later < LegacyApp.new(:agent, :agent), + :puppetca => LegacyApp.new(:cert, :master), + :puppetdoc => LegacyApp.new(:doc, :master), + :filebucket => LegacyApp.new(:filebucket, DefaultRunMode), + :puppet => LegacyApp.new(:apply, DefaultRunMode), + :pi => LegacyApp.new(:describe, DefaultRunMode), + :puppetqd => LegacyApp.new(:queue, DefaultRunMode), + :ralsh => LegacyApp.new(:resource, DefaultRunMode), + :puppetrun => LegacyApp.new(:kick, DefaultRunMode), + :puppetmasterd => LegacyApp.new(:master, :master), + :puppetdevice => LegacyApp.new(:device, :agent), + } + + LEGACY_NAMES = LEGACY_APPS.inject({}) do |result, entry| + key, app = *entry + result[app.name] = key + result + end + end + end + end +end \ No newline at end of file diff --git a/lib/puppet/util/command_line/puppet_option_parser.rb b/lib/puppet/util/command_line/puppet_option_parser.rb new file mode 100644 index 000000000..0fbf9a264 --- /dev/null +++ b/lib/puppet/util/command_line/puppet_option_parser.rb @@ -0,0 +1,95 @@ +module Puppet + module Util + class CommandLine + + class PuppetOptionError < Puppet::Error + end + + class PuppetUnrecognizedOptionError < PuppetOptionError + end + + # This is a command line option parser. It is intended to have an API that is very similar to + # the ruby stdlib 'OptionParser' API, for ease of integration into our existing code... however, + # However, we've removed the OptionParser-based implementation and are only maintaining the + # it's impilemented based on the third-party "trollop" library. This was done because there + # are places where the stdlib OptionParser is not flexible enough to meet our needs. + + class PuppetOptionParser + def initialize(usage_msg = nil) + require "puppet/util/command_line/trollop" + + @create_default_short_options = false + + @wrapped_parser = ::Trollop::Parser.new do + banner usage_msg + create_default_short_options = false + handle_help_and_version = false + end + + end + + # This parameter, if set, will tell the underlying option parser not to throw an + # exception if we pass it options that weren't explicitly registered. We need this + # capability because we need to be able to pass all of the command-line options before + # we know which application/face they are going to be running, but the app/face + # may specify additional command-line arguments that are valid for that app/face. + attr_reader :ignore_invalid_options + + def ignore_invalid_options=(value) + @wrapped_parser.ignore_invalid_options = value + end + + + def on(*args, &block) + # This is ugly and I apologize :) + # I wanted to keep the API for this class compatible with how we were previously + # interacting with the ruby stdlib OptionParser. Unfortunately, that means that + # you can specify options as an array, with three or four elements. The 2nd element + # is an optional "short" representation. This series of shift/pop operations seemed + # the easiest way to avoid breaking compatibility with that syntax. + + # The first argument is always the "--long" representation... + long = args.shift + + # The last argument is always the "type" + type = args.pop + # The second-to-last argument is always the "description" + desc = args.pop + + # if there is anything left, it's the "short" representation. + short = args.shift + + options = { + :long => long, + :short => short, + :required => false, + :callback => block, + } + + case type + when :REQUIRED + options[:type] = :string + when :NONE + options[:type] = :flag + else + raise PuppetOptionError.new("Unsupported type: '#{type}'") + end + + @wrapped_parser.opt long.sub("^--", "").intern, desc, options + end + + def parse(*args) + args = args[0] if args.size == 1 and Array === args[0] + args_copy = args.dup + begin + @wrapped_parser.parse args_copy + rescue ::Trollop::CommandlineError => err + raise PuppetUnrecognizedOptionError.new(err) if err.message =~ /^unknown argument/ + end + end + end + + + end + end +end \ No newline at end of file diff --git a/lib/puppet/util/command_line/trollop.rb b/lib/puppet/util/command_line/trollop.rb new file mode 100644 index 000000000..1402dfbe1 --- /dev/null +++ b/lib/puppet/util/command_line/trollop.rb @@ -0,0 +1,811 @@ +## lib/trollop.rb -- trollop command-line processing library +## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net) +## Copyright:: Copyright 2007 William Morgan +## License:: the same terms as ruby itself + +require 'date' + +module Trollop + +VERSION = "1.16.2" + +## Thrown by Parser in the event of a commandline error. Not needed if +## you're using the Trollop::options entry. +class CommandlineError < StandardError; end + +## Thrown by Parser if the user passes in '-h' or '--help'. Handled +## automatically by Trollop#options. +class HelpNeeded < StandardError; end + +## Thrown by Parser if the user passes in '-h' or '--version'. Handled +## automatically by Trollop#options. +class VersionNeeded < StandardError; end + +## Regex for floating point numbers +FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/ + +## Regex for parameters +PARAM_RE = /^-(-|\.$|[^\d\.])/ + +## The commandline parser. In typical usage, the methods in this class +## will be handled internally by Trollop::options. In this case, only the +## #opt, #banner and #version, #depends, and #conflicts methods will +## typically be called. +## +## If you want to instantiate this class yourself (for more complicated +## argument-parsing logic), call #parse to actually produce the output hash, +## and consider calling it from within +## Trollop::with_standard_exception_handling. +class Parser + + ## The set of values that indicate a flag option when passed as the + ## +:type+ parameter of #opt. + FLAG_TYPES = [:flag, :bool, :boolean] + + ## The set of values that indicate a single-parameter (normal) option when + ## passed as the +:type+ parameter of #opt. + ## + ## A value of +io+ corresponds to a readable IO resource, including + ## a filename, URI, or the strings 'stdin' or '-'. + SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date] + + ## The set of values that indicate a multiple-parameter option (i.e., that + ## takes multiple space-separated values on the commandline) when passed as + ## the +:type+ parameter of #opt. + MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates] + + ## The complete set of legal values for the +:type+ parameter of #opt. + TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES + + INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc: + + ## The values from the commandline that were not interpreted by #parse. + attr_reader :leftovers + + ## The complete configuration hashes for each option. (Mainly useful + ## for testing.) + attr_reader :specs + + ## A flag that determines whether or not to attempt to automatically generate "short" options if they are not + ## explicitly specified. + attr_accessor :create_default_short_options + + ## A flag that determines whether or not to raise an error if the parser is passed one or more + ## options that were not registered ahead of time. If 'true', then the parser will simply + ## ignore options that it does not recognize. + attr_accessor :ignore_invalid_options + + ## A flag indicating whether or not the parser should attempt to handle "--help" and + ## "--version" specially. If 'false', it will treat them just like any other option. + attr_accessor :handle_help_and_version + + ## Initializes the parser, and instance-evaluates any block given. + def initialize *a, &b + @version = nil + @leftovers = [] + @specs = {} + @long = {} + @short = {} + @order = [] + @constraints = [] + @stop_words = [] + @stop_on_unknown = false + + #instance_eval(&b) if b # can't take arguments + cloaker(&b).bind(self).call(*a) if b + end + + ## Define an option. +name+ is the option name, a unique identifier + ## for the option that you will use internally, which should be a + ## symbol or a string. +desc+ is a string description which will be + ## displayed in help messages. + ## + ## Takes the following optional arguments: + ## + ## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s. + ## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+. + ## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given. + ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the the commandline the value will be +false+. + ## [+:required+] If set to +true+, the argument must be provided on the commandline. + ## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.) + ## + ## Note that there are two types of argument multiplicity: an argument + ## can take multiple values, e.g. "--arg 1 2 3". An argument can also + ## be allowed to occur multiple times, e.g. "--arg 1 --arg 2". + ## + ## Arguments that take multiple values should have a +:type+ parameter + ## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+ + ## value of an array of the correct type (e.g. [String]). The + ## value of this argument will be an array of the parameters on the + ## commandline. + ## + ## Arguments that can occur multiple times should be marked with + ## +:multi+ => +true+. The value of this argument will also be an array. + ## In contrast with regular non-multi options, if not specified on + ## the commandline, the default value will be [], not nil. + ## + ## These two attributes can be combined (e.g. +:type+ => +:strings+, + ## +:multi+ => +true+), in which case the value of the argument will be + ## an array of arrays. + ## + ## There's one ambiguous case to be aware of: when +:multi+: is true and a + ## +:default+ is set to an array (of something), it's ambiguous whether this + ## is a multi-value argument as well as a multi-occurrence argument. + ## In thise case, Trollop assumes that it's not a multi-value argument. + ## If you want a multi-value, multi-occurrence argument with a default + ## value, you must specify +:type+ as well. + + def opt name, desc="", opts={} + raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name + + ## fill in :type + opts[:type] = # normalize + case opts[:type] + when :boolean, :bool; :flag + when :integer; :int + when :integers; :ints + when :double; :float + when :doubles; :floats + when Class + case opts[:type].name + when 'TrueClass', 'FalseClass'; :flag + when 'String'; :string + when 'Integer'; :int + when 'Float'; :float + when 'IO'; :io + when 'Date'; :date + else + raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'" + end + when nil; nil + else + raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type]) + opts[:type] + end + + ## for options with :multi => true, an array default doesn't imply + ## a multi-valued argument. for that you have to specify a :type + ## as well. (this is how we disambiguate an ambiguous situation; + ## see the docs for Parser#opt for details.) + disambiguated_default = + if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type] + opts[:default].first + else + opts[:default] + end + + type_from_default = + case disambiguated_default + when Integer; :int + when Numeric; :float + when TrueClass, FalseClass; :flag + when String; :string + when IO; :io + when Date; :date + when Array + if opts[:default].empty? + raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'" + end + case opts[:default][0] # the first element determines the types + when Integer; :ints + when Numeric; :floats + when String; :strings + when IO; :ios + when Date; :dates + else + raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'" + end + when nil; nil + else + raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'" + end + + raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default + + opts[:type] = opts[:type] || type_from_default || :flag + + ## fill in :long + opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-") + opts[:long] = + case opts[:long] + when /^--([^-].*)$/ + $1 + when /^[^-]/ + opts[:long] + else + raise ArgumentError, "invalid long option name #{opts[:long].inspect}" + end + raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]] + + ## fill in :short + opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none + opts[:short] = case opts[:short] + when /^-(.)$/; $1 + when nil, :none, /^.$/; opts[:short] + else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'" + end + + if opts[:short] + raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]] + raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX + end + + ## fill in :default for flags + opts[:default] = false if opts[:type] == :flag && opts[:default].nil? + + ## autobox :default for :multi (multi-occurrence) arguments + opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array) + + ## fill in :multi + opts[:multi] ||= false + + opts[:desc] ||= desc + @long[opts[:long]] = name + @short[opts[:short]] = name if opts[:short] && opts[:short] != :none + @specs[name] = opts + @order << [:opt, name] + end + + ## Sets the version string. If set, the user can request the version + ## on the commandline. Should probably be of the form " + ## ". + def version s=nil; @version = s if s; @version end + + ## Adds text to the help display. Can be interspersed with calls to + ## #opt to build a multi-section help page. + def banner s; @order << [:text, s] end + alias :text :banner + + ## Marks two (or more!) options as requiring each other. Only handles + ## undirected (i.e., mutual) dependencies. Directed dependencies are + ## better modeled with Trollop::die. + def depends *syms + syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } + @constraints << [:depends, syms] + end + + ## Marks two (or more!) options as conflicting. + def conflicts *syms + syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] } + @constraints << [:conflicts, syms] + end + + ## Defines a set of words which cause parsing to terminate when + ## encountered, such that any options to the left of the word are + ## parsed as usual, and options to the right of the word are left + ## intact. + ## + ## A typical use case would be for subcommand support, where these + ## would be set to the list of subcommands. A subsequent Trollop + ## invocation would then be used to parse subcommand options, after + ## shifting the subcommand off of ARGV. + def stop_on *words + @stop_words = [*words].flatten + end + + ## Similar to #stop_on, but stops on any unknown word when encountered + ## (unless it is a parameter for an argument). This is useful for + ## cases where you don't know the set of subcommands ahead of time, + ## i.e., without first parsing the global options. + def stop_on_unknown + @stop_on_unknown = true + end + + ## Parses the commandline. Typically called by Trollop::options, + ## but you can call it directly if you need more control. + ## + ## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions. + def parse cmdline=ARGV + vals = {} + required = {} + + if handle_help_and_version + opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"] + opt :help, "Show this message" unless @specs[:help] || @long["help"] + end + + @specs.each do |sym, opts| + required[sym] = true if opts[:required] + vals[sym] = opts[:default] + vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil + end + + resolve_default_short_options if create_default_short_options + + ## resolve symbols + given_args = {} + @leftovers = each_arg cmdline do |arg, params| + sym = case arg + when /^-([^-])$/ + @short[$1] + when /^--no-([^-]\S*)$/ + @long["[no-]#{$1}"] + when /^--([^-]\S*)$/ + @long[$1] ? @long[$1] : @long["[no-]#{$1}"] + else + raise CommandlineError, "invalid argument syntax: '#{arg}'" + end + + unless sym + next 0 if ignore_invalid_options + raise CommandlineError, "unknown argument '#{arg}'" unless sym + end + + if given_args.include?(sym) && !@specs[sym][:multi] + raise CommandlineError, "option '#{arg}' specified multiple times" + end + + given_args[sym] ||= {} + + given_args[sym][:arg] = arg + given_args[sym][:params] ||= [] + + # The block returns the number of parameters taken. + num_params_taken = 0 + + unless params.nil? + if SINGLE_ARG_TYPES.include?(@specs[sym][:type]) + given_args[sym][:params] << params[0, 1] # take the first parameter + num_params_taken = 1 + elsif MULTI_ARG_TYPES.include?(@specs[sym][:type]) + given_args[sym][:params] << params # take all the parameters + num_params_taken = params.size + end + end + + num_params_taken + end + + if handle_help_and_version + ## check for version and help args + raise VersionNeeded if given_args.include? :version + raise HelpNeeded if given_args.include? :help + end + + ## check constraint satisfaction + @constraints.each do |type, syms| + constraint_sym = syms.find { |sym| given_args[sym] } + next unless constraint_sym + + case type + when :depends + syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym } + when :conflicts + syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) } + end + end + + required.each do |sym, val| + raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym + end + + ## parse parameters + given_args.each do |sym, given_data| + arg = given_data[:arg] + params = given_data[:params] + + opts = @specs[sym] + raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag + + vals["#{sym}_given".intern] = true # mark argument as specified on the commandline + + case opts[:type] + when :flag + if arg =~ /^--no-/ and sym.to_s =~ /^--\[no-\]/ + vals[sym] = opts[:default] + else + vals[sym] = !opts[:default] + end + when :int, :ints + vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } } + when :float, :floats + vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } } + when :string, :strings + vals[sym] = params.map { |pg| pg.map { |p| p.to_s } } + when :io, :ios + vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } } + when :date, :dates + vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } } + end + + if SINGLE_ARG_TYPES.include?(opts[:type]) + unless opts[:multi] # single parameter + vals[sym] = vals[sym][0][0] + else # multiple options, each with a single parameter + vals[sym] = vals[sym].map { |p| p[0] } + end + elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi] + vals[sym] = vals[sym][0] # single option, with multiple parameters + end + # else: multiple options, with multiple parameters + + opts[:callback].call(vals[sym]) if opts.has_key?(:callback) + end + + ## modify input in place with only those + ## arguments we didn't process + cmdline.clear + @leftovers.each { |l| cmdline << l } + + ## allow openstruct-style accessors + class << vals + def method_missing(m, *args) + self[m] || self[m.to_s] + end + end + vals + end + + def parse_date_parameter param, arg #:nodoc: + begin + begin + time = Chronic.parse(param) + rescue NameError + # chronic is not available + end + time ? Date.new(time.year, time.month, time.day) : Date.parse(param) + rescue ArgumentError => e + raise CommandlineError, "option '#{arg}' needs a date" + end + end + + ## Print the help message to +stream+. + def educate stream=$stdout + width # just calculate it now; otherwise we have to be careful not to + # call this unless the cursor's at the beginning of a line. + + left = {} + @specs.each do |name, spec| + left[name] = "--#{spec[:long]}" + + (spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") + + case spec[:type] + when :flag; "" + when :int; " " + when :ints; " " + when :string; " " + when :strings; " " + when :float; " " + when :floats; " " + when :io; " " + when :ios; " " + when :date; " " + when :dates; " " + end + end + + leftcol_width = left.values.map { |s| s.length }.max || 0 + rightcol_start = leftcol_width + 6 # spaces + + unless @order.size > 0 && @order.first.first == :text + stream.puts "#@version\n" if @version + stream.puts "Options:" + end + + @order.each do |what, opt| + if what == :text + stream.puts wrap(opt) + next + end + + spec = @specs[opt] + stream.printf " %#{leftcol_width}s: ", left[opt] + desc = spec[:desc] + begin + default_s = case spec[:default] + when $stdout; "" + when $stdin; "" + when $stderr; "" + when Array + spec[:default].join(", ") + else + spec[:default].to_s + end + + if spec[:default] + if spec[:desc] =~ /\.$/ + " (Default: #{default_s})" + else + " (default: #{default_s})" + end + else + "" + end + end + stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start) + end + end + + def width #:nodoc: + @width ||= if $stdout.tty? + begin + require 'curses' + Curses::init_screen + x = Curses::cols + Curses::close_screen + x + rescue Exception + 80 + end + else + 80 + end + end + + def wrap str, opts={} # :nodoc: + if str == "" + [""] + else + str.split("\n").map { |s| wrap_line s, opts }.flatten + end + end + + ## The per-parser version of Trollop::die (see that for documentation). + def die arg, msg + if msg + $stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}." + else + $stderr.puts "Error: #{arg}." + end + $stderr.puts "Try --help for help." + exit(-1) + end + +private + + ## yield successive arg, parameter pairs + def each_arg args + remains = [] + i = 0 + + until i >= args.length + if @stop_words.member? args[i] + remains += args[i .. -1] + return remains + end + case args[i] + when /^--$/ # arg terminator + remains += args[(i + 1) .. -1] + return remains + when /^--(\S+?)=(.*)$/ # long argument with equals + yield "--#{$1}", [$2] + i += 1 + when /^--(\S+)$/ # long argument + params = collect_argument_parameters(args, i + 1) + unless params.empty? + num_params_taken = yield args[i], params + unless num_params_taken + if @stop_on_unknown + remains += args[i + 1 .. -1] + return remains + else + remains += params + end + end + i += 1 + num_params_taken + else # long argument no parameter + yield args[i], nil + i += 1 + end + when /^-(\S+)$/ # one or more short arguments + shortargs = $1.split(//) + shortargs.each_with_index do |a, j| + if j == (shortargs.length - 1) + params = collect_argument_parameters(args, i + 1) + unless params.empty? + num_params_taken = yield "-#{a}", params + unless num_params_taken + if @stop_on_unknown + remains += args[i + 1 .. -1] + return remains + else + remains += params + end + end + i += 1 + num_params_taken + else # argument no parameter + yield "-#{a}", nil + i += 1 + end + else + yield "-#{a}", nil + end + end + else + if @stop_on_unknown + remains += args[i .. -1] + return remains + else + remains << args[i] + i += 1 + end + end + end + + remains + end + + def parse_integer_parameter param, arg + raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/ + param.to_i + end + + def parse_float_parameter param, arg + raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE + param.to_f + end + + def parse_io_parameter param, arg + case param + when /^(stdin|-)$/i; $stdin + else + require 'open-uri' + begin + open param + rescue SystemCallError => e + raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}" + end + end + end + + def collect_argument_parameters args, start_at + params = [] + pos = start_at + while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do + params << args[pos] + pos += 1 + end + params + end + + def resolve_default_short_options + @order.each do |type, name| + next unless type == :opt + opts = @specs[name] + next if opts[:short] + + c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) } + if c # found a character to use + opts[:short] = c + @short[c] = name + end + end + end + + def wrap_line str, opts={} + prefix = opts[:prefix] || 0 + width = opts[:width] || (self.width - 1) + start = 0 + ret = [] + until start > str.length + nextt = + if start + width >= str.length + str.length + else + x = str.rindex(/\s/, start + width) + x = str.index(/\s/, start) if x && x < start + x || str.length + end + ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt] + start = nextt + 1 + end + ret + end + + ## instance_eval but with ability to handle block arguments + ## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html + def cloaker &b + (class << self; self; end).class_eval do + define_method :cloaker_, &b + meth = instance_method :cloaker_ + remove_method :cloaker_ + meth + end + end +end + +## The easy, syntactic-sugary entry method into Trollop. Creates a Parser, +## passes the block to it, then parses +args+ with it, handling any errors or +## requests for help or version information appropriately (and then exiting). +## Modifies +args+ in place. Returns a hash of option values. +## +## The block passed in should contain zero or more calls to +opt+ +## (Parser#opt), zero or more calls to +text+ (Parser#text), and +## probably a call to +version+ (Parser#version). +## +## The returned block contains a value for every option specified with +## +opt+. The value will be the value given on the commandline, or the +## default value if the option was not specified on the commandline. For +## every option specified on the commandline, a key "