require "digest/md5" require "tempfile" module VagrantPlugins module DockerProvider # This communicator uses the host VM as proxy to communicate to the # actual Docker container via SSH. class Communicator < Vagrant.plugin("2", :communicator) def initialize(machine) @machine = machine @host_vm = machine.provider.host_vm # We only work on the Docker provider if machine.provider_name != :docker raise Errors::CommunicatorNotDocker end end #------------------------------------------------------------------- # Communicator Methods #------------------------------------------------------------------- def ready? # We can't be ready if we can't talk to the host VM return false if !@host_vm.communicate.ready? # We're ready if we can establish an SSH connection to the container command = container_ssh_command return false if !command @host_vm.communicate.test("#{command} exit") end def download(from, to) # Same process as upload, but in reverse # First, we use `cat` to copy that file from the Docker container. temp = "/tmp/docker_d#{Time.now.to_i}_#{rand(100000)}" @host_vm.communicate.execute("#{container_ssh_command} 'cat #{from}' >#{temp}") # Then, we download this from the host VM. @host_vm.communicate.download(temp, to) # Remove the temporary file @host_vm.communicate.execute("rm -f #{temp}", error_check: false) end def execute(command, **opts, &block) fence = {} fence[:stderr] = "VAGRANT FENCE: #{Time.now.to_i} #{rand(100000)}" fence[:stdout] = "VAGRANT FENCE: #{Time.now.to_i} #{rand(100000)}" # We want to emulate how the SSH communicator actually executes # things, so we build up the list of commands to execute in a # giant shell script. tf = Tempfile.new("vagrant") tf.binmode tf.write("export TERM=vt100\n") tf.write("echo #{fence[:stdout]}\n") tf.write("echo #{fence[:stderr]} >&2\n") tf.write("#{command}\n") tf.write("exit\n") tf.close # Upload the temp file to the remote machine remote_temp = "/tmp/docker_#{Time.now.to_i}_#{rand(100000)}" @host_vm.communicate.upload(tf.path, remote_temp) # Determine the shell to execute. Prefer the explicitly passed in shell # over the default configured shell. If we are using `sudo` then we # need to wrap the shell in a `sudo` call. shell_cmd = @machine.config.ssh.shell shell_cmd = opts[:shell] if opts[:shell] shell_cmd = "sudo -E -H #{shell_cmd}" if opts[:sudo] acc = {} fenced = {} result = @host_vm.communicate.execute( "#{container_ssh_command} '#{shell_cmd}' <#{remote_temp}", opts) do |type, data| # If we don't have a block, we don't care about the data next if !block # We only care about stdout and stderr output next if ![:stdout, :stderr].include?(type) # If we reached our fence, then just output if fenced[type] block.call(type, data) next end # Otherwise, accumulate acc[type] = data # Look for the fence index = acc[type].index(fence[type]) next if !index fenced[type] = true index += fence[type].length data = acc[type][index..-1].chomp acc[type] = "" block.call(type, data) end @host_vm.communicate.execute("rm -f #{remote_temp}", error_check: false) return result end def sudo(command, **opts, &block) opts = { sudo: true }.merge(opts) execute(command, opts, &block) end def test(command, **opts) opts = { error_check: false }.merge(opts) execute(command, opts) == 0 end def upload(from, to) # First, we upload this to the host VM to some temporary directory. to_temp = "/tmp/docker_#{Time.now.to_i}_#{rand(100000)}" @host_vm.communicate.upload(from, to_temp) # Then, we use `cat` to get that file into the Docker container. @host_vm.communicate.execute( "#{container_ssh_command} 'cat >#{to}' <#{to_temp}") # Remove the temporary file @host_vm.communicate.execute("rm -f #{to_temp}", error_check: false) end #------------------------------------------------------------------- # Other Methods #------------------------------------------------------------------- # This returns the raw SSH command string that can be used to # connect via SSH to the container if you're on the same machine # as the container. # # @return [String] def container_ssh_command # Get the container's SSH info info = @machine.ssh_info return nil if !info info[:port] ||= 22 # Make sure our private keys are synced over to the host VM ssh_args = sync_private_keys(info).map do |path| "-i #{path}" end # Use ad-hoc SSH options for the hop on the docker proxy if info[:forward_agent] ssh_args << "-o ForwardAgent=yes" end ssh_args.concat(["-o Compression=yes", "-o ConnectTimeout=5", "-o StrictHostKeyChecking=no", "-o UserKnownHostsFile=/dev/null"]) # Build the SSH command "ssh #{info[:username]}@#{info[:host]} -p#{info[:port]} #{ssh_args.join(" ")}" end protected def sync_private_keys(info) @keys ||= {} id = Digest::MD5.hexdigest( @machine.env.root_path.to_s + @machine.name.to_s) result = [] info[:private_key_path].each do |path| if !@keys[path.to_s] # We haven't seen this before, upload it! guest_path = "/tmp/key_#{id}_#{Digest::MD5.hexdigest(path.to_s)}" @host_vm.communicate.upload(path.to_s, guest_path) # Make sure it has the proper chmod @host_vm.communicate.execute("chmod 0600 #{guest_path}") # Set it @keys[path.to_s] = guest_path end result << @keys[path.to_s] end result end end end end