lib/taste_tester/ssh_util.rb (95 lines of code) (raw):

# Copyright 2020-present Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. module TasteTester class SSH module Util def jumps TasteTester::Config.jumps ? "-J #{TasteTester::Config.jumps}" : '' end def ssh_cmd_generator if TasteTester::Config.ssh_cmd_gen_template base_gen_args = { :user => TasteTester::Config.user, :jumps => jumps, :host => @host, } TasteTester::Config.ssh_cmd_gen_template % base_gen_args end end def ssh_generated_cmd if TasteTester::Config.ssh_cmd_gen_template # we store this generated command inside a class variable # so that we can directly refer to this while printing # logs and error messages begin # run the generator command only if it's not run already unless @ssh_generated_cmd generator = Mixlib::ShellOut.new(ssh_cmd_generator).run_command generator.error! @ssh_generated_cmd = generator.stdout.chomp end "#{@ssh_generated_cmd} #{@tunnel_options}".strip rescue Mixlib::ShellOut::ShellCommandFailed => e logger.error("The generator command: #{ssh_cmd_generator} " + 'failed during execution') logger.error(e.message) exit(1) end end end def ssh_vanilla_cmd "#{TasteTester::Config.ssh_command} #{jumps} -T -o BatchMode=yes " + '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no ' + "-o ConnectTimeout=#{TasteTester::Config.ssh_connect_timeout} " + @extra_options.to_s + "#{TasteTester::Config.user}@#{@host}" end def ssh_base_cmd if TasteTester::Config.ssh_cmd_gen_template ssh_generated_cmd else ssh_vanilla_cmd end end def error! error = <<~ERRORMESSAGE SSH returned error while connecting to #{@host} The host might be broken or your SSH access is not working properly Try running the following command to see if ssh is good: #{ssh_base_cmd} -v ERRORMESSAGE if TasteTester::Config.ssh_cmd_gen_template error += <<~ERRORMESSAGE The above command was generated, and it may be useful to run the generator directly instead: #{ssh_cmd_generator} ERRORMESSAGE end error += <<~ERRORMESSAGE If ssh works, add '-v' key to taste-tester to see the list of commands it's trying to execute, and try to run them manually on destination host ERRORMESSAGE logger.error(error) fail TasteTester::Exceptions::SshError end def build_ssh_cmd(ssh, command_list) if TasteTester::Config.windows_target # Powershell has no `&&`. So originally we looked into joining the # various commands with `; if ($LASTEXITCODE -ne 0) { exit 42 }; ` # except that it turns out lots of Powershell commands don't set # $LASTEXITCODE and so that crashes a lot. # # There is an `-and`, but it only works if you group things together # with `()`, but that loses any output. # # Technically in the latest preview of Powershell 7, `&&` exists, but # we cannot rely on this. # # So here we are. Thanks Windows Team. # # Anyway, what we *really* care about is that we exit if we_testing() # errors out, and on Windows, we can do that straight from the # powershell we generate there (we're not forking off awk), so the # `&&` isn't as critical. It's still a bummer that we continue on # if one of the commands fails, but... Well, it's Windows, # whatchyagonnado? cmds = command_list.join(' ; ') else cmds = command_list.join(' && ') end cmd = "#{ssh} " cc = Base64.encode64(cmds).delete("\n") if TasteTester::Config.windows_target # This is pretty horrible, but because there's no way I can find to # take base64 as stdin and output text, we end up having to do use # these PS functions. But they're going to pass through *both* bash # *and* powershell, so in order to preserve the quotes, it gets # pretty ugly. # # The tldr here is that in shell you can't escape quotes you're # using to quote something. So if you use single quotes, there's no # way to escape a single quote inside, and same with double-quotes. # As such we switch between quote-styles as necessary. As long as the # strings are back-to-back, shell handles this well. To make this # clear, imagine you want to echo this: # '"'" # Exactly like that. You would quote the first single quotes in double # quotes: "'" # Then the double quotes in single quotes: '"' # Now repeat twice and you get: echo "'"'"'"'"'"' # And that works reliably. # # We're doing the same thing here. What we want on the other side of # the ssh is: # [Text.Encoding]::Utf8.GetString([Convert]::FromBase64String('...')) # # But for this to work right the command we pass to SSH has to be in # single quotes too. For simplicity lets call those two functions # above GetString() and Base64(). So we'll start with: # ssh host 'GetString(Base64(' # We've closed that string, now we add the single quote we want there, # as well as the stuff inside of those double quotes, so we'll add: # '#{cc}')) # but that must be in double quotes since we're using single quotes. # Put that together: # ssh host 'GetString(Base64('"'#{cc}'))" # ^-----------------^^---------^ # string 1 string2 # No we're doing with needing single quotes inside of our string, go # back to using single-quotes so no variables get interpolated. We now # add: ' | powershell.exe -c -; exit $LASTEXITCODE' # ssh host 'GetString(Base64('"'#{cc}'))"' | powershell.exe ...' # ^-----------------^^---------^^---------------------^ # # More than you ever wanted to know about shell. You're welcome. # # But now we have to put it inside of a ruby string, :) # just for readability, put these crazy function names inside of # variables fun1 = '[Text.Encoding]::Utf8.GetString' fun2 = '[Convert]::FromBase64String' cmd += "'#{fun1}(#{fun2}('\"'#{cc}'))\"' | " # ^----------------^ ^----------^^--- # single-q double-q single-q # string 1 string2 string3 cmd += 'powershell.exe -c -; exit $LASTEXITCODE\'' # ----------------------------------------^ # continued string3 else cmd += "\"echo '#{cc}' | base64 --decode" if TasteTester::Config.user != 'root' cmd += ' | sudo bash -x"' else cmd += ' | bash -x"' end end cmd end end end end