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::HttpNegotiate @see WinRM::HTTP::HttpSSL



42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/winrm/winrm_service.rb', line 42

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)
  begin
    @xfer = send "init_#{transport}_transport", opts.merge({endpoint: endpoint})
  rescue NoMethodError => e
    raise "Invalid transport '#{transport}' specified, expected: negotiate, kerberos, plaintext, ssl."
  end
end

Instance Attribute Details

#endpointObject (readonly)

Returns the value of attribute endpoint.



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

def endpoint
  @endpoint
end

#loggerObject

Returns the value of attribute logger.



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

def logger
  @logger
end

#retry_delayObject (readonly)

Returns the value of attribute retry_delay.



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

def retry_delay
  @retry_delay
end

#retry_limitObject (readonly)

Returns the value of attribute retry_limit.



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

def retry_limit
  @retry_limit
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



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

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:



283
284
285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/winrm/winrm_service.rb', line 283

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.



301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/winrm/winrm_service.rb', line 301

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:



349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/winrm/winrm_service.rb', line 349

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.



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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/winrm/winrm_service.rb', line 232

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

#init_kerberos_transport(opts) ⇒ Object



61
62
63
64
65
# File 'lib/winrm/winrm_service.rb', line 61

def init_kerberos_transport(opts)
  require 'gssapi'
  require 'gssapi/extensions'
  HTTP::HttpGSSAPI.new(opts[:endpoint], opts[:realm], opts[:service], opts[:keytab], opts)
end

#init_negotiate_transport(opts) ⇒ Object



56
57
58
59
# File 'lib/winrm/winrm_service.rb', line 56

def init_negotiate_transport(opts)
  require 'rubyntlm'
  HTTP::HttpNegotiate.new(opts[:endpoint], opts[:user], opts[:pass], opts)
end

#init_plaintext_transport(opts) ⇒ Object



67
68
69
# File 'lib/winrm/winrm_service.rb', line 67

def init_plaintext_transport(opts)
  HTTP::HttpPlaintext.new(opts[:endpoint], opts[:user], opts[:pass], opts)
end

#init_ssl_transport(opts) ⇒ Object



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

def init_ssl_transport(opts)
  HTTP::HttpSSL.new(opts[:endpoint], opts[:user], opts[:pass], opts[:ca_trust_path], opts)
end

#locale(locale) ⇒ Object

Set the locale

Parameters:

  • locale (String)

    the locale to set for future messages

See Also:



102
103
104
# File 'lib/winrm/winrm_service.rb', line 102

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:



95
96
97
# File 'lib/winrm/winrm_service.rb', line 95

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.



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

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



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

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.



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

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



335
336
337
338
339
340
# File 'lib/winrm/winrm_service.rb', line 335

def run_powershell_script(script_file, &block)
  logger.warn("WinRM::WinRMWebService#run_powershell_script is deprecated. Use WinRM::CommandExecutor#run_powershell_script 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:



368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/winrm/winrm_service.rb', line 368

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:



85
86
87
88
89
# File 'lib/winrm/winrm_service.rb', line 85

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



404
405
406
407
# File 'lib/winrm/winrm_service.rb', line 404

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



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/winrm/winrm_service.rb', line 203

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