Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validatorless bootstrap #234

Merged
merged 12 commits into from
May 28, 2015
11 changes: 10 additions & 1 deletion lib/chef/knife/bootstrap/windows-chef-client-msi.erb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 25 additions & 3 deletions lib/chef/knife/bootstrap_windows_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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

Expand Down
11 changes: 9 additions & 2 deletions lib/chef/knife/bootstrap_windows_winrm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
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
class BootstrapWindowsWinrm < Bootstrap

include Chef::Knife::BootstrapWindowsBase
include Chef::Knife::WinrmBase
include Chef::Knife::WinrmCommandSharedFunctions

deps do
require 'chef/knife/core/windows_bootstrap_context'
Expand All @@ -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 ]
Expand Down Expand Up @@ -99,4 +107,3 @@ def elapsed_time_in_minutes(start_time)
end
end
end

12 changes: 9 additions & 3 deletions lib/chef/knife/core/windows_bootstrap_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
require 'knife-windows/path_helper'
# require 'chef/util/path_helper'


class Chef
class Knife
module Core
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions spec/functional/bootstrap_download_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {} }) }
Expand All @@ -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

Expand Down
9 changes: 9 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

7 changes: 3 additions & 4 deletions spec/unit/knife/bootstrap_template_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.*})
Expand Down
62 changes: 49 additions & 13 deletions spec/unit/knife/bootstrap_windows_winrm_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -111,4 +112,39 @@
bootstrap.config[:auth_timeout] = bootstrap.options[:auth_timeout][:default]
expect { bootstrap.bootstrap }.to raise_error RuntimeError
end
end

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