knife-windows 0.8.6 → 1.0.0.rc.0

Sign up to get free protection for your applications and to get access to all the features.
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