winrm 1.3.5 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,43 +1,43 @@
1
- # encoding: UTF-8
2
- #
3
- # Copyright 2014 Max Lincoln <max@devopsy.com>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- module WinRM
18
- # This class holds raw output as a hash, and has convenience methods to parse.
19
- class Output < Hash
20
- def initialize
21
- super
22
- self[:data] = []
23
- end
24
-
25
- def output
26
- self[:data].flat_map do |line|
27
- [line[:stdout], line[:stderr]]
28
- end.compact.join
29
- end
30
-
31
- def stdout
32
- self[:data].map do |line|
33
- line[:stdout]
34
- end.compact.join
35
- end
36
-
37
- def stderr
38
- self[:data].map do |line|
39
- line[:stderr]
40
- end.compact.join
41
- end
42
- end
43
- end
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2014 Max Lincoln <max@devopsy.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ module WinRM
18
+ # This class holds raw output as a hash, and has convenience methods to parse.
19
+ class Output < Hash
20
+ def initialize
21
+ super
22
+ self[:data] = []
23
+ end
24
+
25
+ def output
26
+ self[:data].flat_map do |line|
27
+ [line[:stdout], line[:stderr]]
28
+ end.compact.join
29
+ end
30
+
31
+ def stdout
32
+ self[:data].map do |line|
33
+ line[:stdout]
34
+ end.compact.join
35
+ end
36
+
37
+ def stderr
38
+ self[:data].map do |line|
39
+ line[:stderr]
40
+ end.compact.join
41
+ end
42
+ end
43
+ end
@@ -1,40 +1,40 @@
1
- # encoding: UTF-8
2
- #
3
- # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- require 'httpclient'
18
- require 'builder'
19
- require 'gyoku'
20
- require 'uuidtools'
21
- require 'base64'
22
-
23
- # SOAP constants for WinRM
24
- module WinRM
25
- NS_SOAP_ENV = 's' # http://www.w3.org/2003/05/soap-envelope
26
- NS_ADDRESSING = 'a' # http://schemas.xmlsoap.org/ws/2004/08/addressing
27
- NS_CIMBINDING = 'b' # http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd
28
- NS_ENUM = 'n' # http://schemas.xmlsoap.org/ws/2004/09/enumeration
29
- NS_TRANSFER = 'x' # http://schemas.xmlsoap.org/ws/2004/09/transfer
30
- NS_WSMAN_DMTF = 'w' # http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
31
- NS_WSMAN_MSFT = 'p' # http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd
32
- NS_SCHEMA_INST = 'xsi' # http://www.w3.org/2001/XMLSchema-instance
33
- NS_WIN_SHELL = 'rsp' # http://schemas.microsoft.com/wbem/wsman/1/windows/shell
34
- NS_WSMAN_FAULT = 'f' # http://schemas.microsoft.com/wbem/wsman/1/wsmanfault
35
- NS_WSMAN_CONF = 'cfg' # http://schemas.microsoft.com/wbem/wsman/1/config
36
- end
37
-
38
- require 'winrm/exceptions/exceptions'
39
- require 'winrm/winrm_service'
40
- require 'winrm/http/transport'
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'httpclient'
18
+ require 'builder'
19
+ require 'gyoku'
20
+ require 'uuidtools'
21
+ require 'base64'
22
+
23
+ # SOAP constants for WinRM
24
+ module WinRM
25
+ NS_SOAP_ENV = 's' # http://www.w3.org/2003/05/soap-envelope
26
+ NS_ADDRESSING = 'a' # http://schemas.xmlsoap.org/ws/2004/08/addressing
27
+ NS_CIMBINDING = 'b' # http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd
28
+ NS_ENUM = 'n' # http://schemas.xmlsoap.org/ws/2004/09/enumeration
29
+ NS_TRANSFER = 'x' # http://schemas.xmlsoap.org/ws/2004/09/transfer
30
+ NS_WSMAN_DMTF = 'w' # http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd
31
+ NS_WSMAN_MSFT = 'p' # http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd
32
+ NS_SCHEMA_INST = 'xsi' # http://www.w3.org/2001/XMLSchema-instance
33
+ NS_WIN_SHELL = 'rsp' # http://schemas.microsoft.com/wbem/wsman/1/windows/shell
34
+ NS_WSMAN_FAULT = 'f' # http://schemas.microsoft.com/wbem/wsman/1/wsmanfault
35
+ NS_WSMAN_CONF = 'cfg' # http://schemas.microsoft.com/wbem/wsman/1/config
36
+ end
37
+
38
+ require 'winrm/exceptions/exceptions'
39
+ require 'winrm/winrm_service'
40
+ require 'winrm/http/transport'
@@ -1,488 +1,490 @@
1
- # encoding: UTF-8
2
- #
3
- # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
- #
5
- # Licensed under the Apache License, Version 2.0 (the "License");
6
- # you may not use this file except in compliance with the License.
7
- # You may obtain a copy of the License at
8
- #
9
- # http://www.apache.org/licenses/LICENSE-2.0
10
- #
11
- # Unless required by applicable law or agreed to in writing, software
12
- # distributed under the License is distributed on an "AS IS" BASIS,
13
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
- # See the License for the specific language governing permissions and
15
- # limitations under the License.
16
-
17
- require 'nori'
18
- require 'rexml/document'
19
- require_relative 'helpers/powershell_script'
20
-
21
- module WinRM
22
- # This is the main class that does the SOAP request/response logic. There are a few helper
23
- # classes, but pretty much everything comes through here first.
24
- class WinRMWebService
25
- DEFAULT_TIMEOUT = 'PT60S'
26
- DEFAULT_MAX_ENV_SIZE = 153600
27
- DEFAULT_LOCALE = 'en-US'
28
-
29
- attr_reader :endpoint, :timeout
30
-
31
- # @param [String,URI] endpoint the WinRM webservice endpoint
32
- # @param [Symbol] transport either :kerberos(default)/:ssl/:plaintext
33
- # @param [Hash] opts Misc opts for the various transports.
34
- # @see WinRM::HTTP::HttpTransport
35
- # @see WinRM::HTTP::HttpGSSAPI
36
- # @see WinRM::HTTP::HttpSSL
37
- def initialize(endpoint, transport = :kerberos, opts = {})
38
- @endpoint = endpoint
39
- @timeout = DEFAULT_TIMEOUT
40
- @max_env_sz = DEFAULT_MAX_ENV_SIZE
41
- @locale = DEFAULT_LOCALE
42
- @logger = Logging.logger[self]
43
- case transport
44
- when :kerberos
45
- require 'gssapi'
46
- require 'gssapi/extensions'
47
- @xfer = HTTP::HttpGSSAPI.new(endpoint, opts[:realm], opts[:service], opts[:keytab], opts)
48
- when :plaintext
49
- @xfer = HTTP::HttpPlaintext.new(endpoint, opts[:user], opts[:pass], opts)
50
- when :ssl
51
- @xfer = HTTP::HttpSSL.new(endpoint, opts[:user], opts[:pass], opts[:ca_trust_path], opts)
52
- else
53
- raise "Invalid transport '#{transport}' specified, expected: kerberos, plaintext, ssl."
54
- end
55
- end
56
-
57
- # Operation timeout.
58
- #
59
- # Unless specified the client receive timeout will be 10s + the operation
60
- # timeout.
61
- #
62
- # @see http://msdn.microsoft.com/en-us/library/ee916629(v=PROT.13).aspx
63
- #
64
- # @param [Fixnum] The number of seconds to set the WinRM operation timeout
65
- # @param [Fixnum] The number of seconds to set the Ruby receive timeout
66
- # @return [String] The ISO 8601 formatted operation timeout
67
- def set_timeout(op_timeout_sec, receive_timeout_sec=nil)
68
- @timeout = Iso8601Duration.sec_to_dur(op_timeout_sec)
69
- @xfer.receive_timeout = receive_timeout_sec || op_timeout_sec + 10
70
- @timeout
71
- end
72
- alias :op_timeout :set_timeout
73
-
74
- # Max envelope size
75
- # @see http://msdn.microsoft.com/en-us/library/ee916127(v=PROT.13).aspx
76
- # @param [Fixnum] byte_sz the max size in bytes to allow for the response
77
- def max_env_size(byte_sz)
78
- @max_env_sz = byte_sz
79
- end
80
-
81
- # Set the locale
82
- # @see http://msdn.microsoft.com/en-us/library/gg567404(v=PROT.13).aspx
83
- # @param [String] locale the locale to set for future messages
84
- def locale(locale)
85
- @locale = locale
86
- end
87
-
88
- # Create a Shell on the destination host
89
- # @param [Hash<optional>] shell_opts additional shell options you can pass
90
- # @option shell_opts [String] :i_stream Which input stream to open. Leave this alone unless you know what you're doing (default: stdin)
91
- # @option shell_opts [String] :o_stream Which output stream to open. Leave this alone unless you know what you're doing (default: stdout stderr)
92
- # @option shell_opts [String] :working_directory the directory to create the shell in
93
- # @option shell_opts [Hash] :env_vars environment variables to set for the shell. For instance;
94
- # :env_vars => {:myvar1 => 'val1', :myvar2 => 'var2'}
95
- # @return [String] The ShellId from the SOAP response. This is our open shell instance on the remote machine.
96
- def open_shell(shell_opts = {}, &block)
97
- i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
98
- o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
99
- 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)
100
- noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
101
- h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
102
- :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
103
- shell_body = {
104
- "#{NS_WIN_SHELL}:InputStreams" => i_stream,
105
- "#{NS_WIN_SHELL}:OutputStreams" => o_stream
106
- }
107
- shell_body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
108
- shell_body["#{NS_WIN_SHELL}:IdleTimeOut"] = shell_opts[:idle_timeout] if(shell_opts.has_key?(:idle_timeout) && shell_opts[:idle_timeout].is_a?(String))
109
- if(shell_opts.has_key?(:env_vars) && shell_opts[:env_vars].is_a?(Hash))
110
- keys = shell_opts[:env_vars].keys
111
- vals = shell_opts[:env_vars].values
112
- shell_body["#{NS_WIN_SHELL}:Environment"] = {
113
- "#{NS_WIN_SHELL}:Variable" => vals,
114
- :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
115
- }
116
- end
117
- builder = Builder::XmlMarkup.new
118
- builder.instruct!(:xml, :encoding => 'UTF-8')
119
- builder.tag! :env, :Envelope, namespaces do |env|
120
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_create,h_opts)) }
121
- env.tag! :env, :Body do |body|
122
- body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body)}
123
- end
124
- end
125
-
126
- resp_doc = send_message(builder.target!)
127
- shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
128
-
129
- if block_given?
130
- begin
131
- yield shell_id
132
- ensure
133
- close_shell(shell_id)
134
- end
135
- else
136
- shell_id
137
- end
138
- end
139
-
140
- # Run a command on a machine with an open shell
141
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
142
- # @param [String] command The command to run on the remote machine
143
- # @param [Array<String>] arguments An array of arguments for this command
144
- # @return [String] The CommandId from the SOAP response. This is the ID we need to query in order to get output.
145
- def run_command(shell_id, command, arguments = [], cmd_opts = {}, &block)
146
- consolemode = cmd_opts.has_key?(:console_mode_stdin) ? cmd_opts[:console_mode_stdin] : 'TRUE'
147
- skipcmd = cmd_opts.has_key?(:skip_cmd_shell) ? cmd_opts[:skip_cmd_shell] : 'FALSE'
148
-
149
- h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => {
150
- "#{NS_WSMAN_DMTF}:Option" => [consolemode, skipcmd],
151
- :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_CONSOLEMODE_STDIN','WINRS_SKIP_CMD_SHELL']}}}
152
- }
153
- body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}
154
-
155
- builder = Builder::XmlMarkup.new
156
- builder.instruct!(:xml, :encoding => 'UTF-8')
157
- builder.tag! :env, :Envelope, namespaces do |env|
158
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id))) }
159
- env.tag!(:env, :Body) do |env_body|
160
- env_body.tag!("#{NS_WIN_SHELL}:CommandLine") { |cl| cl << Gyoku.xml(body) }
161
- end
162
- end
163
-
164
- # Grab the command element and unescape any single quotes - issue 69
165
- xml = builder.target!
166
- escaped_cmd = /<#{NS_WIN_SHELL}:Command>(.+)<\/#{NS_WIN_SHELL}:Command>/m.match(xml)[1]
167
- xml[escaped_cmd] = escaped_cmd.gsub(/&#39;/, "'")
168
-
169
- resp_doc = send_message(xml)
170
- command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text
171
-
172
- if block_given?
173
- begin
174
- yield command_id
175
- ensure
176
- cleanup_command(shell_id, command_id)
177
- end
178
- else
179
- command_id
180
- end
181
- end
182
-
183
- def write_stdin(shell_id, command_id, stdin)
184
- # Signal the Command references to terminate (close stdout/stderr)
185
- body = {
186
- "#{NS_WIN_SHELL}:Send" => {
187
- "#{NS_WIN_SHELL}:Stream" => {
188
- "@Name" => 'stdin',
189
- "@CommandId" => command_id,
190
- :content! => Base64.encode64(stdin)
191
- }
192
- }
193
- }
194
- builder = Builder::XmlMarkup.new
195
- builder.instruct!(:xml, :encoding => 'UTF-8')
196
- builder.tag! :env, :Envelope, namespaces do |env|
197
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_send,selector_shell_id(shell_id))) }
198
- env.tag!(:env, :Body) do |env_body|
199
- env_body << Gyoku.xml(body)
200
- end
201
- end
202
- resp = send_message(builder.target!)
203
- true
204
- end
205
-
206
- # Get the Output of the given shell and command
207
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
208
- # @param [String] command_id The command id on the remote machine. See #run_command
209
- # @return [Hash] Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key
210
- # 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
211
- # the console.
212
- def get_command_output(shell_id, command_id, &block)
213
- body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
214
- :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}
215
-
216
- builder = Builder::XmlMarkup.new
217
- builder.instruct!(:xml, :encoding => 'UTF-8')
218
- builder.tag! :env, :Envelope, namespaces do |env|
219
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id))) }
220
- env.tag!(:env, :Body) do |env_body|
221
- env_body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(body) }
222
- end
223
- end
224
-
225
- resp_doc = nil
226
- request_msg = builder.target!
227
- done_elems = []
228
- output = Output.new
229
-
230
- while done_elems.empty?
231
- resp_doc = send_get_output_message(request_msg)
232
-
233
- REXML::XPath.match(resp_doc, "//#{NS_WIN_SHELL}:Stream").each do |n|
234
- next if n.text.nil? || n.text.empty?
235
- stream = { n.attributes['Name'].to_sym => Base64.decode64(n.text).force_encoding('utf-8') }
236
- output[:data] << stream
237
- yield stream[:stdout], stream[:stderr] if block_given?
238
- end
239
-
240
- # We may need to get additional output if the stream has not finished.
241
- # The CommandState will change from Running to Done like so:
242
- # @example
243
- # from...
244
- # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
245
- # to...
246
- # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
247
- # <rsp:ExitCode>0</rsp:ExitCode>
248
- # </rsp:CommandState>
249
- done_elems = REXML::XPath.match(resp_doc, "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']")
250
- end
251
-
252
- output[:exitcode] = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:ExitCode").text.to_i
253
- output
254
- end
255
-
256
- # Clean-up after a command.
257
- # @see #run_command
258
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
259
- # @param [String] command_id The command id on the remote machine. See #run_command
260
- # @return [true] This should have more error checking but it just returns true for now.
261
- def cleanup_command(shell_id, command_id)
262
- # Signal the Command references to terminate (close stdout/stderr)
263
- body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
264
- builder = Builder::XmlMarkup.new
265
- builder.instruct!(:xml, :encoding => 'UTF-8')
266
- builder.tag! :env, :Envelope, namespaces do |env|
267
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id))) }
268
- env.tag!(:env, :Body) do |env_body|
269
- env_body.tag!("#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}) { |cl| cl << Gyoku.xml(body) }
270
- end
271
- end
272
- resp = send_message(builder.target!)
273
- true
274
- end
275
-
276
- # Close the shell
277
- # @param [String] shell_id The shell id on the remote machine. See #open_shell
278
- # @return [true] This should have more error checking but it just returns true for now.
279
- def close_shell(shell_id)
280
- builder = Builder::XmlMarkup.new
281
- builder.instruct!(:xml, :encoding => 'UTF-8')
282
-
283
- builder.tag!('env:Envelope', namespaces) do |env|
284
- env.tag!('env:Header') { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id))) }
285
- env.tag!('env:Body')
286
- end
287
-
288
- resp = send_message(builder.target!)
289
- true
290
- end
291
-
292
- # Run a CMD command
293
- # @param [String] command The command to run on the remote system
294
- # @param [Array <String>] arguments arguments to the command
295
- # @param [String] an existing and open shell id to reuse
296
- # @return [Hash] :stdout and :stderr
297
- def run_cmd(command, arguments = [], &block)
298
- command_output = nil
299
- open_shell do |shell_id|
300
- run_command(shell_id, command, arguments) do |command_id|
301
- command_output = get_command_output(shell_id, command_id, &block)
302
- end
303
- end
304
- command_output
305
- end
306
- alias :cmd :run_cmd
307
-
308
-
309
- # Run a Powershell script that resides on the local box.
310
- # @param [IO,String] script_file an IO reference for reading the Powershell script or the actual file contents
311
- # @param [String] an existing and open shell id to reuse
312
- # @return [Hash] :stdout and :stderr
313
- def run_powershell_script(script_file, &block)
314
- # if an IO object is passed read it..otherwise assume the contents of the file were passed
315
- script_text = script_file.respond_to?(:read) ? script_file.read : script_file
316
- script = WinRM::PowershellScript.new(script_text)
317
- run_cmd("powershell -encodedCommand #{script.encoded()}", &block)
318
- end
319
- alias :powershell :run_powershell_script
320
-
321
-
322
- # Run a WQL Query
323
- # @see http://msdn.microsoft.com/en-us/library/aa394606(VS.85).aspx
324
- # @param [String] wql The WQL query
325
- # @return [Hash] Returns a Hash that contain the key/value pairs returned from the query.
326
- def run_wql(wql)
327
-
328
- body = { "#{NS_WSMAN_DMTF}:OptimizeEnumeration" => nil,
329
- "#{NS_WSMAN_DMTF}:MaxElements" => '32000',
330
- "#{NS_WSMAN_DMTF}:Filter" => wql,
331
- :attributes! => { "#{NS_WSMAN_DMTF}:Filter" => {'Dialect' => 'http://schemas.microsoft.com/wbem/wsman/1/WQL'}}
332
- }
333
-
334
- builder = Builder::XmlMarkup.new
335
- builder.instruct!(:xml, :encoding => 'UTF-8')
336
- builder.tag! :env, :Envelope, namespaces do |env|
337
- env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_wmi,action_enumerate)) }
338
- env.tag!(:env, :Body) do |env_body|
339
- env_body.tag!("#{NS_ENUM}:Enumerate") { |en| en << Gyoku.xml(body) }
340
- end
341
- end
342
-
343
- resp = send_message(builder.target!)
344
- parser = Nori.new(:parser => :rexml, :advanced_typecasting => false, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym }, :strip_namespaces => true)
345
- hresp = parser.parse(resp.to_s)[:envelope][:body]
346
-
347
- # Normalize items so the type always has an array even if it's just a single item.
348
- items = {}
349
- if hresp[:enumerate_response][:items]
350
- hresp[:enumerate_response][:items].each_pair do |k,v|
351
- if v.is_a?(Array)
352
- items[k] = v
353
- else
354
- items[k] = [v]
355
- end
356
- end
357
- end
358
- items
359
- end
360
- alias :wql :run_wql
361
-
362
- def toggle_nori_type_casting(to)
363
- @logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
364
- 'please remove calls to it')
365
- end
366
-
367
- private
368
-
369
- def namespaces
370
- {
371
- 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
372
- 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
373
- 'xmlns:env' => 'http://www.w3.org/2003/05/soap-envelope',
374
- 'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
375
- 'xmlns:b' => 'http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd',
376
- 'xmlns:n' => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration',
377
- 'xmlns:x' => 'http://schemas.xmlsoap.org/ws/2004/09/transfer',
378
- 'xmlns:w' => 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd',
379
- 'xmlns:p' => 'http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd',
380
- 'xmlns:rsp' => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell',
381
- 'xmlns:cfg' => 'http://schemas.microsoft.com/wbem/wsman/1/config',
382
- }
383
- end
384
-
385
- def header
386
- { "#{NS_ADDRESSING}:To" => "#{@xfer.endpoint.to_s}",
387
- "#{NS_ADDRESSING}:ReplyTo" => {
388
- "#{NS_ADDRESSING}:Address" => 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous',
389
- :attributes! => {"#{NS_ADDRESSING}:Address" => {'mustUnderstand' => true}}},
390
- "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => @max_env_sz,
391
- "#{NS_ADDRESSING}:MessageID" => "uuid:#{UUIDTools::UUID.random_create.to_s.upcase}",
392
- "#{NS_WSMAN_DMTF}:Locale/" => '',
393
- "#{NS_WSMAN_MSFT}:DataLocale/" => '',
394
- "#{NS_WSMAN_DMTF}:OperationTimeout" => @timeout,
395
- :attributes! => {
396
- "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => {'mustUnderstand' => true},
397
- "#{NS_WSMAN_DMTF}:Locale/" => {'xml:lang' => @locale, 'mustUnderstand' => false},
398
- "#{NS_WSMAN_MSFT}:DataLocale/" => {'xml:lang' => @locale, 'mustUnderstand' => false}
399
- }}
400
- end
401
-
402
- # merge the various header hashes and make sure we carry all of the attributes
403
- # through instead of overwriting them.
404
- def merge_headers(*headers)
405
- hdr = {}
406
- headers.each do |h|
407
- hdr.merge!(h) do |k,v1,v2|
408
- v1.merge!(v2) if k == :attributes!
409
- end
410
- end
411
- hdr
412
- end
413
-
414
- def send_get_output_message(message)
415
- send_message(message)
416
- rescue WinRMWSManFault => e
417
- # If no output is available before the wsman:OperationTimeout expires,
418
- # the server MUST return a WSManFault with the Code attribute equal to
419
- # 2150858793. When the client receives this fault, it SHOULD issue
420
- # another Receive request.
421
- # http://msdn.microsoft.com/en-us/library/cc251676.aspx
422
- if e.fault_code == '2150858793'
423
- retry
424
- else
425
- raise
426
- end
427
- end
428
-
429
- def send_message(message)
430
- @xfer.send_request(message)
431
- end
432
-
433
-
434
- # Helper methods for SOAP Headers
435
-
436
- def resource_uri_cmd
437
- {"#{NS_WSMAN_DMTF}:ResourceURI" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
438
- :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
439
- end
440
-
441
- def resource_uri_wmi(namespace = 'root/cimv2/*')
442
- {"#{NS_WSMAN_DMTF}:ResourceURI" => "http://schemas.microsoft.com/wbem/wsman/1/wmi/#{namespace}",
443
- :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
444
- end
445
-
446
- def action_create
447
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Create',
448
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
449
- end
450
-
451
- def action_delete
452
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete',
453
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
454
- end
455
-
456
- def action_command
457
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command',
458
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
459
- end
460
-
461
- def action_receive
462
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
463
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
464
- end
465
-
466
- def action_signal
467
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal',
468
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
469
- end
470
-
471
- def action_send
472
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send',
473
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
474
- end
475
-
476
- def action_enumerate
477
- {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate',
478
- :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
479
- end
480
-
481
- def selector_shell_id(shell_id)
482
- {"#{NS_WSMAN_DMTF}:SelectorSet" =>
483
- {"#{NS_WSMAN_DMTF}:Selector" => shell_id, :attributes! => {"#{NS_WSMAN_DMTF}:Selector" => {'Name' => 'ShellId'}}}
484
- }
485
- end
486
-
487
- end # WinRMWebService
488
- end # WinRM
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright 2010 Dan Wanek <dan.wanek@gmail.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'nori'
18
+ require 'rexml/document'
19
+ require_relative 'helpers/powershell_script'
20
+
21
+ module WinRM
22
+ # This is the main class that does the SOAP request/response logic. There are a few helper
23
+ # classes, but pretty much everything comes through here first.
24
+ class WinRMWebService
25
+ DEFAULT_TIMEOUT = 'PT60S'
26
+ DEFAULT_MAX_ENV_SIZE = 153600
27
+ DEFAULT_LOCALE = 'en-US'
28
+
29
+ attr_reader :endpoint, :timeout
30
+
31
+ # @param [String,URI] endpoint the WinRM webservice endpoint
32
+ # @param [Symbol] transport either :kerberos(default)/:ssl/:plaintext
33
+ # @param [Hash] opts Misc opts for the various transports.
34
+ # @see WinRM::HTTP::HttpTransport
35
+ # @see WinRM::HTTP::HttpGSSAPI
36
+ # @see WinRM::HTTP::HttpSSL
37
+ def initialize(endpoint, transport = :kerberos, opts = {})
38
+ @endpoint = endpoint
39
+ @timeout = DEFAULT_TIMEOUT
40
+ @max_env_sz = DEFAULT_MAX_ENV_SIZE
41
+ @locale = DEFAULT_LOCALE
42
+ @logger = Logging.logger[self]
43
+ case transport
44
+ when :kerberos
45
+ require 'gssapi'
46
+ require 'gssapi/extensions'
47
+ @xfer = HTTP::HttpGSSAPI.new(endpoint, opts[:realm], opts[:service], opts[:keytab], opts)
48
+ when :plaintext
49
+ @xfer = HTTP::HttpPlaintext.new(endpoint, opts[:user], opts[:pass], opts)
50
+ when :ssl
51
+ @xfer = HTTP::HttpSSL.new(endpoint, opts[:user], opts[:pass], opts[:ca_trust_path], opts)
52
+ else
53
+ raise "Invalid transport '#{transport}' specified, expected: kerberos, plaintext, ssl."
54
+ end
55
+ end
56
+
57
+ # Operation timeout.
58
+ #
59
+ # Unless specified the client receive timeout will be 10s + the operation
60
+ # timeout.
61
+ #
62
+ # @see http://msdn.microsoft.com/en-us/library/ee916629(v=PROT.13).aspx
63
+ #
64
+ # @param [Fixnum] The number of seconds to set the WinRM operation timeout
65
+ # @param [Fixnum] The number of seconds to set the Ruby receive timeout
66
+ # @return [String] The ISO 8601 formatted operation timeout
67
+ def set_timeout(op_timeout_sec, receive_timeout_sec=nil)
68
+ @timeout = Iso8601Duration.sec_to_dur(op_timeout_sec)
69
+ @xfer.receive_timeout = receive_timeout_sec || op_timeout_sec + 10
70
+ @timeout
71
+ end
72
+ alias :op_timeout :set_timeout
73
+
74
+ # Max envelope size
75
+ # @see http://msdn.microsoft.com/en-us/library/ee916127(v=PROT.13).aspx
76
+ # @param [Fixnum] byte_sz the max size in bytes to allow for the response
77
+ def max_env_size(byte_sz)
78
+ @max_env_sz = byte_sz
79
+ end
80
+
81
+ # Set the locale
82
+ # @see http://msdn.microsoft.com/en-us/library/gg567404(v=PROT.13).aspx
83
+ # @param [String] locale the locale to set for future messages
84
+ def locale(locale)
85
+ @locale = locale
86
+ end
87
+
88
+ # Create a Shell on the destination host
89
+ # @param [Hash<optional>] shell_opts additional shell options you can pass
90
+ # @option shell_opts [String] :i_stream Which input stream to open. Leave this alone unless you know what you're doing (default: stdin)
91
+ # @option shell_opts [String] :o_stream Which output stream to open. Leave this alone unless you know what you're doing (default: stdout stderr)
92
+ # @option shell_opts [String] :working_directory the directory to create the shell in
93
+ # @option shell_opts [Hash] :env_vars environment variables to set for the shell. For instance;
94
+ # :env_vars => {:myvar1 => 'val1', :myvar2 => 'var2'}
95
+ # @return [String] The ShellId from the SOAP response. This is our open shell instance on the remote machine.
96
+ def open_shell(shell_opts = {}, &block)
97
+ i_stream = shell_opts.has_key?(:i_stream) ? shell_opts[:i_stream] : 'stdin'
98
+ o_stream = shell_opts.has_key?(:o_stream) ? shell_opts[:o_stream] : 'stdout stderr'
99
+ 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)
100
+ noprofile = shell_opts.has_key?(:noprofile) ? shell_opts[:noprofile] : 'FALSE'
101
+ h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => { "#{NS_WSMAN_DMTF}:Option" => [noprofile, codepage],
102
+ :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_NOPROFILE','WINRS_CODEPAGE']}}}}
103
+ shell_body = {
104
+ "#{NS_WIN_SHELL}:InputStreams" => i_stream,
105
+ "#{NS_WIN_SHELL}:OutputStreams" => o_stream
106
+ }
107
+ shell_body["#{NS_WIN_SHELL}:WorkingDirectory"] = shell_opts[:working_directory] if shell_opts.has_key?(:working_directory)
108
+ shell_body["#{NS_WIN_SHELL}:IdleTimeOut"] = shell_opts[:idle_timeout] if(shell_opts.has_key?(:idle_timeout) && shell_opts[:idle_timeout].is_a?(String))
109
+ if(shell_opts.has_key?(:env_vars) && shell_opts[:env_vars].is_a?(Hash))
110
+ keys = shell_opts[:env_vars].keys
111
+ vals = shell_opts[:env_vars].values
112
+ shell_body["#{NS_WIN_SHELL}:Environment"] = {
113
+ "#{NS_WIN_SHELL}:Variable" => vals,
114
+ :attributes! => {"#{NS_WIN_SHELL}:Variable" => {'Name' => keys}}
115
+ }
116
+ end
117
+ builder = Builder::XmlMarkup.new
118
+ builder.instruct!(:xml, :encoding => 'UTF-8')
119
+ builder.tag! :env, :Envelope, namespaces do |env|
120
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_create,h_opts)) }
121
+ env.tag! :env, :Body do |body|
122
+ body.tag!("#{NS_WIN_SHELL}:Shell") { |s| s << Gyoku.xml(shell_body)}
123
+ end
124
+ end
125
+
126
+ resp_doc = send_message(builder.target!)
127
+ shell_id = REXML::XPath.first(resp_doc, "//*[@Name='ShellId']").text
128
+
129
+ if block_given?
130
+ begin
131
+ yield shell_id
132
+ ensure
133
+ close_shell(shell_id)
134
+ end
135
+ else
136
+ shell_id
137
+ end
138
+ end
139
+
140
+ # Run a command on a machine with an open shell
141
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
142
+ # @param [String] command The command to run on the remote machine
143
+ # @param [Array<String>] arguments An array of arguments for this command
144
+ # @return [String] The CommandId from the SOAP response. This is the ID we need to query in order to get output.
145
+ def run_command(shell_id, command, arguments = [], cmd_opts = {}, &block)
146
+ consolemode = cmd_opts.has_key?(:console_mode_stdin) ? cmd_opts[:console_mode_stdin] : 'TRUE'
147
+ skipcmd = cmd_opts.has_key?(:skip_cmd_shell) ? cmd_opts[:skip_cmd_shell] : 'FALSE'
148
+
149
+ h_opts = { "#{NS_WSMAN_DMTF}:OptionSet" => {
150
+ "#{NS_WSMAN_DMTF}:Option" => [consolemode, skipcmd],
151
+ :attributes! => {"#{NS_WSMAN_DMTF}:Option" => {'Name' => ['WINRS_CONSOLEMODE_STDIN','WINRS_SKIP_CMD_SHELL']}}}
152
+ }
153
+ body = { "#{NS_WIN_SHELL}:Command" => "\"#{command}\"", "#{NS_WIN_SHELL}:Arguments" => arguments}
154
+
155
+ builder = Builder::XmlMarkup.new
156
+ builder.instruct!(:xml, :encoding => 'UTF-8')
157
+ builder.tag! :env, :Envelope, namespaces do |env|
158
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_command,h_opts,selector_shell_id(shell_id))) }
159
+ env.tag!(:env, :Body) do |env_body|
160
+ env_body.tag!("#{NS_WIN_SHELL}:CommandLine") { |cl| cl << Gyoku.xml(body) }
161
+ end
162
+ end
163
+
164
+ # Grab the command element and unescape any single quotes - issue 69
165
+ xml = builder.target!
166
+ escaped_cmd = /<#{NS_WIN_SHELL}:Command>(.+)<\/#{NS_WIN_SHELL}:Command>/m.match(xml)[1]
167
+ xml[escaped_cmd] = escaped_cmd.gsub(/&#39;/, "'")
168
+
169
+ resp_doc = send_message(xml)
170
+ command_id = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:CommandId").text
171
+
172
+ if block_given?
173
+ begin
174
+ yield command_id
175
+ ensure
176
+ cleanup_command(shell_id, command_id)
177
+ end
178
+ else
179
+ command_id
180
+ end
181
+ end
182
+
183
+ def write_stdin(shell_id, command_id, stdin)
184
+ # Signal the Command references to terminate (close stdout/stderr)
185
+ body = {
186
+ "#{NS_WIN_SHELL}:Send" => {
187
+ "#{NS_WIN_SHELL}:Stream" => {
188
+ "@Name" => 'stdin',
189
+ "@CommandId" => command_id,
190
+ :content! => Base64.encode64(stdin)
191
+ }
192
+ }
193
+ }
194
+ builder = Builder::XmlMarkup.new
195
+ builder.instruct!(:xml, :encoding => 'UTF-8')
196
+ builder.tag! :env, :Envelope, namespaces do |env|
197
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_send,selector_shell_id(shell_id))) }
198
+ env.tag!(:env, :Body) do |env_body|
199
+ env_body << Gyoku.xml(body)
200
+ end
201
+ end
202
+ resp = send_message(builder.target!)
203
+ true
204
+ end
205
+
206
+ # Get the Output of the given shell and command
207
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
208
+ # @param [String] command_id The command id on the remote machine. See #run_command
209
+ # @return [Hash] Returns a Hash with a key :exitcode and :data. Data is an Array of Hashes where the cooresponding key
210
+ # 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
211
+ # the console.
212
+ def get_command_output(shell_id, command_id, &block)
213
+ body = { "#{NS_WIN_SHELL}:DesiredStream" => 'stdout stderr',
214
+ :attributes! => {"#{NS_WIN_SHELL}:DesiredStream" => {'CommandId' => command_id}}}
215
+
216
+ builder = Builder::XmlMarkup.new
217
+ builder.instruct!(:xml, :encoding => 'UTF-8')
218
+ builder.tag! :env, :Envelope, namespaces do |env|
219
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_receive,selector_shell_id(shell_id))) }
220
+ env.tag!(:env, :Body) do |env_body|
221
+ env_body.tag!("#{NS_WIN_SHELL}:Receive") { |cl| cl << Gyoku.xml(body) }
222
+ end
223
+ end
224
+
225
+ resp_doc = nil
226
+ request_msg = builder.target!
227
+ done_elems = []
228
+ output = Output.new
229
+
230
+ while done_elems.empty?
231
+ resp_doc = send_get_output_message(request_msg)
232
+
233
+ REXML::XPath.match(resp_doc, "//#{NS_WIN_SHELL}:Stream").each do |n|
234
+ next if n.text.nil? || n.text.empty?
235
+
236
+ # decode and strip off BOM which win 2008R2 applies
237
+ stream = { n.attributes['Name'].to_sym => Base64.decode64(n.text).force_encoding('utf-8').sub("\xEF\xBB\xBF", "") }
238
+ output[:data] << stream
239
+ yield stream[:stdout], stream[:stderr] if block_given?
240
+ end
241
+
242
+ # We may need to get additional output if the stream has not finished.
243
+ # The CommandState will change from Running to Done like so:
244
+ # @example
245
+ # from...
246
+ # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
247
+ # to...
248
+ # <rsp:CommandState CommandId="..." State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
249
+ # <rsp:ExitCode>0</rsp:ExitCode>
250
+ # </rsp:CommandState>
251
+ done_elems = REXML::XPath.match(resp_doc, "//*[@State='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done']")
252
+ end
253
+
254
+ output[:exitcode] = REXML::XPath.first(resp_doc, "//#{NS_WIN_SHELL}:ExitCode").text.to_i
255
+ output
256
+ end
257
+
258
+ # Clean-up after a command.
259
+ # @see #run_command
260
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
261
+ # @param [String] command_id The command id on the remote machine. See #run_command
262
+ # @return [true] This should have more error checking but it just returns true for now.
263
+ def cleanup_command(shell_id, command_id)
264
+ # Signal the Command references to terminate (close stdout/stderr)
265
+ body = { "#{NS_WIN_SHELL}:Code" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate' }
266
+ builder = Builder::XmlMarkup.new
267
+ builder.instruct!(:xml, :encoding => 'UTF-8')
268
+ builder.tag! :env, :Envelope, namespaces do |env|
269
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_signal,selector_shell_id(shell_id))) }
270
+ env.tag!(:env, :Body) do |env_body|
271
+ env_body.tag!("#{NS_WIN_SHELL}:Signal", {'CommandId' => command_id}) { |cl| cl << Gyoku.xml(body) }
272
+ end
273
+ end
274
+ resp = send_message(builder.target!)
275
+ true
276
+ end
277
+
278
+ # Close the shell
279
+ # @param [String] shell_id The shell id on the remote machine. See #open_shell
280
+ # @return [true] This should have more error checking but it just returns true for now.
281
+ def close_shell(shell_id)
282
+ builder = Builder::XmlMarkup.new
283
+ builder.instruct!(:xml, :encoding => 'UTF-8')
284
+
285
+ builder.tag!('env:Envelope', namespaces) do |env|
286
+ env.tag!('env:Header') { |h| h << Gyoku.xml(merge_headers(header,resource_uri_cmd,action_delete,selector_shell_id(shell_id))) }
287
+ env.tag!('env:Body')
288
+ end
289
+
290
+ resp = send_message(builder.target!)
291
+ true
292
+ end
293
+
294
+ # Run a CMD command
295
+ # @param [String] command The command to run on the remote system
296
+ # @param [Array <String>] arguments arguments to the command
297
+ # @param [String] an existing and open shell id to reuse
298
+ # @return [Hash] :stdout and :stderr
299
+ def run_cmd(command, arguments = [], &block)
300
+ command_output = nil
301
+ open_shell do |shell_id|
302
+ run_command(shell_id, command, arguments) do |command_id|
303
+ command_output = get_command_output(shell_id, command_id, &block)
304
+ end
305
+ end
306
+ command_output
307
+ end
308
+ alias :cmd :run_cmd
309
+
310
+
311
+ # Run a Powershell script that resides on the local box.
312
+ # @param [IO,String] script_file an IO reference for reading the Powershell script or the actual file contents
313
+ # @param [String] an existing and open shell id to reuse
314
+ # @return [Hash] :stdout and :stderr
315
+ def run_powershell_script(script_file, &block)
316
+ # if an IO object is passed read it..otherwise assume the contents of the file were passed
317
+ script_text = script_file.respond_to?(:read) ? script_file.read : script_file
318
+ script = WinRM::PowershellScript.new(script_text)
319
+ run_cmd("powershell -encodedCommand #{script.encoded()}", &block)
320
+ end
321
+ alias :powershell :run_powershell_script
322
+
323
+
324
+ # Run a WQL Query
325
+ # @see http://msdn.microsoft.com/en-us/library/aa394606(VS.85).aspx
326
+ # @param [String] wql The WQL query
327
+ # @return [Hash] Returns a Hash that contain the key/value pairs returned from the query.
328
+ def run_wql(wql)
329
+
330
+ body = { "#{NS_WSMAN_DMTF}:OptimizeEnumeration" => nil,
331
+ "#{NS_WSMAN_DMTF}:MaxElements" => '32000',
332
+ "#{NS_WSMAN_DMTF}:Filter" => wql,
333
+ :attributes! => { "#{NS_WSMAN_DMTF}:Filter" => {'Dialect' => 'http://schemas.microsoft.com/wbem/wsman/1/WQL'}}
334
+ }
335
+
336
+ builder = Builder::XmlMarkup.new
337
+ builder.instruct!(:xml, :encoding => 'UTF-8')
338
+ builder.tag! :env, :Envelope, namespaces do |env|
339
+ env.tag!(:env, :Header) { |h| h << Gyoku.xml(merge_headers(header,resource_uri_wmi,action_enumerate)) }
340
+ env.tag!(:env, :Body) do |env_body|
341
+ env_body.tag!("#{NS_ENUM}:Enumerate") { |en| en << Gyoku.xml(body) }
342
+ end
343
+ end
344
+
345
+ resp = send_message(builder.target!)
346
+ parser = Nori.new(:parser => :rexml, :advanced_typecasting => false, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym }, :strip_namespaces => true)
347
+ hresp = parser.parse(resp.to_s)[:envelope][:body]
348
+
349
+ # Normalize items so the type always has an array even if it's just a single item.
350
+ items = {}
351
+ if hresp[:enumerate_response][:items]
352
+ hresp[:enumerate_response][:items].each_pair do |k,v|
353
+ if v.is_a?(Array)
354
+ items[k] = v
355
+ else
356
+ items[k] = [v]
357
+ end
358
+ end
359
+ end
360
+ items
361
+ end
362
+ alias :wql :run_wql
363
+
364
+ def toggle_nori_type_casting(to)
365
+ @logger.warn('toggle_nori_type_casting is deprecated and has no effect, ' +
366
+ 'please remove calls to it')
367
+ end
368
+
369
+ private
370
+
371
+ def namespaces
372
+ {
373
+ 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
374
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
375
+ 'xmlns:env' => 'http://www.w3.org/2003/05/soap-envelope',
376
+ 'xmlns:a' => 'http://schemas.xmlsoap.org/ws/2004/08/addressing',
377
+ 'xmlns:b' => 'http://schemas.dmtf.org/wbem/wsman/1/cimbinding.xsd',
378
+ 'xmlns:n' => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration',
379
+ 'xmlns:x' => 'http://schemas.xmlsoap.org/ws/2004/09/transfer',
380
+ 'xmlns:w' => 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd',
381
+ 'xmlns:p' => 'http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd',
382
+ 'xmlns:rsp' => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell',
383
+ 'xmlns:cfg' => 'http://schemas.microsoft.com/wbem/wsman/1/config',
384
+ }
385
+ end
386
+
387
+ def header
388
+ { "#{NS_ADDRESSING}:To" => "#{@xfer.endpoint.to_s}",
389
+ "#{NS_ADDRESSING}:ReplyTo" => {
390
+ "#{NS_ADDRESSING}:Address" => 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous',
391
+ :attributes! => {"#{NS_ADDRESSING}:Address" => {'mustUnderstand' => true}}},
392
+ "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => @max_env_sz,
393
+ "#{NS_ADDRESSING}:MessageID" => "uuid:#{UUIDTools::UUID.random_create.to_s.upcase}",
394
+ "#{NS_WSMAN_DMTF}:Locale/" => '',
395
+ "#{NS_WSMAN_MSFT}:DataLocale/" => '',
396
+ "#{NS_WSMAN_DMTF}:OperationTimeout" => @timeout,
397
+ :attributes! => {
398
+ "#{NS_WSMAN_DMTF}:MaxEnvelopeSize" => {'mustUnderstand' => true},
399
+ "#{NS_WSMAN_DMTF}:Locale/" => {'xml:lang' => @locale, 'mustUnderstand' => false},
400
+ "#{NS_WSMAN_MSFT}:DataLocale/" => {'xml:lang' => @locale, 'mustUnderstand' => false}
401
+ }}
402
+ end
403
+
404
+ # merge the various header hashes and make sure we carry all of the attributes
405
+ # through instead of overwriting them.
406
+ def merge_headers(*headers)
407
+ hdr = {}
408
+ headers.each do |h|
409
+ hdr.merge!(h) do |k,v1,v2|
410
+ v1.merge!(v2) if k == :attributes!
411
+ end
412
+ end
413
+ hdr
414
+ end
415
+
416
+ def send_get_output_message(message)
417
+ send_message(message)
418
+ rescue WinRMWSManFault => e
419
+ # If no output is available before the wsman:OperationTimeout expires,
420
+ # the server MUST return a WSManFault with the Code attribute equal to
421
+ # 2150858793. When the client receives this fault, it SHOULD issue
422
+ # another Receive request.
423
+ # http://msdn.microsoft.com/en-us/library/cc251676.aspx
424
+ if e.fault_code == '2150858793'
425
+ retry
426
+ else
427
+ raise
428
+ end
429
+ end
430
+
431
+ def send_message(message)
432
+ @xfer.send_request(message)
433
+ end
434
+
435
+
436
+ # Helper methods for SOAP Headers
437
+
438
+ def resource_uri_cmd
439
+ {"#{NS_WSMAN_DMTF}:ResourceURI" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
440
+ :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
441
+ end
442
+
443
+ def resource_uri_wmi(namespace = 'root/cimv2/*')
444
+ {"#{NS_WSMAN_DMTF}:ResourceURI" => "http://schemas.microsoft.com/wbem/wsman/1/wmi/#{namespace}",
445
+ :attributes! => {"#{NS_WSMAN_DMTF}:ResourceURI" => {'mustUnderstand' => true}}}
446
+ end
447
+
448
+ def action_create
449
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Create',
450
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
451
+ end
452
+
453
+ def action_delete
454
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete',
455
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
456
+ end
457
+
458
+ def action_command
459
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command',
460
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
461
+ end
462
+
463
+ def action_receive
464
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive',
465
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
466
+ end
467
+
468
+ def action_signal
469
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Signal',
470
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
471
+ end
472
+
473
+ def action_send
474
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send',
475
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
476
+ end
477
+
478
+ def action_enumerate
479
+ {"#{NS_ADDRESSING}:Action" => 'http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate',
480
+ :attributes! => {"#{NS_ADDRESSING}:Action" => {'mustUnderstand' => true}}}
481
+ end
482
+
483
+ def selector_shell_id(shell_id)
484
+ {"#{NS_WSMAN_DMTF}:SelectorSet" =>
485
+ {"#{NS_WSMAN_DMTF}:Selector" => shell_id, :attributes! => {"#{NS_WSMAN_DMTF}:Selector" => {'Name' => 'ShellId'}}}
486
+ }
487
+ end
488
+
489
+ end # WinRMWebService
490
+ end # WinRM