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
@@ -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
@@ -1,6 +1,6 @@
1
1
  module Knife
2
2
  module Windows
3
- VERSION = "0.8.6"
3
+ VERSION = "1.0.0.rc.0"
4
4
  MAJOR, MINOR, TINY = VERSION.split('.')
5
5
  end
6
6
  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(:encrypted_data_bag_secret).and_return("echo.encrypted_data_bag_secret")
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
- clean_test_case
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
- # Execute the commands locally that would normally be executed via WinRM
100
- allow(winrm_bootstrapper).to receive(:run_command) do |command|
101
- system(command)
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
- end
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 "While Windows Bootstrapping" do
24
- context "the default Windows bootstrapping template" do
25
- bootstrap = Chef::Knife::BootstrapWindowsWinrm.new
26
- bootstrap.config[:template_file] = TEMPLATE_FILE
27
-
28
- template = bootstrap.load_template
29
- template_file_lines = template.split('\n')
30
- it "should download Platform specific MSI" do
31
- download_url=template_file_lines.find {|l| l.include?("url=")}
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] = 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 (:bootstrap) { Chef::Knife::BootstrapWindowsWinrm.new(['winrm', '-d', 'windows-chef-client-msi', '-x', 'Administrator', 'localhost']) }
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('401', '401')}}
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('500', '500')}}
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('500', '500')}}
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(:configure_session)
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.new('401', '401')}
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