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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +17 -3
- data/CHANGELOG.md +25 -6
- data/DOC_CHANGES.md +323 -0
- data/Gemfile +2 -1
- data/README.md +160 -29
- data/RELEASE_NOTES.md +59 -6
- data/appveyor.yml +42 -0
- data/ci.gemfile +15 -0
- data/knife-windows.gemspec +4 -2
- data/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +35 -21
- data/lib/chef/knife/bootstrap_windows_base.rb +155 -31
- data/lib/chef/knife/bootstrap_windows_ssh.rb +1 -1
- data/lib/chef/knife/bootstrap_windows_winrm.rb +17 -10
- data/lib/chef/knife/core/windows_bootstrap_context.rb +67 -16
- data/lib/chef/knife/windows_cert_generate.rb +155 -0
- data/lib/chef/knife/windows_cert_install.rb +62 -0
- data/lib/chef/knife/windows_helper.rb +3 -1
- data/lib/chef/knife/windows_listener_create.rb +100 -0
- data/lib/chef/knife/winrm.rb +84 -208
- data/lib/chef/knife/winrm_base.rb +36 -10
- data/lib/chef/knife/winrm_knife_base.rb +201 -0
- data/lib/chef/knife/winrm_session.rb +72 -0
- data/lib/chef/knife/winrm_shared_options.rb +47 -0
- data/lib/chef/knife/wsman_endpoint.rb +44 -0
- data/lib/chef/knife/wsman_test.rb +96 -0
- data/lib/knife-windows/path_helper.rb +77 -0
- data/lib/knife-windows/version.rb +1 -1
- data/spec/functional/bootstrap_download_spec.rb +41 -23
- data/spec/spec_helper.rb +11 -1
- data/spec/unit/knife/bootstrap_template_spec.rb +27 -27
- data/spec/unit/knife/bootstrap_windows_winrm_spec.rb +67 -23
- data/spec/unit/knife/core/windows_bootstrap_context_spec.rb +47 -0
- data/spec/unit/knife/windows_cert_generate_spec.rb +90 -0
- data/spec/unit/knife/windows_cert_install_spec.rb +35 -0
- data/spec/unit/knife/windows_listener_create_spec.rb +61 -0
- data/spec/unit/knife/winrm_session_spec.rb +47 -0
- data/spec/unit/knife/winrm_spec.rb +222 -56
- data/spec/unit/knife/wsman_test_spec.rb +176 -0
- 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
|
data/lib/chef/knife/winrm.rb
CHANGED
@@ -17,67 +17,80 @@
|
|
17
17
|
#
|
18
18
|
|
19
19
|
require 'chef/knife'
|
20
|
-
require 'chef/knife/
|
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::
|
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
|
-
|
50
|
-
|
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
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
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
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
141
|
+
relay_winrm_command(command, server_list)
|
251
142
|
else
|
252
|
-
|
143
|
+
relay_winrm_command(command)
|
253
144
|
end
|
254
145
|
end
|
255
146
|
end
|
256
147
|
|
257
|
-
def check_for_errors!
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
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
|
-
|
279
|
-
|
280
|
-
|
166
|
+
if command.nil?
|
167
|
+
command = "exit"
|
168
|
+
puts(command)
|
281
169
|
else
|
282
|
-
|
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
|
-
|
297
|
-
|
298
|
-
|
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
|