Class: WinRM::WinRMWebService

Inherits:
Object
  • Object
show all
Defined in:
lib/winrm/winrm_service.rb

Overview

This is the main class that does the SOAP request/response logic. There are a few helper classes, but pretty much everything comes through here first.

Constant Summary collapse

DEFAULT_TIMEOUT =
'PT60S'
DEFAULT_MAX_ENV_SIZE =
153600
DEFAULT_LOCALE =
'en-US'

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(endpoint, transport = :kerberos, opts = {}) ⇒ WinRMWebService

Returns a new instance of WinRMWebService.

Parameters:

  • endpoint (String, URI)

    the WinRM webservice endpoint

  • transport (Symbol) (defaults to: :kerberos)

    either :kerberos(default)/:ssl/:plaintext

  • opts (Hash) (defaults to: {})

    Misc opts for the various transports. @see WinRM::HTTP::HttpTransport @see WinRM::HTTP::HttpGSSAPI @see WinRM::HTTP::HttpSSL



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/winrm/winrm_service.rb', line 40

def initialize(endpoint, transport = :kerberos, opts = {})
  @endpoint = endpoint
  @timeout = DEFAULT_TIMEOUT
  @max_env_sz = DEFAULT_MAX_ENV_SIZE
  @locale = DEFAULT_LOCALE
  setup_logger
  configure_retries(opts)
  case transport
  when :kerberos
    require 'gssapi'
    require 'gssapi/extensions'
    @xfer = HTTP::HttpGSSAPI.new(endpoint, opts[:realm], opts[:service], opts[:keytab], opts)
  when :plaintext
    @xfer = HTTP::HttpPlaintext.new(endpoint, opts[:user], opts[:pass], opts)
  when :ssl
    @xfer = HTTP::HttpSSL.new(endpoint, opts[:user], opts[:pass], opts[:ca_trust_path], opts)
  else
    raise "Invalid transport '#{transport}' specified, expected: kerberos, plaintext, ssl."
  end
end

Instance Attribute Details

#endpointObject (readonly)

Returns the value of attribute endpoint.



30
31
32
# File 'lib/winrm/winrm_service.rb', line 30

def endpoint
  @endpoint
end

#loggerObject

Returns the value of attribute logger.



32
33
34
# File 'lib/winrm/winrm_service.rb', line 32

def logger
  @logger
end

#retry_delayObject (readonly)

Returns the value of attribute retry_delay.



30
31
32
# File 'lib/winrm/winrm_service.rb', line 30

def retry_delay
  @retry_delay
end

#retry_limitObject (readonly)

Returns the value of attribute retry_limit.



30
31
32
# File 'lib/winrm/winrm_service.rb', line 30

def retry_limit
  @retry_limit
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



30
31
32
# File 'lib/winrm/winrm_service.rb', line 30

def timeout
  @timeout
end

Instance Method Details

#cleanup_command(shell_id, command_id) ⇒ true

Clean-up after a command.

Parameters:

  • shell_id (String)

    The shell id on the remote machine. See #open_shell

  • command_id (String)

    The command id on the remote machine. See #run_command

Returns:

  • (true)

    This should have more error checking but it just returns true for now.

See Also:



269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/winrm/winrm_service.rb', line 269

def cleanup_command(shell_id, command_id)
  # Signal the Command references to terminate (close stdout/stderr)
  body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id))) }
    env.tag!(:env, :Body) do |env_body|
      env_body.tag!("#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}) { |cl| cl << Gyoku.xml(body) }
    end
  end
  resp = send_message(builder.target!)
  true
end

#close_shell(shell_id) ⇒ true

Close the shell

Parameters:

  • shell_id (String)

    The shell id on the remote machine. See #open_shell

Returns:

  • (true)

    This should have more error checking but it just returns true for now.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/winrm/winrm_service.rb', line 287

def close_shell(shell_id)
  logger.debug("[WinRM] closing remote shell #{shell_id} on #{@endpoint}")
  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')

  builder.tag!('env:Envelope', namespaces) do |env|
    env.tag!('env:Header') { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id))) }
    env.tag!('env:Body')
  end

  resp = send_message(builder.target!)
  logger.debug("[WinRM] remote shell #{shell_id} closed")
  true
end

#create_executor {|a| ... } ⇒ CommandExecutor

Creates a CommandExecutor initialized with this WinRMWebService If called with a block, create_executor yields an executor and ensures that the executor is closed after the block completes. The CommandExecutor is simply returned if no block is given.

Yield Parameters:

Returns:



335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'lib/winrm/winrm_service.rb', line 335

def create_executor(&block)
  executor = CommandExecutor.new(self)
  executor.open

  if block_given?
    begin
      yield executor
    ensure
      executor.close
    end
  else
    executor
  end
end

#get_command_output(shell_id, command_id, &block) ⇒ Hash

Get the Output of the given shell and command

Parameters:

  • shell_id (String)

    The shell id on the remote machine. See #open_shell

  • command_id (String)

    The command id on the remote machine. See #run_command

Returns:

  • (Hash)

    Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key is either :stdout or :stderr. The reason it is in an Array so so we can get the output in the order it ocurrs on the console.



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
256
257
258
259
260
261
262
# File 'lib/winrm/winrm_service.rb', line 218

def get_command_output(shell_id, command_id, &block)
  body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
    :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}

  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id))) }
    env.tag!(:env, :Body) do |env_body|
      env_body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(body) }
    end
  end

  resp_doc = nil
  request_msg = builder.target!
  done_elems = []
  output = Output.new

  while done_elems.empty?
    resp_doc = send_get_output_message(request_msg)

    REXML::XPath.match(resp_doc, "//#{NS_WIN_SHELL}:Stream").each do |n|
      next if n.text.nil? || n.text.empty?

      # decode and strip off BOM which win 2008R2 applies
      stream = { n.attributes['Name'].to_sym => Base64.decode64(n.text).force_encoding('utf-8').sub("\xEF\xBB\xBF", "") }
      output[:data] << stream
      yield stream[:stdout], stream[:stderr] if block_given?
    end

    # We may need to get additional output if the stream has not finished.
    # The CommandState will change from Running to Done like so:
    # @example
    #   from...
    #   <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
    #   to...
    #   <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
    #     <rsp:ExitCode>0</rsp:ExitCode>
    #   </rsp:CommandState>
    done_elems = REXML::XPath.match(resp_doc, "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']")
  end

  output[:exitcode] = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:ExitCode").text.to_i
  output
end

#locale(locale) ⇒ Object

Set the locale

Parameters:

  • locale (String)

    the locale to set for future messages

See Also:



88
89
90
# File 'lib/winrm/winrm_service.rb', line 88

def locale(locale)
  @locale = locale
end

#max_env_size(byte_sz) ⇒ Object

Max envelope size

Parameters:

  • byte_sz (Fixnum)

    the max size in bytes to allow for the response

See Also:



81
82
83
# File 'lib/winrm/winrm_service.rb', line 81

def max_env_size(byte_sz)
  @max_env_sz = byte_sz
end

#open_shell(shell_opts = {}, &block) ⇒ String

Create a Shell on the destination host

Parameters:

  • shell_opts (Hash<optional>) (defaults to: {})

    additional shell options you can pass

Options Hash (shell_opts):

  • :i_stream (String)

    Which input stream to open. Leave this alone unless you know what you’re doing (default: stdin)

  • :o_stream (String)

    Which output stream to open. Leave this alone unless you know what you’re doing (default: stdout stderr)

  • :working_directory (String)

    the directory to create the shell in

  • :env_vars (Hash)

    environment variables to set for the shell. For instance; :env_vars => => ‘val1’, :myvar2 => ‘var2’

Returns:

  • (String)

    The ShellId from the SOAP response. This is our open shell instance on the remote machine.



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
# File 'lib/winrm/winrm_service.rb', line 100

def open_shell(shell_opts = {}, &block)
  logger.debug("[WinRM] opening remote shell on #{@endpoint}")
  i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
  o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
  codepage = shell_opts.has_key?(:codepage) ? shell_opts[:codepage] : 65001 # utf8 as default codepage (from https://msdn.microsoft.com/en-us/library/dd317756(VS.85).aspx)
  noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
  h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
    :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
  shell_body = {
    "#{NS_WIN_SHELL}:InputStreams" => i_stream,
    "#{NS_WIN_SHELL}:OutputStreams" => o_stream
  }
  shell_body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
  shell_body["#{NS_WIN_SHELL}:IdleTimeOut"] = shell_opts[:idle_timeout] if(shell_opts.has_key?(:idle_timeout) && shell_opts[:idle_timeout].is_a?(String))
  if(shell_opts.has_key?(:env_vars) && shell_opts[:env_vars].is_a?(Hash))
    keys = shell_opts[:env_vars].keys
    vals = shell_opts[:env_vars].values
    shell_body["#{NS_WIN_SHELL}:Environment"] = {
      "#{NS_WIN_SHELL}:Variable" => vals,
      :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
    }
  end
  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_create,h_opts)) }
    env.tag! :env, :Body do |body|
      body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body)}
    end
  end

  resp_doc = send_message(builder.target!)
  shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
  logger.debug("[WinRM] remote shell #{shell_id} is open on #{@endpoint}")

  if block_given?
    begin
      yield shell_id
    ensure
      close_shell(shell_id)
    end
  else
    shell_id
  end
end

#run_cmd(command, arguments = [], &block) ⇒ Hash Also known as: cmd

DEPRECATED: Use WinRM::CommandExecutor#run_cmd instead Run a CMD command

Parameters:

  • command (String)

    The command to run on the remote system

  • arguments (Array <String>) (defaults to: [])

    arguments to the command

  • an (String)

    existing and open shell id to reuse

Returns:

  • (Hash)

    :stdout and :stderr



308
309
310
311
312
313
# File 'lib/winrm/winrm_service.rb', line 308

def run_cmd(command, arguments = [], &block)
  logger.warn("WinRM::WinRMWebService#run_cmd is deprecated. Use WinRM::CommandExecutor#run_cmd instead")
  create_executor do |executor|
    executor.run_cmd(command, arguments, &block)
  end
end

#run_command(shell_id, command, arguments = [], cmd_opts = {}, &block) ⇒ String

Run a command on a machine with an open shell

Parameters:

  • shell_id (String)

    The shell id on the remote machine. See #open_shell

  • command (String)

    The command to run on the remote machine

  • arguments (Array<String>) (defaults to: [])

    An array of arguments for this command

Returns:

  • (String)

    The CommandId from the SOAP response. This is the ID we need to query in order to get output.



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
# File 'lib/winrm/winrm_service.rb', line 151

def run_command(shell_id, command, arguments = [], cmd_opts = {}, &block)
  consolemode = cmd_opts.has_key?(:console_mode_stdin) ? cmd_opts[:console_mode_stdin] : 'TRUE'
  skipcmd     = cmd_opts.has_key?(:skip_cmd_shell) ? cmd_opts[:skip_cmd_shell] : 'FALSE'

  h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => {
    "#{NS_WSMAN_DMTF}:Option" => [consolemode, skipcmd],
    :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_CONSOLEMODE_STDIN','WINRS_SKIP_CMD_SHELL']}}}
  }
  body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}

  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id))) }
    env.tag!(:env, :Body) do |env_body|
      env_body.tag!("#{NS_WIN_SHELL}:CommandLine") { |cl| cl << Gyoku.xml(body) }
    end
  end

  # Grab the command element and unescape any single quotes - issue 69
  xml = builder.target!
  escaped_cmd = /<#{NS_WIN_SHELL}:Command>(.+)<\/#{NS_WIN_SHELL}:Command>/m.match(xml)[1]
  xml[escaped_cmd] = escaped_cmd.gsub(/&#39;/, "'")

  resp_doc = send_message(xml)
  command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text

  if block_given?
    begin
      yield command_id
    ensure
      cleanup_command(shell_id, command_id)
    end
  else
    command_id
  end
end

#run_powershell_script(script_file, &block) ⇒ Hash Also known as: powershell

DEPRECATED: Use WinRM::CommandExecutor#run_powershell_script instead Run a Powershell script that resides on the local box.

Parameters:

  • script_file (IO, String)

    an IO reference for reading the Powershell script or the actual file contents

  • an (String)

    existing and open shell id to reuse

Returns:

  • (Hash)

    :stdout and :stderr



321
322
323
324
325
326
# File 'lib/winrm/winrm_service.rb', line 321

def run_powershell_script(script_file, &block)
  logger.warn("WinRM::WinRMWebService#run_powershell_script is deprecated. Use WinRM::CommandExecutor#run_cmd instead")
  create_executor do |executor|
    executor.run_powershell_script(script_file, &block)
  end
end

#run_wql(wql) ⇒ Hash Also known as: wql

Run a WQL Query

Parameters:

  • wql (String)

    The WQL query

Returns:

  • (Hash)

    Returns a Hash that contain the key/value pairs returned from the query.

See Also:



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
# File 'lib/winrm/winrm_service.rb', line 354

def run_wql(wql)

  body = { "#{NS_WSMAN_DMTF}:OptimizeEnumeration" => nil,
    "#{NS_WSMAN_DMTF}:MaxElements" => '32000',
    "#{NS_WSMAN_DMTF}:Filter" => wql,
    :attributes! => { "#{NS_WSMAN_DMTF}:Filter" => {'Dialect' => 'http://schemas.microsoft.com/wbem/wsman/1/WQL'}}
  }

  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_wmi,action_enumerate)) }
    env.tag!(:env, :Body) do |env_body|
      env_body.tag!("#{NS_ENUM}:Enumerate") { |en| en << Gyoku.xml(body) }
    end
  end

  resp = send_message(builder.target!)
  parser = Nori.new(:parser => :rexml, :advanced_typecasting => false, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym }, :strip_namespaces => true)
  hresp = parser.parse(resp.to_s)[:envelope][:body]
  
  # Normalize items so the type always has an array even if it's just a single item.
  items = {}
  if hresp[:enumerate_response][:items]
    hresp[:enumerate_response][:items].each_pair do |k,v|
      if v.is_a?(Array)
        items[k] = v
      else
        items[k] = [v]
      end
    end
  end
  items
end

#set_timeout(op_timeout_sec, receive_timeout_sec = nil) ⇒ String Also known as: op_timeout

Operation timeout.

Unless specified the client receive timeout will be 10s + the operation timeout.

Parameters:

  • The (Fixnum)

    number of seconds to set the WinRM operation timeout

  • The (Fixnum)

    number of seconds to set the Ruby receive timeout

Returns:

  • (String)

    The ISO 8601 formatted operation timeout

See Also:



71
72
73
74
75
# File 'lib/winrm/winrm_service.rb', line 71

def set_timeout(op_timeout_sec, receive_timeout_sec=nil)
  @timeout = Iso8601Duration.sec_to_dur(op_timeout_sec)
  @xfer.receive_timeout = receive_timeout_sec || op_timeout_sec + 10
  @timeout
end

#toggle_nori_type_casting(to) ⇒ Object



390
391
392
393
# File 'lib/winrm/winrm_service.rb', line 390

def toggle_nori_type_casting(to)
  logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
    'please remove calls to it')
end

#write_stdin(shell_id, command_id, stdin) ⇒ Object



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/winrm/winrm_service.rb', line 189

def write_stdin(shell_id, command_id, stdin)
  # Signal the Command references to terminate (close stdout/stderr)
  body = {
    "#{NS_WIN_SHELL}:Send" => {
      "#{NS_WIN_SHELL}:Stream" => {
        "@Name" => 'stdin',
        "@CommandId" => command_id,
        :content! => Base64.encode64(stdin)
      }
    }
  }
  builder = Builder::XmlMarkup.new
  builder.instruct!(:xml, :encoding => 'UTF-8')
  builder.tag! :env, :Envelope, namespaces do |env|
    env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_send,selector_shell_id(shell_id))) }
    env.tag!(:env, :Body) do |env_body|
      env_body << Gyoku.xml(body)
    end
  end
  resp = send_message(builder.target!)
  true
end