diff --git a/between-meals/changes.rb b/between-meals/changes.rb deleted file mode 100644 index 8597d72..0000000 --- a/between-meals/changes.rb +++ /dev/null @@ -1,183 +0,0 @@ -# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 -# rubocop:disable ClassVars - -module BetweenMeals - # A set of classes that represent a given item's change (a cookbook - # that's changed, a role that's changed or a databag item that's changed). - # - # You almost certainly don't want to use this directly, and instead want - # BetweenMeals::Changeset - module Changes - # Common functionality - class Change - @@logger = nil - attr_accessor :name, :status - def to_s - @name - end - - # People who use us through find() can just pass in logger, - # for everyone else, here's a setter - def logger=(log) - @@logger = log - end - - def self.info(msg) - if @@logger - @@logger.info(msg) - end - end - - def self.debug(msg) - if @@logger - @@logger.debug(msg) - end - end - - def info(msg) - BetweenMeals::Changes::Change.info(msg) - end - - def debug(msg) - BetweenMeals::Changes::Change.debug(msg) - end - end - - # Changeset aware cookbook - class Cookbook < Change - def self.meaningful_cookbook_file?(path, cookbook_dirs) - cookbook_dirs.each do |dir| - re = %r{^#{dir}/([^/]+)/.*/.*} - m = path.match(re) - debug("[cookbook] #{path} meaningful? [#{re}]: #{m}") - return true if m - end - false - end - - def self.explode_path(path, cookbook_dirs) - cookbook_dirs.each do |dir| - re = %r{^#{dir}/([^/]+)/.*} - debug("[cookbook] Matching #{path} against ^#{re}") - m = path.match(re) - next unless m - info("Cookbook is #{m[1]}") - return { - :cookbook_dir => dir, - :name => m[1] } - end - nil - end - - def initialize(files, cookbook_dirs) - @files = files - @name = self.class.explode_path( - files.sample[:path], - cookbook_dirs - )[:name] - # if metadata.rb is being deleted - # cookbook is marked for deletion - # otherwise it was modified - # and will be re-uploaded - if files. - select { |x| x[:status] == :deleted }. - map { |x| x[:path].match(%{.*metadata\.rb$}) }. - compact. - any? - @status = :deleted - else - @status = :modified - end - end - - # Given a list of changed files - # create a list of Cookbook objects - def self.find(list, cookbook_dirs, logger) - @@logger = logger - return [] if list.nil? || list.empty? - # rubocop:disable MultilineBlockChain - list. - group_by do |x| - # Group by prefix of cookbok_dir + cookbook_name - # so that we treat deletes and modifications across - # two locations separately - g = self.explode_path(x[:path], cookbook_dirs) - g[:cookbook_dir] + '/' + g[:name] if g - end. - map do |_, change| - # Confirm we're dealing with a cookbook - # Changes to OWNERS or other stuff that might end up - # in [core, other, secure] dirs are ignored - is_cookbook = change.select do |c| - self.meaningful_cookbook_file?(c[:path], cookbook_dirs) - end.any? - if is_cookbook - BetweenMeals::Changes::Cookbook.new(change, cookbook_dirs) - end - end.compact - # rubocop:enable MultilineBlockChain - end - end - - # Changeset aware role - class Role < Change - def self.name_from_path(path, role_dir) - re = "^#{role_dir}\/(.+)\.rb" - debug("[role] Matching #{path} against #{re}") - m = path.match(re) - if m - info("Name is #{m[1]}") - return m[1] - end - nil - end - - def initialize(file, role_dir) - @status = file[:status] == :deleted ? :deleted : :modified - @name = self.class.name_from_path(file[:path], role_dir) - end - - # Given a list of changed files - # create a list of Role objects - def self.find(list, role_dir, logger) - @@logger = logger - return [] if list.nil? || list.empty? - list. - select { |x| self.name_from_path(x[:path], role_dir) }. - map do |x| - BetweenMeals::Changes::Role.new(x, role_dir) - end - end - end - - # Changeset aware databag - class Databag < Change - attr_accessor :item - def self.name_from_path(path, databag_dir) - re = %r{^#{databag_dir}/([^/]+)/([^/]+)\.json} - debug("[databag] Matching #{path} against #{re}") - m = path.match(re) - if m - info("Databag is #{m[1]} item is #{m[2]}") - return m[1], m[2] - end - nil - end - - def initialize(file, databag_dir) - @status = file[:status] - @name, @item = self.class.name_from_path(file[:path], databag_dir) - end - - def self.find(list, databag_dir, logger) - @@logger = logger - return [] if list.nil? || list.empty? - list. - select { |x| self.name_from_path(x[:path], databag_dir) }. - map do |x| - BetweenMeals::Changes::Databag.new(x, databag_dir) - end - end - end - end -end diff --git a/between-meals/repo/git.rb b/between-meals/repo/git.rb index 8862a4e..535e700 100755 --- a/between-meals/repo/git.rb +++ b/between-meals/repo/git.rb @@ -192,11 +192,11 @@ def parse_status(changes) fail 'No match' end end.flatten.map do |x| - { - :status => x[:status], - :path => x[:path].sub("#{@repo_path}/", '') - } - end + { + :status => x[:status], + :path => x[:path].sub("#{@repo_path}/", '') + } + end # rubocop:enable MultilineBlockChain end end diff --git a/between-meals/util.rb b/between-meals/util.rb index b943a88..fa051e7 100644 --- a/between-meals/util.rb +++ b/between-meals/util.rb @@ -1,6 +1,8 @@ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 require 'colorize' +require 'socket' +require 'timeout' module BetweenMeals # A set of simple utility functions used throughout BetweenMeals @@ -51,5 +53,22 @@ def execute(command) end return c end + + def port_open?(port) + begin + Timeout.timeout(1) do + begin + s = TCPSocket.new('localhost', port) + s.close + return true + rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH + return false + end + end + rescue Timeout::Error + return false + end + return false + end end end diff --git a/spec/between-meals/changes_spec.rb b/spec/between-meals/changes_spec.rb deleted file mode 100644 index 99ebcd6..0000000 --- a/spec/between-meals/changes_spec.rb +++ /dev/null @@ -1,116 +0,0 @@ -# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 -require_relative '../../between-meals/changes' -require_relative '../../between-meals/changeset' -require 'logger' - -describe 'BetweenMeals::Changes::Cookbook' do - let(:logger) do - Logger.new('/dev/null') - end - let(:cookbook_dirs) do - ['cookbooks/one', 'cookbooks/two'] - end - - FIXTURES = [ - { - :name => 'empty filelists', - :files => [], - :result => [], - }, - { - :name => 'modifying of a cookbook', - :files => [ - { - :status => :modified, - :path => 'cookbooks/two/cb_one/recipes/test.rb' - }, - { - :status => :modified, - :path => 'cookbooks/two/cb_one/metadata.rb' - }, - ], - :result => [ - ['cb_one', :modified], - ], - }, - { - :name => 'a mix of in-place modifications and deletes', - :files => [ - { - :status => :modified, - :path => 'cookbooks/one/cb_one/recipes/test.rb' - }, - { - :status => :deleted, - :path => 'cookbooks/one/cb_one/recipes/test2.rb' - }, - { - :status => :modified, - :path => 'cookbooks/one/cb_one/recipes/test3.rb' - }, - ], - :result => [ - ['cb_one', :modified], - ], - }, - { - :name => 'removing metadata.rb - invalid cookbook, delete it', - :files => [ - { - :status => :modified, - :path => 'cookbooks/one/cb_one/recipes/test.rb' - }, - { - :status => :deleted, - :path => 'cookbooks/one/cb_one/metadata.rb' - }, - ], - :result => [ - ['cb_one', :deleted], - ], - }, - { - :name => 'changing cookbook location', - :files => [ - { - :status => :deleted, - :path => 'cookbooks/one/cb_one/recipes/test.rb' - }, - { - :status => :deleted, - :path => 'cookbooks/one/cb_one/metadata.rb' - }, - { - :status => :modified, - :path => 'cookbooks/two/cb_one/recipes/test.rb' - }, - { - :status => :modified, - :path => 'cookbooks/two/cb_one/recipes/test2.rb' - }, - { - :status => :modified, - :path => 'cookbooks/two/cb_one/metadata.rb' - }, - ], - :result => [ - ['cb_one', :deleted], - ['cb_one', :modified], - ], - }, - ] - - FIXTURES.each do |fixture| - it "should handle #{fixture[:name]}" do - BetweenMeals::Changes::Cookbook.find( - fixture[:files], - cookbook_dirs, - logger - ).map do |cb| - [cb.name, cb.status] - end. - should eq(fixture[:result]) - end - end - -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9bb7d1c..7d9868d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,7 @@ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 +require 'logger' + module TasteTester # Null logger module Logging @@ -11,7 +13,3 @@ def self.logger end end end - -require_relative '../taste-tester/util' -require_relative '../taste-tester/config' -require_relative '../taste-tester/taste-tester' diff --git a/taste-tester/taste-tester.rb b/taste-tester/taste-tester.rb index 6cb077a..892fd9d 100755 --- a/taste-tester/taste-tester.rb +++ b/taste-tester/taste-tester.rb @@ -27,7 +27,7 @@ # Command line parsing and param descriptions module TasteTester verify = 'Verify your changes were actually applied as intended!'.red - cmd = TasteTester::Config.command + cmd = TasteTester::Config.chef_client_command description = <<-EOF Welcome to taste-tester! @@ -35,13 +35,13 @@ module TasteTester TLDR; Most common usage is: vi cookbooks/... # Make your changes and commit locally - taste-tester test -s [host] # Put host in taste-tester mode - ssh root@[host] # Log in to host - #{cmd} # Run chef and watch it break + taste-tester test -s [host] # Put host in test mode + ssh root@[host] # Log on host + #{format('%-28s', " #{cmd}")} # Run chef and watch it break vi cookbooks/... # Fix your cookbooks taste-tester upload # Upload the diff - ssh root@[host] - #{cmd} # Run chef and watch it succeed + ssh root@[host] # Log on host + #{format('%-28s', " #{cmd}")} # Run chef and watch it succeed <#{verify}> taste-tester untest [host] # Put host back in production # (optional - will revert itself after 1 hour) @@ -167,6 +167,23 @@ module TasteTester options[:force_upload] = true end + opts.on( + '--chef-port-range PORT1,PORT2', Array, + 'Port range for chef-zero' + ) do |ports| + unless ports.count == 2 + logger.error("Invalid port range: #{ports}") + exit 1 + end + options[:chef_port_range] = ports + end + + opts.on( + '--tunnel-port PORT', 'Port for ssh tunnel' + ) do |port| + options[:user_tunnel_port] = port + end + opts.on( '-l', '--linkonly', 'Only setup the remote server, skip uploading.' ) do @@ -233,6 +250,13 @@ module TasteTester options[:servers] = s end + opts.on( + '--user USER', 'Custom username for SSH, defaults to "root".' + + ' If custom user is specified, we will use sudo for all commands.' + ) do |user| + options[:user] = user + end + opts.on( '-S', '--[no-]use-ssh-tunnels', 'Protect Chef traffic with SSH tunnels' ) do |s| diff --git a/taste-tester/taste-tester/client.rb b/taste-tester/taste-tester/client.rb index 1ccc74b..50d11c5 100755 --- a/taste-tester/taste-tester/client.rb +++ b/taste-tester/taste-tester/client.rb @@ -72,7 +72,12 @@ def upload @server.latest_uploaded_ref, @repo.head_rev) end - time(logger) { partial } + begin + time(logger) { partial } + rescue BetweenMeals::Changeset::ReferenceError + logger.warn('Something changed with your repo, doing full upload') + time(logger) { full } + end unless TasteTester::Config.skip_post_upload_hook TasteTester::Hooks.post_upload(TasteTester::Config.dryrun, @repo, diff --git a/taste-tester/taste-tester/config.rb b/taste-tester/taste-tester/config.rb index 81dad8a..d913162 100644 --- a/taste-tester/taste-tester/config.rb +++ b/taste-tester/taste-tester/config.rb @@ -1,6 +1,8 @@ # vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 require 'mixlib/config' +require_relative 'logging' +require_relative '../../between-meals/util' module TasteTester # Config file parser and config object @@ -8,6 +10,8 @@ module TasteTester # it's compatible with v2, so it should work in 11 too. module Config extend Mixlib::Config + extend TasteTester::Logging + extend BetweenMeals::Util repo "#{ENV['HOME']}/ops" repo_type 'git' @@ -17,13 +21,18 @@ module Config databag_dir 'databags' config_file '/etc/taste-tester-config.rb' plugin_path '/etc/taste-tester-plugin.rb' + chef_zero_path '/opt/chef/embedded/bin/chef-zero' verbosity Logger::WARN timestamp false - ref_file "#{ENV['HOME']}/.chef/taste-tester-ref.txt" + user 'root' + ref_file "#{ENV['HOME']}/.chef/taste-tester-ref.json" checksum_dir "#{ENV['HOME']}/.chef/checksums" skip_repo_checks false chef_client_command 'chef-client' testing_time 3600 + chef_port_range [5000, 5500] + tunnel_port 4001 + timestamp_file '/etc/chef/test_timestamp' use_ssh_tunnels true skip_pre_upload_hook false @@ -59,5 +68,25 @@ def self.databags def self.relative_databag_dir File.join(base_dir, databag_dir) end + + def self.chef_port + range = chef_port_range.first.to_i..chef_port_range.last.to_i + range.to_a.shuffle.each do |port| + unless port_open?(port) + return port + end + end + logger.error 'Could not find a free port in range' + + " [#{chef_port_range.first}, #{chef_port_range.last}]" + exit 1 + end + + def self.testing_end_time + if TasteTester::Config.testing_until + TasteTester::Config.testing_until.strftime('%y%m%d%H%M.%S') + else + (Time.now + TasteTester::Config.testing_time).strftime('%y%m%d%H%M.%S') + end + end end end diff --git a/taste-tester/taste-tester/host.rb b/taste-tester/taste-tester/host.rb index de5bf7d..517d7f9 100755 --- a/taste-tester/taste-tester/host.rb +++ b/taste-tester/taste-tester/host.rb @@ -6,9 +6,10 @@ require 'colorize' require_relative 'ssh' +require_relative 'tunnel' module TasteTester - # Manage remote cehftest node + # Manage state of the remote node class Host include TasteTester::Logging @@ -17,25 +18,21 @@ class Host def initialize(name, server) @name = name @user = ENV['USER'] - @chef_server = server.host - @serialized_config = Base64.encode64(config).gsub(/\n/, '') - if TasteTester::Config.testing_until - @timestamp = TasteTester::Config.testing_until. - strftime('%y%m%d%H%M.%S') - @delta_secs = TasteTester::Config.testing_until.strftime('%s').to_i - - Time.now.strftime('%s').to_i - else - @timestamp = (Time.now + TasteTester::Config.testing_time). - strftime('%y%m%d%H%M.%S') - @delta_secs = TasteTester::Config.testing_time - end - @tsfile = '/etc/chef/test_timestamp' + @server = server + @tunnel = TasteTester::Tunnel.new(@name, @server) end def runchef logger.warn("Running '#{TasteTester::Config.command}' on #{@name}") + cmd = "ssh #{TasteTester::Config.user}@#{@name} " + if TasteTester::Config.user != 'root' + cc = Base64.encode64(cmds).gsub(/\n/, '') + cmd += "\"echo '#{cc}' | base64 --decode | sudo bash -x\"" + else + cmd += "\"#{cmds}\"" + end status = IO.popen( - "ssh root@#{@name} #{TasteTester::Config.command}" + cmd ) do |io| # rubocop:disable AssignmentInCondition while line = io.gets @@ -46,7 +43,7 @@ def runchef $CHILD_STATUS.to_i end logger.warn("Finished #{TasteTester::Config.command}" + - " on #{@name} with status #{status}") + " on #{@name} with status #{status}") if status == 0 msg = "#{TasteTester::Config.command} was successful" + ' - please log to the host and confirm all the intended' + @@ -55,35 +52,22 @@ def runchef end end - def nuke_old_tunnel - ssh = TasteTester::SSH.new(@name) - # Since commands are &&'d together, and we're using &&, we need to - # surround this in paryns, and make sure as a whole it evaluates - # to true so it doesn't mess up other things... even though this is - # the only thing we're currently executing in this SSH. - ssh << "( [ -s #{@tsfile} ] && kill -- -\$(cat #{@tsfile}); true )" - ssh.run! - end - - def setup_tunnel - ssh = TasteTester::SSH.new(@name, 5, true) - ssh << "echo \\\$\\\$ > #{@tsfile}" - ssh << "touch -t #{@timestamp} #{@tsfile}" - ssh << "sleep #{@delta_secs}" - ssh.run! - end - def test logger.warn("Taste-testing on #{@name}") # Nuke any existing tunnels that may be there - nuke_old_tunnel + TasteTester::Tunnel.kill(@name) + + # Then setup the tunnel + @tunnel.run + @serialized_config = Base64.encode64(config).gsub(/\n/, '') # Then setup the testing ssh = TasteTester::SSH.new(@name) ssh << 'logger -t taste-tester Moving server into taste-tester' + " for #{@user}" - ssh << "touch -t #{@timestamp} #{@tsfile}" + ssh << "touch -t #{TasteTester::Config.testing_end_time}" + + " #{TasteTester::Config.timestamp_file}" ssh << "echo -n '#{@serialized_config}' | base64 --decode" + ' > /etc/chef/client-taste-tester.rb' ssh << 'rm -vf /etc/chef/client.rb' @@ -91,12 +75,12 @@ def test ' /etc/chef/client.rb; true )' ssh.run! - # Then setup the tunnel - setup_tunnel - # Then run any other stuff they wanted - cmds = TasteTester::Hooks.test_remote_cmds(TasteTester::Config.dryrun, - @name) + cmds = TasteTester::Hooks.test_remote_cmds( + TasteTester::Config.dryrun, + @name + ) + if cmds && cmds.any? ssh = TasteTester::SSH.new(@name) cmds.each { |c| ssh << c } @@ -107,43 +91,46 @@ def test def untest logger.warn("Removing #{@name} from taste-tester") ssh = TasteTester::SSH.new(@name) - # see above for why this command is funky - # We do this even if use_ssh_tunnels is false because we may be switching - # from one to the other - ssh << "( [ -s #{@tsfile} ] && kill -- -\$(cat #{@tsfile}); true )" + TasteTester::Tunnel.kill(@name) ssh << 'rm -vf /etc/chef/client.rb' ssh << 'rm -vf /etc/chef/client-taste-tester.rb' ssh << 'ln -vs /etc/chef/client-prod.rb /etc/chef/client.rb' ssh << 'rm -vf /etc/chef/client.pem' ssh << 'ln -vs /etc/chef/client-prod.pem /etc/chef/client.pem' - ssh << 'rm -vf /etc/chef/test_timestamp' + ssh << "rm -vf #{TasteTester::Config.timestamp_file}" ssh << 'logger -t taste-tester Returning server to production' ssh.run! end def who_is_testing ssh = TasteTester::SSH.new(@name) - ssh << 'grep \'^# TasteTester by\' /etc/chef/client.rb' - user = ssh.run.last.match(/# TasteTester by (.*)$/) - if user - user[1] - else - # Legacy FB stuff, remove after migration. Safe for everyone else. - ssh = TasteTester::SSH.new(@name) - ssh << 'file /etc/chef/client.rb' - user = ssh.run.last.match(/client-(.*)-(taste-tester|test).rb/) + ssh << 'grep "^# TasteTester by" /etc/chef/client.rb' + output = ssh.run + if output.first == 0 + user = output.last.match(/# TasteTester by (.*)$/) if user - user[1] - else - nil + return user[1] end end + + # Legacy FB stuff, remove after migration. Safe for everyone else. + ssh = TasteTester::SSH.new(@name) + ssh << 'file /etc/chef/client.rb' + output = ssh.run + if output.first == 0 + user = output.last.match(/client-(.*)-(taste-tester|test).rb/) + if user + return user[1] + end + end + + return nil end def in_test? ssh = TasteTester::SSH.new(@name) - ssh << 'test -f /etc/chef/test_timestamp' - if ssh.run.first == 0 && who_is_testing != ENV['USER'] + ssh << "test -f #{TasteTester::Config.timestamp_file}" + if ssh.run.first == 0 && who_is_testing && who_is_testing != ENV['USER'] true else false @@ -151,18 +138,20 @@ def in_test? end def keeptesting - logger.warn("Renewing taste-tester on #{@name} until #{@timestamp}") - nuke_old_tunnel - setup_tunnel + logger.warn("Renewing taste-tester on #{@name} until" + + " #{TasteTester::Config.testing_end_time}") + TasteTester::Tunnel.kill(@name) + @tunnel = TasteTester::Tunnel.new(@name, @server) + @tunnel.run end private def config if TasteTester::Config.use_ssh_tunnels - url = 'http://localhost:4001' + url = "http://localhost:#{@tunnel.port}" else - url = "http://#{@chef_server}:4000" + url = "http://#{@server.host}:#{TasteTester::State.port}" end ttconfig = <<-eos # TasteTester by #{@user} @@ -183,7 +172,7 @@ def config if extra ttconfig += <<-eos # Begin user-hook specified code -#{extra} + #{extra} # End user-hook secified code eos diff --git a/taste-tester/taste-tester/logging.rb b/taste-tester/taste-tester/logging.rb index b729131..824a2b6 100755 --- a/taste-tester/taste-tester/logging.rb +++ b/taste-tester/taste-tester/logging.rb @@ -43,9 +43,9 @@ def formatter end else proc do |severity, datetime, progname, msg| - msg.prepend("#{severity}: ") unless severity == 'WARN' + msg.to_s.prepend("#{severity}: ") unless severity == 'WARN' if severity == 'ERROR' - msg = msg.red + msg = msg.to_s.red end "#{msg}\n" end diff --git a/taste-tester/taste-tester/server.rb b/taste-tester/taste-tester/server.rb index 7755aac..0abfd11 100755 --- a/taste-tester/taste-tester/server.rb +++ b/taste-tester/taste-tester/server.rb @@ -6,19 +6,22 @@ require_relative '../../between-meals/util' require_relative 'config' +require_relative 'state' module TasteTester # Stateless chef-zero server management class Server include TasteTester::Config include TasteTester::Logging - include ::BetweenMeals::Util + extend ::BetweenMeals::Util - attr_accessor :user, :host, :port + attr_accessor :user, :host - def initialize(port = 4000) + def initialize + @state = TasteTester::State.new @ref_file = TasteTester::Config.ref_file ref_dir = File.dirname(File.expand_path(@ref_file)) + @zero_path = TasteTester::Config.chef_zero_path unless File.directory?(ref_dir) begin FileUtils.mkpath(ref_dir) @@ -27,8 +30,9 @@ def initialize(port = 4000) logger.warn(e) end end + @user = ENV['USER'] - @port = port + # If we are using SSH tunneling listen on localhost, otherwise listen # on all addresses - both v4 and v6. Note that on localhost, ::1 is # v6-only, so we default to 127.0.0.1 instead. @@ -36,83 +40,83 @@ def initialize(port = 4000) @host = 'localhost' end - def _start - knife = BetweenMeals::Knife.new( - :logger => logger, - :user => @user, - :host => @host, - :port => @port, - :role_dir => TasteTester::Config.roles, - :cookbook_dirs => TasteTester::Config.cookbooks, - :checksum_dir => TasteTester::Config.checksum_dir, - ) - knife.write_user_config - - FileUtils.touch(@ref_file) - Mixlib::ShellOut.new( - "/opt/chef/embedded/bin/chef-zero --host #{@addr} --port #{@port} -d" - ).run_command.error! - end - def start - return if running? - File.delete(@ref_file) if File.exists?(@ref_file) + return if TasteTester::Server.running? + @state.wipe logger.warn('Starting taste-tester server') - _start - end - - def _stop - File.delete(@ref_file) if File.exists?(@ref_file) - s = Mixlib::ShellOut.new("pkill -9 -u #{ENV['USER']} -f bin/chef-zero") - s.run_command + write_config + start_chef_zero end def stop logger.warn('Stopping taste-tester server') - _stop + stop_chef_zero end def restart logger.warn('Restarting taste-tester server') - if running? - _stop - # you have to give it a moment to stop or the stat fails - sleep(1) + if TasteTester::Server.running? + stop_chef_zero end - _start + write_config + start_chef_zero end - def self.running?(port = 4000) - begin - Timeout.timeout(1) do - begin - s = TCPSocket.new('127.0.0.1', port) - s.close - return true - rescue Errno::ECONNREFUSED, Errno::EHOSTEACH - return false - end - end - rescue Timeout::Error - return false - end - return false + def port + @state.port + end + + def port=(port) + @state.port = port end def latest_uploaded_ref - File.open(@ref_file, 'r').readlines.first.strip - rescue - false + @state.ref end def latest_uploaded_ref=(ref) - File.write(@ref_file, ref) - rescue - logger.error('Unable to write the reffile') + @state.ref = ref + end + + def self.running? + if TasteTester::State.port + return port_open?(TasteTester::State.port) + end + false end - def running? - TasteTester::Server.running? + private + + def write_config + knife = BetweenMeals::Knife.new( + :logger => logger, + :user => @user, + :host => @host, + :port => port, + :role_dir => TasteTester::Config.roles, + :cookbook_dirs => TasteTester::Config.cookbooks, + :checksum_dir => TasteTester::Config.checksum_dir, + ) + knife.write_user_config + end + + def start_chef_zero + @state.wipe + @state.port = TasteTester::Config.chef_port + logger.info("Starting chef-zero of port #{@state.port}") + Mixlib::ShellOut.new( + "/opt/chef/embedded/bin/chef-zero --host #{@addr}" + + " --port #{@state.port} -d" + ).run_command.error! + end + + def stop_chef_zero + @state.wipe + logger.info('Killing your chef-zero instances') + s = Mixlib::ShellOut.new("pkill -9 -u #{ENV['USER']} -f bin/chef-zero") + s.run_command + # You have to give it a moment to stop or the stat fails + sleep(1) end end end diff --git a/taste-tester/taste-tester/ssh.rb b/taste-tester/taste-tester/ssh.rb index 3ad1086..e45cbc3 100755 --- a/taste-tester/taste-tester/ssh.rb +++ b/taste-tester/taste-tester/ssh.rb @@ -22,35 +22,39 @@ def add(string) alias_method :<<, :add def run - prepare - @status, @output = exec(@cmd, logger) + @status, @output = exec(cmd, logger) end def run! - prepare - @status, @output = exec!(@cmd, logger) + @status, @output = exec!(cmd, logger) rescue => e + # rubocop:disable LineLength error = <<-MSG -SSH returned error while connecting to root@#{@host} +SSH returned error while connecting to #{TasteTester::Config.user}@#{@host} The host might be broken or your SSH access is not working properly -Try doing 'ssh -v root@#{@host}' and come back once that works +Try doing 'ssh -v #{TasteTester::Config.user}@#{@host}' and come back once that works MSG + # rubocop:enable LineLength error.lines.each { |x| logger.error x.strip } logger.error(e.message) end private - def prepare + def cmd @cmds.each do |cmd| logger.info("Will run: '#{cmd}' on #{@host}") end cmds = @cmds.join(' && ') - @cmd = "ssh -T -o BatchMode=yes -o ConnectTimeout=#{@timeout} " - if @tunnel - @cmd += ' -f -R 4001:localhost:4000 ' + cmd = "ssh -T -o BatchMode=yes -o ConnectTimeout=#{@timeout} " + cmd += "#{TasteTester::Config.user}@#{@host} " + if TasteTester::Config.user != 'root' + cc = Base64.encode64(cmds).gsub(/\n/, '') + cmd += "\"echo '#{cc}' | base64 --decode | sudo bash -x\"" + else + cmd += "\'#{cmds}\'" end - @cmd += "root@#{@host} \"#{cmds}\"" + cmd end end end diff --git a/taste-tester/taste-tester/state.rb b/taste-tester/taste-tester/state.rb new file mode 100644 index 0000000..c379741 --- /dev/null +++ b/taste-tester/taste-tester/state.rb @@ -0,0 +1,87 @@ +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 + +require 'fileutils' +require 'socket' +require 'timeout' + +require_relative '../../between-meals/util' +require_relative 'config' + +module TasteTester + # State of taste-tester processes + class State + include TasteTester::Config + include TasteTester::Logging + include ::BetweenMeals::Util + + def initialize + ref_dir = File.dirname(File.expand_path( + TasteTester::Config.ref_file + )) + unless File.directory?(ref_dir) + begin + FileUtils.mkpath(ref_dir) + rescue => e + logger.error("Chef temp dir #{ref_dir} missing and can't be created") + logger.error(e) + exit(1) + end + end + end + + def port + TasteTester::State.read(:port) + end + + def port=(port) + write(:port, port) + end + + def ref + TasteTester::State.read(:ref) + end + + def ref=(ref) + write(:ref, ref) + end + + def self.port + TasteTester::State.read(:port) + end + + def wipe + if TasteTester::Config.ref_file && + File.exists?(TasteTester::Config.ref_file) + File.delete(TasteTester::Config.ref_file) + end + end + + private + + def write(key, value) + begin + state = JSON.parse(File.read(TasteTester::Config.ref_file)) + rescue + state = {} + end + state[key.to_s] = value + ff = File.open( + TasteTester::Config.ref_file, + 'w' + ) + ff.write(state.to_json) + ff.close + rescue => e + logger.error('Unable to write the reffile') + logger.debug(e) + exit 0 + end + + def self.read(key) + JSON.parse(File.read(TasteTester::Config.ref_file))[key.to_s] + rescue => e + logger.debug(e) + nil + end + end +end diff --git a/taste-tester/taste-tester/tunnel.rb b/taste-tester/taste-tester/tunnel.rb new file mode 100755 index 0000000..62d4664 --- /dev/null +++ b/taste-tester/taste-tester/tunnel.rb @@ -0,0 +1,53 @@ +# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2 + +module TasteTester + # Thin ssh tunnel wrapper + class Tunnel + include TasteTester::Logging + include BetweenMeals::Util + + attr_reader :port + + def initialize(host, server, timeout = 5) + @host = host + @server = server + @timeout = timeout + if TasteTester::Config.testing_until + @delta_secs = TasteTester::Config.testing_until.strftime('%s').to_i - + Time.now.strftime('%s').to_i + else + @delta_secs = TasteTester::Config.testing_time + end + end + + def run + @port = TasteTester::Config.tunnel_port + logger.info("Setting up tunnel on port #{@port}") + @status, @output = exec!(cmd, logger) + rescue + logger.error 'Failed bringing up ssh tunnel' + exit(1) + end + + def cmd + cmds = "echo \\\$\\\$ > #{TasteTester::Config.timestamp_file} &&" + + " touch -t #{TasteTester::Config.testing_end_time}" + + " #{TasteTester::Config.timestamp_file} && sleep #{@delta_secs}" + cmd = "ssh -T -o BatchMode=yes -o ConnectTimeout=#{@timeout} " + + "-o ExitOnForwardFailure=yes -f -R #{@port}:localhost:" + + "#{@server.port} root@#{@host} \"#{cmds}\"" + cmd + end + + def self.kill(name) + ssh = TasteTester::SSH.new(name) + # Since commands are &&'d together, and we're using &&, we need to + # surround this in paryns, and make sure as a whole it evaluates + # to true so it doesn't mess up other things... even though this is + # the only thing we're currently executing in this SSH. + ssh << "( [ -s #{TasteTester::Config.timestamp_file} ]" + + " && kill -- -\$(cat #{TasteTester::Config.timestamp_file}); true )" + ssh.run! + end + end +end diff --git a/taste-tester/taste-untester b/taste-tester/taste-untester index 33b902a..11cd567 100755 --- a/taste-tester/taste-untester +++ b/taste-tester/taste-untester @@ -39,7 +39,7 @@ check_server() { current_config=$(readlink $CONFLINK) if [ "$current_config" = $PRODCONF ]; then if [ -f "$STAMPFILE" ]; then - run_cmd "rm -f $STAMPFILE" + rm -f $STAMPFILE fi return fi