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
@@ -0,0 +1,96 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Steven Murawski (<smurawski@chef.io>)
|
3
|
+
# Copyright:: Copyright (c) 2015 Chef Software, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'httpclient'
|
20
|
+
require 'nokogiri'
|
21
|
+
require 'chef/knife'
|
22
|
+
require 'chef/knife/winrm_knife_base'
|
23
|
+
require 'chef/knife/wsman_endpoint'
|
24
|
+
require 'pry'
|
25
|
+
|
26
|
+
class Chef
|
27
|
+
class Knife
|
28
|
+
class WsmanTest < Knife
|
29
|
+
|
30
|
+
include Chef::Knife::WinrmCommandSharedFunctions
|
31
|
+
|
32
|
+
deps do
|
33
|
+
require 'chef/search/query'
|
34
|
+
end
|
35
|
+
|
36
|
+
banner "knife wsman test QUERY (options)"
|
37
|
+
|
38
|
+
def run
|
39
|
+
@config[:winrm_authentication_protocol] = 'basic'
|
40
|
+
configure_session
|
41
|
+
verify_wsman_accessiblity_for_nodes
|
42
|
+
end
|
43
|
+
|
44
|
+
def verify_wsman_accessiblity_for_nodes
|
45
|
+
error_count = 0
|
46
|
+
@winrm_sessions.each do |item|
|
47
|
+
Chef::Log.debug("checking for WSMAN availability at #{item.endpoint}")
|
48
|
+
|
49
|
+
xml = '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsmid="http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.xsd"><s:Header/><s:Body><wsmid:Identify/></s:Body></s:Envelope>'
|
50
|
+
header = {
|
51
|
+
'WSMANIDENTIFY' => 'unauthenticated',
|
52
|
+
'Content-Type' => 'application/soap+xml; charset=UTF-8'
|
53
|
+
}
|
54
|
+
output_object = Chef::Knife::WsmanEndpoint.new(item.host, item.port, item.endpoint)
|
55
|
+
error_message = nil
|
56
|
+
begin
|
57
|
+
client = HTTPClient.new
|
58
|
+
response = client.post(item.endpoint, xml, header)
|
59
|
+
rescue Exception => e
|
60
|
+
error_message = e.message
|
61
|
+
else
|
62
|
+
ui.msg "Connected successfully to #{item.host} at #{item.endpoint}."
|
63
|
+
output_object.response_status_code = response.status_code
|
64
|
+
end
|
65
|
+
|
66
|
+
if response.nil? || output_object.response_status_code != 200
|
67
|
+
error_message = "No valid WSMan endoint listening at #{item.endpoint}."
|
68
|
+
else
|
69
|
+
doc = Nokogiri::XML response.body
|
70
|
+
namespace = 'http://schemas.dmtf.org/wbem/wsman/identity/1/wsmanidentity.xsd'
|
71
|
+
output_object.protocol_version = doc.xpath('//wsmid:ProtocolVersion', 'wsmid' => namespace).text
|
72
|
+
output_object.product_version = doc.xpath('//wsmid:ProductVersion', 'wsmid' => namespace).text
|
73
|
+
output_object.product_vendor = doc.xpath('//wsmid:ProductVendor', 'wsmid' => namespace).text
|
74
|
+
if output_object.protocol_version.to_s.strip.length == 0
|
75
|
+
error_message = "Endpoint #{item.endpoint} on #{item.host} does not appear to be a WSMAN endpoint."
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
unless error_message.nil?
|
80
|
+
ui.warn "Failed to connect to #{item.host} at #{item.endpoint}."
|
81
|
+
output_object.error_message = error_message
|
82
|
+
error_count += 1
|
83
|
+
end
|
84
|
+
|
85
|
+
if config[:verbosity] >= 1
|
86
|
+
output(output_object)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
if error_count > 0
|
90
|
+
ui.error "Failed to connect to #{error_count} nodes."
|
91
|
+
exit 1
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -148,6 +148,83 @@ module Knife
|
|
148
148
|
def self.relative_path_from(from, to)
|
149
149
|
pathname = Pathname.new(Chef::Util::PathHelper.cleanpath(to)).relative_path_from(Pathname.new(Chef::Util::PathHelper.cleanpath(from)))
|
150
150
|
end
|
151
|
+
|
152
|
+
# Retrieves the "home directory" of the current user while trying to ascertain the existence
|
153
|
+
# of said directory. The path returned uses / for all separators (the ruby standard format).
|
154
|
+
# If the home directory doesn't exist or an error is otherwise encountered, nil is returned.
|
155
|
+
#
|
156
|
+
# If a set of path elements is provided, they are appended as-is to the home path if the
|
157
|
+
# homepath exists.
|
158
|
+
#
|
159
|
+
# If an optional block is provided, the joined path is passed to that block if the home path is
|
160
|
+
# valid and the result of the block is returned instead.
|
161
|
+
#
|
162
|
+
# Home-path discovery is performed once. If a path is discovered, that value is memoized so
|
163
|
+
# that subsequent calls to home_dir don't bounce around.
|
164
|
+
#
|
165
|
+
# See self.all_homes.
|
166
|
+
def self.home(*args)
|
167
|
+
@@home_dir ||= self.all_homes { |p| break p }
|
168
|
+
if @@home_dir
|
169
|
+
path = File.join(@@home_dir, *args)
|
170
|
+
block_given? ? (yield path) : path
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# See self.home. This method performs a similar operation except that it yields all the different
|
175
|
+
# possible values of 'HOME' that one could have on this platform. Hence, on windows, if
|
176
|
+
# HOMEDRIVE\HOMEPATH and USERPROFILE are different, the provided block will be called twice.
|
177
|
+
# This method goes out and checks the existence of each location at the time of the call.
|
178
|
+
#
|
179
|
+
# The return is a list of all the returned values from each block invocation or a list of paths
|
180
|
+
# if no block is provided.
|
181
|
+
def self.all_homes(*args)
|
182
|
+
paths = []
|
183
|
+
if Chef::Platform.windows?
|
184
|
+
# By default, Ruby uses the the following environment variables to determine Dir.home:
|
185
|
+
# HOME
|
186
|
+
# HOMEDRIVE HOMEPATH
|
187
|
+
# USERPROFILE
|
188
|
+
# Ruby only checks to see if the variable is specified - not if the directory actually exists.
|
189
|
+
# On Windows, HOMEDRIVE HOMEPATH can point to a different location (such as an unavailable network mounted drive)
|
190
|
+
# while USERPROFILE points to the location where the user application settings and profile are stored. HOME
|
191
|
+
# is not defined as an environment variable (usually). If the home path actually uses UNC, then the prefix is
|
192
|
+
# HOMESHARE instead of HOMEDRIVE.
|
193
|
+
#
|
194
|
+
# We instead walk down the following and only include paths that actually exist.
|
195
|
+
# HOME
|
196
|
+
# HOMEDRIVE HOMEPATH
|
197
|
+
# HOMESHARE HOMEPATH
|
198
|
+
# USERPROFILE
|
199
|
+
|
200
|
+
paths << ENV['HOME']
|
201
|
+
paths << ENV['HOMEDRIVE'] + ENV['HOMEPATH'] if ENV['HOMEDRIVE'] && ENV['HOMEPATH']
|
202
|
+
paths << ENV['HOMESHARE'] + ENV['HOMEPATH'] if ENV['HOMESHARE'] && ENV['HOMEPATH']
|
203
|
+
paths << ENV['USERPROFILE']
|
204
|
+
end
|
205
|
+
paths << Dir.home if ENV['HOME']
|
206
|
+
|
207
|
+
# Depending on what environment variables we're using, the slashes can go in any which way.
|
208
|
+
# Just change them all to / to keep things consistent.
|
209
|
+
# Note: Maybe this is a bad idea on some unixy systems where \ might be a valid character depending on
|
210
|
+
# the particular brand of kool-aid you consume. This code assumes that \ and / are both
|
211
|
+
# path separators on any system being used.
|
212
|
+
paths = paths.map { |home_path| home_path.gsub(path_separator, ::File::SEPARATOR) if home_path }
|
213
|
+
|
214
|
+
# Filter out duplicate paths and paths that don't exist.
|
215
|
+
valid_paths = paths.select { |home_path| home_path && Dir.exists?(home_path) }
|
216
|
+
valid_paths = valid_paths.uniq
|
217
|
+
|
218
|
+
# Join all optional path elements at the end.
|
219
|
+
# If a block is provided, invoke it - otherwise just return what we've got.
|
220
|
+
joined_paths = valid_paths.map { |home_path| File.join(home_path, *args) }
|
221
|
+
if block_given?
|
222
|
+
joined_paths.each { |p| yield p }
|
223
|
+
else
|
224
|
+
joined_paths
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
151
228
|
end
|
152
229
|
end
|
153
230
|
end
|
@@ -25,7 +25,7 @@ require 'tmpdir'
|
|
25
25
|
# Windows nodes. The test modifies the Windows batch file generated
|
26
26
|
# from an erb template in the plugin source in order to enable execution
|
27
27
|
# of only the download functionality contained in the bootstrap template.
|
28
|
-
# The test relies on knowledge of the fields of the template itself and
|
28
|
+
# The test relies on knowledge of the fields of the template itself and
|
29
29
|
# also on knowledge of the contents and structure of the Windows batch
|
30
30
|
# file generated by the template.
|
31
31
|
#
|
@@ -34,7 +34,7 @@ require 'tmpdir'
|
|
34
34
|
# occur, the bootstrap code should be refactored to explicitly expose
|
35
35
|
# the download funcitonality separately from other tasks to make the
|
36
36
|
# test more robust.
|
37
|
-
describe 'Knife::Windows::Core msi download functionality for knife Windows winrm bootstrap template' do
|
37
|
+
describe 'Knife::Windows::Core msi download functionality for knife Windows winrm bootstrap template' do
|
38
38
|
|
39
39
|
before(:all) do
|
40
40
|
# Since we're always running 32-bit Ruby, fix the
|
@@ -43,7 +43,7 @@ describe 'Knife::Windows::Core msi download functionality for knife Windows winr
|
|
43
43
|
if ENV["PROCESSOR_ARCHITEW6432"]
|
44
44
|
ENV["PROCESSOR_ARCHITECTURE"] = ENV["PROCESSOR_ARCHITEW6432"]
|
45
45
|
end
|
46
|
-
|
46
|
+
|
47
47
|
# All file artifacts from this test will be written into this directory
|
48
48
|
@temp_directory = Dir.mktmpdir("bootstrap_test")
|
49
49
|
|
@@ -51,8 +51,8 @@ describe 'Knife::Windows::Core msi download functionality for knife Windows winr
|
|
51
51
|
# the downloaded msi
|
52
52
|
@local_file_download_destination = "#{@temp_directory}/chef-client-latest.msi"
|
53
53
|
|
54
|
-
source_code_directory = File.dirname(__FILE__)
|
55
|
-
@template_file_path ="#{source_code_directory}/../../lib/chef/knife/bootstrap/windows-chef-client-msi.erb"
|
54
|
+
source_code_directory = File.dirname(__FILE__)
|
55
|
+
@template_file_path ="#{source_code_directory}/../../lib/chef/knife/bootstrap/windows-chef-client-msi.erb"
|
56
56
|
end
|
57
57
|
|
58
58
|
after(:all) do
|
@@ -71,7 +71,7 @@ describe 'Knife::Windows::Core msi download functionality for knife Windows winr
|
|
71
71
|
# from being populated, i.e. chef installation and first chef
|
72
72
|
# run sections
|
73
73
|
allow(mock_bootstrap_context).to receive(:validation_key).and_return("echo.validation_key")
|
74
|
-
allow(mock_bootstrap_context).to receive(:
|
74
|
+
allow(mock_bootstrap_context).to receive(:secret).and_return("echo.encrypted_data_bag_secret")
|
75
75
|
allow(mock_bootstrap_context).to receive(:config_content).and_return("echo.config_content")
|
76
76
|
allow(mock_bootstrap_context).to receive(:start_chef).and_return("echo.echo start_chef_command")
|
77
77
|
allow(mock_bootstrap_context).to receive(:run_list).and_return("echo.run_list")
|
@@ -86,25 +86,19 @@ describe 'Knife::Windows::Core msi download functionality for knife Windows winr
|
|
86
86
|
allow(Chef::Knife::Winrm).to receive(:new).and_return(mock_winrm)
|
87
87
|
|
88
88
|
allow(Chef::Knife::Core::WindowsBootstrapContext).to receive(:new).and_return(mock_bootstrap_context)
|
89
|
+
Chef::Config[:knife] = {:winrm_transport => 'plaintext', :chef_node_name => 'foo.example.com', :winrm_authentication_protocol => 'negotiate'}
|
89
90
|
end
|
90
91
|
|
91
|
-
it "downloads the chef-client MSI during winrm bootstrap" do
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
winrm_bootstrapper = Chef::Knife::BootstrapWindowsWinrm.new([ "127.0.0.1" ])
|
96
|
-
allow(winrm_bootstrapper).to receive(:wait_for_remote_response)
|
97
|
-
winrm_bootstrapper.config[:template_file] = @template_file_path
|
92
|
+
it "downloads the chef-client MSI from the default location during winrm bootstrap" do
|
93
|
+
run_download_scenario
|
94
|
+
end
|
98
95
|
|
99
|
-
|
100
|
-
|
101
|
-
|
96
|
+
context "when provided a custom msi_url to fetch from" do
|
97
|
+
let(:mock_bootstrap_context) { Chef::Knife::Core::WindowsBootstrapContext.new(
|
98
|
+
{ :msi_url => "file:///C:/Windows/System32/xcopy.exe" }, nil, { :knife => {} }) }
|
99
|
+
it "downloads the chef-client MSI from a custom path during winrm bootstrap" do
|
100
|
+
run_download_scenario
|
102
101
|
end
|
103
|
-
|
104
|
-
winrm_bootstrapper.run
|
105
|
-
|
106
|
-
# Download should succeed
|
107
|
-
expect(download_succeeded?).to be true
|
108
102
|
end
|
109
103
|
end
|
110
104
|
|
@@ -118,5 +112,29 @@ describe 'Knife::Windows::Core msi download functionality for knife Windows winr
|
|
118
112
|
File.delete(@local_file_download_destination)
|
119
113
|
end
|
120
114
|
end
|
121
|
-
|
122
|
-
|
115
|
+
|
116
|
+
def run_download_scenario
|
117
|
+
clean_test_case
|
118
|
+
|
119
|
+
winrm_bootstrapper = Chef::Knife::BootstrapWindowsWinrm.new([ "127.0.0.1" ])
|
120
|
+
|
121
|
+
if chef_gte_12?
|
122
|
+
winrm_bootstrapper.client_builder = instance_double("Chef::Knife::Bootstrap::ClientBuilder", :run => nil, :client_path => nil)
|
123
|
+
elsif chef_lt_12?
|
124
|
+
allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true)
|
125
|
+
end
|
126
|
+
|
127
|
+
allow(winrm_bootstrapper).to receive(:wait_for_remote_response)
|
128
|
+
winrm_bootstrapper.config[:template_file] = @template_file_path
|
129
|
+
|
130
|
+
# Execute the commands locally that would normally be executed via WinRM
|
131
|
+
allow(winrm_bootstrapper).to receive(:run_command) do |command|
|
132
|
+
system(command)
|
133
|
+
end
|
134
|
+
|
135
|
+
winrm_bootstrapper.run
|
136
|
+
|
137
|
+
# Download should succeed
|
138
|
+
expect(download_succeeded?).to be true
|
139
|
+
end
|
140
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -23,6 +23,7 @@ end
|
|
23
23
|
|
24
24
|
require_relative '../lib/chef/knife/core/windows_bootstrap_context'
|
25
25
|
require_relative '../lib/chef/knife/bootstrap_windows_winrm'
|
26
|
+
require_relative '../lib/chef/knife/wsman_test'
|
26
27
|
|
27
28
|
if windows?
|
28
29
|
require 'ruby-wmi'
|
@@ -30,7 +31,7 @@ end
|
|
30
31
|
|
31
32
|
def windows2012?
|
32
33
|
is_win2k12 = false
|
33
|
-
|
34
|
+
|
34
35
|
if windows?
|
35
36
|
this_operating_system = WMI::Win32_OperatingSystem.find(:first)
|
36
37
|
os_version = this_operating_system.send('Version')
|
@@ -53,9 +54,18 @@ def windows2012?
|
|
53
54
|
is_win2k12
|
54
55
|
end
|
55
56
|
|
57
|
+
def chef_gte_12?
|
58
|
+
Chef::VERSION.split('.').first.to_i >= 12
|
59
|
+
end
|
60
|
+
|
61
|
+
def chef_lt_12?
|
62
|
+
Chef::VERSION.split('.').first.to_i < 12
|
63
|
+
end
|
56
64
|
|
57
65
|
RSpec.configure do |config|
|
58
66
|
config.filter_run_excluding :windows_only => true unless windows?
|
59
67
|
config.filter_run_excluding :windows_2012_only => true unless windows2012?
|
68
|
+
config.filter_run_excluding :chef_gte_12_only => true unless chef_gte_12?
|
69
|
+
config.filter_run_excluding :chef_lt_12_only => true unless chef_lt_12?
|
60
70
|
end
|
61
71
|
|
@@ -20,25 +20,17 @@ TEMPLATE_FILE = File.expand_path(File.dirname(__FILE__)) + "/../../../lib/chef/k
|
|
20
20
|
|
21
21
|
require 'spec_helper'
|
22
22
|
|
23
|
-
describe
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
download_url.include?("%MACHINE_OS%") && download_url.include?("%MACHINE_ARCH%")
|
33
|
-
end
|
34
|
-
it "should download specific version of MSI if supplied" do
|
35
|
-
download_url_ext= template_file_lines.find {|l| l.include?("url +=")}
|
36
|
-
download_url_ext.include?("[:bootstrap_version]")
|
37
|
-
end
|
23
|
+
describe Chef::Knife::BootstrapWindowsWinrm do
|
24
|
+
let(:template_file) { TEMPLATE_FILE }
|
25
|
+
let(:options) { [] }
|
26
|
+
let(:rendered_template) do
|
27
|
+
knife.instance_variable_set("@template_file", template_file)
|
28
|
+
knife.parse_options(options)
|
29
|
+
# Avoid referencing a validation keyfile we won't find during #render_template
|
30
|
+
template = IO.read(template_file).chomp
|
31
|
+
knife.render_template(template)
|
38
32
|
end
|
39
|
-
end
|
40
33
|
|
41
|
-
describe Chef::Knife::BootstrapWindowsWinrm do
|
42
34
|
before(:all) do
|
43
35
|
@original_config = Chef::Config.hash_dup
|
44
36
|
@original_knife_config = Chef::Config[:knife].dup
|
@@ -54,7 +46,7 @@ describe Chef::Knife::BootstrapWindowsWinrm do
|
|
54
46
|
@knife = Chef::Knife::BootstrapWindowsWinrm.new
|
55
47
|
# Merge default settings in.
|
56
48
|
@knife.merge_configs
|
57
|
-
@knife.config[:template_file] =
|
49
|
+
@knife.config[:template_file] = template_file
|
58
50
|
@stdout = StringIO.new
|
59
51
|
allow(@knife.ui).to receive(:stdout).and_return(@stdout)
|
60
52
|
@stderr = StringIO.new
|
@@ -64,15 +56,6 @@ describe Chef::Knife::BootstrapWindowsWinrm do
|
|
64
56
|
describe "specifying no_proxy with various entries" do
|
65
57
|
subject(:knife) { described_class.new }
|
66
58
|
let(:options){ ["--bootstrap-proxy", "", "--bootstrap-no-proxy", setting] }
|
67
|
-
let(:template_file) { TEMPLATE_FILE }
|
68
|
-
let(:rendered_template) do
|
69
|
-
knife.instance_variable_set("@template_file", template_file)
|
70
|
-
knife.parse_options(options)
|
71
|
-
# Avoid referencing a validation keyfile we won't find during #render_template
|
72
|
-
template = IO.read(template_file).chomp
|
73
|
-
template_string = template.gsub(/^.*[Vv]alidation_key.*$/, '')
|
74
|
-
knife.render_template(template_string)
|
75
|
-
end
|
76
59
|
|
77
60
|
context "via --bootstrap-no-proxy" do
|
78
61
|
let(:setting) { "api.opscode.com" }
|
@@ -89,4 +72,21 @@ describe Chef::Knife::BootstrapWindowsWinrm do
|
|
89
72
|
end
|
90
73
|
end
|
91
74
|
end
|
75
|
+
|
76
|
+
describe "specifying msi_url" do
|
77
|
+
subject(:knife) { described_class.new }
|
78
|
+
|
79
|
+
context "with explicitly provided msi_url" do
|
80
|
+
let(:options) { ["--msi_url", "file:///something.msi"] }
|
81
|
+
|
82
|
+
it "bootstrap batch file must fetch from provided url" do
|
83
|
+
expect(rendered_template).to match(%r{.*REMOTE_SOURCE_MSI_URL=file:///something\.msi.*})
|
84
|
+
end
|
85
|
+
end
|
86
|
+
context "with no provided msi_url" do
|
87
|
+
it "bootstrap batch file must fetch from provided url" do
|
88
|
+
expect(rendered_template).to match(%r{.*REMOTE_SOURCE_MSI_URL=https://www\.chef\.io/.*})
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
92
|
end
|
@@ -18,6 +18,7 @@
|
|
18
18
|
|
19
19
|
require 'spec_helper'
|
20
20
|
|
21
|
+
Chef::Knife::Winrm.load_deps
|
21
22
|
|
22
23
|
describe Chef::Knife::BootstrapWindowsWinrm do
|
23
24
|
before(:all) do
|
@@ -27,6 +28,7 @@ describe Chef::Knife::BootstrapWindowsWinrm do
|
|
27
28
|
before do
|
28
29
|
# Kernel.stub(:sleep).and_return 10
|
29
30
|
allow(bootstrap).to receive(:sleep).and_return(10)
|
31
|
+
allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true)
|
30
32
|
end
|
31
33
|
|
32
34
|
after do
|
@@ -34,73 +36,115 @@ describe Chef::Knife::BootstrapWindowsWinrm do
|
|
34
36
|
allow(bootstrap).to receive(:sleep).and_return(10)
|
35
37
|
end
|
36
38
|
|
37
|
-
let
|
39
|
+
let(:bootstrap) { Chef::Knife::BootstrapWindowsWinrm.new(['winrm', '-d', 'windows-chef-client-msi', '-x', 'Administrator', 'localhost']) }
|
40
|
+
let(:session) { Chef::Knife::Winrm::WinrmSession.new({ :host => 'winrm.cloudapp.net', :port => '5986', :transport => :ssl }) }
|
41
|
+
|
42
|
+
let(:initial_fail_count) { 4 }
|
38
43
|
|
39
|
-
let(:initial_fail_count) { 4 }
|
40
44
|
it 'should retry if a 401 is received from WinRM' do
|
41
|
-
call_result_sequence = Array.new(initial_fail_count) {lambda {raise WinRM::WinRMHTTPTransportError.new('
|
45
|
+
call_result_sequence = Array.new(initial_fail_count) {lambda {raise WinRM::WinRMHTTPTransportError.new('', '401')}}
|
42
46
|
call_result_sequence.push(0)
|
43
47
|
allow(bootstrap).to receive(:run_command).and_return(*call_result_sequence)
|
48
|
+
allow(bootstrap).to receive(:print)
|
49
|
+
allow(bootstrap.ui).to receive(:info)
|
44
50
|
|
45
51
|
expect(bootstrap).to receive(:run_command).exactly(call_result_sequence.length).times
|
46
52
|
bootstrap.send(:wait_for_remote_response, 2)
|
47
53
|
end
|
48
54
|
|
49
55
|
it 'should retry if something other than a 401 is received from WinRM' do
|
50
|
-
call_result_sequence = Array.new(initial_fail_count) {lambda {raise WinRM::WinRMHTTPTransportError.new('
|
56
|
+
call_result_sequence = Array.new(initial_fail_count) {lambda {raise WinRM::WinRMHTTPTransportError.new('', '500')}}
|
51
57
|
call_result_sequence.push(0)
|
52
58
|
allow(bootstrap).to receive(:run_command).and_return(*call_result_sequence)
|
59
|
+
allow(bootstrap).to receive(:print)
|
60
|
+
allow(bootstrap.ui).to receive(:info)
|
53
61
|
|
54
62
|
expect(bootstrap).to receive(:run_command).exactly(call_result_sequence.length).times
|
55
63
|
bootstrap.send(:wait_for_remote_response, 2)
|
56
64
|
end
|
57
65
|
|
58
|
-
it 'should have a wait timeout of 2 minutes by default' do
|
59
|
-
allow(bootstrap).to receive(:run_command).and_raise(WinRM::WinRMHTTPTransportError.new('',''))
|
60
|
-
allow(bootstrap).to receive(:create_bootstrap_bat_command).and_raise(SystemExit)
|
61
|
-
expect(bootstrap).to receive(:wait_for_remote_response).with(2)
|
62
|
-
allow(bootstrap).to receive(:validate_name_args!).and_return(nil)
|
63
|
-
bootstrap.config[:auth_timeout] = bootstrap.options[:auth_timeout][:default]
|
64
|
-
expect { bootstrap.bootstrap }.to raise_error(SystemExit)
|
65
|
-
end
|
66
|
-
|
67
66
|
it 'should keep retrying at 10s intervals if the timeout in minutes has not elapsed' do
|
68
|
-
call_result_sequence = Array.new(initial_fail_count) {lambda {raise WinRM::WinRMHTTPTransportError.new('
|
67
|
+
call_result_sequence = Array.new(initial_fail_count) {lambda {raise WinRM::WinRMHTTPTransportError.new('', '500')}}
|
69
68
|
call_result_sequence.push(0)
|
70
69
|
allow(bootstrap).to receive(:run_command).and_return(*call_result_sequence)
|
70
|
+
allow(bootstrap).to receive(:print)
|
71
|
+
allow(bootstrap.ui).to receive(:info)
|
71
72
|
|
72
73
|
expect(bootstrap).to receive(:run_command).exactly(call_result_sequence.length).times
|
73
74
|
bootstrap.send(:wait_for_remote_response, 2)
|
74
75
|
end
|
75
76
|
|
77
|
+
it 'should have a wait timeout of 2 minutes by default' do
|
78
|
+
allow(bootstrap).to receive(:run_command).and_raise(WinRM::WinRMHTTPTransportError.new('','500'))
|
79
|
+
allow(bootstrap).to receive(:create_bootstrap_bat_command).and_raise(SystemExit)
|
80
|
+
expect(bootstrap).to receive(:wait_for_remote_response).with(2)
|
81
|
+
allow(bootstrap).to receive(:validate_name_args!).and_return(nil)
|
82
|
+
allow(bootstrap.ui).to receive(:info)
|
83
|
+
bootstrap.config[:auth_timeout] = bootstrap.options[:auth_timeout][:default]
|
84
|
+
expect { bootstrap.bootstrap }.to raise_error(SystemExit)
|
85
|
+
end
|
86
|
+
|
76
87
|
it "should exit bootstrap with non-zero status if the bootstrap fails" do
|
77
88
|
command_status = 1
|
78
89
|
|
79
90
|
#Stub out calls to create the session and just get the exit codes back
|
80
91
|
winrm_mock = Chef::Knife::Winrm.new
|
81
92
|
allow(Chef::Knife::Winrm).to receive(:new).and_return(winrm_mock)
|
82
|
-
allow(winrm_mock).to receive(:
|
83
|
-
allow(winrm_mock).to receive(:winrm_command)
|
84
|
-
session_mock = EventMachine::WinRM::Session.new
|
85
|
-
allow(EventMachine::WinRM::Session).to receive(:new).and_return(session_mock)
|
86
|
-
allow(session_mock).to receive(:exit_codes).and_return({"thishost" => command_status})
|
87
|
-
|
93
|
+
allow(winrm_mock).to receive(:run).and_raise(SystemExit.new(command_status))
|
88
94
|
#Skip over templating stuff and checking with the remote end
|
89
95
|
allow(bootstrap).to receive(:create_bootstrap_bat_command)
|
90
96
|
allow(bootstrap).to receive(:wait_for_remote_response)
|
91
|
-
|
97
|
+
allow(bootstrap.ui).to receive(:info)
|
98
|
+
|
92
99
|
expect { bootstrap.run_with_pretty_exceptions }.to raise_error(SystemExit) { |e| expect(e.status).to eq(command_status) }
|
93
100
|
end
|
94
101
|
|
95
|
-
|
96
102
|
it 'should stop retrying if more than 2 minutes has elapsed' do
|
97
103
|
times = [ Time.new(2014, 4, 1, 22, 25), Time.new(2014, 4, 1, 22, 51), Time.new(2014, 4, 1, 22, 28) ]
|
98
104
|
allow(Time).to receive(:now).and_return(*times)
|
99
|
-
run_command_result = lambda {raise WinRM::WinRMHTTPTransportError
|
105
|
+
run_command_result = lambda {raise WinRM::WinRMHTTPTransportError, '401'}
|
100
106
|
allow(bootstrap).to receive(:validate_name_args!).and_return(nil)
|
101
107
|
allow(bootstrap).to receive(:run_command).and_return(run_command_result)
|
108
|
+
allow(bootstrap).to receive(:print)
|
109
|
+
allow(bootstrap.ui).to receive(:info)
|
110
|
+
allow(bootstrap.ui).to receive(:error)
|
102
111
|
expect(bootstrap).to receive(:run_command).exactly(1).times
|
103
112
|
bootstrap.config[:auth_timeout] = bootstrap.options[:auth_timeout][:default]
|
104
113
|
expect { bootstrap.bootstrap }.to raise_error RuntimeError
|
105
114
|
end
|
115
|
+
|
116
|
+
context "when validation_key is not present" do
|
117
|
+
context "using chef 11", :chef_lt_12_only do
|
118
|
+
before do
|
119
|
+
allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(false)
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'raises an exception if validation_key is not present in chef 11' do
|
123
|
+
expect(bootstrap.ui).to receive(:error)
|
124
|
+
expect { bootstrap.bootstrap }.to raise_error(SystemExit)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "using chef 12", :chef_gte_12_only do
|
129
|
+
before do
|
130
|
+
allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(false)
|
131
|
+
bootstrap.client_builder = instance_double("Chef::Knife::Bootstrap::ClientBuilder", :run => nil, :client_path => nil)
|
132
|
+
Chef::Config[:knife] = {:chef_node_name => 'foo.example.com'}
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'raises an exception if winrm_authentication_protocol is basic and transport is plaintext' do
|
136
|
+
Chef::Config[:knife] = {:winrm_authentication_protocol => 'basic', :winrm_transport => 'plaintext', :chef_node_name => 'foo.example.com'}
|
137
|
+
expect(bootstrap.ui).to receive(:error)
|
138
|
+
expect { bootstrap.run }.to raise_error(SystemExit)
|
139
|
+
end
|
140
|
+
|
141
|
+
it 'raises an exception if chef_node_name is not present ' do
|
142
|
+
Chef::Config[:knife] = {:chef_node_name => nil}
|
143
|
+
expect(bootstrap.client_builder).not_to receive(:run)
|
144
|
+
expect(bootstrap.client_builder).not_to receive(:client_path)
|
145
|
+
expect(bootstrap.ui).to receive(:error)
|
146
|
+
expect { bootstrap.bootstrap }.to raise_error(SystemExit)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
106
150
|
end
|