1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
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
|