knife-windows 0.8.6 → 1.0.0.rc.0

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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +17 -3
  4. data/CHANGELOG.md +25 -6
  5. data/DOC_CHANGES.md +323 -0
  6. data/Gemfile +2 -1
  7. data/README.md +160 -29
  8. data/RELEASE_NOTES.md +59 -6
  9. data/appveyor.yml +42 -0
  10. data/ci.gemfile +15 -0
  11. data/knife-windows.gemspec +4 -2
  12. data/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +35 -21
  13. data/lib/chef/knife/bootstrap_windows_base.rb +155 -31
  14. data/lib/chef/knife/bootstrap_windows_ssh.rb +1 -1
  15. data/lib/chef/knife/bootstrap_windows_winrm.rb +17 -10
  16. data/lib/chef/knife/core/windows_bootstrap_context.rb +67 -16
  17. data/lib/chef/knife/windows_cert_generate.rb +155 -0
  18. data/lib/chef/knife/windows_cert_install.rb +62 -0
  19. data/lib/chef/knife/windows_helper.rb +3 -1
  20. data/lib/chef/knife/windows_listener_create.rb +100 -0
  21. data/lib/chef/knife/winrm.rb +84 -208
  22. data/lib/chef/knife/winrm_base.rb +36 -10
  23. data/lib/chef/knife/winrm_knife_base.rb +201 -0
  24. data/lib/chef/knife/winrm_session.rb +72 -0
  25. data/lib/chef/knife/winrm_shared_options.rb +47 -0
  26. data/lib/chef/knife/wsman_endpoint.rb +44 -0
  27. data/lib/chef/knife/wsman_test.rb +96 -0
  28. data/lib/knife-windows/path_helper.rb +77 -0
  29. data/lib/knife-windows/version.rb +1 -1
  30. data/spec/functional/bootstrap_download_spec.rb +41 -23
  31. data/spec/spec_helper.rb +11 -1
  32. data/spec/unit/knife/bootstrap_template_spec.rb +27 -27
  33. data/spec/unit/knife/bootstrap_windows_winrm_spec.rb +67 -23
  34. data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +47 -0
  35. data/spec/unit/knife/windows_cert_generate_spec.rb +90 -0
  36. data/spec/unit/knife/windows_cert_install_spec.rb +35 -0
  37. data/spec/unit/knife/windows_listener_create_spec.rb +61 -0
  38. data/spec/unit/knife/winrm_session_spec.rb +47 -0
  39. data/spec/unit/knife/winrm_spec.rb +222 -56
  40. data/spec/unit/knife/wsman_test_spec.rb +176 -0
  41. metadata +51 -20
@@ -20,6 +20,7 @@ require 'chef/knife'
20
20
  require 'chef/knife/winrm'
21
21
  require 'chef/knife/bootstrap_windows_ssh'
22
22
  require 'chef/knife/bootstrap_windows_winrm'
23
+ require 'chef/knife/wsman_test'
23
24
 
24
25
  class Chef
25
26
  class Knife
@@ -27,7 +28,8 @@ class Chef
27
28
 
28
29
  banner "#{BootstrapWindowsWinrm.banner}\n" +
29
30
  "#{BootstrapWindowsSsh.banner}\n" +
30
- "#{Winrm.banner}"
31
+ "#{Winrm.banner}\n" +
32
+ "#{WsmanTest.banner}"
31
33
  end
32
34
  end
33
35
  end
@@ -0,0 +1,100 @@
1
+ # Author:: Mukta Aphale (<mukta.aphale@clogeny.com>)
2
+ # Copyright:: Copyright (c) 2014 Opscode, Inc.
3
+ # License:: Apache License, Version 2.0
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
+
18
+ require 'chef/knife'
19
+ require 'chef/knife/winrm_base'
20
+ require 'openssl'
21
+
22
+ class Chef
23
+ class Knife
24
+ class WindowsListenerCreate < Knife
25
+
26
+ banner "knife windows listener create (options)"
27
+
28
+ option :cert_install,
29
+ :short => "-c CERT_PATH",
30
+ :long => "--cert-install CERT_PATH",
31
+ :description => "Adds specified certificate to the Windows Certificate Store's Local Machine personal store before creating listener."
32
+
33
+ option :port,
34
+ :short => "-p PORT",
35
+ :long => "--port PORT",
36
+ :description => "Specify port. Default is 5986",
37
+ :default => "5986"
38
+
39
+ option :hostname,
40
+ :short => "-h HOSTNAME",
41
+ :long => "--hostname HOSTNAME",
42
+ :description => "Hostname on the listener. Default is blank",
43
+ :default => ""
44
+
45
+ option :cert_thumbprint,
46
+ :short => "-t THUMBPRINT",
47
+ :long => "--cert-thumbprint THUMBPRINT",
48
+ :description => "Thumbprint of the certificate. Required only if --cert-install option is not used."
49
+
50
+ option :cert_passphrase,
51
+ :short => "-cp PASSWORD",
52
+ :long => "--cert-passphrase PASSWORD",
53
+ :description => "Password for certificate."
54
+
55
+ def get_cert_passphrase
56
+ print "Enter given certificate's passphrase (empty for no passphrase):"
57
+ passphrase = STDIN.gets
58
+ passphrase.strip
59
+ end
60
+
61
+ def run
62
+ STDOUT.sync = STDERR.sync = true
63
+
64
+ begin
65
+ if config[:cert_install]
66
+ config[:cert_passphrase] = get_cert_passphrase unless config[:cert_passphrase]
67
+ result = %x{powershell.exe -Command " '#{config[:cert_passphrase]}' | certutil -importPFX '#{config[:cert_install]}' AT_KEYEXCHANGE"}
68
+ if $?.exitstatus
69
+ ui.info "Certificate installed to Certificate Store"
70
+ result = %x{powershell.exe -Command " echo (Get-PfxCertificate #{config[:cert_install]}).thumbprint "}
71
+ ui.info "Certificate Thumbprint: #{result}"
72
+ config[:cert_thumbprint] = result.strip
73
+ else
74
+ ui.error "Error installing certificate to Certificate Store"
75
+ ui.error result
76
+ exit 1
77
+ end
78
+ end
79
+
80
+ unless config[:cert_thumbprint]
81
+ ui.error "Please specify the --cert-thumbprint"
82
+ exit 1
83
+ end
84
+
85
+ result = %x{winrm create winrm/config/Listener?Address=*+Transport=HTTPS @{Hostname="#{config[:hostname]}";CertificateThumbprint="#{config[:cert_thumbprint]}";Port="#{config[:port]}"}}
86
+ Chef::Log.debug result
87
+
88
+ if ($?.exitstatus == 0)
89
+ ui.info "WinRM listener created with Port: #{config[:port]} and CertificateThumbprint: #{config[:cert_thumbprint]}"
90
+ else
91
+ ui.error "Error creating WinRM listener. use -VV for more details."
92
+ end
93
+
94
+ rescue => e
95
+ puts "ERROR: + #{e}"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -17,67 +17,80 @@
17
17
  #
18
18
 
19
19
  require 'chef/knife'
20
- require 'chef/knife/winrm_base'
20
+ require 'chef/knife/winrm_knife_base'
21
+ require 'chef/knife/windows_cert_generate'
22
+ require 'chef/knife/windows_cert_install'
23
+ require 'chef/knife/windows_listener_create'
24
+ require 'chef/knife/winrm_session'
21
25
 
22
26
  class Chef
23
27
  class Knife
24
- class Winrm < Knife
28
+ class Winrm < Knife
25
29
 
26
- include Chef::Knife::WinrmBase
30
+ include Chef::Knife::WinrmCommandSharedFunctions
27
31
 
28
32
  deps do
29
33
  require 'readline'
30
34
  require 'chef/search/query'
31
- require 'em-winrm'
32
35
  end
33
36
 
34
37
  attr_writer :password
35
38
 
36
- banner "knife winrm QUERY COMMAND (options)"
37
-
38
- option :attribute,
39
- :short => "-a ATTR",
40
- :long => "--attribute ATTR",
41
- :description => "The attribute to use for opening the connection - default is fqdn",
42
- :default => "fqdn"
39
+ banner "knife winrm QUERY COMMAND (options)"
43
40
 
44
41
  option :returns,
45
42
  :long => "--returns CODES",
46
43
  :description => "A comma delimited list of return codes which indicate success",
47
- :default => "0"
44
+ :default => "0"
48
45
 
49
- option :manual,
50
- :short => "-m",
51
- :long => "--manual-list",
52
- :boolean => true,
53
- :description => "QUERY is a space separated list of servers",
54
- :default => false
46
+ def run
47
+ STDOUT.sync = STDERR.sync = true
55
48
 
49
+ configure_session
50
+ validate_password
51
+ execute_remote_command
52
+ end
56
53
 
57
- def session
58
- session_opts = {}
59
- session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug
60
- @session ||= begin
61
- s = EventMachine::WinRM::Session.new(session_opts)
62
- s.on_output do |host, data|
63
- print_data(host, data)
64
- end
65
- s.on_error do |host, err|
66
- print_data(host, err, :red)
54
+ def execute_remote_command
55
+ begin
56
+ case @name_args[1]
57
+ when "interactive"
58
+ interactive
59
+ else
60
+ relay_winrm_command(@name_args[1..-1].join(" "))
61
+
62
+ if config[:returns]
63
+ check_for_errors!
64
+ end
65
+
66
+ # Knife seems to ignore the return value of this method,
67
+ # so we exit to force the process exit code for this
68
+ # subcommand if returns is set
69
+ exit @exit_code if @exit_code && @exit_code != 0
70
+ @exit_code || 0
67
71
  end
68
- s.on_command_complete do |host|
69
- host = host == :all ? 'All Servers' : host
70
- Chef::Log.debug("command complete on #{host}")
72
+ rescue WinRM::WinRMHTTPTransportError => e
73
+ case e.message
74
+ when /401/
75
+ if ! config[:suppress_auth_failure]
76
+ # Display errors if the caller hasn't opted to retry
77
+ ui.error "Failed to authenticate to #{@name_args[0].split(" ")} as #{locate_config_value(:winrm_user)}"
78
+ ui.info "Response: #{e.message}"
79
+ ui.info "Hint: Please check winrm configuration 'winrm get winrm/config/service' AllowUnencrypted flag on remote server."
80
+ raise e
81
+ end
82
+ @exit_code = 401
83
+ else
84
+ raise e
71
85
  end
72
- s
73
86
  end
74
-
75
87
  end
76
88
 
77
- def success_return_codes
78
- #Redundant if the CLI options parsing occurs
79
- return [0] unless config[:returns]
80
- return config[:returns].split(',').collect {|item| item.to_i}
89
+ def relay_winrm_command(command)
90
+ Chef::Log.debug(command)
91
+ @winrm_sessions.each do |s|
92
+ s.relay_command(command)
93
+ end
81
94
  end
82
95
 
83
96
  # TODO: Copied from Knife::Core:GenericPresenter. Should be extracted
@@ -100,131 +113,10 @@ class Chef
100
113
  ( !data.kind_of?(Array) && data.respond_to?(:to_hash) ) ? data.to_hash : data
101
114
  end
102
115
 
103
- def configure_session
104
-
105
- list = case config[:manual]
106
- when true
107
- @name_args[0].split(" ")
108
- when false
109
- r = Array.new
110
- q = Chef::Search::Query.new
111
- @action_nodes = q.search(:node, @name_args[0])[0]
112
- @action_nodes.each do |item|
113
- i = extract_nested_value(item, config[:attribute])
114
- r.push(i) unless i.nil?
115
- end
116
- r
117
- end
118
- if list.length == 0
119
- if @action_nodes.length == 0
120
- ui.fatal("No nodes returned from search!")
121
- else
122
- ui.fatal("#{@action_nodes.length} #{@action_nodes.length > 1 ? "nodes":"node"} found, " +
123
- "but does not have the required attribute (#{config[:attribute]}) to establish the connection. " +
124
- "Try setting another attribute to open the connection using --attribute.")
125
- end
126
- exit 10
127
- end
128
- session_from_list(list)
129
- end
130
-
131
- def session_from_list(list)
132
- list.each do |item|
133
- Chef::Log.debug("Adding #{item}")
134
- session_opts = {}
135
- session_opts[:user] = config[:winrm_user] = Chef::Config[:knife][:winrm_user] || config[:winrm_user]
136
- session_opts[:password] = config[:winrm_password] = Chef::Config[:knife][:winrm_password] || config[:winrm_password]
137
- session_opts[:port] = Chef::Config[:knife][:winrm_port] || config[:winrm_port]
138
- session_opts[:keytab] = Chef::Config[:knife][:kerberos_keytab_file] if Chef::Config[:knife][:kerberos_keytab_file]
139
- session_opts[:realm] = Chef::Config[:knife][:kerberos_realm] if Chef::Config[:knife][:kerberos_realm]
140
- session_opts[:service] = Chef::Config[:knife][:kerberos_service] if Chef::Config[:knife][:kerberos_service]
141
- session_opts[:ca_trust_path] = Chef::Config[:knife][:ca_trust_file] if Chef::Config[:knife][:ca_trust_file]
142
- session_opts[:operation_timeout] = 1800 # 30 min OperationTimeout for long bootstraps fix for KNIFE_WINDOWS-8
143
-
144
- ## If you have a \\ in your name you need to use NTLM domain authentication
145
- username_contains_domain = session_opts[:user].split("\\").length.eql?(2)
146
-
147
- if username_contains_domain
148
- # We cannot use basic_auth for domain authentication
149
- session_opts[:basic_auth_only] = false
150
- else
151
- session_opts[:basic_auth_only] = true
152
- end
153
-
154
- if config.keys.any? {|k| k.to_s =~ /kerberos/ }
155
- session_opts[:transport] = :kerberos
156
- session_opts[:basic_auth_only] = false
157
- else
158
- session_opts[:transport] = (Chef::Config[:knife][:winrm_transport] || config[:winrm_transport]).to_sym
159
-
160
- if Chef::Platform.windows? && session_opts[:transport] == :plaintext && username_contains_domain
161
- ui.warn("Switching to Negotiate authentication, Basic does not support Domain Authentication")
162
- # windows - force only encrypted communication
163
- require 'winrm-s'
164
- session_opts[:transport] = :sspinegotiate
165
- session_opts[:disable_sspi] = false
166
- else
167
- session_opts[:disable_sspi] = true
168
- end
169
- if session_opts[:user] and
170
- (not session_opts[:password])
171
- session_opts[:password] = Chef::Config[:knife][:winrm_password] = config[:winrm_password] = get_password
172
- end
173
- end
174
-
175
- session.use(item, session_opts)
176
-
177
- @longest = item.length if item.length > @longest
178
- end
179
- session
180
- end
181
-
182
- def print_data(host, data, color = :cyan)
183
- if data =~ /\n/
184
- data.split(/\n/).each { |d| print_data(host, d, color) }
185
- else
186
- padding = @longest - host.length
187
- print ui.color(host, color)
188
- padding.downto(0) { print " " }
189
- puts data.chomp
190
- end
191
- end
192
-
193
- def winrm_command(command, subsession=nil)
194
- subsession ||= session
195
- subsession.relay_command(command)
196
- end
197
-
198
- def get_password
199
- @password ||= ui.ask("Enter your password: ") { |q| q.echo = false }
200
- end
201
-
202
- # Present the prompt and read a single line from the console. It also
203
- # detects ^D and returns "exit" in that case. Adds the input to the
204
- # history, unless the input is empty. Loops repeatedly until a non-empty
205
- # line is input.
206
- def read_line
207
- loop do
208
- command = reader.readline("#{ui.color('knife-winrm>', :bold)} ", true)
209
-
210
- if command.nil?
211
- command = "exit"
212
- puts(command)
213
- else
214
- command.strip!
215
- end
216
-
217
- unless command.empty?
218
- return command
219
- end
220
- end
221
- end
222
-
223
- def reader
224
- Readline
225
- end
116
+ private
226
117
 
227
118
  def interactive
119
+ puts "WARN: Deprecated functionality. This will not be supported in future knife-windows releases."
228
120
  puts "Connected to #{ui.list(session.servers.collect { |s| ui.color(s.host, :cyan) }, :inline, " and ")}"
229
121
  puts
230
122
  puts "To run a command on a list of servers, do:"
@@ -238,77 +130,61 @@ class Chef
238
130
  case command
239
131
  when 'quit!'
240
132
  puts 'Bye!'
241
- session.close
242
133
  break
243
134
  when /^on (.+?); (.+)$/
244
135
  raw_list = $1.split(" ")
245
136
  server_list = Array.new
246
- session.servers.each do |session_server|
137
+ @winrm_sessions.each do |session_server|
247
138
  server_list << session_server if raw_list.include?(session_server.host)
248
139
  end
249
140
  command = $2
250
- winrm_command(command, session.on(*server_list))
141
+ relay_winrm_command(command, server_list)
251
142
  else
252
- winrm_command(command)
143
+ relay_winrm_command(command)
253
144
  end
254
145
  end
255
146
  end
256
147
 
257
- def check_for_errors!(exit_codes)
258
-
259
- exit_codes.each do |host, value|
260
- Chef::Log.debug("Exit code found: #{value}")
261
- unless success_return_codes.include? value.to_i
262
- @exit_code = value.to_i
263
- ui.error "Failed to execute command on #{host} return code #{value}"
148
+ def check_for_errors!
149
+ @winrm_sessions.each do |session|
150
+ session_exit_code = session.exit_code
151
+ unless success_return_codes.include? session_exit_code.to_i
152
+ @exit_code = session_exit_code.to_i
153
+ ui.error "Failed to execute command on #{session.host} return code #{session_exit_code}"
264
154
  end
265
155
  end
266
-
267
156
  end
268
157
 
269
- def run
270
-
271
- STDOUT.sync = STDERR.sync = true
272
-
273
- begin
274
- @longest = 0
275
-
276
- configure_session
158
+ # Present the prompt and read a single line from the console. It also
159
+ # detects ^D and returns "exit" in that case. Adds the input to the
160
+ # history, unless the input is empty. Loops repeatedly until a non-empty
161
+ # line is input.
162
+ def read_line
163
+ loop do
164
+ command = reader.readline("#{ui.color('knife-winrm>', :bold)} ", true)
277
165
 
278
- case @name_args[1]
279
- when "interactive"
280
- interactive
166
+ if command.nil?
167
+ command = "exit"
168
+ puts(command)
281
169
  else
282
- winrm_command(@name_args[1..-1].join(" "))
283
-
284
- if config[:returns]
285
- check_for_errors! session.exit_codes
286
- end
287
-
288
- session.close
289
-
290
- # Knife seems to ignore the return value of this method,
291
- # so we exit to force the process exit code for this
292
- # subcommand if returns is set
293
- exit @exit_code if @exit_code && @exit_code != 0
294
- @exit_code || 0
170
+ command.strip!
295
171
  end
296
- rescue WinRM::WinRMHTTPTransportError => e
297
- case e.message
298
- when /401/
299
- if ! config[:suppress_auth_failure]
300
- # Display errors if the caller hasn't opted to retry
301
- ui.error "Failed to authenticate to #{@name_args[0].split(" ")} as #{config[:winrm_user]}"
302
- ui.info "Response: #{e.message}"
303
- raise e
304
- end
305
- @exit_code = 401
306
- else
307
- raise e
172
+
173
+ unless command.empty?
174
+ return command
308
175
  end
309
176
  end
310
177
  end
311
178
 
179
+ def reader
180
+ Readline
181
+ end
182
+
183
+ def success_return_codes
184
+ #Redundant if the CLI options parsing occurs
185
+ return [0] unless config[:returns]
186
+ return @success_return_codes ||= config[:returns].split(',').collect {|item| item.to_i}
187
+ end
312
188
  end
313
189
  end
314
190
  end