spec/unit/knife/bootstrap_spec.rb in knife-18.2.7 vs spec/unit/knife/bootstrap_spec.rb in knife-18.3.0

- old
+ new

@@ -1,2233 +1,2288 @@ -# -# Author:: Ian Meyer (<ianmmeyer@gmail.com>) -# Copyright:: Copyright 2010-2016, Ian Meyer -# Copyright:: Copyright (c) Chef Software Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require "knife_spec_helper" - -Chef::Knife::Bootstrap.load_deps - -describe Chef::Knife::Bootstrap do - let(:bootstrap_template) { nil } - let(:stderr) { StringIO.new } - let(:bootstrap_cli_options) { [ ] } - let(:linux_test) { true } - let(:windows_test) { false } - let(:linux_test) { false } - let(:unix_test) { false } - let(:ssh_test) { false } - - let(:connection) do - double("TrainConnector", - windows?: windows_test, - linux?: linux_test, - unix?: unix_test) - end - - let(:knife) do - Chef::Log.logger = Logger.new(StringIO.new) - Chef::Config[:knife][:bootstrap_template] = bootstrap_template unless bootstrap_template.nil? - - k = Chef::Knife::Bootstrap.new(bootstrap_cli_options) - allow(k.ui).to receive(:stderr).and_return(stderr) - allow(k).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false) - allow(k).to receive(:connection).and_return connection - k.merge_configs - k - end - - context "#check_license" do - let(:acceptor) { instance_double(LicenseAcceptance::Acceptor) } - - before do - expect(LicenseAcceptance::Acceptor).to receive(:new).and_return(acceptor) - end - - describe "when a license is not required" do - it "does not set the chef_license" do - expect(acceptor).to receive(:license_required?).and_return(false) - knife.check_license - expect(Chef::Config[:chef_license]).to eq(nil) - end - end - - describe "when a license is required" do - it "sets the chef_license" do - expect(acceptor).to receive(:license_required?).and_return(true) - expect(acceptor).to receive(:id_from_mixlib).and_return("id") - expect(acceptor).to receive(:check_and_persist) - expect(acceptor).to receive(:acceptance_value).and_return("accept-no-persist") - knife.check_license - expect(Chef::Config[:chef_license]).to eq("accept-no-persist") - end - end - end - - context "#bootstrap_template" do - it "should default to chef-full" do - expect(knife.bootstrap_template).to be_a_kind_of(String) - expect(File.basename(knife.bootstrap_template)).to eq("chef-full") - end - end - - context "#render_template - when using the chef-full default template" do - let(:rendered_template) do - knife.merge_configs - knife.render_template - end - - it "should render client.rb" do - expect(rendered_template).to match("cat > /etc/chef/client.rb <<'EOP'") - expect(rendered_template).to match("chef_server_url \"https://localhost:443\"") - expect(rendered_template).to match("validation_client_name \"chef-validator\"") - expect(rendered_template).to match("log_location STDOUT") - end - - it "should render first-boot.json" do - expect(rendered_template).to match("cat > /etc/chef/first-boot.json <<'EOP'") - expect(rendered_template).to match('{"run_list":\[\]}') - end - - context "and encrypted_data_bag_secret was provided" do - it "should render encrypted_data_bag_secret file" do - expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) - expect(knife).to receive(:read_secret).and_return("secrets") - expect(rendered_template).to match("cat > /etc/chef/encrypted_data_bag_secret <<'EOP'") - expect(rendered_template).to match('{"run_list":\[\]}') - expect(rendered_template).to match(/secrets/) - end - end - end - - context "with --bootstrap-vault-item" do - let(:bootstrap_cli_options) { [ "--bootstrap-vault-item", "vault1:item1", "--bootstrap-vault-item", "vault1:item2", "--bootstrap-vault-item", "vault2:item1" ] } - it "sets the knife config cli option correctly" do - expect(knife.config[:bootstrap_vault_item]).to eq({ "vault1" => %w{item1 item2}, "vault2" => ["item1"] }) - end - end - - context "with --bootstrap-preinstall-command" do - command = "while sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; do\n echo 'waiting for dpkg lock';\n sleep 1;\n done;" - let(:bootstrap_cli_options) { [ "--bootstrap-preinstall-command", command ] } - let(:rendered_template) do - knife.merge_configs - knife.render_template - end - it "configures the preinstall command in the bootstrap template correctly" do - expect(rendered_template).to match(/command/) - end - end - - context "with --bootstrap-proxy" do - let(:bootstrap_cli_options) { [ "--bootstrap-proxy", "1.1.1.1" ] } - let(:rendered_template) do - knife.merge_configs - knife.render_template - end - it "configures the https_proxy environment variable in the bootstrap template correctly" do - expect(rendered_template).to match(/https_proxy="1.1.1.1" export https_proxy/) - end - end - - context "with --bootstrap-no-proxy" do - let(:bootstrap_cli_options) { [ "--bootstrap-no-proxy", "localserver" ] } - let(:rendered_template) do - knife.merge_configs - knife.render_template - end - it "configures the https_proxy environment variable in the bootstrap template correctly" do - expect(rendered_template).to match(/no_proxy="localserver" export no_proxy/) - end - end - - context "with :bootstrap_template and :template_file cli options" do - let(:bootstrap_cli_options) { [ "--bootstrap-template", "my-template", "other-template" ] } - - it "should select bootstrap template" do - expect(File.basename(knife.bootstrap_template)).to eq("my-template") - end - end - - context "when finding templates" do - context "when :bootstrap_template config is set to a file" do - context "that doesn't exist" do - let(:bootstrap_template) { "/opt/blah/not/exists/template.erb" } - - it "raises an error" do - expect { knife.find_template }.to raise_error(Errno::ENOENT) - end - end - - context "that exists" do - let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test.erb")) } - - it "loads the given file as the template" do - expect(Chef::Log).to receive(:trace) - expect(knife.find_template).to eq(File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test.erb"))) - end - end - end - - context "when :bootstrap_template config is set to a template name" do - let(:bootstrap_template) { "example" } - - let(:builtin_template_path) { File.expand_path(File.join(__dir__, "../../../lib/chef/knife/bootstrap/templates", "example.erb")) } - - let(:chef_config_dir_template_path) { "/knife/chef/config/bootstrap/example.erb" } - - let(:env_home_template_path) { "/env/home/.chef/bootstrap/example.erb" } - - let(:gem_files_template_path) { "/Users/schisamo/.rvm/gems/ruby-1.9.2-p180@chef-0.10/gems/knife-windows-0.5.4/lib/chef/knife/bootstrap/fake-bootstrap-template.erb" } - - def configure_chef_config_dir - allow(Chef::Knife).to receive(:chef_config_dir).and_return("/knife/chef/config") - end - - def configure_env_home - allow(Chef::Util::PathHelper).to receive(:home).with(".chef", "bootstrap", "example.erb").and_yield(env_home_template_path) - end - - def configure_gem_files - allow(Gem).to receive(:find_files).and_return([ gem_files_template_path ]) - end - - before(:each) do - expect(File).to receive(:exist?).with(bootstrap_template).and_return(false) - end - - context "when file is available everywhere" do - before do - configure_chef_config_dir - configure_env_home - configure_gem_files - - expect(File).to receive(:exist?).with(builtin_template_path).and_return(true) - end - - it "should load the template from built-in templates" do - expect(knife.find_template).to eq(builtin_template_path) - end - end - - context "when file is available in chef_config_dir" do - before do - configure_chef_config_dir - configure_env_home - configure_gem_files - - expect(File).to receive(:exist?).with(builtin_template_path).and_return(false) - expect(File).to receive(:exist?).with(chef_config_dir_template_path).and_return(true) - - it "should load the template from chef_config_dir" do - knife.find_template.should eq(chef_config_dir_template_path) - end - end - end - - context "when file is available in home directory" do - before do - configure_chef_config_dir - configure_env_home - configure_gem_files - - expect(File).to receive(:exist?).with(builtin_template_path).and_return(false) - expect(File).to receive(:exist?).with(chef_config_dir_template_path).and_return(false) - expect(File).to receive(:exist?).with(env_home_template_path).and_return(true) - end - - it "should load the template from chef_config_dir" do - expect(knife.find_template).to eq(env_home_template_path) - end - end - - context "when file is available in Gem files" do - before do - configure_chef_config_dir - configure_env_home - configure_gem_files - - expect(File).to receive(:exist?).with(builtin_template_path).and_return(false) - expect(File).to receive(:exist?).with(chef_config_dir_template_path).and_return(false) - expect(File).to receive(:exist?).with(env_home_template_path).and_return(false) - expect(File).to receive(:exist?).with(gem_files_template_path).and_return(true) - end - - it "should load the template from Gem files" do - expect(knife.find_template).to eq(gem_files_template_path) - end - end - - context "when file is available in Gem files and home dir doesn't exist" do - before do - configure_chef_config_dir - configure_gem_files - allow(Chef::Util::PathHelper).to receive(:home).with(".chef", "bootstrap", "example.erb").and_return(nil) - - expect(File).to receive(:exist?).with(builtin_template_path).and_return(false) - expect(File).to receive(:exist?).with(chef_config_dir_template_path).and_return(false) - expect(File).to receive(:exist?).with(gem_files_template_path).and_return(true) - end - - it "should load the template from Gem files" do - expect(knife.find_template).to eq(gem_files_template_path) - end - end - end - end - - ["-t", "--bootstrap-template"].each do |t| - context "when #{t} option is given in the command line" do - it "sets the knife :bootstrap_template config" do - knife.parse_options([t, "blahblah"]) - knife.merge_configs - expect(knife.bootstrap_template).to eq("blahblah") - end - end - end - - context "with run_list template" do - let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test.erb")) } - - it "should return an empty run_list" do - expect(knife.render_template).to eq('{"run_list":[]}') - end - - it "should have role[base] in the run_list" do - knife.parse_options(["-r", "role[base]"]) - knife.merge_configs - expect(knife.render_template).to eq('{"run_list":["role[base]"]}') - end - - it "should have role[base] and recipe[cupcakes] in the run_list" do - knife.parse_options(["-r", "role[base],recipe[cupcakes]"]) - knife.merge_configs - expect(knife.render_template).to eq('{"run_list":["role[base]","recipe[cupcakes]"]}') - end - - context "with bootstrap_attribute options" do - let(:jsonfile) do - file = Tempfile.new(["node", ".json"]) - File.open(file.path, "w") { |f| f.puts '{"foo":{"bar":"baz"}}' } - file - end - - it "should have foo => {bar => baz} in the first_boot from cli" do - knife.parse_options(["-j", '{"foo":{"bar":"baz"}}']) - knife.merge_configs - expected_hash = FFI_Yajl::Parser.new.parse('{"foo":{"bar":"baz"},"run_list":[]}') - actual_hash = FFI_Yajl::Parser.new.parse(knife.render_template) - expect(actual_hash).to eq(expected_hash) - end - - it "should have foo => {bar => baz} in the first_boot from file" do - knife.parse_options(["--json-attribute-file", jsonfile.path]) - knife.merge_configs - expected_hash = FFI_Yajl::Parser.new.parse('{"foo":{"bar":"baz"},"run_list":[]}') - actual_hash = FFI_Yajl::Parser.new.parse(knife.render_template) - expect(actual_hash).to eq(expected_hash) - jsonfile.close - end - - it "raises a Chef::Exceptions::BootstrapCommandInputError with the proper error message" do - knife.parse_options(["-j", '{"foo":{"bar":"baz"}}']) - knife.parse_options(["--json-attribute-file", jsonfile.path]) - knife.merge_configs - allow(knife).to receive(:validate_name_args!) - expect(knife).to receive(:check_license) - - expect { knife.run }.to raise_error(Chef::Exceptions::BootstrapCommandInputError) - jsonfile.close - end - end - end - - context "with hints template" do - let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test-hints.erb")) } - - it "should create a hint file when told to" do - knife.parse_options(["--hint", "openstack"]) - knife.merge_configs - expect(knife.render_template).to match(%r{/etc/chef/ohai/hints/openstack.json}) - end - - it "should populate a hint file with JSON when given a file to read" do - allow(::File).to receive(:read).and_return('{ "foo" : "bar" }') - knife.parse_options(["--hint", "openstack=hints/openstack.json"]) - knife.merge_configs - expect(knife.render_template).to match(/\{\"foo\":\"bar\"\}/) - end - end - - describe "specifying no_proxy with various entries" do - subject(:knife) do - k = described_class.new - Chef::Config[:knife][:bootstrap_template] = template_file - allow(k).to receive(:connection).and_return connection - k.parse_options(options) - k.merge_configs - k - end - - let(:options) { ["--bootstrap-no-proxy", setting] } - - let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) } - - let(:rendered_template) do - knife.render_template - end - - context "via --bootstrap-no-proxy" do - let(:setting) { "api.opscode.com" } - - it "renders the client.rb with a single FQDN no_proxy entry" do - expect(rendered_template).to match(/.*no_proxy\s*"api.opscode.com".*/) - end - end - - context "via --bootstrap-no-proxy multiple" do - let(:setting) { "api.opscode.com,172.16.10.*" } - - it "renders the client.rb with comma-separated FQDN and wildcard IP address no_proxy entries" do - expect(rendered_template).to match(/.*no_proxy\s*"api.opscode.com,172.16.10.\*".*/) - end - end - - context "via --ssl-verify-mode none" do - let(:options) { ["--node-ssl-verify-mode", "none"] } - - it "renders the client.rb with ssl_verify_mode set to :verify_none" do - expect(rendered_template).to match(/ssl_verify_mode :verify_none/) - end - end - - context "via --node-ssl-verify-mode peer" do - let(:options) { ["--node-ssl-verify-mode", "peer"] } - - it "renders the client.rb with ssl_verify_mode set to :verify_peer" do - expect(rendered_template).to match(/ssl_verify_mode :verify_peer/) - end - end - - context "via --node-ssl-verify-mode all" do - let(:options) { ["--node-ssl-verify-mode", "all"] } - - it "raises error" do - expect { rendered_template }.to raise_error(RuntimeError) - end - end - - context "via --node-verify-api-cert" do - let(:options) { ["--node-verify-api-cert"] } - - it "renders the client.rb with verify_api_cert set to true" do - expect(rendered_template).to match(/verify_api_cert true/) - end - end - - context "via --no-node-verify-api-cert" do - let(:options) { ["--no-node-verify-api-cert"] } - - it "renders the client.rb with verify_api_cert set to false" do - expect(rendered_template).to match(/verify_api_cert false/) - end - end - end - - describe "specifying the encrypted data bag secret key" do - let(:secret) { "supersekret" } - let(:options) { [] } - let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "secret.erb")) } - let(:rendered_template) do - knife.parse_options(options) - knife.merge_configs - knife.render_template - end - - it "creates a secret file" do - expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) - expect(knife).to receive(:read_secret).and_return(secret) - expect(rendered_template).to match(/#{secret}/) - end - - it "renders the client.rb with an encrypted_data_bag_secret entry" do - expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) - expect(knife).to receive(:read_secret).and_return(secret) - expect(rendered_template).to match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) - end - - end - - describe "when transferring trusted certificates" do - let(:rendered_template) do - knife.merge_configs - knife.render_template - end - - before do - Chef::Config[:trusted_certs_dir] = Chef::Util::PathHelper.cleanpath(File.join(CHEF_SPEC_DATA, "trusted_certs")) - end - - it "creates /etc/chef/trusted_certs" do - expect(rendered_template).to match(%r{mkdir -p /etc/chef/trusted_certs}) - end - - it "copies the certificates in the directory" do - certificates = Dir[File.join(Chef::Config[:trusted_certs_dir], "*.{crt,pem}")] - - certificates.each do |cert| - expect(rendered_template).to match(%r{cat > /etc/chef/trusted_certs/#{File.basename(cert)} <<'EOP'}) - end - end - - it "doesn't create /etc/chef/trusted_certs if :trusted_certs_dir is empty" do - Dir.mktmpdir do |dir| - Chef::Config[:trusted_certs_dir] = dir - expect(rendered_template).not_to match(%r{mkdir -p /etc/chef/trusted_certs}) - end - end - end - - context "when doing fips things" do - let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) } - - before do - Chef::Config[:knife][:bootstrap_template] = template_file - knife.merge_configs - end - - let(:rendered_template) do - knife.render_template - end - - context "when knife is in fips mode" do - before do - Chef::Config[:fips] = true - end - - it "renders 'fips true'" do - expect(rendered_template).to match("fips") - end - end - - context "when knife is not in fips mode" do - before do - # This is required because the chef-fips pipeline does - # has a default value of true for fips - Chef::Config[:fips] = false - end - - it "does not render anything about fips" do - expect(rendered_template).not_to match("fips") - end - end - end - - describe "when transferring client.d" do - - let(:rendered_template) do - knife.merge_configs - knife.render_template - end - - before do - Chef::Config[:client_d_dir] = client_d_dir - end - - context "when client_d_dir is nil" do - let(:client_d_dir) { nil } - - it "does not create /etc/chef/client.d" do - expect(rendered_template).not_to match(%r{mkdir -p /etc/chef/client\.d}) - end - end - - context "when client_d_dir is set" do - let(:client_d_dir) do - Chef::Util::PathHelper.cleanpath( - File.join(__dir__, "../../data/client.d_00") - ) - end - - it "creates /etc/chef/client.d" do - expect(rendered_template).to match("mkdir -p /etc/chef/client\.d") - end - - context "a flat directory structure" do - it "escapes single-quotes" do - expect(rendered_template).to match("cat > /etc/chef/client.d/02-strings.rb <<'EOP'") - expect(rendered_template).to match("something '\\\\''/foo/bar'\\\\''") - end - - it "creates a file 00-foo.rb" do - expect(rendered_template).to match("cat > /etc/chef/client.d/00-foo.rb <<'EOP'") - expect(rendered_template).to match("d6f9b976-289c-4149-baf7-81e6ffecf228") - end - it "creates a file bar" do - expect(rendered_template).to match("cat > /etc/chef/client.d/bar <<'EOP'") - expect(rendered_template).to match("1 / 0") - end - end - - context "a nested directory structure" do - let(:client_d_dir) do - Chef::Util::PathHelper.cleanpath( - File.join(__dir__, "../../data/client.d_01") - ) - end - it "creates a file foo/bar.rb" do - expect(rendered_template).to match("cat > /etc/chef/client.d/foo/bar.rb <<'EOP'") - expect(rendered_template).to match("1 / 0") - end - end - end - end - - describe "#connection_protocol" do - let(:host_descriptor) { "example.com" } - let(:config) { {} } - let(:knife_connection_protocol) { nil } - before do - allow(knife).to receive(:config).and_return config - allow(knife).to receive(:host_descriptor).and_return host_descriptor - if knife_connection_protocol - Chef::Config[:knife][:connection_protocol] = knife_connection_protocol - knife.merge_configs - end - end - - context "when protocol is part of the host argument" do - let(:host_descriptor) { "winrm://myhost" } - - it "returns the value provided by the host argument" do - expect(knife.connection_protocol).to eq "winrm" - end - end - - context "when protocol is provided via the CLI flag" do - let(:config) { { connection_protocol: "winrm" } } - it "returns that value" do - expect(knife.connection_protocol).to eq "winrm" - end - - end - context "when protocol is provided via the host argument and the CLI flag" do - let(:host_descriptor) { "ssh://example.com" } - let(:config) { { connection_protocol: "winrm" } } - - it "returns the value provided by the host argument" do - expect(knife.connection_protocol).to eq "ssh" - end - end - - context "when no explicit protocol is provided" do - let(:config) { {} } - let(:host_descriptor) { "example.com" } - let(:knife_connection_protocol) { "winrm" } - it "falls back to knife config" do - expect(knife.connection_protocol).to eq "winrm" - end - context "and there is no knife bootstrap_protocol" do - let(:knife_connection_protocol) { nil } - it "falls back to 'ssh'" do - expect(knife.connection_protocol).to eq "ssh" - end - end - end - - end - - describe "#validate_protocol!" do - let(:host_descriptor) { "example.com" } - let(:config) { {} } - let(:connection_protocol) { "ssh" } - before do - allow(knife).to receive(:config).and_return config - allow(knife).to receive(:connection_protocol).and_return connection_protocol - allow(knife).to receive(:host_descriptor).and_return host_descriptor - end - - context "when protocol is provided both in the URL and via --protocol" do - - context "and they do not match" do - let(:connection_protocol) { "ssh" } - let(:config) { { connection_protocol: "winrm" } } - it "outputs an error and exits" do - expect(knife.ui).to receive(:error) - expect { knife.validate_protocol! }.to raise_error SystemExit - end - end - - context "and they do match" do - let(:connection_protocol) { "winrm" } - let(:config) { { connection_protocol: "winrm" } } - it "returns true" do - expect(knife.validate_protocol!).to eq true - end - end - end - - context "and the protocol is supported" do - - Chef::Knife::Bootstrap::SUPPORTED_CONNECTION_PROTOCOLS.each do |proto| - let(:connection_protocol) { proto } - it "returns true for #{proto}" do - expect(knife.validate_protocol!).to eq true - end - end - end - - context "and the protocol is not supported" do - let(:connection_protocol) { "invalid" } - it "outputs an error and exits" do - expect(knife.ui).to receive(:error).with(/Unsupported protocol '#{connection_protocol}'/) - expect { knife.validate_protocol! }.to raise_error SystemExit - end - end - end - - describe "#validate_policy_options!" do - - context "when only policy_name is given" do - - let(:bootstrap_cli_options) { %w{ --policy-name my-app-server } } - - it "returns an error stating that policy_name and policy_group must be given together" do - expect { knife.validate_policy_options! }.to raise_error(SystemExit) - expect(stderr.string).to include("ERROR: --policy-name and --policy-group must be specified together") - end - - end - - context "when only policy_group is given" do - - let(:bootstrap_cli_options) { %w{ --policy-group staging } } - - it "returns an error stating that policy_name and policy_group must be given together" do - expect { knife.validate_policy_options! }.to raise_error(SystemExit) - expect(stderr.string).to include("ERROR: --policy-name and --policy-group must be specified together") - end - - end - - context "when both policy_name and policy_group are given, but run list is also given" do - - let(:bootstrap_cli_options) { %w{ --policy-name my-app --policy-group staging --run-list cookbook } } - - it "returns an error stating that policyfile and run_list are exclusive" do - expect { knife.validate_policy_options! }.to raise_error(SystemExit) - expect(stderr.string).to include("ERROR: Policyfile options and --run-list are exclusive") - end - - end - - context "when policy_name and policy_group are given with no conflicting options" do - - let(:bootstrap_cli_options) { %w{ --policy-name my-app --policy-group staging } } - - it "passes options validation" do - expect { knife.validate_policy_options! }.to_not raise_error - end - - it "passes them into the bootstrap context" do - expect(knife.bootstrap_context.first_boot).to have_key(:policy_name) - expect(knife.bootstrap_context.first_boot).to have_key(:policy_group) - end - - it "ensures that run_list is not set in the bootstrap context" do - expect(knife.bootstrap_context.first_boot).to_not have_key(:run_list) - end - - end - - # https://github.com/chef/chef/issues/4131 - # Arguably a bug in the plugin: it shouldn't be setting this to nil, but it - # worked before, so make it work now. - context "when a plugin sets the run list option to nil" do - before do - knife.config[:run_list] = nil - end - - it "passes options validation" do - expect { knife.validate_policy_options! }.to_not raise_error - end - end - end - - # TODO - this is the only cli option we validate the _option_ itself - - # so we'll know if someone accidentally deletes or renames use_sudo_password - # Is this worht keeping? If so, then it seems we should expand it - # to cover all options. - context "validating use_sudo_password option" do - it "use_sudo_password contains description and long params for help" do - expect(knife.options).to(have_key(:use_sudo_password)) \ - && expect(knife.options[:use_sudo_password][:description].to_s).not_to(eq(""))\ - && expect(knife.options[:use_sudo_password][:long].to_s).not_to(eq("")) - end - end - - context "#connection_opts" do - let(:connection_protocol) { "ssh" } - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - context "behavioral test: " do - let(:expected_connection_opts) do - { base_opts: true, - ssh_identity_opts: true, - ssh_opts: true, - gateway_opts: true, - host_verify_opts: true, - sudo_opts: true, - winrm_opts: true } - end - - it "queries and merges only expected configurations" do - expect(knife).to receive(:base_opts).and_return({ base_opts: true }) - expect(knife).to receive(:host_verify_opts).and_return({ host_verify_opts: true }) - expect(knife).to receive(:gateway_opts).and_return({ gateway_opts: true }) - expect(knife).to receive(:sudo_opts).and_return({ sudo_opts: true }) - expect(knife).to receive(:winrm_opts).and_return({ winrm_opts: true }) - expect(knife).to receive(:ssh_opts).and_return({ ssh_opts: true }) - expect(knife).to receive(:ssh_identity_opts).and_return({ ssh_identity_opts: true }) - expect(knife.connection_opts).to match expected_connection_opts - end - end - - context "functional test: " do - context "when protocol is winrm" do - let(:connection_protocol) { "winrm" } - # context "and neither CLI nor Chef::Config config entries have been provided" - # end - context "and all supported values are provided as Chef::Config entries" do - before do - # Set everything to easily identifiable and obviously fake values - # to verify that Chef::Config is being sourced instead of knife.config - knife.config = {} - Chef::Config[:knife][:max_wait] = 9999.0 - Chef::Config[:knife][:winrm_user] = "winbob" - Chef::Config[:knife][:winrm_port] = 9999 - Chef::Config[:knife][:ca_trust_file] = "trust.me" - Chef::Config[:knife][:kerberos_realm] = "realm" - Chef::Config[:knife][:kerberos_service] = "service" - Chef::Config[:knife][:winrm_auth_method] = "kerberos" # default is negotiate - Chef::Config[:knife][:winrm_basic_auth_only] = true - Chef::Config[:knife][:winrm_no_verify_cert] = true - Chef::Config[:knife][:session_timeout] = 9999 - Chef::Config[:knife][:winrm_ssl] = true - Chef::Config[:knife][:winrm_ssl_peer_fingerprint] = "ABCDEF" - end - - context "and no CLI options have been given" do - let(:expected_result) do - { - logger: Chef::Log, # not configurable - ca_trust_path: "trust.me", - max_wait_until_ready: 9999, # converted to int - operation_timeout: 9999, - ssl_peer_fingerprint: "ABCDEF", - winrm_transport: "kerberos", - winrm_basic_auth_only: true, - user: "winbob", - port: 9999, - self_signed: true, - ssl: true, - kerberos_realm: "realm", - kerberos_service: "service", - } - end - - it "generates a config hash using the Chef::Config values" do - knife.merge_configs - expect(knife.connection_opts).to match expected_result - end - - end - - context "and some CLI options have been given" do - let(:expected_result) do - { - logger: Chef::Log, # not configurable - ca_trust_path: "no trust", - max_wait_until_ready: 9999, - operation_timeout: 9999, - ssl_peer_fingerprint: "ABCDEF", - winrm_transport: "kerberos", - winrm_basic_auth_only: true, - user: "microsoftbob", - port: 12, - self_signed: true, - ssl: true, - kerberos_realm: "realm", - kerberos_service: "service", - password: "lobster", - } - end - - before do - knife.config[:ca_trust_file] = "no trust" - knife.config[:connection_user] = "microsoftbob" - knife.config[:connection_port] = 12 - knife.config[:winrm_port] = "13" # indirectly verify we're not looking for the wrong CLI flag - knife.config[:connection_password] = "lobster" - end - - it "generates a config hash using the CLI options when available and falling back to Chef::Config values" do - knife.merge_configs - expect(knife.connection_opts).to match expected_result - end - end - - context "and all CLI options have been given" do - before do - # We'll force kerberos vi knife.config because it - # causes additional options to populate - make sure - # Chef::Config is different so we can be sure that we didn't - # pull in the Chef::Config value - Chef::Config[:knife][:winrm_auth_method] = "negotiate" - knife.config[:connection_password] = "blue" - knife.config[:max_wait] = 1000.0 - knife.config[:connection_user] = "clippy" - knife.config[:connection_port] = 1000 - knife.config[:winrm_port] = 1001 # We should not see this value get used - - knife.config[:ca_trust_file] = "trust.the.internet" - knife.config[:kerberos_realm] = "otherrealm" - knife.config[:kerberos_service] = "otherservice" - knife.config[:winrm_auth_method] = "kerberos" # default is negotiate - knife.config[:winrm_basic_auth_only] = false - knife.config[:winrm_no_verify_cert] = false - knife.config[:session_timeout] = 1000 - knife.config[:winrm_ssl] = false - knife.config[:winrm_ssl_peer_fingerprint] = "FEDCBA" - end - let(:expected_result) do - { - logger: Chef::Log, # not configurable - ca_trust_path: "trust.the.internet", - max_wait_until_ready: 1000, # converted to int - operation_timeout: 1000, - ssl_peer_fingerprint: "FEDCBA", - winrm_transport: "kerberos", - winrm_basic_auth_only: false, - user: "clippy", - port: 1000, - self_signed: false, - ssl: false, - kerberos_realm: "otherrealm", - kerberos_service: "otherservice", - password: "blue", - } - end - it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do - knife.merge_configs - expect(knife.connection_opts).to match expected_result - end - end - end # with underlying Chef::Config values - - context "and no values are provided from Chef::Config or CLI" do - before do - # We will use knife's actual config since these tests - # have assumptions based on CLI default values - end - let(:expected_result) do - { - logger: Chef::Log, - operation_timeout: 60, - self_signed: false, - ssl: false, - ssl_peer_fingerprint: nil, - winrm_basic_auth_only: false, - winrm_transport: "negotiate", - } - end - it "populates appropriate defaults" do - knife.merge_configs - expect(knife.connection_opts).to match expected_result - end - end - end # winrm - - context "when protocol is ssh" do - let(:connection_protocol) { "ssh" } - # context "and neither CLI nor Chef::Config config entries have been provided" - # end - context "and all supported values are provided as Chef::Config entries" do - before do - # Set everything to easily identifiable and obviously fake values - # to verify that Chef::Config is being sourced instead of knife.config - knife.config = {} - Chef::Config[:knife][:max_wait] = 9999.0 - Chef::Config[:knife][:session_timeout] = 9999 - Chef::Config[:knife][:ssh_user] = "sshbob" - Chef::Config[:knife][:ssh_port] = 9999 - Chef::Config[:knife][:host_key_verify] = false - Chef::Config[:knife][:ssh_gateway_identity] = "/gateway.pem" - Chef::Config[:knife][:ssh_gateway] = "admin@mygateway.local:1234" - Chef::Config[:knife][:ssh_identity_file] = "/identity.pem" - Chef::Config[:knife][:use_sudo_password] = false # We have no password. - end - - context "and no CLI options have been given" do - let(:expected_result) do - { - logger: Chef::Log, # not configurable - max_wait_until_ready: 9999, # converted to int - connection_timeout: 9999, - user: "sshbob", - bastion_host: "mygateway.local", - bastion_port: 1234, - bastion_user: "admin", - forward_agent: false, - keys_only: true, - key_files: ["/identity.pem", "/gateway.pem"], - sudo: false, - verify_host_key: "always", - port: 9999, - non_interactive: true, - } - end - - it "generates a correct config hash using the Chef::Config values" do - knife.merge_configs - expect(knife.connection_opts).to match expected_result - end - end - - context "and unsupported Chef::Config options are given in Chef::Config, not in CLI" do - before do - Chef::Config[:knife][:password] = "blah" - Chef::Config[:knife][:ssh_password] = "blah" - Chef::Config[:knife][:preserve_home] = true - Chef::Config[:knife][:use_sudo] = true - Chef::Config[:knife][:ssh_forward_agent] = "blah" - end - it "does not include the corresponding option in the connection options" do - knife.merge_configs - expect(knife.connection_opts.key?(:password)).to eq false - expect(knife.connection_opts.key?(:ssh_forward_agent)).to eq false - expect(knife.connection_opts.key?(:use_sudo)).to eq false - expect(knife.connection_opts.key?(:preserve_home)).to eq false - end - end - - context "and some CLI options have been given" do - before do - knife.config = {} - knife.config[:connection_user] = "sshalice" - knife.config[:connection_port] = 12 - knife.config[:ssh_port] = "13" # canary to indirectly verify we're not looking for the wrong CLI flag - knife.config[:connection_password] = "feta cheese" - knife.config[:max_wait] = 150.0 - knife.config[:session_timeout] = 120 - knife.config[:use_sudo] = true - knife.config[:use_sudo_pasword] = true - knife.config[:ssh_forward_agent] = true - end - - let(:expected_result) do - { - logger: Chef::Log, # not configurable - max_wait_until_ready: 150, # cli (converted to int) - connection_timeout: 120, # cli - user: "sshalice", # cli - password: "feta cheese", # cli - bastion_host: "mygateway.local", # Config - bastion_port: 1234, # Config - bastion_user: "admin", # Config - forward_agent: true, # cli - keys_only: false, # implied false from config password present - key_files: ["/identity.pem", "/gateway.pem"], # Config - sudo: true, # ccli - verify_host_key: "always", # Config - port: 12, # cli - non_interactive: true, - } - end - - it "generates a config hash using the CLI options when available and falling back to Chef::Config values" do - knife.merge_configs - expect(knife.connection_opts).to match expected_result - end - end - - context "and all CLI options have been given" do - before do - knife.config = {} - knife.config[:max_wait] = 150.0 - knife.config[:session_timeout] = 120 - knife.config[:connection_user] = "sshroot" - knife.config[:connection_port] = 1000 - knife.config[:connection_password] = "blah" - knife.config[:forward_agent] = true - knife.config[:use_sudo] = true - knife.config[:use_sudo_password] = true - knife.config[:preserve_home] = true - knife.config[:use_sudo_pasword] = true - knife.config[:ssh_forward_agent] = true - knife.config[:ssh_verify_host_key] = true - knife.config[:ssh_gateway_identity] = "/gateway-identity.pem" - knife.config[:ssh_gateway] = "me@example.com:10" - knife.config[:ssh_identity_file] = "/my-identity.pem" - - # We'll set these as canaries - if one of these values shows up - # in a failed test, then the behavior of not pulling from these keys - # out of knife.config is broken: - knife.config[:ssh_user] = "do not use" - knife.config[:ssh_port] = 1001 - end - let(:expected_result) do - { - logger: Chef::Log, # not configurable - max_wait_until_ready: 150, # converted to int - connection_timeout: 120, - user: "sshroot", - password: "blah", - port: 1000, - bastion_host: "example.com", - bastion_port: 10, - bastion_user: "me", - forward_agent: true, - keys_only: false, - key_files: ["/my-identity.pem", "/gateway-identity.pem"], - sudo: true, - sudo_options: "-H", - sudo_password: "blah", - verify_host_key: true, - non_interactive: true, - } - end - it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do - knife.merge_configs - expect(knife.connection_opts).to match expected_result - end - end - end - context "and no values are provided from Chef::Config or CLI" do - before do - # We will use knife's actual config since these tests - # have assumptions based on CLI default values - config = {} - end - - let(:expected_result) do - { - forward_agent: false, - key_files: [], - logger: Chef::Log, - keys_only: false, - sudo: false, - verify_host_key: "always", - non_interactive: true, - connection_timeout: 60, - } - end - it "populates appropriate defaults" do - knife.merge_configs - expect(knife.connection_opts).to match expected_result - end - end - - end # ssh - end # functional tests - - end # connection_opts - - context "#base_opts" do - let(:connection_protocol) { nil } - - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "for all protocols" do - context "when password is provided" do - before do - knife.config[:connection_port] = 250 - knife.config[:connection_user] = "test" - knife.config[:connection_password] = "opscode" - end - - let(:expected_opts) do - { - port: 250, - user: "test", - logger: Chef::Log, - password: "opscode", - } - end - it "generates the correct options" do - expect(knife.base_opts).to eq expected_opts - end - - end - - context "when password is not provided" do - before do - knife.config[:connection_port] = 250 - knife.config[:connection_user] = "test" - end - - let(:expected_opts) do - { - port: 250, - user: "test", - logger: Chef::Log, - } - end - it "generates the correct options" do - expect(knife.base_opts).to eq expected_opts - end - end - end - end - - context "#host_verify_opts" do - let(:connection_protocol) { nil } - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "for winrm" do - let(:connection_protocol) { "winrm" } - it "returns the expected configuration" do - knife.config[:winrm_no_verify_cert] = true - expect(knife.host_verify_opts).to eq( { self_signed: true } ) - end - it "provides a correct default when no option given" do - expect(knife.host_verify_opts).to eq( { self_signed: false } ) - end - end - - context "for ssh" do - let(:connection_protocol) { "ssh" } - it "returns the expected configuration" do - knife.config[:ssh_verify_host_key] = false - expect(knife.host_verify_opts).to eq( { verify_host_key: false } ) - end - it "provides a correct default when no option given" do - expect(knife.host_verify_opts).to eq( { verify_host_key: "always" } ) - end - end - end - - # TODO - test keys_only, password, config source behavior - context "#ssh_identity_opts" do - let(:connection_protocol) { nil } - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "for winrm" do - let(:connection_protocol) { "winrm" } - it "returns an empty hash" do - expect(knife.ssh_identity_opts).to eq({}) - end - end - - context "for ssh" do - let(:connection_protocol) { "ssh" } - context "when an identity file is specified" do - before do - knife.config[:ssh_identity_file] = "/identity.pem" - end - it "generates the expected configuration" do - expect(knife.ssh_identity_opts).to eq({ - key_files: [ "/identity.pem" ], - keys_only: true, - }) - end - context "and a password is also specified" do - before do - knife.config[:connection_password] = "blah" - end - it "generates the expected configuration (key, keys_only false)" do - expect(knife.ssh_identity_opts).to eq({ - key_files: [ "/identity.pem" ], - keys_only: false, - }) - end - end - - context "and a gateway is not specified" do - context "but a gateway identity file is specified" do - it "does not include the gateway identity file in keys" do - expect(knife.ssh_identity_opts).to eq({ - key_files: ["/identity.pem"], - keys_only: true, - }) - end - - end - - end - - context "and a gatway is specified" do - before do - knife.config[:ssh_gateway] = "example.com" - end - context "and a gateway identity file is not specified" do - it "config includes only identity file and not gateway identity" do - expect(knife.ssh_identity_opts).to eq({ - key_files: [ "/identity.pem" ], - keys_only: true, - }) - end - end - - context "and a gateway identity file is also specified" do - before do - knife.config[:ssh_gateway_identity] = "/gateway.pem" - end - - it "generates the expected configuration (both keys, keys_only true)" do - expect(knife.ssh_identity_opts).to eq({ - key_files: [ "/identity.pem", "/gateway.pem" ], - keys_only: true, - }) - end - end - end - end - - context "when no identity file is specified" do - it "generates the expected configuration (no keys, keys_only false)" do - expect(knife.ssh_identity_opts).to eq( { - key_files: [], - keys_only: false, - }) - end - context "and a gateway with gateway identity file is specified" do - before do - knife.config[:ssh_gateway] = "host" - knife.config[:ssh_gateway_identity] = "/gateway.pem" - end - - it "generates the expected configuration (gateway key, keys_only false)" do - expect(knife.ssh_identity_opts).to eq({ - key_files: [ "/gateway.pem" ], - keys_only: false, - }) - end - end - end - end - end - - context "#gateway_opts" do - let(:connection_protocol) { nil } - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "for winrm" do - let(:connection_protocol) { "winrm" } - it "returns an empty hash" do - expect(knife.gateway_opts).to eq({}) - end - end - - context "for ssh" do - let(:connection_protocol) { "ssh" } - context "and ssh_gateway with hostname, user and port provided" do - before do - knife.config[:ssh_gateway] = "testuser@gateway:9021" - end - it "returns a proper bastion host config subset" do - expect(knife.gateway_opts).to eq({ - bastion_user: "testuser", - bastion_host: "gateway", - bastion_port: 9021, - }) - end - end - context "and ssh_gateway with only hostname is given" do - before do - knife.config[:ssh_gateway] = "gateway" - end - it "returns a proper bastion host config subset" do - expect(knife.gateway_opts).to eq({ - bastion_user: nil, - bastion_host: "gateway", - bastion_port: nil, - }) - end - end - context "and ssh_gateway with hostname and user is is given" do - before do - knife.config[:ssh_gateway] = "testuser@gateway" - end - it "returns a proper bastion host config subset" do - expect(knife.gateway_opts).to eq({ - bastion_user: "testuser", - bastion_host: "gateway", - bastion_port: nil, - }) - end - end - - context "and ssh_gateway with hostname and port is is given" do - before do - knife.config[:ssh_gateway] = "gateway:11234" - end - it "returns a proper bastion host config subset" do - expect(knife.gateway_opts).to eq({ - bastion_user: nil, - bastion_host: "gateway", - bastion_port: 11234, - }) - end - end - - context "and ssh_gateway is not provided" do - it "returns an empty hash" do - expect(knife.gateway_opts).to eq({}) - end - end - end - end - - context "#sudo_opts" do - let(:connection_protocol) { nil } - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "for winrm" do - let(:connection_protocol) { "winrm" } - it "returns an empty hash" do - expect(knife.sudo_opts).to eq({}) - end - end - - context "for ssh" do - let(:connection_protocol) { "ssh" } - context "when use_sudo is set" do - before do - knife.config[:use_sudo] = true - end - - it "returns a config that enables sudo" do - expect(knife.sudo_opts).to eq( { sudo: true } ) - end - - context "when use_sudo_password is also set" do - before do - knife.config[:use_sudo_password] = true - knife.config[:connection_password] = "opscode" - end - it "includes :connection_password value in a sudo-enabled configuration" do - expect(knife.sudo_opts).to eq({ - sudo: true, - sudo_password: "opscode", - }) - end - end - - context "when preserve_home is set" do - before do - knife.config[:preserve_home] = true - end - it "enables sudo with sudo_option to preserve home" do - expect(knife.sudo_opts).to eq({ - sudo_options: "-H", - sudo: true, - }) - end - end - end - - context "when use_sudo is not set" do - before do - knife.config[:use_sudo_password] = true - knife.config[:preserve_home] = true - end - it "returns configuration for sudo off, ignoring other related options" do - expect(knife.sudo_opts).to eq( { sudo: false } ) - end - end - end - end - - context "#ssh_opts" do - let(:connection_protocol) { nil } - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "for ssh" do - let(:connection_protocol) { "ssh" } - let(:default_opts) do - { - non_interactive: true, - forward_agent: false, - connection_timeout: 60, - } - end - - context "by default" do - it "returns a configuration hash with appropriate defaults" do - expect(knife.ssh_opts).to eq default_opts - end - end - - context "when ssh_forward_agent has a value" do - before do - knife.config[:ssh_forward_agent] = true - end - it "returns a default configuration hash with forward_agent set to true" do - expect(knife.ssh_opts).to eq(default_opts.merge(forward_agent: true)) - end - end - context "when session_timeout has a value" do - before do - knife.config[:session_timeout] = 120 - end - it "returns a default configuration hash with updated timeout value." do - expect(knife.ssh_opts).to eq(default_opts.merge(connection_timeout: 120)) - end - end - - end - - context "for winrm" do - let(:connection_protocol) { "winrm" } - it "returns an empty has because ssh is not winrm" do - expect(knife.ssh_opts).to eq({}) - end - end - - end - - context "#winrm_opts" do - let(:connection_protocol) { nil } - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "for winrm" do - let(:connection_protocol) { "winrm" } - let(:expected) do - { - winrm_transport: "negotiate", - winrm_basic_auth_only: false, - ssl: false, - ssl_peer_fingerprint: nil, - operation_timeout: 60, - } - end - - it "generates a correct configuration hash with expected defaults" do - expect(knife.winrm_opts).to eq expected - end - - context "with ssl_peer_fingerprint" do - let(:ssl_peer_fingerprint_expected) do - expected.merge({ ssl_peer_fingerprint: "ABCD" }) - end - - before do - knife.config[:winrm_ssl_peer_fingerprint] = "ABCD" - end - - it "generates a correct options hash with ssl_peer_fingerprint from the config provided" do - expect(knife.winrm_opts).to eq ssl_peer_fingerprint_expected - end - end - - context "with winrm_ssl" do - let(:ssl_expected) do - expected.merge({ ssl: true }) - end - before do - knife.config[:winrm_ssl] = true - end - - it "generates a correct options hash with ssl from the config provided" do - expect(knife.winrm_opts).to eq ssl_expected - end - end - - context "with winrm_auth_method" do - let(:winrm_auth_method_expected) do - expected.merge({ winrm_transport: "freeaccess" }) - end - - before do - knife.config[:winrm_auth_method] = "freeaccess" - end - - it "generates a correct options hash with winrm_transport from the config provided" do - expect(knife.winrm_opts).to eq winrm_auth_method_expected - end - end - - context "with ca_trust_file" do - let(:ca_trust_expected) do - expected.merge({ ca_trust_path: "/trust.me" }) - end - before do - knife.config[:ca_trust_file] = "/trust.me" - end - - it "generates a correct options hash with ca_trust_file from the config provided" do - expect(knife.winrm_opts).to eq ca_trust_expected - end - end - - context "with kerberos auth" do - let(:kerberos_expected) do - expected.merge({ - kerberos_service: "testsvc", - kerberos_realm: "TESTREALM", - winrm_transport: "kerberos", - }) - end - - before do - knife.config[:winrm_auth_method] = "kerberos" - knife.config[:kerberos_service] = "testsvc" - knife.config[:kerberos_realm] = "TESTREALM" - end - - it "generates a correct options hash containing kerberos auth configuration from the config provided" do - expect(knife.winrm_opts).to eq kerberos_expected - end - end - - context "with winrm_basic_auth_only" do - before do - knife.config[:winrm_basic_auth_only] = true - end - let(:basic_auth_expected) do - expected.merge( { winrm_basic_auth_only: true } ) - end - it "generates a correct options hash containing winrm_basic_auth_only from the config provided" do - expect(knife.winrm_opts).to eq basic_auth_expected - end - end - end - - context "for ssh" do - let(:connection_protocol) { "ssh" } - it "returns an empty hash because ssh is not winrm" do - expect(knife.winrm_opts).to eq({}) - end - end - end - describe "#run" do - it "performs the steps we expect to run a bootstrap" do - expect(knife).to receive(:check_license) - expect(knife).to receive(:validate_name_args!).ordered - expect(knife).to receive(:validate_protocol!).ordered - expect(knife).to receive(:validate_first_boot_attributes!).ordered - expect(knife).to receive(:validate_winrm_transport_opts!).ordered - expect(knife).to receive(:validate_policy_options!).ordered - expect(knife).to receive(:winrm_warn_no_ssl_verification).ordered - expect(knife).to receive(:warn_on_short_session_timeout).ordered - expect(knife).to receive(:connect!).ordered - expect(knife).to receive(:register_client).ordered - expect(knife).to receive(:render_template).and_return "content" - expect(knife).to receive(:upload_bootstrap).with("content").and_return "/remote/path.sh" - expect(knife).to receive(:perform_bootstrap).with("/remote/path.sh") - expect(connection).to receive(:del_file!) # Make sure cleanup happens - - knife.run - - # Post-run verify expected state changes (not many directly in #run) - expect($stdout.sync).to eq true - end - end - - describe "#register_client" do - let(:vault_handler_mock) { double("ChefVaultHandler") } - let(:client_builder_mock) { double("ClientBuilder") } - let(:node_name) { nil } - before do - allow(knife).to receive(:chef_vault_handler).and_return vault_handler_mock - allow(knife).to receive(:client_builder).and_return client_builder_mock - knife.config[:chef_node_name] = node_name - end - - shared_examples_for "creating the client locally" do - context "when a valid node name is present" do - let(:node_name) { "test" } - before do - allow(client_builder_mock).to receive(:client).and_return "client" - allow(client_builder_mock).to receive(:client_path).and_return "/key.pem" - end - - it "runs client_builder and vault_handler" do - expect(client_builder_mock).to receive(:run) - expect(vault_handler_mock).to receive(:run).with("client") - knife.register_client - end - - it "sets the path to the client key in the bootstrap context" do - allow(client_builder_mock).to receive(:run) - allow(vault_handler_mock).to receive(:run).with("client") - knife.register_client - expect(knife.bootstrap_context.client_pem).to eq "/key.pem" - end - end - - context "when no valid node name is present" do - let(:node_name) { nil } - it "shows an error and exits" do - expect(knife.ui).to receive(:error) - expect { knife.register_client }.to raise_error(SystemExit) - end - end - end - context "when chef_vault_handler says we're using vault" do - let(:vault_handler_mock) { double("ChefVaultHandler") } - before do - allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return true - end - it_behaves_like "creating the client locally" - end - - context "when an non-existant validation key is specified in chef config" do - before do - Chef::Config[:validation_key] = "/blah" - allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return false - allow(File).to receive(:exist?).with(%r{/blah}).and_return false - end - it_behaves_like "creating the client locally" - end - - context "when a valid validation key is given and we're doing old-style client creation" do - before do - Chef::Config[:validation_key] = "/blah" - allow(File).to receive(:exist?).with(%r{/blah}).and_return true - allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return false - end - - it "shows a warning message" do - expect(knife.ui).to receive(:warn).twice - knife.register_client - end - end - end - - describe "#perform_bootstrap" do - let(:exit_status) { 0 } - let(:stdout) { "" } - let(:result_mock) { double("result", exit_status: exit_status, stderr: "A message", stdout: stdout) } - - before do - allow(connection).to receive(:hostname).and_return "testhost" - end - it "runs the remote script and logs the output" do - expect(knife.ui).to receive(:info).with(/Bootstrapping.*/) - expect(knife).to receive(:bootstrap_command) - .with("/path.sh") - .and_return("sh /path.sh") - expect(connection) - .to receive(:run_command) - .with("sh /path.sh") - .and_yield("output here", nil) - .and_return result_mock - - expect(knife.ui).to receive(:msg).with(/testhost/) - knife.perform_bootstrap("/path.sh") - end - - context "when the remote command fails" do - let(:exit_status) { 1 } - it "shows an error and exits" do - expect(knife.ui).to receive(:info).with(/Bootstrapping.*/) - expect(knife).to receive(:bootstrap_command) - .with("/path.sh") - .and_return("sh /path.sh") - expect(connection).to receive(:run_command).with("sh /path.sh").and_return result_mock - expect { knife.perform_bootstrap("/path.sh") }.to raise_error(SystemExit) - end - end - - context "when the remote command failed due to su auth error" do - let(:exit_status) { 1 } - let(:stdout) { "su: Authentication failure" } - let(:connection_obj) { double("connection", transport_options: {}) } - it "shows an error and exits" do - allow(connection).to receive(:connection).and_return(connection_obj) - expect(knife.ui).to receive(:info).with(/Bootstrapping.*/) - expect(knife).to receive(:bootstrap_command) - .with("/path.sh") - .and_return("su - USER -c 'sh /path.sh'") - expect(connection) - .to receive(:run_command) - .with("su - USER -c 'sh /path.sh'") - .and_yield("output here", nil) - .and_raise(Train::UserError) - expect { knife.perform_bootstrap("/path.sh") }.to raise_error(Train::UserError) - end - end - end - - describe "#connect!" do - before do - # These are not required at run-time because train will handle its own - # protocol loading. In this case, we're simulating train failures and have to load - # them ourselves. - require "net/ssh" - require "train/transports/ssh" - end - - context "in the normal case" do - it "connects using the connection_opts and notifies the operator of progress" do - expect(knife.ui).to receive(:info).with(/Connecting to.*/) - expect(knife).to receive(:connection_opts).and_return( { opts: "here" }) - expect(knife).to receive(:do_connect).with( { opts: "here" } ) - knife.connect! - end - end - - context "when a general non-auth-failure occurs" do - let(:expected_error) { RuntimeError.new } - before do - allow(knife).to receive(:do_connect).and_raise(expected_error) - end - it "re-raises the exception" do - expect { knife.connect! }.to raise_error(expected_error) - end - end - - context "when ssh fingerprint is invalid" do - let(:expected_error) { Train::Error.new("fingerprint AA:BB is unknown for \"blah,127.0.0.1\"") } - before do - allow(knife).to receive(:do_connect).and_raise(expected_error) - end - it "warns, prompts to accept, then connects with verify_host_key of accept_new" do - expect(knife).to receive(:do_connect).and_raise(expected_error) - expect(knife.ui).to receive(:confirm) - .with(/.*host 'blah \(127.0.0.1\)'.*AA:BB.*Are you sure you want to continue.*/m) - .and_return(true) - expect(knife).to receive(:do_connect) do |opts| - expect(opts[:verify_host_key]).to eq :accept_new - end - knife.connect! - end - end - - context "when an auth failure occurs" do - let(:expected_error) do - e = Train::Error.new - actual = Net::SSH::AuthenticationFailed.new - # Simulate train's nested error - they wrap - # ssh/network errors in a TrainError. - allow(e).to receive(:cause).and_return(actual) - e - end - - let(:expected_error_password_prompt) do - e = Train::ClientError.new - reason = :no_ssh_password_or_key_available - allow(e).to receive(:reason).and_return(reason) - e - end - - let(:expected_error_password_prompt_winrm) do - e = RuntimeError.new - message = "password is a required option" - allow(e).to receive(:message).and_return(message) - e - end - - context "and password auth was used" do - before do - allow(connection).to receive(:password_auth?).and_return true - end - - it "re-raises the error so as not to resubmit the same failing password" do - expect(knife).to receive(:do_connect).and_raise(expected_error) - expect { knife.connect! }.to raise_error(expected_error) - end - end - - context "and password auth was not used" do - before do - allow(connection).to receive(:password_auth?).and_return false - allow(connection).to receive(:user).and_return "testuser" - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "when using ssh" do - let(:connection_protocol) { "ssh" } - - it "warns, prompts for password, then reconnects with a password-enabled configuration using the new password" do - expect(knife).to receive(:do_connect).and_raise(expected_error_password_prompt) - expect(knife.ui).to receive(:warn).with(/Failed to auth.*/) - expect(knife.ui).to receive(:ask).and_return("newpassword") - # Ensure that we set echo off to prevent showing password on the screen - expect(knife).to receive(:do_connect) do |opts| - expect(opts[:password]).to eq "newpassword" - end - knife.connect! - end - end - - context "when using winrm" do - let(:connection_protocol) { "winrm" } - - it "warns, prompts for password, then reconnects with a password-enabled configuration using the new password for" do - expect(knife).to receive(:do_connect).and_raise(expected_error_password_prompt_winrm) - expect(knife.ui).to receive(:warn).with(/Failed to auth.*/) - expect(knife.ui).to receive(:ask).and_return("newpassword") - # Ensure that we set echo off to prevent showing password on the screen - expect(knife).to receive(:do_connect) do |opts| - expect(opts[:password]).to eq "newpassword" - end - knife.connect! - end - end - end - end - end - - it "verifies that a server to bootstrap was given as a command line arg" do - knife.name_args = nil - expect(knife).to receive(:check_license) - expect { knife.run }.to raise_error(SystemExit) - expect(stderr.string).to match(/ERROR:.+FQDN or ip/) - end - - describe "#bootstrap_context" do - context "under Windows" do - let(:windows_test) { true } - it "creates a WindowsBootstrapContext" do - require "chef/knife/core/windows_bootstrap_context" - expect(knife.bootstrap_context.class).to eq Chef::Knife::Core::WindowsBootstrapContext - end - end - - context "under linux" do - let(:linux_test) { true } - it "creates a BootstrapContext" do - require "chef/knife/core/bootstrap_context" - expect(knife.bootstrap_context.class).to eq Chef::Knife::Core::BootstrapContext - end - end - end - - describe "#config_value" do - before do - knife.config[:test_key_a] = "a from cli" - knife.config[:test_key_b] = "b from cli" - Chef::Config[:knife][:test_key_a] = "a from Chef::Config" - Chef::Config[:knife][:test_key_c] = "c from Chef::Config" - Chef::Config[:knife][:alt_test_key_c] = "alt c from Chef::Config" - knife.merge_configs - Chef::Config[:treat_deprecation_warnings_as_errors] = false - end - - it "returns the Chef::Config value from the cli when the CLI key is set" do - expect(knife.config_value(:test_key_a, :alt_test_key_c)).to eq "a from cli" - end - - it "returns the Chef::Config value from the alternative key when the CLI key is not set" do - expect(knife.config_value(:test_key_d, :alt_test_key_c)).to eq "alt c from Chef::Config" - end - - it "returns the default value when the key is not provided by CLI or Chef::Config" do - expect(knife.config_value(:missing_key, :missing_key, "found")).to eq "found" - end - end - - describe "#upload_bootstrap" do - before do - allow(connection).to receive(:temp_dir).and_return(temp_dir) - allow(connection).to receive(:normalize_path) { |a| a } - end - - let(:content) { "bootstrap script content" } - context "under Windows" do - let(:windows_test) { true } - let(:temp_dir) { "C:/Temp/bootstrap" } - it "creates a bat file in the temp dir provided by connection, using given content" do - expect(connection).to receive(:upload_file_content!).with(content, "C:/Temp/bootstrap/bootstrap.bat") - expect(knife.upload_bootstrap(content)).to eq "C:/Temp/bootstrap/bootstrap.bat" - end - end - - context "under Linux" do - let(:linux_test) { true } - let(:temp_dir) { "/tmp/bootstrap" } - it "creates a 'sh file in the temp dir provided by connection, using given content" do - expect(connection).to receive(:upload_file_content!).with(content, "/tmp/bootstrap/bootstrap.sh") - expect(knife.upload_bootstrap(content)).to eq "/tmp/bootstrap/bootstrap.sh" - end - end - end - - describe "#bootstrap_command" do - context "under Windows" do - let(:windows_test) { true } - it "prefixes the command to run under cmd.exe" do - expect(knife.bootstrap_command("autoexec.bat")).to eq "cmd.exe /C autoexec.bat" - end - - end - context "under Linux" do - let(:linux_test) { true } - it "prefixes the command to run under sh" do - expect(knife.bootstrap_command("bootstrap.sh")).to eq "sh bootstrap.sh" - end - - context "with --su-user option" do - let(:connection_obj) { double("connection", transport_options: {}) } - before do - knife.config[:su_user] = "root" - allow(connection).to receive(:connection).and_return(connection_obj) - end - it "prefixes the command to run using su -USER -c" do - expect(knife.bootstrap_command("bootstrap.sh")).to eq "su - #{knife.config[:su_user]} -c 'sh bootstrap.sh'" - expect(connection_obj.transport_options.key?(:pty)).to eq true - end - - it "sudo appended if --sudo option enabled" do - knife.config[:use_sudo] = true - expect(knife.bootstrap_command("bootstrap.sh")).to eq "sudo su - #{knife.config[:su_user]} -c 'sh bootstrap.sh'" - expect(connection_obj.transport_options.key?(:pty)).to eq true - end - end - end - end - - describe "#default_bootstrap_template" do - context "under Windows" do - let(:windows_test) { true } - it "is windows-chef-client-msi" do - expect(knife.default_bootstrap_template).to eq "windows-chef-client-msi" - end - - end - context "under Linux" do - let(:linux_test) { true } - it "is chef-full" do - expect(knife.default_bootstrap_template).to eq "chef-full" - end - end - end - - describe "#do_connect" do - let(:host_descriptor) { "example.com" } - let(:connection) { double("TrainConnector") } - let(:connector_mock) { double("TargetResolver", targets: [ connection ]) } - before do - allow(knife).to receive(:host_descriptor).and_return host_descriptor - end - - it "creates a TrainConnector and connects it" do - expect(Chef::Knife::Bootstrap::TrainConnector).to receive(:new).and_return connection - expect(connection).to receive(:connect!) - knife.do_connect({}) - end - - context "when sshd configured with requiretty" do - let(:pty_err_msg) { "Sudo requires a TTY. Please see the README on how to configure sudo to allow for non-interactive usage." } - let(:expected_error) { Train::UserError.new(pty_err_msg, :sudo_no_tty) } - before do - allow(connection).to receive(:connect!).and_raise(expected_error) - end - it "retry with pty true request option" do - expect(Chef::Knife::Bootstrap::TrainConnector).to receive(:new).and_return(connection).exactly(2).times - expect(knife.ui).to receive(:warn).with("#{pty_err_msg} - trying with pty request") - expect { knife.do_connect({}) }.to raise_error(expected_error) - end - end - - context "when a train sudo error is thrown for missing terminal" do - let(:ui_error_msg) { "Sudo password is required for this operation. Please enter password using -P or --ssh-password option" } - let(:expected_error) { Train::UserError.new(ui_error_msg, :sudo_missing_terminal) } - before do - allow(connection).to receive(:connect!).and_raise(expected_error) - end - it "outputs user friendly error message" do - expect { knife.do_connect({}) }.not_to raise_error - expect(stderr.string).to include(ui_error_msg) - end - end - - end - - describe "validate_winrm_transport_opts!" do - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "when using ssh" do - let(:connection_protocol) { "ssh" } - it "returns true" do - expect(knife.validate_winrm_transport_opts!).to eq true - end - end - context "when using winrm" do - let(:connection_protocol) { "winrm" } - context "with plaintext auth" do - before do - knife.config[:winrm_auth_method] = "plaintext" - end - context "with ssl" do - before do - knife.config[:winrm_ssl] = true - end - it "will not error because we won't send anything in plaintext regardless" do - expect(knife.validate_winrm_transport_opts!).to eq true - end - end - context "without ssl" do - before do - knife.config[:winrm_ssl] = false - end - context "and no validation key exists" do - before do - Chef::Config[:validation_key] = "validation_key.pem" - allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return false - end - - it "will error because we will generate and send a client key over the wire in plaintext" do - expect { knife.validate_winrm_transport_opts! }.to raise_error(SystemExit) - end - - end - context "and a validation key exists" do - before do - Chef::Config[:validation_key] = "validation_key.pem" - allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return true - end - # TODO - don't we still send validation key? - it "will not error because we don not send client key over the wire" do - expect(knife.validate_winrm_transport_opts!).to eq true - end - end - end - end - - context "with other auth" do - before do - knife.config[:winrm_auth_method] = "kerberos" - end - - context "and no validation key exists" do - before do - - Chef::Config[:validation_key] = "validation_key.pem" - allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return false - end - - it "will not error because we're not using plaintext auth" do - expect(knife.validate_winrm_transport_opts!).to eq true - end - end - context "and a validation key exists" do - before do - Chef::Config[:validation_key] = "validation_key.pem" - allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return true - end - - it "will not error because a client key won't be sent over the wire in plaintext when a validation key is present" do - expect(knife.validate_winrm_transport_opts!).to eq true - end - end - - end - - end - - end - - describe "#winrm_warn_no_ssl_verification" do - before do - allow(knife).to receive(:connection_protocol).and_return connection_protocol - end - - context "when using ssh" do - let(:connection_protocol) { "ssh" } - it "does not issue a warning" do - expect(knife.ui).to_not receive(:warn) - knife.winrm_warn_no_ssl_verification - end - end - context "when using winrm" do - let(:connection_protocol) { "winrm" } - context "winrm_no_verify_cert is set" do - before do - knife.config[:winrm_no_verify_cert] = true - end - - context "and ca_trust_file is present" do - before do - knife.config[:ca_trust_file] = "file" - end - - it "does not issue a warning" do - expect(knife.ui).to_not receive(:warn) - knife.winrm_warn_no_ssl_verification - end - end - - context "and winrm_ssl_peer_fingerprint is present" do - before do - knife.config[:winrm_ssl_peer_fingerprint] = "ABCD" - end - it "does not issue a warning" do - expect(knife.ui).to_not receive(:warn) - knife.winrm_warn_no_ssl_verification - end - end - context "and neither ca_trust_file nor winrm_ssl_peer_fingerprint is present" do - it "issues a warning" do - expect(knife.ui).to receive(:warn) - knife.winrm_warn_no_ssl_verification - end - end - end - end - end - - describe "#warn_on_short_session_timeout" do - let(:session_timeout) { 60 } - - before do - allow(knife).to receive(:session_timeout).and_return(session_timeout) - end - - context "timeout is not set at all" do - let(:session_timeout) { nil } - it "does not issue a warning" do - expect(knife.ui).to_not receive(:warn) - knife.warn_on_short_session_timeout - end - end - - context "timeout is more than 15" do - let(:session_timeout) { 16 } - it "does not issue a warning" do - expect(knife.ui).to_not receive(:warn) - knife.warn_on_short_session_timeout - end - end - context "timeout is 15 or less" do - let(:session_timeout) { 15 } - it "issues a warning" do - expect(knife.ui).to receive(:warn) - knife.warn_on_short_session_timeout - end - end - end -end +# +# Author:: Ian Meyer (<ianmmeyer@gmail.com>) +# Copyright:: Copyright 2010-2016, Ian Meyer +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "knife_spec_helper" + +Chef::Knife::Bootstrap.load_deps + +describe Chef::Knife::Bootstrap do + let(:bootstrap_template) { nil } + let(:stderr) { StringIO.new } + let(:bootstrap_cli_options) { [ ] } + let(:linux_test) { true } + let(:windows_test) { false } + let(:linux_test) { false } + let(:unix_test) { false } + let(:ssh_test) { false } + + let(:connection) do + double("TrainConnector", + windows?: windows_test, + linux?: linux_test, + unix?: unix_test) + end + + let(:knife) do + Chef::Log.logger = Logger.new(StringIO.new) + Chef::Config[:knife][:bootstrap_template] = bootstrap_template unless bootstrap_template.nil? + + k = Chef::Knife::Bootstrap.new(bootstrap_cli_options) + allow(k.ui).to receive(:stderr).and_return(stderr) + allow(k).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(false) + allow(k).to receive(:connection).and_return connection + k.merge_configs + k + end + + context "#check_license" do + let(:acceptor) { instance_double(LicenseAcceptance::Acceptor) } + + before do + expect(LicenseAcceptance::Acceptor).to receive(:new).and_return(acceptor) + end + + describe "when a license is not required" do + it "does not set the chef_license" do + expect(acceptor).to receive(:license_required?).and_return(false) + knife.check_license + expect(Chef::Config[:chef_license]).to eq(nil) + end + end + + describe "when a license is required" do + it "sets the chef_license" do + expect(acceptor).to receive(:license_required?).and_return(true) + expect(acceptor).to receive(:id_from_mixlib).and_return("id") + expect(acceptor).to receive(:check_and_persist) + expect(acceptor).to receive(:acceptance_value).and_return("accept-no-persist") + knife.check_license + expect(Chef::Config[:chef_license]).to eq("accept-no-persist") + end + end + end + + context "#bootstrap_template" do + it "should default to chef-full" do + expect(knife.bootstrap_template).to be_a_kind_of(String) + expect(File.basename(knife.bootstrap_template)).to eq("chef-full") + end + end + + context "#render_template - when using the chef-full default template" do + let(:rendered_template) do + knife.merge_configs + knife.render_template + end + + it "should render client.rb" do + expect(rendered_template).to match("cat > /etc/chef/client.rb <<'EOP'") + expect(rendered_template).to match("chef_server_url \"https://localhost:443\"") + expect(rendered_template).to match("validation_client_name \"chef-validator\"") + expect(rendered_template).to match("log_location STDOUT") + end + + it "should render first-boot.json" do + expect(rendered_template).to match("cat > /etc/chef/first-boot.json <<'EOP'") + expect(rendered_template).to match('{"run_list":\[\]}') + end + + context "and encrypted_data_bag_secret was provided" do + it "should render encrypted_data_bag_secret file" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return("secrets") + expect(rendered_template).to match("cat > /etc/chef/encrypted_data_bag_secret <<'EOP'") + expect(rendered_template).to match('{"run_list":\[\]}') + expect(rendered_template).to match(/secrets/) + end + end + end + + context "with --bootstrap-vault-item" do + let(:bootstrap_cli_options) { [ "--bootstrap-vault-item", "vault1:item1", "--bootstrap-vault-item", "vault1:item2", "--bootstrap-vault-item", "vault2:item1" ] } + it "sets the knife config cli option correctly" do + expect(knife.config[:bootstrap_vault_item]).to eq({ "vault1" => %w{item1 item2}, "vault2" => ["item1"] }) + end + end + + context "with --bootstrap-preinstall-command" do + command = "while sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; do\n echo 'waiting for dpkg lock';\n sleep 1;\n done;" + let(:bootstrap_cli_options) { [ "--bootstrap-preinstall-command", command ] } + let(:rendered_template) do + knife.merge_configs + knife.render_template + end + it "configures the preinstall command in the bootstrap template correctly" do + expect(rendered_template).to match(/command/) + end + end + + context "with --bootstrap-proxy" do + let(:bootstrap_cli_options) { [ "--bootstrap-proxy", "1.1.1.1" ] } + let(:rendered_template) do + knife.merge_configs + knife.render_template + end + it "configures the https_proxy environment variable in the bootstrap template correctly" do + expect(rendered_template).to match(/https_proxy="1.1.1.1" export https_proxy/) + end + end + + context "with --bootstrap-no-proxy" do + let(:bootstrap_cli_options) { [ "--bootstrap-no-proxy", "localserver" ] } + let(:rendered_template) do + knife.merge_configs + knife.render_template + end + it "configures the https_proxy environment variable in the bootstrap template correctly" do + expect(rendered_template).to match(/no_proxy="localserver" export no_proxy/) + end + end + + context "with :bootstrap_template and :template_file cli options" do + let(:bootstrap_cli_options) { [ "--bootstrap-template", "my-template", "other-template" ] } + + it "should select bootstrap template" do + expect(File.basename(knife.bootstrap_template)).to eq("my-template") + end + end + + context "when finding templates" do + context "when :bootstrap_template config is set to a file" do + context "that doesn't exist" do + let(:bootstrap_template) { "/opt/blah/not/exists/template.erb" } + + it "raises an error" do + expect { knife.find_template }.to raise_error(Errno::ENOENT) + end + end + + context "that exists" do + let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test.erb")) } + + it "loads the given file as the template" do + expect(Chef::Log).to receive(:trace) + expect(knife.find_template).to eq(File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test.erb"))) + end + end + end + + context "when :bootstrap_template config is set to a template name" do + let(:bootstrap_template) { "example" } + + let(:builtin_template_path) { File.expand_path(File.join(__dir__, "../../../lib/chef/knife/bootstrap/templates", "example.erb")) } + + let(:chef_config_dir_template_path) { "/knife/chef/config/bootstrap/example.erb" } + + let(:env_home_template_path) { "/env/home/.chef/bootstrap/example.erb" } + + let(:gem_files_template_path) { "/Users/schisamo/.rvm/gems/ruby-1.9.2-p180@chef-0.10/gems/knife-windows-0.5.4/lib/chef/knife/bootstrap/fake-bootstrap-template.erb" } + + def configure_chef_config_dir + allow(Chef::Knife).to receive(:chef_config_dir).and_return("/knife/chef/config") + end + + def configure_env_home + allow(Chef::Util::PathHelper).to receive(:home).with(".chef", "bootstrap", "example.erb").and_yield(env_home_template_path) + end + + def configure_gem_files + allow(Gem).to receive(:find_files).and_return([ gem_files_template_path ]) + end + + before(:each) do + expect(File).to receive(:exist?).with(bootstrap_template).and_return(false) + end + + context "when file is available everywhere" do + before do + configure_chef_config_dir + configure_env_home + configure_gem_files + + expect(File).to receive(:exist?).with(builtin_template_path).and_return(true) + end + + it "should load the template from built-in templates" do + expect(knife.find_template).to eq(builtin_template_path) + end + end + + context "when file is available in chef_config_dir" do + before do + configure_chef_config_dir + configure_env_home + configure_gem_files + + expect(File).to receive(:exist?).with(builtin_template_path).and_return(false) + expect(File).to receive(:exist?).with(chef_config_dir_template_path).and_return(true) + + it "should load the template from chef_config_dir" do + knife.find_template.should eq(chef_config_dir_template_path) + end + end + end + + context "when file is available in home directory" do + before do + configure_chef_config_dir + configure_env_home + configure_gem_files + + expect(File).to receive(:exist?).with(builtin_template_path).and_return(false) + expect(File).to receive(:exist?).with(chef_config_dir_template_path).and_return(false) + expect(File).to receive(:exist?).with(env_home_template_path).and_return(true) + end + + it "should load the template from chef_config_dir" do + expect(knife.find_template).to eq(env_home_template_path) + end + end + + context "when file is available in Gem files" do + before do + configure_chef_config_dir + configure_env_home + configure_gem_files + + expect(File).to receive(:exist?).with(builtin_template_path).and_return(false) + expect(File).to receive(:exist?).with(chef_config_dir_template_path).and_return(false) + expect(File).to receive(:exist?).with(env_home_template_path).and_return(false) + expect(File).to receive(:exist?).with(gem_files_template_path).and_return(true) + end + + it "should load the template from Gem files" do + expect(knife.find_template).to eq(gem_files_template_path) + end + end + + context "when file is available in Gem files and home dir doesn't exist" do + before do + configure_chef_config_dir + configure_gem_files + allow(Chef::Util::PathHelper).to receive(:home).with(".chef", "bootstrap", "example.erb").and_return(nil) + + expect(File).to receive(:exist?).with(builtin_template_path).and_return(false) + expect(File).to receive(:exist?).with(chef_config_dir_template_path).and_return(false) + expect(File).to receive(:exist?).with(gem_files_template_path).and_return(true) + end + + it "should load the template from Gem files" do + expect(knife.find_template).to eq(gem_files_template_path) + end + end + end + end + + ["-t", "--bootstrap-template"].each do |t| + context "when #{t} option is given in the command line" do + it "sets the knife :bootstrap_template config" do + knife.parse_options([t, "blahblah"]) + knife.merge_configs + expect(knife.bootstrap_template).to eq("blahblah") + end + end + end + + context "with run_list template" do + let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test.erb")) } + + it "should return an empty run_list" do + expect(knife.render_template).to eq('{"run_list":[]}') + end + + it "should have role[base] in the run_list" do + knife.parse_options(["-r", "role[base]"]) + knife.merge_configs + expect(knife.render_template).to eq('{"run_list":["role[base]"]}') + end + + it "should have role[base] and recipe[cupcakes] in the run_list" do + knife.parse_options(["-r", "role[base],recipe[cupcakes]"]) + knife.merge_configs + expect(knife.render_template).to eq('{"run_list":["role[base]","recipe[cupcakes]"]}') + end + + context "with bootstrap_attribute options" do + let(:jsonfile) do + file = Tempfile.new(["node", ".json"]) + File.open(file.path, "w") { |f| f.puts '{"foo":{"bar":"baz"}}' } + file + end + + it "should have foo => {bar => baz} in the first_boot from cli" do + knife.parse_options(["-j", '{"foo":{"bar":"baz"}}']) + knife.merge_configs + expected_hash = FFI_Yajl::Parser.new.parse('{"foo":{"bar":"baz"},"run_list":[]}') + actual_hash = FFI_Yajl::Parser.new.parse(knife.render_template) + expect(actual_hash).to eq(expected_hash) + end + + it "should have foo => {bar => baz} in the first_boot from file" do + knife.parse_options(["--json-attribute-file", jsonfile.path]) + knife.merge_configs + expected_hash = FFI_Yajl::Parser.new.parse('{"foo":{"bar":"baz"},"run_list":[]}') + actual_hash = FFI_Yajl::Parser.new.parse(knife.render_template) + expect(actual_hash).to eq(expected_hash) + jsonfile.close + end + + it "raises a Chef::Exceptions::BootstrapCommandInputError with the proper error message" do + knife.parse_options(["-j", '{"foo":{"bar":"baz"}}']) + knife.parse_options(["--json-attribute-file", jsonfile.path]) + knife.merge_configs + allow(knife).to receive(:validate_name_args!) + expect(knife).to receive(:check_license) + + expect { knife.run }.to raise_error(Chef::Exceptions::BootstrapCommandInputError) + jsonfile.close + end + end + end + + context "with hints template" do + let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "test-hints.erb")) } + + it "should create a hint file when told to" do + knife.parse_options(["--hint", "openstack"]) + knife.merge_configs + expect(knife.render_template).to match(%r{/etc/chef/ohai/hints/openstack.json}) + end + + it "should populate a hint file with JSON when given a file to read" do + allow(::File).to receive(:read).and_return('{ "foo" : "bar" }') + knife.parse_options(["--hint", "openstack=hints/openstack.json"]) + knife.merge_configs + expect(knife.render_template).to match(/\{\"foo\":\"bar\"\}/) + end + end + + describe "specifying no_proxy with various entries" do + subject(:knife) do + k = described_class.new + Chef::Config[:knife][:bootstrap_template] = template_file + allow(k).to receive(:connection).and_return connection + k.parse_options(options) + k.merge_configs + k + end + + let(:options) { ["--bootstrap-no-proxy", setting] } + + let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) } + + let(:rendered_template) do + knife.render_template + end + + context "via --bootstrap-no-proxy" do + let(:setting) { "api.opscode.com" } + + it "renders the client.rb with a single FQDN no_proxy entry" do + expect(rendered_template).to match(/.*no_proxy\s*"api.opscode.com".*/) + end + end + + context "via --bootstrap-no-proxy multiple" do + let(:setting) { "api.opscode.com,172.16.10.*" } + + it "renders the client.rb with comma-separated FQDN and wildcard IP address no_proxy entries" do + expect(rendered_template).to match(/.*no_proxy\s*"api.opscode.com,172.16.10.\*".*/) + end + end + + context "via --ssl-verify-mode none" do + let(:options) { ["--node-ssl-verify-mode", "none"] } + + it "renders the client.rb with ssl_verify_mode set to :verify_none" do + expect(rendered_template).to match(/ssl_verify_mode :verify_none/) + end + end + + context "via --node-ssl-verify-mode peer" do + let(:options) { ["--node-ssl-verify-mode", "peer"] } + + it "renders the client.rb with ssl_verify_mode set to :verify_peer" do + expect(rendered_template).to match(/ssl_verify_mode :verify_peer/) + end + end + + context "via --node-ssl-verify-mode all" do + let(:options) { ["--node-ssl-verify-mode", "all"] } + + it "raises error" do + expect { rendered_template }.to raise_error(RuntimeError) + end + end + + context "via --node-verify-api-cert" do + let(:options) { ["--node-verify-api-cert"] } + + it "renders the client.rb with verify_api_cert set to true" do + expect(rendered_template).to match(/verify_api_cert true/) + end + end + + context "via --no-node-verify-api-cert" do + let(:options) { ["--no-node-verify-api-cert"] } + + it "renders the client.rb with verify_api_cert set to false" do + expect(rendered_template).to match(/verify_api_cert false/) + end + end + end + + describe "specifying the encrypted data bag secret key" do + let(:secret) { "supersekret" } + let(:options) { [] } + let(:bootstrap_template) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "secret.erb")) } + let(:rendered_template) do + knife.parse_options(options) + knife.merge_configs + knife.render_template + end + + it "creates a secret file" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + expect(rendered_template).to match(/#{secret}/) + end + + it "renders the client.rb with an encrypted_data_bag_secret entry" do + expect(knife).to receive(:encryption_secret_provided_ignore_encrypt_flag?).and_return(true) + expect(knife).to receive(:read_secret).and_return(secret) + expect(rendered_template).to match(%r{encrypted_data_bag_secret\s*"/etc/chef/encrypted_data_bag_secret"}) + end + + end + + describe "when transferring trusted certificates" do + let(:rendered_template) do + knife.merge_configs + knife.render_template + end + + before do + Chef::Config[:trusted_certs_dir] = Chef::Util::PathHelper.cleanpath(File.join(CHEF_SPEC_DATA, "trusted_certs")) + end + + it "creates /etc/chef/trusted_certs" do + expect(rendered_template).to match(%r{mkdir -p /etc/chef/trusted_certs}) + end + + it "copies the certificates in the directory" do + certificates = Dir[File.join(Chef::Config[:trusted_certs_dir], "*.{crt,pem}")] + + certificates.each do |cert| + expect(rendered_template).to match(%r{cat > /etc/chef/trusted_certs/#{File.basename(cert)} <<'EOP'}) + end + end + + it "doesn't create /etc/chef/trusted_certs if :trusted_certs_dir is empty" do + Dir.mktmpdir do |dir| + Chef::Config[:trusted_certs_dir] = dir + expect(rendered_template).not_to match(%r{mkdir -p /etc/chef/trusted_certs}) + end + end + end + + context "when doing fips things" do + let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) } + + before do + Chef::Config[:knife][:bootstrap_template] = template_file + knife.merge_configs + end + + let(:rendered_template) do + knife.render_template + end + + context "when knife is in fips mode" do + before do + Chef::Config[:fips] = true + end + + it "renders 'fips true'" do + expect(rendered_template).to match("fips") + end + end + + context "when knife is not in fips mode" do + before do + # This is required because the chef-fips pipeline does + # has a default value of true for fips + Chef::Config[:fips] = false + end + + it "does not render anything about fips" do + expect(rendered_template).not_to match("fips") + end + end + end + + describe "when transferring client.d" do + + let(:rendered_template) do + knife.merge_configs + knife.render_template + end + + before do + Chef::Config[:client_d_dir] = client_d_dir + end + + context "when client_d_dir is nil" do + let(:client_d_dir) { nil } + + it "does not create /etc/chef/client.d" do + expect(rendered_template).not_to match(%r{mkdir -p /etc/chef/client\.d}) + end + end + + context "when client_d_dir is set" do + let(:client_d_dir) do + Chef::Util::PathHelper.cleanpath( + File.join(__dir__, "../../data/client.d_00") + ) + end + + it "creates /etc/chef/client.d" do + expect(rendered_template).to match("mkdir -p /etc/chef/client\.d") + end + + context "a flat directory structure" do + it "escapes single-quotes" do + expect(rendered_template).to match("cat > /etc/chef/client.d/02-strings.rb <<'EOP'") + expect(rendered_template).to match("something '\\\\''/foo/bar'\\\\''") + end + + it "creates a file 00-foo.rb" do + expect(rendered_template).to match("cat > /etc/chef/client.d/00-foo.rb <<'EOP'") + expect(rendered_template).to match("d6f9b976-289c-4149-baf7-81e6ffecf228") + end + it "creates a file bar" do + expect(rendered_template).to match("cat > /etc/chef/client.d/bar <<'EOP'") + expect(rendered_template).to match("1 / 0") + end + end + + context "a nested directory structure" do + let(:client_d_dir) do + Chef::Util::PathHelper.cleanpath( + File.join(__dir__, "../../data/client.d_01") + ) + end + it "creates a file foo/bar.rb" do + expect(rendered_template).to match("cat > /etc/chef/client.d/foo/bar.rb <<'EOP'") + expect(rendered_template).to match("1 / 0") + end + end + end + end + + describe "#connection_protocol" do + let(:host_descriptor) { "example.com" } + let(:config) { {} } + let(:knife_connection_protocol) { nil } + before do + allow(knife).to receive(:config).and_return config + allow(knife).to receive(:host_descriptor).and_return host_descriptor + if knife_connection_protocol + Chef::Config[:knife][:connection_protocol] = knife_connection_protocol + knife.merge_configs + end + end + + context "when protocol is part of the host argument" do + let(:host_descriptor) { "winrm://myhost" } + + it "returns the value provided by the host argument" do + expect(knife.connection_protocol).to eq "winrm" + end + end + + context "when protocol is provided via the CLI flag" do + let(:config) { { connection_protocol: "winrm" } } + it "returns that value" do + expect(knife.connection_protocol).to eq "winrm" + end + + end + context "when protocol is provided via the host argument and the CLI flag" do + let(:host_descriptor) { "ssh://example.com" } + let(:config) { { connection_protocol: "winrm" } } + + it "returns the value provided by the host argument" do + expect(knife.connection_protocol).to eq "ssh" + end + end + + context "when no explicit protocol is provided" do + let(:config) { {} } + let(:host_descriptor) { "example.com" } + let(:knife_connection_protocol) { "winrm" } + it "falls back to knife config" do + expect(knife.connection_protocol).to eq "winrm" + end + context "and there is no knife bootstrap_protocol" do + let(:knife_connection_protocol) { nil } + it "falls back to 'ssh'" do + expect(knife.connection_protocol).to eq "ssh" + end + end + end + + end + + describe "#validate_protocol!" do + let(:host_descriptor) { "example.com" } + let(:config) { {} } + let(:connection_protocol) { "ssh" } + before do + allow(knife).to receive(:config).and_return config + allow(knife).to receive(:connection_protocol).and_return connection_protocol + allow(knife).to receive(:host_descriptor).and_return host_descriptor + end + + context "when protocol is provided both in the URL and via --protocol" do + + context "and they do not match" do + let(:connection_protocol) { "ssh" } + let(:config) { { connection_protocol: "winrm" } } + it "outputs an error and exits" do + expect(knife.ui).to receive(:error) + expect { knife.validate_protocol! }.to raise_error SystemExit + end + end + + context "and they do match" do + let(:connection_protocol) { "winrm" } + let(:config) { { connection_protocol: "winrm" } } + it "returns true" do + expect(knife.validate_protocol!).to eq true + end + end + end + + context "and the protocol is supported" do + + Chef::Knife::Bootstrap::SUPPORTED_CONNECTION_PROTOCOLS.each do |proto| + let(:connection_protocol) { proto } + it "returns true for #{proto}" do + expect(knife.validate_protocol!).to eq true + end + end + end + + context "and the protocol is not supported" do + let(:connection_protocol) { "invalid" } + it "outputs an error and exits" do + expect(knife.ui).to receive(:error).with(/Unsupported protocol '#{connection_protocol}'/) + expect { knife.validate_protocol! }.to raise_error SystemExit + end + end + end + + describe "#validate_policy_options!" do + + context "when only policy_name is given" do + + let(:bootstrap_cli_options) { %w{ --policy-name my-app-server } } + + it "returns an error stating that policy_name and policy_group must be given together" do + expect { knife.validate_policy_options! }.to raise_error(SystemExit) + expect(stderr.string).to include("ERROR: --policy-name and --policy-group must be specified together") + end + + end + + context "when only policy_group is given" do + + let(:bootstrap_cli_options) { %w{ --policy-group staging } } + + it "returns an error stating that policy_name and policy_group must be given together" do + expect { knife.validate_policy_options! }.to raise_error(SystemExit) + expect(stderr.string).to include("ERROR: --policy-name and --policy-group must be specified together") + end + + end + + context "when both policy_name and policy_group are given, but run list is also given" do + + let(:bootstrap_cli_options) { %w{ --policy-name my-app --policy-group staging --run-list cookbook } } + + it "returns an error stating that policyfile and run_list are exclusive" do + expect { knife.validate_policy_options! }.to raise_error(SystemExit) + expect(stderr.string).to include("ERROR: Policyfile options and --run-list are exclusive") + end + + end + + context "when policy_name and policy_group are given with no conflicting options" do + + let(:bootstrap_cli_options) { %w{ --policy-name my-app --policy-group staging } } + + it "passes options validation" do + expect { knife.validate_policy_options! }.to_not raise_error + end + + it "passes them into the bootstrap context" do + expect(knife.bootstrap_context.first_boot).to have_key(:policy_name) + expect(knife.bootstrap_context.first_boot).to have_key(:policy_group) + end + + it "ensures that run_list is not set in the bootstrap context" do + expect(knife.bootstrap_context.first_boot).to_not have_key(:run_list) + end + + end + + # https://github.com/chef/chef/issues/4131 + # Arguably a bug in the plugin: it shouldn't be setting this to nil, but it + # worked before, so make it work now. + context "when a plugin sets the run list option to nil" do + before do + knife.config[:run_list] = nil + end + + it "passes options validation" do + expect { knife.validate_policy_options! }.to_not raise_error + end + end + end + + # TODO - this is the only cli option we validate the _option_ itself - + # so we'll know if someone accidentally deletes or renames use_sudo_password + # Is this worht keeping? If so, then it seems we should expand it + # to cover all options. + context "validating use_sudo_password option" do + it "use_sudo_password contains description and long params for help" do + expect(knife.options).to(have_key(:use_sudo_password)) \ + && expect(knife.options[:use_sudo_password][:description].to_s).not_to(eq(""))\ + && expect(knife.options[:use_sudo_password][:long].to_s).not_to(eq("")) + end + end + + context "#connection_opts" do + let(:connection_protocol) { "ssh" } + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + context "behavioral test: " do + let(:expected_connection_opts) do + { base_opts: true, + ssh_identity_opts: true, + ssh_opts: true, + gateway_opts: true, + host_verify_opts: true, + sudo_opts: true, + winrm_opts: true } + end + + it "queries and merges only expected configurations" do + expect(knife).to receive(:base_opts).and_return({ base_opts: true }) + expect(knife).to receive(:host_verify_opts).and_return({ host_verify_opts: true }) + expect(knife).to receive(:gateway_opts).and_return({ gateway_opts: true }) + expect(knife).to receive(:sudo_opts).and_return({ sudo_opts: true }) + expect(knife).to receive(:winrm_opts).and_return({ winrm_opts: true }) + expect(knife).to receive(:ssh_opts).and_return({ ssh_opts: true }) + expect(knife).to receive(:ssh_identity_opts).and_return({ ssh_identity_opts: true }) + expect(knife.connection_opts).to match expected_connection_opts + end + end + + context "functional test: " do + context "when protocol is winrm" do + let(:connection_protocol) { "winrm" } + # context "and neither CLI nor Chef::Config config entries have been provided" + # end + context "and all supported values are provided as Chef::Config entries" do + before do + # Set everything to easily identifiable and obviously fake values + # to verify that Chef::Config is being sourced instead of knife.config + knife.config = {} + Chef::Config[:knife][:max_wait] = 9999.0 + Chef::Config[:knife][:winrm_user] = "winbob" + Chef::Config[:knife][:winrm_port] = 9999 + Chef::Config[:knife][:ca_trust_file] = "trust.me" + Chef::Config[:knife][:kerberos_realm] = "realm" + Chef::Config[:knife][:kerberos_service] = "service" + Chef::Config[:knife][:winrm_auth_method] = "kerberos" # default is negotiate + Chef::Config[:knife][:winrm_basic_auth_only] = true + Chef::Config[:knife][:winrm_no_verify_cert] = true + Chef::Config[:knife][:session_timeout] = 9999 + Chef::Config[:knife][:winrm_ssl] = true + Chef::Config[:knife][:winrm_ssl_peer_fingerprint] = "ABCDEF" + end + + context "and no CLI options have been given" do + let(:expected_result) do + { + logger: Chef::Log, # not configurable + ca_trust_path: "trust.me", + max_wait_until_ready: 9999, # converted to int + operation_timeout: 9999, + ssl_peer_fingerprint: "ABCDEF", + winrm_transport: "kerberos", + winrm_basic_auth_only: true, + user: "winbob", + port: 9999, + self_signed: true, + ssl: true, + kerberos_realm: "realm", + kerberos_service: "service", + } + end + + it "generates a config hash using the Chef::Config values" do + knife.merge_configs + expect(knife.connection_opts).to match expected_result + end + + end + + context "and some CLI options have been given" do + let(:expected_result) do + { + logger: Chef::Log, # not configurable + ca_trust_path: "no trust", + max_wait_until_ready: 9999, + operation_timeout: 9999, + ssl_peer_fingerprint: "ABCDEF", + winrm_transport: "kerberos", + winrm_basic_auth_only: true, + user: "microsoftbob", + port: 12, + self_signed: true, + ssl: true, + kerberos_realm: "realm", + kerberos_service: "service", + password: "lobster", + } + end + + before do + knife.config[:ca_trust_file] = "no trust" + knife.config[:connection_user] = "microsoftbob" + knife.config[:connection_port] = 12 + knife.config[:winrm_port] = "13" # indirectly verify we're not looking for the wrong CLI flag + knife.config[:connection_password] = "lobster" + end + + it "generates a config hash using the CLI options when available and falling back to Chef::Config values" do + knife.merge_configs + expect(knife.connection_opts).to match expected_result + end + end + + context "and all CLI options have been given" do + before do + # We'll force kerberos vi knife.config because it + # causes additional options to populate - make sure + # Chef::Config is different so we can be sure that we didn't + # pull in the Chef::Config value + Chef::Config[:knife][:winrm_auth_method] = "negotiate" + knife.config[:connection_password] = "blue" + knife.config[:max_wait] = 1000.0 + knife.config[:connection_user] = "clippy" + knife.config[:connection_port] = 1000 + knife.config[:winrm_port] = 1001 # We should not see this value get used + + knife.config[:ca_trust_file] = "trust.the.internet" + knife.config[:kerberos_realm] = "otherrealm" + knife.config[:kerberos_service] = "otherservice" + knife.config[:winrm_auth_method] = "kerberos" # default is negotiate + knife.config[:winrm_basic_auth_only] = false + knife.config[:winrm_no_verify_cert] = false + knife.config[:session_timeout] = 1000 + knife.config[:winrm_ssl] = false + knife.config[:winrm_ssl_peer_fingerprint] = "FEDCBA" + end + let(:expected_result) do + { + logger: Chef::Log, # not configurable + ca_trust_path: "trust.the.internet", + max_wait_until_ready: 1000, # converted to int + operation_timeout: 1000, + ssl_peer_fingerprint: "FEDCBA", + winrm_transport: "kerberos", + winrm_basic_auth_only: false, + user: "clippy", + port: 1000, + self_signed: false, + ssl: false, + kerberos_realm: "otherrealm", + kerberos_service: "otherservice", + password: "blue", + } + end + it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do + knife.merge_configs + expect(knife.connection_opts).to match expected_result + end + end + end # with underlying Chef::Config values + + context "and no values are provided from Chef::Config or CLI" do + before do + # We will use knife's actual config since these tests + # have assumptions based on CLI default values + end + let(:expected_result) do + { + logger: Chef::Log, + operation_timeout: 60, + self_signed: false, + ssl: false, + ssl_peer_fingerprint: nil, + winrm_basic_auth_only: false, + winrm_transport: "negotiate", + } + end + it "populates appropriate defaults" do + knife.merge_configs + expect(knife.connection_opts).to match expected_result + end + end + end # winrm + + context "when protocol is ssh" do + let(:connection_protocol) { "ssh" } + # context "and neither CLI nor Chef::Config config entries have been provided" + # end + context "and all supported values are provided as Chef::Config entries" do + before do + # Set everything to easily identifiable and obviously fake values + # to verify that Chef::Config is being sourced instead of knife.config + knife.config = {} + Chef::Config[:knife][:max_wait] = 9999.0 + Chef::Config[:knife][:session_timeout] = 9999 + Chef::Config[:knife][:ssh_user] = "sshbob" + Chef::Config[:knife][:ssh_port] = 9999 + Chef::Config[:knife][:host_key_verify] = false + Chef::Config[:knife][:ssh_gateway_identity] = "/gateway.pem" + Chef::Config[:knife][:ssh_gateway] = "admin@mygateway.local:1234" + Chef::Config[:knife][:ssh_identity_file] = "/identity.pem" + Chef::Config[:knife][:use_sudo_password] = false # We have no password. + end + + context "and no CLI options have been given" do + let(:expected_result) do + { + logger: Chef::Log, # not configurable + max_wait_until_ready: 9999, # converted to int + connection_timeout: 9999, + user: "sshbob", + bastion_host: "mygateway.local", + bastion_port: 1234, + bastion_user: "admin", + forward_agent: false, + keys_only: true, + key_files: ["/identity.pem", "/gateway.pem"], + sudo: false, + verify_host_key: "always", + port: 9999, + non_interactive: true, + } + end + + it "generates a correct config hash using the Chef::Config values" do + knife.merge_configs + expect(knife.connection_opts).to match expected_result + end + end + + context "and unsupported Chef::Config options are given in Chef::Config, not in CLI" do + before do + Chef::Config[:knife][:password] = "blah" + Chef::Config[:knife][:ssh_password] = "blah" + Chef::Config[:knife][:preserve_home] = true + Chef::Config[:knife][:use_sudo] = true + Chef::Config[:knife][:ssh_forward_agent] = "blah" + end + it "does not include the corresponding option in the connection options" do + knife.merge_configs + expect(knife.connection_opts.key?(:password)).to eq false + expect(knife.connection_opts.key?(:ssh_forward_agent)).to eq false + expect(knife.connection_opts.key?(:use_sudo)).to eq false + expect(knife.connection_opts.key?(:preserve_home)).to eq false + end + end + + context "and some CLI options have been given" do + before do + knife.config = {} + knife.config[:connection_user] = "sshalice" + knife.config[:connection_port] = 12 + knife.config[:ssh_port] = "13" # canary to indirectly verify we're not looking for the wrong CLI flag + knife.config[:connection_password] = "feta cheese" + knife.config[:max_wait] = 150.0 + knife.config[:session_timeout] = 120 + knife.config[:use_sudo] = true + knife.config[:use_sudo_pasword] = true + knife.config[:ssh_forward_agent] = true + end + + let(:expected_result) do + { + logger: Chef::Log, # not configurable + max_wait_until_ready: 150, # cli (converted to int) + connection_timeout: 120, # cli + user: "sshalice", # cli + password: "feta cheese", # cli + bastion_host: "mygateway.local", # Config + bastion_port: 1234, # Config + bastion_user: "admin", # Config + forward_agent: true, # cli + keys_only: false, # implied false from config password present + key_files: ["/identity.pem", "/gateway.pem"], # Config + sudo: true, # ccli + verify_host_key: "always", # Config + port: 12, # cli + non_interactive: true, + } + end + + it "generates a config hash using the CLI options when available and falling back to Chef::Config values" do + knife.merge_configs + expect(knife.connection_opts).to match expected_result + end + end + + context "and all CLI options have been given" do + before do + knife.config = {} + knife.config[:max_wait] = 150.0 + knife.config[:session_timeout] = 120 + knife.config[:connection_user] = "sshroot" + knife.config[:connection_port] = 1000 + knife.config[:connection_password] = "blah" + knife.config[:forward_agent] = true + knife.config[:use_sudo] = true + knife.config[:use_sudo_password] = true + knife.config[:preserve_home] = true + knife.config[:use_sudo_pasword] = true + knife.config[:ssh_forward_agent] = true + knife.config[:ssh_verify_host_key] = true + knife.config[:ssh_gateway_identity] = "/gateway-identity.pem" + knife.config[:ssh_gateway] = "me@example.com:10" + knife.config[:ssh_identity_file] = "/my-identity.pem" + + # We'll set these as canaries - if one of these values shows up + # in a failed test, then the behavior of not pulling from these keys + # out of knife.config is broken: + knife.config[:ssh_user] = "do not use" + knife.config[:ssh_port] = 1001 + end + let(:expected_result) do + { + logger: Chef::Log, # not configurable + max_wait_until_ready: 150, # converted to int + connection_timeout: 120, + user: "sshroot", + password: "blah", + port: 1000, + bastion_host: "example.com", + bastion_port: 10, + bastion_user: "me", + forward_agent: true, + keys_only: false, + key_files: ["/my-identity.pem", "/gateway-identity.pem"], + sudo: true, + sudo_options: "-H", + sudo_password: "blah", + verify_host_key: true, + non_interactive: true, + } + end + it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do + knife.merge_configs + expect(knife.connection_opts).to match expected_result + end + end + end + context "and no values are provided from Chef::Config or CLI" do + before do + # We will use knife's actual config since these tests + # have assumptions based on CLI default values + config = {} + end + + let(:expected_result) do + { + forward_agent: false, + key_files: [], + logger: Chef::Log, + keys_only: false, + sudo: false, + verify_host_key: "always", + non_interactive: true, + connection_timeout: 60, + } + end + it "populates appropriate defaults" do + knife.merge_configs + expect(knife.connection_opts).to match expected_result + end + end + + end # ssh + end # functional tests + + end # connection_opts + + context "#base_opts" do + let(:connection_protocol) { nil } + + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "for all protocols" do + context "when password is provided" do + before do + knife.config[:connection_port] = 250 + knife.config[:connection_user] = "test" + knife.config[:connection_password] = "opscode" + end + + let(:expected_opts) do + { + port: 250, + user: "test", + logger: Chef::Log, + password: "opscode", + } + end + it "generates the correct options" do + expect(knife.base_opts).to eq expected_opts + end + + end + + context "when password is not provided" do + before do + knife.config[:connection_port] = 250 + knife.config[:connection_user] = "test" + end + + let(:expected_opts) do + { + port: 250, + user: "test", + logger: Chef::Log, + } + end + it "generates the correct options" do + expect(knife.base_opts).to eq expected_opts + end + end + end + end + + context "#host_verify_opts" do + let(:connection_protocol) { nil } + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "for winrm" do + let(:connection_protocol) { "winrm" } + it "returns the expected configuration" do + knife.config[:winrm_no_verify_cert] = true + expect(knife.host_verify_opts).to eq( { self_signed: true } ) + end + it "provides a correct default when no option given" do + expect(knife.host_verify_opts).to eq( { self_signed: false } ) + end + end + + context "for ssh" do + let(:connection_protocol) { "ssh" } + it "returns the expected configuration" do + knife.config[:ssh_verify_host_key] = false + expect(knife.host_verify_opts).to eq( { verify_host_key: false } ) + end + it "provides a correct default when no option given" do + expect(knife.host_verify_opts).to eq( { verify_host_key: "always" } ) + end + end + end + + # TODO - test keys_only, password, config source behavior + context "#ssh_identity_opts" do + let(:connection_protocol) { nil } + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "for winrm" do + let(:connection_protocol) { "winrm" } + it "returns an empty hash" do + expect(knife.ssh_identity_opts).to eq({}) + end + end + + context "for ssh" do + let(:connection_protocol) { "ssh" } + context "when an identity file is specified" do + before do + knife.config[:ssh_identity_file] = "/identity.pem" + end + it "generates the expected configuration" do + expect(knife.ssh_identity_opts).to eq({ + key_files: [ "/identity.pem" ], + keys_only: true, + }) + end + context "and a password is also specified" do + before do + knife.config[:connection_password] = "blah" + end + it "generates the expected configuration (key, keys_only false)" do + expect(knife.ssh_identity_opts).to eq({ + key_files: [ "/identity.pem" ], + keys_only: false, + }) + end + end + + context "and a gateway is not specified" do + context "but a gateway identity file is specified" do + it "does not include the gateway identity file in keys" do + expect(knife.ssh_identity_opts).to eq({ + key_files: ["/identity.pem"], + keys_only: true, + }) + end + + end + + end + + context "and a gatway is specified" do + before do + knife.config[:ssh_gateway] = "example.com" + end + context "and a gateway identity file is not specified" do + it "config includes only identity file and not gateway identity" do + expect(knife.ssh_identity_opts).to eq({ + key_files: [ "/identity.pem" ], + keys_only: true, + }) + end + end + + context "and a gateway identity file is also specified" do + before do + knife.config[:ssh_gateway_identity] = "/gateway.pem" + end + + it "generates the expected configuration (both keys, keys_only true)" do + expect(knife.ssh_identity_opts).to eq({ + key_files: [ "/identity.pem", "/gateway.pem" ], + keys_only: true, + }) + end + end + end + end + + context "when no identity file is specified" do + it "generates the expected configuration (no keys, keys_only false)" do + expect(knife.ssh_identity_opts).to eq( { + key_files: [], + keys_only: false, + }) + end + context "and a gateway with gateway identity file is specified" do + before do + knife.config[:ssh_gateway] = "host" + knife.config[:ssh_gateway_identity] = "/gateway.pem" + end + + it "generates the expected configuration (gateway key, keys_only false)" do + expect(knife.ssh_identity_opts).to eq({ + key_files: [ "/gateway.pem" ], + keys_only: false, + }) + end + end + end + end + end + + context "#gateway_opts" do + let(:connection_protocol) { nil } + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "for winrm" do + let(:connection_protocol) { "winrm" } + it "returns an empty hash" do + expect(knife.gateway_opts).to eq({}) + end + end + + context "for ssh" do + let(:connection_protocol) { "ssh" } + context "and ssh_gateway with hostname, user and port provided" do + before do + knife.config[:ssh_gateway] = "testuser@gateway:9021" + end + it "returns a proper bastion host config subset" do + expect(knife.gateway_opts).to eq({ + bastion_user: "testuser", + bastion_host: "gateway", + bastion_port: 9021, + }) + end + end + context "and ssh_gateway with only hostname is given" do + before do + knife.config[:ssh_gateway] = "gateway" + end + it "returns a proper bastion host config subset" do + expect(knife.gateway_opts).to eq({ + bastion_user: nil, + bastion_host: "gateway", + bastion_port: nil, + }) + end + end + context "and ssh_gateway with hostname and user is is given" do + before do + knife.config[:ssh_gateway] = "testuser@gateway" + end + it "returns a proper bastion host config subset" do + expect(knife.gateway_opts).to eq({ + bastion_user: "testuser", + bastion_host: "gateway", + bastion_port: nil, + }) + end + end + + context "and ssh_gateway with hostname and port is is given" do + before do + knife.config[:ssh_gateway] = "gateway:11234" + end + it "returns a proper bastion host config subset" do + expect(knife.gateway_opts).to eq({ + bastion_user: nil, + bastion_host: "gateway", + bastion_port: 11234, + }) + end + end + + context "and ssh_gateway is not provided" do + it "returns an empty hash" do + expect(knife.gateway_opts).to eq({}) + end + end + end + end + + context "#sudo_opts" do + let(:connection_protocol) { nil } + let(:sudo_pass) { nil } + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "for winrm" do + let(:connection_protocol) { "winrm" } + it "returns an empty hash" do + expect(knife.sudo_opts(sudo_pass)).to eq({}) + end + end + + context "for ssh" do + let(:connection_protocol) { "ssh" } + context "when use_sudo is set" do + before do + knife.config[:use_sudo] = true + end + + it "returns a config that enables sudo" do + expect(knife.sudo_opts(sudo_pass)).to eq( { sudo: true } ) + end + + context "when use_sudo_password is also set" do + before do + knife.config[:use_sudo_password] = true + knife.config[:connection_password] = "opscode" + end + it "includes :connection_password value in a sudo-enabled configuration" do + expect(knife.sudo_opts(sudo_pass)).to eq({ + sudo: true, + sudo_password: "opscode", + }) + end + + context "when sudo_pass is set, connection_password is not set" do + before do + knife.config[:connection_password] = nil + end + let(:sudo_pass) { "progress" } + it "includes :connection_password value in a sudo-enabled configuration" do + expect(knife.sudo_opts(sudo_pass)).to eq({ + sudo: true, + sudo_password: "progress", + }) + end + end + + context "when connection_password is set, sudo_pass is not set" do + before do + knife.config[:connection_password] = "opscode" + end + let(:sudo_pass) { nil } + it "includes :connection_password value in a sudo-enabled configuration" do + expect(knife.sudo_opts(sudo_pass)).to eq({ + sudo: true, + sudo_password: "opscode", + }) + end + end + + context "when connection_password is not set, sudo_pass is not set" do + before do + knife.config[:connection_password] = nil + end + let(:sudo_pass) { nil } + it "includes :connection_password value in a sudo-enabled configuration" do + expect(knife.sudo_opts(sudo_pass)).to eq({ + sudo: true, + sudo_password: nil, + }) + end + end + + # connection_password will take precedence here + context "when connection_password is set, sudo_pass is set" do + before do + knife.config[:connection_password] = "opscode" + end + let(:sudo_pass) { "progress" } + it "includes :connection_password value in a sudo-enabled configuration" do + expect(knife.sudo_opts(sudo_pass)).to eq({ + sudo: true, + sudo_password: "opscode", + }) + end + end + + end + + context "when preserve_home is set" do + before do + knife.config[:preserve_home] = true + end + it "enables sudo with sudo_option to preserve home" do + expect(knife.sudo_opts(sudo_pass)).to eq({ + sudo_options: "-H", + sudo: true, + }) + end + end + end + + context "when use_sudo is not set" do + before do + knife.config[:use_sudo_password] = true + knife.config[:preserve_home] = true + end + it "returns configuration for sudo off, ignoring other related options" do + expect(knife.sudo_opts(sudo_pass)).to eq( { sudo: false } ) + end + end + end + end + + context "#ssh_opts" do + let(:connection_protocol) { nil } + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "for ssh" do + let(:connection_protocol) { "ssh" } + let(:default_opts) do + { + non_interactive: true, + forward_agent: false, + connection_timeout: 60, + } + end + + context "by default" do + it "returns a configuration hash with appropriate defaults" do + expect(knife.ssh_opts).to eq default_opts + end + end + + context "when ssh_forward_agent has a value" do + before do + knife.config[:ssh_forward_agent] = true + end + it "returns a default configuration hash with forward_agent set to true" do + expect(knife.ssh_opts).to eq(default_opts.merge(forward_agent: true)) + end + end + context "when session_timeout has a value" do + before do + knife.config[:session_timeout] = 120 + end + it "returns a default configuration hash with updated timeout value." do + expect(knife.ssh_opts).to eq(default_opts.merge(connection_timeout: 120)) + end + end + + end + + context "for winrm" do + let(:connection_protocol) { "winrm" } + it "returns an empty has because ssh is not winrm" do + expect(knife.ssh_opts).to eq({}) + end + end + + end + + context "#winrm_opts" do + let(:connection_protocol) { nil } + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "for winrm" do + let(:connection_protocol) { "winrm" } + let(:expected) do + { + winrm_transport: "negotiate", + winrm_basic_auth_only: false, + ssl: false, + ssl_peer_fingerprint: nil, + operation_timeout: 60, + } + end + + it "generates a correct configuration hash with expected defaults" do + expect(knife.winrm_opts).to eq expected + end + + context "with ssl_peer_fingerprint" do + let(:ssl_peer_fingerprint_expected) do + expected.merge({ ssl_peer_fingerprint: "ABCD" }) + end + + before do + knife.config[:winrm_ssl_peer_fingerprint] = "ABCD" + end + + it "generates a correct options hash with ssl_peer_fingerprint from the config provided" do + expect(knife.winrm_opts).to eq ssl_peer_fingerprint_expected + end + end + + context "with winrm_ssl" do + let(:ssl_expected) do + expected.merge({ ssl: true }) + end + before do + knife.config[:winrm_ssl] = true + end + + it "generates a correct options hash with ssl from the config provided" do + expect(knife.winrm_opts).to eq ssl_expected + end + end + + context "with winrm_auth_method" do + let(:winrm_auth_method_expected) do + expected.merge({ winrm_transport: "freeaccess" }) + end + + before do + knife.config[:winrm_auth_method] = "freeaccess" + end + + it "generates a correct options hash with winrm_transport from the config provided" do + expect(knife.winrm_opts).to eq winrm_auth_method_expected + end + end + + context "with ca_trust_file" do + let(:ca_trust_expected) do + expected.merge({ ca_trust_path: "/trust.me" }) + end + before do + knife.config[:ca_trust_file] = "/trust.me" + end + + it "generates a correct options hash with ca_trust_file from the config provided" do + expect(knife.winrm_opts).to eq ca_trust_expected + end + end + + context "with kerberos auth" do + let(:kerberos_expected) do + expected.merge({ + kerberos_service: "testsvc", + kerberos_realm: "TESTREALM", + winrm_transport: "kerberos", + }) + end + + before do + knife.config[:winrm_auth_method] = "kerberos" + knife.config[:kerberos_service] = "testsvc" + knife.config[:kerberos_realm] = "TESTREALM" + end + + it "generates a correct options hash containing kerberos auth configuration from the config provided" do + expect(knife.winrm_opts).to eq kerberos_expected + end + end + + context "with winrm_basic_auth_only" do + before do + knife.config[:winrm_basic_auth_only] = true + end + let(:basic_auth_expected) do + expected.merge( { winrm_basic_auth_only: true } ) + end + it "generates a correct options hash containing winrm_basic_auth_only from the config provided" do + expect(knife.winrm_opts).to eq basic_auth_expected + end + end + end + + context "for ssh" do + let(:connection_protocol) { "ssh" } + it "returns an empty hash because ssh is not winrm" do + expect(knife.winrm_opts).to eq({}) + end + end + end + describe "#run" do + it "performs the steps we expect to run a bootstrap" do + expect(knife).to receive(:check_license) + expect(knife).to receive(:validate_name_args!).ordered + expect(knife).to receive(:validate_protocol!).ordered + expect(knife).to receive(:validate_first_boot_attributes!).ordered + expect(knife).to receive(:validate_winrm_transport_opts!).ordered + expect(knife).to receive(:validate_policy_options!).ordered + expect(knife).to receive(:winrm_warn_no_ssl_verification).ordered + expect(knife).to receive(:warn_on_short_session_timeout).ordered + expect(knife).to receive(:connect!).ordered + expect(knife).to receive(:register_client).ordered + expect(knife).to receive(:render_template).and_return "content" + expect(knife).to receive(:upload_bootstrap).with("content").and_return "/remote/path.sh" + expect(knife).to receive(:perform_bootstrap).with("/remote/path.sh") + expect(connection).to receive(:del_file!) # Make sure cleanup happens + + knife.run + + # Post-run verify expected state changes (not many directly in #run) + expect($stdout.sync).to eq true + end + end + + describe "#register_client" do + let(:vault_handler_mock) { double("ChefVaultHandler") } + let(:client_builder_mock) { double("ClientBuilder") } + let(:node_name) { nil } + before do + allow(knife).to receive(:chef_vault_handler).and_return vault_handler_mock + allow(knife).to receive(:client_builder).and_return client_builder_mock + knife.config[:chef_node_name] = node_name + end + + shared_examples_for "creating the client locally" do + context "when a valid node name is present" do + let(:node_name) { "test" } + before do + allow(client_builder_mock).to receive(:client).and_return "client" + allow(client_builder_mock).to receive(:client_path).and_return "/key.pem" + end + + it "runs client_builder and vault_handler" do + expect(client_builder_mock).to receive(:run) + expect(vault_handler_mock).to receive(:run).with("client") + knife.register_client + end + + it "sets the path to the client key in the bootstrap context" do + allow(client_builder_mock).to receive(:run) + allow(vault_handler_mock).to receive(:run).with("client") + knife.register_client + expect(knife.bootstrap_context.client_pem).to eq "/key.pem" + end + end + + context "when no valid node name is present" do + let(:node_name) { nil } + it "shows an error and exits" do + expect(knife.ui).to receive(:error) + expect { knife.register_client }.to raise_error(SystemExit) + end + end + end + context "when chef_vault_handler says we're using vault" do + let(:vault_handler_mock) { double("ChefVaultHandler") } + before do + allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return true + end + it_behaves_like "creating the client locally" + end + + context "when an non-existant validation key is specified in chef config" do + before do + Chef::Config[:validation_key] = "/blah" + allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return false + allow(File).to receive(:exist?).with(%r{/blah}).and_return false + end + it_behaves_like "creating the client locally" + end + + context "when a valid validation key is given and we're doing old-style client creation" do + before do + Chef::Config[:validation_key] = "/blah" + allow(File).to receive(:exist?).with(%r{/blah}).and_return true + allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return false + end + + it "shows a warning message" do + expect(knife.ui).to receive(:warn).twice + knife.register_client + end + end + end + + describe "#perform_bootstrap" do + let(:exit_status) { 0 } + let(:stdout) { "" } + let(:result_mock) { double("result", exit_status: exit_status, stderr: "A message", stdout: stdout) } + + before do + allow(connection).to receive(:hostname).and_return "testhost" + end + it "runs the remote script and logs the output" do + expect(knife.ui).to receive(:info).with(/Bootstrapping.*/) + expect(knife).to receive(:bootstrap_command) + .with("/path.sh") + .and_return("sh /path.sh") + expect(connection) + .to receive(:run_command) + .with("sh /path.sh") + .and_yield("output here", nil) + .and_return result_mock + + expect(knife.ui).to receive(:msg).with(/testhost/) + knife.perform_bootstrap("/path.sh") + end + + context "when the remote command fails" do + let(:exit_status) { 1 } + it "shows an error and exits" do + expect(knife.ui).to receive(:info).with(/Bootstrapping.*/) + expect(knife).to receive(:bootstrap_command) + .with("/path.sh") + .and_return("sh /path.sh") + expect(connection).to receive(:run_command).with("sh /path.sh").and_return result_mock + expect { knife.perform_bootstrap("/path.sh") }.to raise_error(SystemExit) + end + end + + context "when the remote command failed due to su auth error" do + let(:exit_status) { 1 } + let(:stdout) { "su: Authentication failure" } + let(:connection_obj) { double("connection", transport_options: {}) } + it "shows an error and exits" do + allow(connection).to receive(:connection).and_return(connection_obj) + expect(knife.ui).to receive(:info).with(/Bootstrapping.*/) + expect(knife).to receive(:bootstrap_command) + .with("/path.sh") + .and_return("su - USER -c 'sh /path.sh'") + expect(connection) + .to receive(:run_command) + .with("su - USER -c 'sh /path.sh'") + .and_yield("output here", nil) + .and_raise(Train::UserError) + expect { knife.perform_bootstrap("/path.sh") }.to raise_error(Train::UserError) + end + end + end + + describe "#connect!" do + before do + # These are not required at run-time because train will handle its own + # protocol loading. In this case, we're simulating train failures and have to load + # them ourselves. + require "net/ssh" + require "train/transports/ssh" + end + + context "in the normal case" do + it "connects using the connection_opts and notifies the operator of progress" do + expect(knife.ui).to receive(:info).with(/Connecting to.*/) + expect(knife).to receive(:connection_opts).and_return( { opts: "here" }) + expect(knife).to receive(:do_connect).with( { opts: "here" } ) + knife.connect! + end + end + + context "when a general non-auth-failure occurs" do + let(:expected_error) { RuntimeError.new } + before do + allow(knife).to receive(:do_connect).and_raise(expected_error) + end + it "re-raises the exception" do + expect { knife.connect! }.to raise_error(expected_error) + end + end + + context "when ssh fingerprint is invalid" do + let(:expected_error) { Train::Error.new("fingerprint AA:BB is unknown for \"blah,127.0.0.1\"") } + before do + allow(knife).to receive(:do_connect).and_raise(expected_error) + end + it "warns, prompts to accept, then connects with verify_host_key of accept_new" do + expect(knife).to receive(:do_connect).and_raise(expected_error) + expect(knife.ui).to receive(:confirm) + .with(/.*host 'blah \(127.0.0.1\)'.*AA:BB.*Are you sure you want to continue.*/m) + .and_return(true) + expect(knife).to receive(:do_connect) do |opts| + expect(opts[:verify_host_key]).to eq :accept_new + end + knife.connect! + end + end + + context "when an auth failure occurs" do + let(:expected_error) do + e = Train::Error.new + actual = Net::SSH::AuthenticationFailed.new + # Simulate train's nested error - they wrap + # ssh/network errors in a TrainError. + allow(e).to receive(:cause).and_return(actual) + e + end + + let(:expected_error_password_prompt) do + e = Train::ClientError.new + reason = :no_ssh_password_or_key_available + allow(e).to receive(:reason).and_return(reason) + e + end + + let(:expected_error_password_prompt_winrm) do + e = RuntimeError.new + message = "password is a required option" + allow(e).to receive(:message).and_return(message) + e + end + + context "and password auth was used" do + before do + allow(connection).to receive(:password_auth?).and_return true + end + + it "re-raises the error so as not to resubmit the same failing password" do + expect(knife).to receive(:do_connect).and_raise(expected_error) + expect { knife.connect! }.to raise_error(expected_error) + end + end + + context "and password auth was not used" do + before do + allow(connection).to receive(:password_auth?).and_return false + allow(connection).to receive(:user).and_return "testuser" + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "when using ssh" do + let(:connection_protocol) { "ssh" } + + it "warns, prompts for password, then reconnects with a password-enabled configuration using the new password" do + expect(knife).to receive(:do_connect).and_raise(expected_error_password_prompt) + expect(knife.ui).to receive(:warn).with(/Failed to auth.*/) + expect(knife.ui).to receive(:ask).and_return("newpassword") + # Ensure that we set echo off to prevent showing password on the screen + expect(knife).to receive(:do_connect) do |opts| + expect(opts[:password]).to eq "newpassword" + end + knife.connect! + end + end + + context "when using winrm" do + let(:connection_protocol) { "winrm" } + + it "warns, prompts for password, then reconnects with a password-enabled configuration using the new password for" do + expect(knife).to receive(:do_connect).and_raise(expected_error_password_prompt_winrm) + expect(knife.ui).to receive(:warn).with(/Failed to auth.*/) + expect(knife.ui).to receive(:ask).and_return("newpassword") + # Ensure that we set echo off to prevent showing password on the screen + expect(knife).to receive(:do_connect) do |opts| + expect(opts[:password]).to eq "newpassword" + end + knife.connect! + end + end + end + end + end + + it "verifies that a server to bootstrap was given as a command line arg" do + knife.name_args = nil + expect(knife).to receive(:check_license) + expect { knife.run }.to raise_error(SystemExit) + expect(stderr.string).to match(/ERROR:.+FQDN or ip/) + end + + describe "#bootstrap_context" do + context "under Windows" do + let(:windows_test) { true } + it "creates a WindowsBootstrapContext" do + require "chef/knife/core/windows_bootstrap_context" + expect(knife.bootstrap_context.class).to eq Chef::Knife::Core::WindowsBootstrapContext + end + end + + context "under linux" do + let(:linux_test) { true } + it "creates a BootstrapContext" do + require "chef/knife/core/bootstrap_context" + expect(knife.bootstrap_context.class).to eq Chef::Knife::Core::BootstrapContext + end + end + end + + describe "#config_value" do + before do + knife.config[:test_key_a] = "a from cli" + knife.config[:test_key_b] = "b from cli" + Chef::Config[:knife][:test_key_a] = "a from Chef::Config" + Chef::Config[:knife][:test_key_c] = "c from Chef::Config" + Chef::Config[:knife][:alt_test_key_c] = "alt c from Chef::Config" + knife.merge_configs + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + it "returns the Chef::Config value from the cli when the CLI key is set" do + expect(knife.config_value(:test_key_a, :alt_test_key_c)).to eq "a from cli" + end + + it "returns the Chef::Config value from the alternative key when the CLI key is not set" do + expect(knife.config_value(:test_key_d, :alt_test_key_c)).to eq "alt c from Chef::Config" + end + + it "returns the default value when the key is not provided by CLI or Chef::Config" do + expect(knife.config_value(:missing_key, :missing_key, "found")).to eq "found" + end + end + + describe "#upload_bootstrap" do + before do + allow(connection).to receive(:temp_dir).and_return(temp_dir) + allow(connection).to receive(:normalize_path) { |a| a } + end + + let(:content) { "bootstrap script content" } + context "under Windows" do + let(:windows_test) { true } + let(:temp_dir) { "C:/Temp/bootstrap" } + it "creates a bat file in the temp dir provided by connection, using given content" do + expect(connection).to receive(:upload_file_content!).with(content, "C:/Temp/bootstrap/bootstrap.bat") + expect(knife.upload_bootstrap(content)).to eq "C:/Temp/bootstrap/bootstrap.bat" + end + end + + context "under Linux" do + let(:linux_test) { true } + let(:temp_dir) { "/tmp/bootstrap" } + it "creates a 'sh file in the temp dir provided by connection, using given content" do + expect(connection).to receive(:upload_file_content!).with(content, "/tmp/bootstrap/bootstrap.sh") + expect(knife.upload_bootstrap(content)).to eq "/tmp/bootstrap/bootstrap.sh" + end + end + end + + describe "#bootstrap_command" do + context "under Windows" do + let(:windows_test) { true } + it "prefixes the command to run under cmd.exe" do + expect(knife.bootstrap_command("autoexec.bat")).to eq "cmd.exe /C autoexec.bat" + end + + end + context "under Linux" do + let(:linux_test) { true } + it "prefixes the command to run under sh" do + expect(knife.bootstrap_command("bootstrap.sh")).to eq "sh bootstrap.sh" + end + + context "with --su-user option" do + let(:connection_obj) { double("connection", transport_options: {}) } + before do + knife.config[:su_user] = "root" + allow(connection).to receive(:connection).and_return(connection_obj) + end + it "prefixes the command to run using su -USER -c" do + expect(knife.bootstrap_command("bootstrap.sh")).to eq "su - #{knife.config[:su_user]} -c 'sh bootstrap.sh'" + expect(connection_obj.transport_options.key?(:pty)).to eq true + end + + it "sudo appended if --sudo option enabled" do + knife.config[:use_sudo] = true + expect(knife.bootstrap_command("bootstrap.sh")).to eq "sudo su - #{knife.config[:su_user]} -c 'sh bootstrap.sh'" + expect(connection_obj.transport_options.key?(:pty)).to eq true + end + end + end + end + + describe "#default_bootstrap_template" do + context "under Windows" do + let(:windows_test) { true } + it "is windows-chef-client-msi" do + expect(knife.default_bootstrap_template).to eq "windows-chef-client-msi" + end + + end + context "under Linux" do + let(:linux_test) { true } + it "is chef-full" do + expect(knife.default_bootstrap_template).to eq "chef-full" + end + end + end + + describe "#do_connect" do + let(:host_descriptor) { "example.com" } + let(:connection) { double("TrainConnector") } + let(:connector_mock) { double("TargetResolver", targets: [ connection ]) } + before do + allow(knife).to receive(:host_descriptor).and_return host_descriptor + end + + it "creates a TrainConnector and connects it" do + expect(Chef::Knife::Bootstrap::TrainConnector).to receive(:new).and_return connection + expect(connection).to receive(:connect!) + knife.do_connect({}) + end + + context "when sshd configured with requiretty" do + let(:pty_err_msg) { "Sudo requires a TTY. Please see the README on how to configure sudo to allow for non-interactive usage." } + let(:expected_error) { Train::UserError.new(pty_err_msg, :sudo_no_tty) } + before do + allow(connection).to receive(:connect!).and_raise(expected_error) + end + it "retry with pty true request option" do + expect(Chef::Knife::Bootstrap::TrainConnector).to receive(:new).and_return(connection).exactly(2).times + expect(knife.ui).to receive(:warn).with("#{pty_err_msg} - trying with pty request") + expect { knife.do_connect({}) }.to raise_error(expected_error) + end + end + + context "when a train sudo error is thrown for missing terminal" do + let(:ui_error_msg) { "Sudo password is required for this operation. Please enter password using -P or --ssh-password option" } + let(:expected_error) { Train::UserError.new(ui_error_msg, :sudo_missing_terminal) } + before do + allow(connection).to receive(:connect!).and_raise(expected_error) + end + it "outputs user friendly error message" do + expect { knife.do_connect({}) }.not_to raise_error + expect(stderr.string).to include(ui_error_msg) + end + end + + end + + describe "validate_winrm_transport_opts!" do + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "when using ssh" do + let(:connection_protocol) { "ssh" } + it "returns true" do + expect(knife.validate_winrm_transport_opts!).to eq true + end + end + context "when using winrm" do + let(:connection_protocol) { "winrm" } + context "with plaintext auth" do + before do + knife.config[:winrm_auth_method] = "plaintext" + end + context "with ssl" do + before do + knife.config[:winrm_ssl] = true + end + it "will not error because we won't send anything in plaintext regardless" do + expect(knife.validate_winrm_transport_opts!).to eq true + end + end + context "without ssl" do + before do + knife.config[:winrm_ssl] = false + end + context "and no validation key exists" do + before do + Chef::Config[:validation_key] = "validation_key.pem" + allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return false + end + + it "will error because we will generate and send a client key over the wire in plaintext" do + expect { knife.validate_winrm_transport_opts! }.to raise_error(SystemExit) + end + + end + context "and a validation key exists" do + before do + Chef::Config[:validation_key] = "validation_key.pem" + allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return true + end + # TODO - don't we still send validation key? + it "will not error because we don not send client key over the wire" do + expect(knife.validate_winrm_transport_opts!).to eq true + end + end + end + end + + context "with other auth" do + before do + knife.config[:winrm_auth_method] = "kerberos" + end + + context "and no validation key exists" do + before do + + Chef::Config[:validation_key] = "validation_key.pem" + allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return false + end + + it "will not error because we're not using plaintext auth" do + expect(knife.validate_winrm_transport_opts!).to eq true + end + end + context "and a validation key exists" do + before do + Chef::Config[:validation_key] = "validation_key.pem" + allow(File).to receive(:exist?).with(/.*validation_key.pem/).and_return true + end + + it "will not error because a client key won't be sent over the wire in plaintext when a validation key is present" do + expect(knife.validate_winrm_transport_opts!).to eq true + end + end + + end + + end + + end + + describe "#winrm_warn_no_ssl_verification" do + before do + allow(knife).to receive(:connection_protocol).and_return connection_protocol + end + + context "when using ssh" do + let(:connection_protocol) { "ssh" } + it "does not issue a warning" do + expect(knife.ui).to_not receive(:warn) + knife.winrm_warn_no_ssl_verification + end + end + context "when using winrm" do + let(:connection_protocol) { "winrm" } + context "winrm_no_verify_cert is set" do + before do + knife.config[:winrm_no_verify_cert] = true + end + + context "and ca_trust_file is present" do + before do + knife.config[:ca_trust_file] = "file" + end + + it "does not issue a warning" do + expect(knife.ui).to_not receive(:warn) + knife.winrm_warn_no_ssl_verification + end + end + + context "and winrm_ssl_peer_fingerprint is present" do + before do + knife.config[:winrm_ssl_peer_fingerprint] = "ABCD" + end + it "does not issue a warning" do + expect(knife.ui).to_not receive(:warn) + knife.winrm_warn_no_ssl_verification + end + end + context "and neither ca_trust_file nor winrm_ssl_peer_fingerprint is present" do + it "issues a warning" do + expect(knife.ui).to receive(:warn) + knife.winrm_warn_no_ssl_verification + end + end + end + end + end + + describe "#warn_on_short_session_timeout" do + let(:session_timeout) { 60 } + + before do + allow(knife).to receive(:session_timeout).and_return(session_timeout) + end + + context "timeout is not set at all" do + let(:session_timeout) { nil } + it "does not issue a warning" do + expect(knife.ui).to_not receive(:warn) + knife.warn_on_short_session_timeout + end + end + + context "timeout is more than 15" do + let(:session_timeout) { 16 } + it "does not issue a warning" do + expect(knife.ui).to_not receive(:warn) + knife.warn_on_short_session_timeout + end + end + context "timeout is 15 or less" do + let(:session_timeout) { 15 } + it "issues a warning" do + expect(knife.ui).to receive(:warn) + knife.warn_on_short_session_timeout + end + end + end +end