File: communicator.rb

package info (click to toggle)
vagrant 2.2.14%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 9,800 kB
  • sloc: ruby: 97,301; sh: 375; makefile: 16; lisp: 1
file content (196 lines) | stat: -rw-r--r-- 6,493 bytes parent folder | download | duplicates (4)
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