require 'timeout' require 'inifile' require 'resolv' module Beaker module DSL module Helpers # Methods that help you interact with your puppet installation, puppet must be installed # for these methods to execute correctly module PuppetHelpers # @!macro [new] common_opts # @param [Hash{Symbol=>String}] opts Options to alter execution. # @option opts [Boolean] :silent (false) Do not produce log output # @option opts [Array] :acceptable_exit_codes ([0]) An array # (or range) of integer exit codes that should be considered # acceptable. An error will be thrown if the exit code does not # match one of the values in this list. # @option opts [Boolean] :accept_all_exit_codes (false) Consider all # exit codes as passing. # @option opts [Boolean] :dry_run (false) Do not actually execute any # commands on the SUT # @option opts [String] :stdin (nil) Input to be provided during command # execution on the SUT. # @option opts [Boolean] :pty (false) Execute this command in a pseudoterminal. # @option opts [Boolean] :expect_connection_failure (false) Expect this command # to result in a connection failure, reconnect and continue execution. # @option opts [Hash{String=>String}] :environment ({}) These will be # treated as extra environment variables that should be set before # running the command. # # Return the name of the puppet user. # # @param [Host] host One object that acts like a Beaker::Host # # @note This method assumes puppet is installed on the host. # def puppet_user(host) return host.puppet('master')['user'] end # Return the name of the puppet group. # # @param [Host] host One object that acts like a Beaker::Host # # @note This method assumes puppet is installed on the host. # def puppet_group(host) return host.puppet('master')['group'] end # Test Puppet running in a certain run mode with specific options. # This ensures the following steps are performed: # 1. The pre-test Puppet configuration is backed up # 2. A new Puppet configuraton file is layed down # 3. Puppet is started or restarted in the specified run mode # 4. Ensure Puppet has started correctly # 5. Further tests are yielded to # 6. Revert Puppet to the pre-test state # 7. Testing artifacts are saved in a folder named for the test # # @param [Host] host One object that act like Host # # @param [Hash{Symbol=>String}] conf_opts Represents puppet settings. # Sections of the puppet.conf may be # specified, if no section is specified the # a puppet.conf file will be written with the # options put in a section named after [mode] # @option conf_opts [String] :__commandline_args__ A special setting for # command_line arguments such as --debug or # --logdest, which cannot be set in # puppet.conf. For example: # # :__commandline_args__ => '--logdest /tmp/a.log' # # These will only be applied when starting a FOSS # master, as a pe master is just bounced. # @option conf_opts [Hash] :__service_args__ A special setting of options # for controlling how the puppet master service is # handled. The only setting currently is # :bypass_service_script, which if set true will # force stopping and starting a webrick master # using the start_puppet_from_source_* methods, # even if it seems the host has passenger. # This is needed in FOSS tests to initialize # SSL. # @param [File] testdir The temporary directory which will hold backup # configuration, and other test artifacts. # # @param [Block] block The point of this method, yields so # tests may be ran. After the block is finished # puppet will revert to a previous state. # # @example A simple use case to ensure a master is running # with_puppet_running_on( master ) do # ...tests that require a master... # end # # @example Fully utilizing the possiblities of config options # with_puppet_running_on( master, # :main => {:logdest => '/var/blah'}, # :master => {:masterlog => '/elswhere'}, # :agent => {:server => 'localhost'} ) do # # ...tests to be ran... # end # def with_puppet_running_on host, conf_opts, testdir = host.tmpdir(File.basename(@path)), &block raise(ArgumentError, "with_puppet_running_on's conf_opts must be a Hash. You provided a #{conf_opts.class}: '#{conf_opts}'") if !conf_opts.kind_of?(Hash) cmdline_args = conf_opts[:__commandline_args__] service_args = conf_opts[:__service_args__] || {} conf_opts = conf_opts.reject { |k,v| [:__commandline_args__, :__service_args__].include?(k) } curl_retries = host['master-start-curl-retries'] || options['master-start-curl-retries'] logger.debug "Setting curl retries to #{curl_retries}" if options[:is_puppetserver] confdir = host.puppet('master')['confdir'] vardir = host.puppet('master')['vardir'] if cmdline_args split_args = cmdline_args.split() split_args.each do |arg| case arg when /--confdir=(.*)/ confdir = $1 when /--vardir=(.*)/ vardir = $1 end end end puppetserver_opts = { "jruby-puppet" => { "master-conf-dir" => confdir, "master-var-dir" => vardir, }} puppetserver_conf = File.join("#{host['puppetserver-confdir']}", "puppetserver.conf") modify_tk_config(host, puppetserver_conf, puppetserver_opts) end begin backup_file = backup_the_file(host, host.puppet('master')['confdir'], testdir, 'puppet.conf') lay_down_new_puppet_conf host, conf_opts, testdir if host.use_service_scripts? && !service_args[:bypass_service_script] bounce_service( host, host['puppetservice'], curl_retries ) else puppet_master_started = start_puppet_from_source_on!( host, cmdline_args ) end yield self if block_given? rescue Beaker::DSL::Assertions, Minitest::Assertion => early_assertion fail_test(early_assertion) rescue Exception => early_exception original_exception = RuntimeError.new("PuppetAcceptance::DSL::Helpers.with_puppet_running_on failed (check backtrace for location) because: #{early_exception}\n#{early_exception.backtrace.join("\n")}\n") raise(original_exception) ensure begin if host.use_service_scripts? && !service_args[:bypass_service_script] restore_puppet_conf_from_backup( host, backup_file ) bounce_service( host, host['puppetservice'], curl_retries ) else if puppet_master_started stop_puppet_from_source_on( host ) else dump_puppet_log(host) end restore_puppet_conf_from_backup( host, backup_file ) end rescue Exception => teardown_exception begin if !host.is_pe? dump_puppet_log(host) end rescue Exception => dumping_exception logger.error("Raised during attempt to dump puppet logs: #{dumping_exception}") end if original_exception logger.error("Raised during attempt to teardown with_puppet_running_on: #{teardown_exception}\n---\n") raise original_exception else raise teardown_exception end end end end # Test Puppet running in a certain run mode with specific options, # on the default host # @see #with_puppet_running_on def with_puppet_running conf_opts, testdir = host.tmpdir(File.basename(@path)), &block with_puppet_running_on(default, conf_opts, testdir, &block) end # @!visibility private def restore_puppet_conf_from_backup( host, backup_file ) puppet_conf = host.puppet('master')['config'] if backup_file host.exec( Command.new( "if [ -f '#{backup_file}' ]; then " + "cat '#{backup_file}' > " + "'#{puppet_conf}'; " + "rm -f '#{backup_file}'; " + "fi" ) ) else host.exec( Command.new( "rm -f '#{puppet_conf}'" )) end end # @!visibility private def start_puppet_from_source_on! host, args = '' host.exec( puppet( 'master', args ) ) logger.debug 'Waiting for the puppet master to start' unless port_open_within?( host, 8140, 10 ) raise Beaker::DSL::FailTest, 'Puppet master did not start in a timely fashion' end logger.debug 'The puppet master has started' return true end # @!visibility private def stop_puppet_from_source_on( host ) pid = host.exec( Command.new('cat `puppet master --configprint pidfile`') ).stdout.chomp host.exec( Command.new( "kill #{pid}" ) ) Timeout.timeout(10) do while host.exec( Command.new( "kill -0 #{pid}"), :acceptable_exit_codes => [0,1] ).exit_code == 0 do # until kill -0 finds no process and we know that puppet has finished cleaning up sleep 1 end end end # @!visibility private def dump_puppet_log(host) syslogfile = case host['platform'] when /fedora|centos|el|redhat|scientific/ then '/var/log/messages' when /ubuntu|debian|cumulus/ then '/var/log/syslog' else return end logger.notify "\n*************************" logger.notify "* Dumping master log *" logger.notify "*************************" host.exec( Command.new( "tail -n 100 #{syslogfile}" ), :acceptable_exit_codes => [0,1]) logger.notify "*************************\n" end # @!visibility private def lay_down_new_puppet_conf( host, configuration_options, testdir ) puppetconf_main = host.puppet('master')['config'] puppetconf_filename = File.basename(puppetconf_main) puppetconf_test = File.join(testdir, puppetconf_filename) new_conf = puppet_conf_for( host, configuration_options ) create_remote_file host, puppetconf_test, new_conf.to_s host.exec( Command.new( "cat #{puppetconf_test} > #{puppetconf_main}" ), :silent => true ) host.exec( Command.new( "cat #{puppetconf_main}" ) ) end # @!visibility private def puppet_conf_for host, conf_opts puppetconf = host.exec( Command.new( "cat #{host.puppet('master')['config']}" ) ).stdout new_conf = IniFile.new( puppetconf ).merge( conf_opts ) new_conf end # @!visibility private def bounce_service host, service, curl_retries = 120 if host.graceful_restarts? apachectl_path = host.is_pe? ? "#{host['puppetsbindir']}/apache2ctl" : 'apache2ctl' host.exec(Command.new("#{apachectl_path} graceful")) else host.exec puppet_resource('service', service, 'ensure=stopped') host.exec puppet_resource('service', service, 'ensure=running') end curl_with_retries(" #{service} ", host, "https://localhost:8140", [35, 60], curl_retries) end # Runs 'puppet apply' on a remote host, piping manifest through stdin # # @param [Host] host The host that this command should be run on # # @param [String] manifest The puppet manifest to apply # # @!macro common_opts # @option opts [Boolean] :parseonly (false) If this key is true, the # "--parseonly" command line parameter will # be passed to the 'puppet apply' command. # # @option opts [Boolean] :trace (false) If this key exists in the Hash, # the "--trace" command line parameter will be # passed to the 'puppet apply' command. # # @option opts [Array] :acceptable_exit_codes ([0]) The list of exit # codes that will NOT raise an error when found upon # command completion. If provided, these values will # be combined with those used in :catch_failures and # :expect_failures to create the full list of # passing exit codes. # # @option opts [Hash] :environment Additional environment variables to be # passed to the 'puppet apply' command # # @option opts [Boolean] :catch_failures (false) By default `puppet # --apply` will exit with 0, which does not count # as a test failure, even if there were errors or # changes when applying the manifest. This option # enables detailed exit codes and causes a test # failure if `puppet --apply` indicates there was # a failure during its execution. # # @option opts [Boolean] :catch_changes (false) This option enables # detailed exit codes and causes a test failure # if `puppet --apply` indicates that there were # changes or failures during its execution. # # @option opts [Boolean] :expect_changes (false) This option enables # detailed exit codes and causes a test failure # if `puppet --apply` indicates that there were # no resource changes during its execution. # # @option opts [Boolean] :expect_failures (false) This option enables # detailed exit codes and causes a test failure # if `puppet --apply` indicates there were no # failure during its execution. # # @option opts [Boolean] :future_parser (false) This option enables # the future parser option that is available # from Puppet verion 3.2 # By default it will use the 'current' parser. # # @option opts [Boolean] :noop (false) If this option exists, the # the "--noop" command line parameter will be # passed to the 'puppet apply' command. # # @option opts [String] :modulepath 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 ‘;’.) # # @option opts [String] :debug (false) If this option exists, # the "--debug" command line parameter # will be passed to the 'puppet apply' command. # # @param [Block] block This method will yield to a block of code passed # by the caller; this can be used for additional # validation, etc. # def apply_manifest_on(host, manifest, opts = {}, &block) block_on host do | host | on_options = {} on_options[:acceptable_exit_codes] = Array(opts[:acceptable_exit_codes]) puppet_apply_opts = {} if opts[:debug] puppet_apply_opts[:debug] = nil else puppet_apply_opts[:verbose] = nil end puppet_apply_opts[:parseonly] = nil if opts[:parseonly] puppet_apply_opts[:trace] = nil if opts[:trace] puppet_apply_opts[:parser] = 'future' if opts[:future_parser] puppet_apply_opts[:modulepath] = opts[:modulepath] if opts[:modulepath] puppet_apply_opts[:noop] = nil if opts[:noop] # From puppet help: # "... 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." if [opts[:catch_changes],opts[:catch_failures],opts[:expect_failures],opts[:expect_changes]].compact.length > 1 raise(ArgumentError, 'Cannot specify more than one of `catch_failures`, ' + '`catch_changes`, `expect_failures`, or `expect_changes` ' + 'for a single manifest') end if opts[:catch_changes] puppet_apply_opts['detailed-exitcodes'] = nil # We're after idempotency so allow exit code 0 only. on_options[:acceptable_exit_codes] |= [0] elsif opts[:catch_failures] puppet_apply_opts['detailed-exitcodes'] = nil # We're after only complete success so allow exit codes 0 and 2 only. on_options[:acceptable_exit_codes] |= [0, 2] elsif opts[:expect_failures] puppet_apply_opts['detailed-exitcodes'] = nil # We're after failures specifically so allow exit codes 1, 4, and 6 only. on_options[:acceptable_exit_codes] |= [1, 4, 6] elsif opts[:expect_changes] puppet_apply_opts['detailed-exitcodes'] = nil # We're after changes specifically so allow exit code 2 only. on_options[:acceptable_exit_codes] |= [2] else # Either use the provided acceptable_exit_codes or default to [0] on_options[:acceptable_exit_codes] |= [0] end # Not really thrilled with this implementation, might want to improve it # later. Basically, there is a magic trick in the constructor of # PuppetCommand which allows you to pass in a Hash for the last value in # the *args Array; if you do so, it will be treated specially. So, here # we check to see if our caller passed us a hash of environment variables # that they want to set for the puppet command. If so, we set the final # value of *args to a new hash with just one entry (the value of which # is our environment variables hash) if opts.has_key?(:environment) puppet_apply_opts['ENV'] = opts[:environment] end file_path = host.tmpfile('apply_manifest.pp') create_remote_file(host, file_path, manifest + "\n") if host[:default_apply_opts].respond_to? :merge puppet_apply_opts = host[:default_apply_opts].merge( puppet_apply_opts ) end on host, puppet('apply', file_path, puppet_apply_opts), on_options, &block end end # Runs 'puppet apply' on default host, piping manifest through stdin # @see #apply_manifest_on def apply_manifest(manifest, opts = {}, &block) apply_manifest_on(default, manifest, opts, &block) end # @deprecated def run_agent_on(host, arg='--no-daemonize --verbose --onetime --test', options={}, &block) block_on host do | host | on host, puppet_agent(arg), options, &block end end # This method using the puppet resource 'host' will setup host aliases # and register the remove of host aliases via Beaker::TestCase#teardown # # A teardown step is also added to make sure unstubbing of the host is # removed always. # # @param [Host, Array, String, Symbol] machine One or more hosts to act upon, # or a role (String or Symbol) that identifies one or more hosts. # @param ip_spec [Hash{String=>String}] a hash containing the host to ip # mappings # @example Stub puppetlabs.com on the master to 127.0.0.1 # stub_hosts_on(master, 'puppetlabs.com' => '127.0.0.1') def stub_hosts_on(machine, ip_spec) block_on machine do | host | ip_spec.each do |address, ip| logger.notify("Stubbing address #{address} to IP #{ip} on machine #{host}") on( host, puppet('resource', 'host', address, 'ensure=present', "ip=#{ip}") ) end teardown do ip_spec.each do |address, ip| logger.notify("Unstubbing address #{address} to IP #{ip} on machine #{host}") on( host, puppet('resource', 'host', address, 'ensure=absent') ) end end end end # This method accepts a block and using the puppet resource 'host' will # setup host aliases before and after that block. # # @param [Host, Array, String, Symbol] host One or more hosts to act upon, # or a role (String or Symbol) that identifies one or more hosts. # @param ip_spec [Hash{String=>String}] a hash containing the host to ip # mappings # @example Stub puppetlabs.com on the master to 127.0.0.1 # with_host_stubbed_on(master, 'forgeapi.puppetlabs.com' => '127.0.0.1') do # puppet( "module install puppetlabs-stdlib" ) # end def with_host_stubbed_on(host, ip_spec, &block) begin block_on host do |host| ip_spec.each_pair do |address, ip| logger.notify("Stubbing address #{address} to IP #{ip} on machine #{host}") on( host, puppet('resource', 'host', address, 'ensure=present', "ip=#{ip}") ) end end block.call ensure ip_spec.each do |address, ip| logger.notify("Unstubbing address #{address} to IP #{ip} on machine #{host}") on( host, puppet('resource', 'host', address, 'ensure=absent') ) end end end # This method accepts a block and using the puppet resource 'host' will # setup host aliases before and after that block on the default host # # @example Stub puppetlabs.com on the default host to 127.0.0.1 # stub_hosts('puppetlabs.com' => '127.0.0.1') # @see #stub_hosts_on def stub_hosts(ip_spec) stub_hosts_on(default, ip_spec) end # This wraps the method `stub_hosts_on` and makes the stub specific to # the forge alias. # # forge api v1 canonical source is forge.puppetlabs.com # forge api v3 canonical source is forgeapi.puppetlabs.com # # @param machine [String] the host to perform the stub on # @param forge_host [String] The URL to use as the forge alias, will default to using :forge_host in the # global options hash def stub_forge_on(machine, forge_host = nil) #use global options hash forge_host ||= options[:forge_host] @forge_ip ||= Resolv.getaddress(forge_host) block_on machine do | host | stub_hosts_on(host, 'forge.puppetlabs.com' => @forge_ip) stub_hosts_on(host, 'forgeapi.puppetlabs.com' => @forge_ip) end end # This wraps the method `with_host_stubbed_on` and makes the stub specific to # the forge alias. # # forge api v1 canonical source is forge.puppetlabs.com # forge api v3 canonical source is forgeapi.puppetlabs.com # # @param host [String] the host to perform the stub on # @param forge_host [String] The URL to use as the forge alias, will default to using :forge_host in the # global options hash def with_forge_stubbed_on( host, forge_host = nil, &block ) #use global options hash forge_host ||= options[:forge_host] @forge_ip ||= Resolv.getaddress(forge_host) with_host_stubbed_on( host, {'forge.puppetlabs.com' => @forge_ip, 'forgeapi.puppetlabs.com' => @forge_ip}, &block ) end # This wraps `with_forge_stubbed_on` and provides it the default host # @see with_forge_stubbed_on def with_forge_stubbed( forge_host = nil, &block ) with_forge_stubbed_on( default, forge_host, &block ) end # This wraps the method `stub_hosts` and makes the stub specific to # the forge alias. # # @see #stub_forge_on def stub_forge(forge_host = nil) #use global options hash forge_host ||= options[:forge_host] stub_forge_on(default, forge_host) end def sleep_until_puppetdb_started(host) curl_with_retries("start puppetdb", host, "http://localhost:8080", 0, 120) curl_with_retries("start puppetdb (ssl)", host, "https://#{host.node_name}:8081", [35, 60]) end def sleep_until_puppetserver_started(host) curl_with_retries("start puppetserver (ssl)", host, "https://#{host.node_name}:8140", [35, 60]) end def sleep_until_nc_started(host) curl_with_retries("start nodeclassifier (ssl)", host, "https://#{host.node_name}:4433", [35, 60]) end #stops the puppet agent running on the host # @param [Host, Array, String, Symbol] agent One or more hosts to act upon, # or a role (String or Symbol) that identifies one or more hosts. def stop_agent_on(agent) block_on agent do | host | vardir = agent.puppet['vardir'] agent_running = true while agent_running agent_running = agent.file_exist?("#{vardir}/state/agent_catalog_run.lock") if agent_running sleep 2 end end # The agent service is `pe-puppet` everywhere EXCEPT certain linux distros on PE 2.8 # In all the case that it is different, this init script will exist. So we can assume # that if the script doesn't exist, we should just use `pe-puppet` agent_service = 'pe-puppet-agent' agent_service = 'pe-puppet' unless agent.file_exist?('/etc/init.d/pe-puppet-agent') # In 4.0 this was changed to just be `puppet` agent_service = 'puppet' unless version_is_less(agent['pe_ver'], '4.0') # Under a number of stupid circumstances, we can't stop the # agent using puppet. This is usually because of issues with # the init script or system on that particular configuration. avoid_puppet_at_all_costs = false avoid_puppet_at_all_costs ||= agent['platform'] =~ /el-4/ avoid_puppet_at_all_costs ||= agent['pe_ver'] && version_is_less(agent['pe_ver'], '3.2') && agent['platform'] =~ /sles/ if avoid_puppet_at_all_costs # When upgrading, puppet is already stopped. On EL4, this causes an exit code of '1' on agent, "/etc/init.d/#{agent_service} stop", :acceptable_exit_codes => [0, 1] else on agent, puppet_resource('service', agent_service, 'ensure=stopped') end end end #stops the puppet agent running on the default host # @see #stop_agent_on def stop_agent stop_agent_on(default) end #wait for a given host to appear in the dashboard def wait_for_host_in_dashboard(host) hostname = host.node_name retry_on(dashboard, "! curl --tlsv1 -k -I https://#{dashboard}/nodes/#{hostname} | grep '404 Not Found'") end # Ensure the host has requested a cert, then sign it # # @param [Host, Array, String, Symbol] host One or more hosts to act upon, # or a role (String or Symbol) that identifies one or more hosts. # # @return nil # @raise [FailTest] if process times out def sign_certificate_for(host) block_on host do | host | if [master, dashboard, database].include? host on host, puppet( 'agent -t' ), :acceptable_exit_codes => [0,1,2] on master, puppet( "cert --allow-dns-alt-names sign #{host}" ), :acceptable_exit_codes => [0,24] else hostname = Regexp.escape host.node_name last_sleep = 0 next_sleep = 1 (0..10).each do |i| fail_test("Failed to sign cert for #{hostname}") if i == 10 on master, puppet("cert --sign --all --allow-dns-alt-names"), :acceptable_exit_codes => [0,24] break if on(master, puppet("cert --list --all")).stdout =~ /\+ "?#{hostname}"?/ sleep next_sleep (last_sleep, next_sleep) = next_sleep, last_sleep+next_sleep end end end end #prompt the master to sign certs then check to confirm the cert for the default host is signed #@see #sign_certificate_for def sign_certificate sign_certificate_for(default) end # Create a temp directory on remote host with a user. Default user # is puppet master user. # # @param [Host] host A single remote host on which to create and adjust # the ownership of a temp directory. # @param [String] name A remote path prefix for the new temp # directory. Default value is '/tmp/beaker' # @param [String] user The name of user that should own the temp # directory. If no username is specified, use `puppet master # --configprint user` to obtain username from master. Raise RuntimeError # if this puppet command returns a non-zero exit code. # # @return [String] Returns the name of the newly-created dir. def create_tmpdir_for_user(host, name='/tmp/beaker', user=nil) if not user result = on host, puppet("master --configprint user") if not result.exit_code == 0 raise "`puppet master --configprint` failed, check that puppet is installed on #{host} or explicitly pass in a user name." end user = result.stdout.strip end create_tmpdir_on(host, name, user) end end end end end