File: shell.rb

package info (click to toggle)
vagrant 2.2.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 8,072 kB
  • sloc: ruby: 80,731; sh: 369; makefile: 9; lisp: 1
file content (255 lines) | stat: -rw-r--r-- 8,315 bytes parent folder | download
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
require "timeout"

require "log4r"

require "vagrant/util/retryable"
require "vagrant/util/silence_warnings"

Vagrant::Util::SilenceWarnings.silence! do
  require "winrm"
end

require "winrm-elevated"
require "winrm-fs"

module VagrantPlugins
  module CommunicatorWinRM
    class WinRMShell
      include Vagrant::Util::Retryable

      # Exit code generated when user is invalid. Can occur
      # after a hostname update
      INVALID_USERID_EXITCODE = -196608

      # These are the exceptions that we retry because they represent
      # errors that are generally fixed from a retry and don't
      # necessarily represent immediate failure cases.
      @@exceptions_to_retry_on = [
        HTTPClient::KeepAliveDisconnected,
        WinRM::WinRMHTTPTransportError,
        WinRM::WinRMAuthorizationError,
        WinRM::WinRMWSManFault,
        Errno::EACCES,
        Errno::EADDRINUSE,
        Errno::ECONNREFUSED,
        Errno::ECONNRESET,
        Errno::ENETUNREACH,
        Errno::EHOSTUNREACH,
        Timeout::Error
      ]

      attr_reader :logger
      attr_reader :host
      attr_reader :port
      attr_reader :username
      attr_reader :password
      attr_reader :execution_time_limit
      attr_reader :config

      def initialize(host, port, config)
        @logger = Log4r::Logger.new("vagrant::communication::winrmshell")
        @logger.debug("initializing WinRMShell")

        @host                  = host
        @port                  = port
        @username              = config.username
        @password              = config.password
        @execution_time_limit  = config.execution_time_limit
        @config                = config
      end

      def powershell(command, opts = {}, &block)
        connection.shell(:powershell) do |shell|
          execute_with_rescue(shell, command, &block)
        end
      end

      def cmd(command, opts = {}, &block)
        shell_opts = {}
        shell_opts[:codepage] = @config.codepage if @config.codepage
        connection.shell(:cmd, shell_opts) do |shell|
          execute_with_rescue(shell, command, &block)
        end
      end

      def elevated(command, opts = {}, &block)
        connection.shell(:elevated) do |shell|
          shell.interactive_logon = opts[:interactive] || false
          result = execute_with_rescue(shell, command, &block)
          if result.exitcode == INVALID_USERID_EXITCODE && result.stderr.include?(":UserId:")
            uname = shell.username
            ename = elevated_username
            if uname != ename
              @logger.warn("elevated command failed due to username error")
              @logger.warn("retrying command using machine prefixed username - #{ename}")
              begin
                shell.username = ename
                result = execute_with_rescue(shell, command, &block)
              ensure
                shell.username = uname
              end
            end
          end
          result
        end
      end

      def wql(query, opts = {}, &block)
        retryable(tries: @config.max_tries, on: @@exceptions_to_retry_on, sleep: @config.retry_delay) do
          connection.run_wql(query)
        end
      rescue => e
        raise_winrm_exception(e, "run_wql", query)
      end

      # @param from [Array<String>, String] a single path or folder, or an
      #        array of paths and folders to upload to the guest
      # @param to [String] a path or folder on the guest to upload to
      # @return [FixNum] Total size transfered from host to guest
      def upload(from, to)
        file_manager = WinRM::FS::FileManager.new(connection)
        if from.is_a?(Array)
          # Preserve return FixNum of bytes transfered
          return_bytes = 0
          from.each do |file|
            return_bytes += file_manager.upload(file, to)
          end
          return return_bytes
        else
          file_manager.upload(from, to)
        end
      end

      def download(from, to)
        file_manager = WinRM::FS::FileManager.new(connection)
        file_manager.download(from, to)
      end

      protected

      def execute_with_rescue(shell, command, &block)
        handle_output(shell, command, &block)
      rescue => e
        raise_winrm_exception(e, shell.class.name.split("::").last, command)
      end

      def handle_output(shell, command, &block)
        output = shell.run(command) do |out, err|
          block.call(:stdout, out) if block_given? && out
          block.call(:stderr, err) if block_given? && err
        end

        @logger.debug("Output: #{output.inspect}")

        # Verify that we didn't get a parser error, and if so we should
        # set the exit code to 1. Parse errors return exit code 0 so we
        # need to do this.
        if output.exitcode == 0
          if output.stderr.include?("ParserError")
            @logger.warn("Detected ParserError, setting exit code to 1")
            output.exitcode = 1
          end
        end

        return output
      end

      def raise_winrm_exception(exception, shell = nil, command = nil)
        case exception
        when WinRM::WinRMAuthorizationError
          raise Errors::AuthenticationFailed,
              user: @config.username,
              password: @config.password,
              endpoint: endpoint,
              message: exception.message
        when WinRM::WinRMHTTPTransportError
          raise Errors::ExecutionError,
            shell: shell,
            command: command,
            message: exception.message
        when OpenSSL::SSL::SSLError
          raise Errors::SSLError, message: exception.message
        when HTTPClient::TimeoutError
          raise Errors::ConnectionTimeout, message: exception.message
        when Errno::ETIMEDOUT
          raise Errors::ConnectionTimeout
          # This is raised if the connection timed out
        when Errno::ECONNREFUSED
          # This is raised if we failed to connect the max amount of times
          raise Errors::ConnectionRefused
        when Errno::ECONNRESET
          # This is raised if we failed to connect the max number of times
          # due to an ECONNRESET.
          raise Errors::ConnectionReset
        when Errno::EHOSTDOWN
          # This is raised if we get an ICMP DestinationUnknown error.
          raise Errors::HostDown
        when Errno::EHOSTUNREACH
          # This is raised if we can't work out how to route traffic.
          raise Errors::NoRoute
        else
          raise Errors::ExecutionError,
            shell: shell,
            command: command,
            message: exception.message
        end
      end

      def new_connection
        @logger.info("Attempting to connect to WinRM...")
        @logger.info("  - Host: #{@host}")
        @logger.info("  - Port: #{@port}")
        @logger.info("  - Username: #{@config.username}")
        @logger.info("  - Transport: #{@config.transport}")

        client = ::WinRM::Connection.new(endpoint_options)
        client.logger = @logger
        client
      end

      def connection
        @connection ||= new_connection
      end

      def endpoint
        case @config.transport.to_sym
        when :ssl
          "https://#{@host}:#{@port}/wsman"
        when :plaintext, :negotiate
          "http://#{@host}:#{@port}/wsman"
        else
          raise Errors::WinRMInvalidTransport, transport: @config.transport
        end
      end

      def endpoint_options
        { endpoint: endpoint,
          transport: @config.transport,
          operation_timeout: @config.timeout,
          user: @username,
          password: @password,
          host: @host,
          port: @port,
          basic_auth_only: @config.basic_auth_only,
          no_ssl_peer_verification: !@config.ssl_peer_verification,
          retry_delay: @config.retry_delay,
          retry_limit: @config.max_tries }
      end

      def elevated_username
        if username.include?("\\")
          return username
        end
        computername = ""
        powershell("Write-Output $env:computername") do |type, data|
          computername << data if type == :stdout
        end
        computername.strip!
        if computername.empty?
          return username
        end
        "#{computername}\\#{username}"
      end
    end #WinShell class
  end
end