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



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/winrm/winrm_service.rb', line 36

def initialize(endpoint, transport = :kerberos, opts = {})
  @endpoint = endpoint
  @timeout = DEFAULT_TIMEOUT
  @max_env_sz = DEFAULT_MAX_ENV_SIZE 
  @locale = DEFAULT_LOCALE
  case transport
  when :kerberos
    require 'gssapi'
    # TODO: check fo keys and throw error if missing
    @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)
  end
end

Instance Attribute Details

#endpointObject (readonly)

Returns the value of attribute endpoint.



28
29
30
# File 'lib/winrm/winrm_service.rb', line 28

def endpoint
  @endpoint
end

#timeoutObject (readonly)

Returns the value of attribute timeout.



28
29
30
# File 'lib/winrm/winrm_service.rb', line 28

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:



188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/winrm/winrm_service.rb', line 188

def cleanup_command(shell_id, command_id)
  s = Savon::SOAP::XML.new
  s.version = 2
  s.namespaces.merge!(namespaces)
  s.header.merge!(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id)))

  # Signal the Command references to terminate (close stdout/stderr)
  s.input = ["#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}]

  s.body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
  resp = send_message(s.to_xml)
  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.



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

def close_shell(shell_id)
  s = Savon::SOAP::XML.new
  s.version = 2
  s.namespaces.merge!(namespaces)
  s.namespaces.merge!(Savon::SOAP::XML::SchemaTypes)
  s.header.merge!(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id)))

  # Because Savon does not support a nil Body we have to build it ourselves.
  s.xml do |b|
    b.tag!('env:Envelope', s.namespaces) do
      b.tag!('env:Header') do |bh|
        bh << Gyoku.xml(s.header) unless s.header.empty?
      end
      if(s.input.nil?)
        b.tag! 'env:Body'
      else
        b.tag! 'env:Body' do |bb|
          bb.tag! s.input do |bbb|
            bbb << Gyoku.xml(s.body) if s.body
          end
        end
      end
    end
  end

  resp = send_message(s.to_xml)
  true
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.



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

def get_command_output(shell_id, command_id, &block)
  s = Savon::SOAP::XML.new
  s.version = 2
  s.namespaces.merge!(namespaces)
  s.header.merge!(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id)))
  s.input = "#{NS_WIN_SHELL}:Receive"
  s.body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
    :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}

  resp = send_message(s.to_xml)
  output = {:data => []}
  (resp/"//#{NS_WIN_SHELL}:Stream").each do |n|
    next if n.text.nil? || n.text.empty?
    stream = {n['Name'].to_sym => Base64.decode64(n.text)}
    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>
  if((resp/"//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']").empty?)
    output.merge!(get_command_output(shell_id,command_id,&block)) do |key, old_data, new_data|
      old_data += new_data
    end
  else
    output[:exitcode] = (resp/"//#{NS_WIN_SHELL}:ExitCode").text.to_i
  end
  output
end

#locale(locale) ⇒ Object

Set the locale

Parameters:

  • locale (String)

    the locale to set for future messages

See Also:



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

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:



64
65
66
# File 'lib/winrm/winrm_service.rb', line 64

def max_env_size(byte_sz)
  @max_env_sz = byte_sz
end

#open_shell(shell_opts = {}) ⇒ 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.



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

def open_shell(shell_opts = {})
  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] : 437
  noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
  s = Savon::SOAP::XML.new
  s.version = 2
  s.namespaces.merge!(namespaces)
  h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
    :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
  s.header.merge!(merge_headers(header,resource_uri_cmd,action_create,h_opts))
  s.input = "#{NS_WIN_SHELL}:Shell"
  s.body = {
    "#{NS_WIN_SHELL}:InputStreams" => i_stream,
    "#{NS_WIN_SHELL}:OutputStreams" => o_stream
  }
  s.body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
  # TODO: research Lifetime a bit more: http://msdn.microsoft.com/en-us/library/cc251546(v=PROT.13).aspx
  #s.body["#{NS_WIN_SHELL}:Lifetime"] = Iso8601Duration.sec_to_dur(shell_opts[:lifetime]) if(shell_opts.has_key?(:lifetime) && shell_opts[:lifetime].is_a?(Fixnum))
  # @todo make it so the input is given in milliseconds and converted to xs:duration
  s.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
    s.body["#{NS_WIN_SHELL}:Environment"] = {
      "#{NS_WIN_SHELL}:Variable" => vals,
      :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
    }
  end

  resp = send_message(s.to_xml)
  (resp/"//*[@Name='ShellId']").text
end

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

Run a CMD command

Parameters:

  • command (String)

    The command to run on the remote system

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

    arguments to the command

Returns:

  • (Hash)

    :stdout and :stderr



238
239
240
241
242
243
244
245
# File 'lib/winrm/winrm_service.rb', line 238

def run_cmd(command, arguments = [], &block)
  shell_id = open_shell
  command_id =  run_command(shell_id, command, arguments)
  command_output = get_command_output(shell_id, command_id, &block)
  cleanup_command(shell_id, command_id)
  close_shell(shell_id)
  command_output
end

#run_command(shell_id, command, arguments = [], cmd_opts = {}) ⇒ 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.



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/winrm/winrm_service.rb', line 122

def run_command(shell_id, command, arguments = [], cmd_opts = {})
  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'
  s = Savon::SOAP::XML.new
  s.version = 2
  s.namespaces.merge!(namespaces)
  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']}}}
  }
  s.header.merge!(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id)))
  s.input = "#{NS_WIN_SHELL}:CommandLine"
  s.body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}

  resp = send_message(s.to_xml)
  (resp/"//#{NS_WIN_SHELL}:CommandId").text
end

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

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

Returns:

  • (Hash)

    :stdout and :stderr



252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/winrm/winrm_service.rb', line 252

def run_powershell_script(script_file, &block)
  # if an IO object is passed read it..otherwise assume the contents of the file were passed
  script = script_file.kind_of?(IO) ? script_file.read : script_file

  script = script.chars.to_a.join("\x00").chomp
  script << "\x00" unless script[-1].eql? "\x00"
  if(defined?(script.encode))
    script = script.encode('ASCII-8BIT')
    script = Base64.strict_encode64(script)
  else
    script = Base64.encode64(script).chomp
  end

  shell_id = open_shell
  command_id = run_command(shell_id, "powershell -encodedCommand #{script}")
  command_output = get_command_output(shell_id, command_id, &block)
  cleanup_command(shell_id, command_id)
  close_shell(shell_id)
  command_output
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:



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/winrm/winrm_service.rb', line 279

def run_wql(wql)
  s = Savon::SOAP::XML.new
  s.version = 2
  s.namespaces.merge!(namespaces)
  s.header.merge!(merge_headers(header,resource_uri_wmi,action_enumerate))
  s.input = "#{NS_ENUM}:Enumerate"
  s.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'}}
  }

  resp = send_message(s.to_xml)
  toggle_nori_type_casting :off
  hresp = Nori.parse(resp.to_xml)[:envelope][:body]
  toggle_nori_type_casting :original
  # Normalize items so the type always has an array even if it's just a single item.
  items = {}
  hresp[:enumerate_response][:items].each_pair do |k,v|
    if v.is_a?(Array)
      items[k] = v
    else
      items[k] = [v]
    end
  end
  items
end

#set_timeout(sec) ⇒ Object Also known as: op_timeout

Operation timeout

Parameters:

  • sec (Fixnum)

    the number of seconds to set the timeout to. It will be converted to an ISO8601 format.

See Also:



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

def set_timeout(sec)
  @timeout = Iso8601Duration.sec_to_dur(sec)
end

#toggle_nori_type_casting(to) ⇒ Object



308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/winrm/winrm_service.rb', line 308

def toggle_nori_type_casting(to)
  @nori_type_casting ||= Nori.advanced_typecasting?
  case to.to_sym
  when :original
    Nori.advanced_typecasting = @nori_type_casting
  when :on
    Nori.advanced_typecasting = true
  when :off
    Nori.advanced_typecasting = false
  else
    raise ArgumentError, "Cannot toggle type casting to '#{to}', it is not a valid argument"
  end

end