diff --git a/lib/chef/knife/bootstrap/windows-chef-client-msi.erb b/lib/chef/knife/bootstrap/windows-chef-client-msi.erb index 224fd7c9..5bcf1ce8 100644 --- a/lib/chef/knife/bootstrap/windows-chef-client-msi.erb +++ b/lib/chef/knife/bootstrap/windows-chef-client-msi.erb @@ -188,11 +188,20 @@ goto install @endlocal @echo off + +<% if client_pem -%> +> <%= bootstrap_directory %>\client.pem ( + <%= escape_and_echo(::File.read(::File.expand_path(client_pem))) %> +) +<% end -%> + echo Writing validation key... +<% if validation_key -%> > <%= bootstrap_directory %>\validation.pem ( - <%= validation_key %> + <%= escape_and_echo(validation_key) %> ) +<% end -%> echo Validation key written. @echo on diff --git a/lib/chef/knife/bootstrap_windows_base.rb b/lib/chef/knife/bootstrap_windows_base.rb index 416beba9..215f20b8 100644 --- a/lib/chef/knife/bootstrap_windows_base.rb +++ b/lib/chef/knife/bootstrap_windows_base.rb @@ -190,12 +190,15 @@ def load_template(template=nil) IO.read(template).chomp end + def bootstrap_context + @bootstrap_context ||= Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config) + end + def render_template(template=nil) if config[:secret_file] config[:secret] = Chef::EncryptedDataBagItem.load_secret(config[:secret_file]) end - context = Knife::Core::WindowsBootstrapContext.new(config, config[:run_list], Chef::Config) - Erubis::Eruby.new(template).evaluate(context) + Erubis::Eruby.new(template).evaluate(bootstrap_context) end def bootstrap(proto=nil) @@ -207,13 +210,31 @@ def bootstrap(proto=nil) validate_name_args! - @node_name = Array(@name_args).first # back compat--templates may use this setting: config[:server_name] = @node_name STDOUT.sync = STDERR.sync = true + if (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))) + if Chef::VERSION.split('.').first.to_i == 11 + ui.error("Unable to find validation key. Please verify your configuration file for validation_key config value.") + exit 1 + end + + unless locate_config_value(:chef_node_name) + ui.error("You must pass a node name with -N when bootstrapping with user credentials") + exit 1 + end + + client_builder.run + bootstrap_context.client_pem = client_builder.client_path + else + ui.info("Doing old-style registration with the validation key at #{Chef::Config[:validation_key]}...") + ui.info("Delete your validation key in order to use your user credentials instead") + ui.info("") + end + wait_for_remote_response( config[:auth_timeout].to_i ) ui.info("Bootstrapping Chef on #{ui.color(@node_name, :bold)}") # create a bootstrap.bat file on the node @@ -231,6 +252,7 @@ def bootstrap(proto=nil) # execute the bootstrap.bat file bootstrap_command_result = run_command(bootstrap_command) ui.error("Bootstrap command returned #{bootstrap_command_result}") if bootstrap_command_result != 0 + bootstrap_command_result end diff --git a/lib/chef/knife/bootstrap_windows_winrm.rb b/lib/chef/knife/bootstrap_windows_winrm.rb index 333755e5..3227cb38 100644 --- a/lib/chef/knife/bootstrap_windows_winrm.rb +++ b/lib/chef/knife/bootstrap_windows_winrm.rb @@ -19,6 +19,8 @@ require 'chef/knife/bootstrap_windows_base' require 'chef/knife/winrm' require 'chef/knife/winrm_base' +require 'chef/knife/winrm_knife_base' + class Chef class Knife @@ -26,6 +28,7 @@ class BootstrapWindowsWinrm < Bootstrap include Chef::Knife::BootstrapWindowsBase include Chef::Knife::WinrmBase + include Chef::Knife::WinrmCommandSharedFunctions deps do require 'chef/knife/core/windows_bootstrap_context' @@ -37,10 +40,15 @@ class BootstrapWindowsWinrm < Bootstrap banner "knife bootstrap windows winrm FQDN (options)" def run + if (Chef::Config[:validation_key] && !File.exist?(File.expand_path(Chef::Config[:validation_key]))) + if !negotiate_auth? && !(locate_config_value(:winrm_transport) == 'ssl') + ui.error("Validatorless bootstrap over unsecure winrm channels could expose your key to network sniffing") + exit 1 + end + end bootstrap end - def run_command(command = '') winrm = Chef::Knife::Winrm.new winrm.name_args = [ server_name, command ] @@ -99,4 +107,3 @@ def elapsed_time_in_minutes(start_time) end end end - diff --git a/lib/chef/knife/core/windows_bootstrap_context.rb b/lib/chef/knife/core/windows_bootstrap_context.rb index 5819dd9b..9d0c52f3 100644 --- a/lib/chef/knife/core/windows_bootstrap_context.rb +++ b/lib/chef/knife/core/windows_bootstrap_context.rb @@ -22,6 +22,7 @@ require 'knife-windows/path_helper' # require 'chef/util/path_helper' + class Chef class Knife module Core @@ -34,10 +35,13 @@ module Core class WindowsBootstrapContext < BootstrapContext PathHelper = ::Knife::Windows::PathHelper + attr_accessor :client_pem + def initialize(config, run_list, chef_config, secret=nil) @config = config @run_list = run_list @chef_config = chef_config + @secret = secret # Compatibility with Chef 12 and Chef 11 versions begin # Pass along the secret parameter for Chef 12 @@ -49,7 +53,11 @@ def initialize(config, run_list, chef_config, secret=nil) end def validation_key - escape_and_echo(super) + if File.exist?(File.expand_path(@chef_config[:validation_key])) + IO.read(File.expand_path(@chef_config[:validation_key])) + else + false + end end def secret @@ -67,8 +75,6 @@ def config_content chef_server_url "#{@chef_config[:chef_server_url]}" validation_client_name "#{@chef_config[:validation_client_name]}" -client_key "c:/chef/client.pem" -validation_key "c:/chef/validation.pem" file_cache_path "c:/chef/cache" file_backup_path "c:/chef/backup" diff --git a/spec/functional/bootstrap_download_spec.rb b/spec/functional/bootstrap_download_spec.rb index 51a72b4b..e56c9094 100644 --- a/spec/functional/bootstrap_download_spec.rb +++ b/spec/functional/bootstrap_download_spec.rb @@ -86,11 +86,13 @@ allow(Chef::Knife::Winrm).to receive(:new).and_return(mock_winrm) allow(Chef::Knife::Core::WindowsBootstrapContext).to receive(:new).and_return(mock_bootstrap_context) + Chef::Config[:knife] = {:winrm_transport => 'plaintext', :chef_node_name => 'foo.example.com', :winrm_authentication_protocol => 'negotiate'} end it "downloads the chef-client MSI from the default location during winrm bootstrap" do run_download_scenario end + context "when provided a custom msi_url to fetch from" do let(:mock_bootstrap_context) { Chef::Knife::Core::WindowsBootstrapContext.new( { :msi_url => "file:///C:/Windows/System32/xcopy.exe" }, nil, { :knife => {} }) } @@ -115,6 +117,13 @@ def run_download_scenario clean_test_case winrm_bootstrapper = Chef::Knife::BootstrapWindowsWinrm.new([ "127.0.0.1" ]) + + if chef_12? + winrm_bootstrapper.client_builder = instance_double("Chef::Knife::Bootstrap::ClientBuilder", :run => nil, :client_path => nil) + elsif chef_11? + allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true) + end + allow(winrm_bootstrapper).to receive(:wait_for_remote_response) winrm_bootstrapper.config[:template_file] = @template_file_path diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 08e9988c..0230ba3d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -54,9 +54,18 @@ def windows2012? is_win2k12 end +def chef_11? + Chef::VERSION.split('.').first.to_i == 11 +end + +def chef_12? + Chef::VERSION.split('.').first.to_i == 12 +end RSpec.configure do |config| config.filter_run_excluding :windows_only => true unless windows? config.filter_run_excluding :windows_2012_only => true unless windows2012? + config.filter_run_excluding :chef_11_only unless chef_11? + config.filter_run_excluding :chef_12_only unless chef_12? end diff --git a/spec/unit/knife/bootstrap_template_spec.rb b/spec/unit/knife/bootstrap_template_spec.rb index 9af918fa..fc586e19 100644 --- a/spec/unit/knife/bootstrap_template_spec.rb +++ b/spec/unit/knife/bootstrap_template_spec.rb @@ -28,10 +28,9 @@ knife.parse_options(options) # Avoid referencing a validation keyfile we won't find during #render_template template = IO.read(template_file).chomp - template_string = template.gsub(/^.*[Vv]alidation_key.*$/, '') - knife.render_template(template_string) + knife.render_template(template) end - + before(:all) do @original_config = Chef::Config.hash_dup @original_knife_config = Chef::Config[:knife].dup @@ -78,7 +77,7 @@ subject(:knife) { described_class.new } context "with explicitly provided msi_url" do - let(:options) { ["--msi_url", "file:///something.msi"] } + let(:options) { ["--msi_url", "file:///something.msi"] } it "bootstrap batch file must fetch from provided url" do expect(rendered_template).to match(%r{.*REMOTE_SOURCE_MSI_URL=file:///something\.msi.*}) diff --git a/spec/unit/knife/bootstrap_windows_winrm_spec.rb b/spec/unit/knife/bootstrap_windows_winrm_spec.rb index 6b46ddcb..3e42470e 100644 --- a/spec/unit/knife/bootstrap_windows_winrm_spec.rb +++ b/spec/unit/knife/bootstrap_windows_winrm_spec.rb @@ -28,6 +28,7 @@ before do # Kernel.stub(:sleep).and_return 10 allow(bootstrap).to receive(:sleep).and_return(10) + allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true) end after do @@ -39,6 +40,7 @@ let(:session) { Chef::Knife::Winrm::WinrmSession.new({ :host => 'winrm.cloudapp.net', :port => '5986', :transport => :ssl }) } let(:initial_fail_count) { 4 } + it 'should retry if a 401 is received from WinRM' do call_result_sequence = Array.new(initial_fail_count) {lambda {raise WinRM::WinRMHTTPTransportError.new('', '401')}} call_result_sequence.push(0) @@ -61,16 +63,6 @@ bootstrap.send(:wait_for_remote_response, 2) end - it 'should have a wait timeout of 2 minutes by default' do - allow(bootstrap).to receive(:run_command).and_raise(WinRM::WinRMHTTPTransportError.new('','500')) - allow(bootstrap).to receive(:create_bootstrap_bat_command).and_raise(SystemExit) - expect(bootstrap).to receive(:wait_for_remote_response).with(2) - allow(bootstrap).to receive(:validate_name_args!).and_return(nil) - allow(bootstrap.ui).to receive(:info) - bootstrap.config[:auth_timeout] = bootstrap.options[:auth_timeout][:default] - expect { bootstrap.bootstrap }.to raise_error(SystemExit) - end - it 'should keep retrying at 10s intervals if the timeout in minutes has not elapsed' do call_result_sequence = Array.new(initial_fail_count) {lambda {raise WinRM::WinRMHTTPTransportError.new('', '500')}} call_result_sequence.push(0) @@ -82,12 +74,22 @@ bootstrap.send(:wait_for_remote_response, 2) end + it 'should have a wait timeout of 2 minutes by default' do + allow(bootstrap).to receive(:run_command).and_raise(WinRM::WinRMHTTPTransportError.new('','500')) + allow(bootstrap).to receive(:create_bootstrap_bat_command).and_raise(SystemExit) + expect(bootstrap).to receive(:wait_for_remote_response).with(2) + allow(bootstrap).to receive(:validate_name_args!).and_return(nil) + allow(bootstrap.ui).to receive(:info) + bootstrap.config[:auth_timeout] = bootstrap.options[:auth_timeout][:default] + expect { bootstrap.bootstrap }.to raise_error(SystemExit) + end + it "should exit bootstrap with non-zero status if the bootstrap fails" do command_status = 1 #Stub out calls to create the session and just get the exit codes back winrm_mock = Chef::Knife::Winrm.new - allow(Chef::Knife::Winrm).to receive(:new).and_return(winrm_mock) + allow(Chef::Knife::Winrm).to receive(:new).and_return(winrm_mock) allow(winrm_mock).to receive(:run).and_raise(SystemExit.new(command_status)) #Skip over templating stuff and checking with the remote end allow(bootstrap).to receive(:create_bootstrap_bat_command) @@ -97,7 +99,6 @@ expect { bootstrap.run_with_pretty_exceptions }.to raise_error(SystemExit) { |e| expect(e.status).to eq(command_status) } end - it 'should stop retrying if more than 2 minutes has elapsed' do times = [ Time.new(2014, 4, 1, 22, 25), Time.new(2014, 4, 1, 22, 51), Time.new(2014, 4, 1, 22, 28) ] allow(Time).to receive(:now).and_return(*times) @@ -111,4 +112,39 @@ bootstrap.config[:auth_timeout] = bootstrap.options[:auth_timeout][:default] expect { bootstrap.bootstrap }.to raise_error RuntimeError end -end \ No newline at end of file + + context "when validation_key is not present" do + context "using chef 11", :chef_11_only do + before do + allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(false) + end + + it 'raises an exception if validation_key is not present in chef 11' do + expect(bootstrap.ui).to receive(:error) + expect { bootstrap.bootstrap }.to raise_error(SystemExit) + end + end + + context "using chef 12", :chef_12_only do + before do + allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(false) + bootstrap.client_builder = instance_double("Chef::Knife::Bootstrap::ClientBuilder", :run => nil, :client_path => nil) + Chef::Config[:knife] = {:chef_node_name => 'foo.example.com'} + end + + it 'raises an exception if winrm_authentication_protocol is basic and transport is plaintext' do + Chef::Config[:knife] = {:winrm_authentication_protocol => 'basic', :winrm_transport => 'plaintext', :chef_node_name => 'foo.example.com'} + expect(bootstrap.ui).to receive(:error) + expect { bootstrap.run }.to raise_error(SystemExit) + end + + it 'raises an exception if chef_node_name is not present ' do + Chef::Config[:knife] = {:chef_node_name => nil} + expect(bootstrap.client_builder).not_to receive(:run) + expect(bootstrap.client_builder).not_to receive(:client_path) + expect(bootstrap.ui).to receive(:error) + expect { bootstrap.bootstrap }.to raise_error(SystemExit) + end + end + end +end