summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorMarc A. Paradise <marc.paradise@gmail.com>2019-02-20 10:55:01 -0500
committerMarc A. Paradise <marc.paradise@gmail.com>2019-04-24 13:25:58 -0400
commitdf96e7420dd1c258c794d2181d911add3eea8c47 (patch)
treee09cf59c7f3f5b3291846122a18e9c4f08e5f2f0 /spec
parentf492fe53eac1b74a0d184f0e9cf7412b70770e29 (diff)
downloadchef-df96e7420dd1c258c794d2181d911add3eea8c47.tar.gz
Bootstrap via chef_core
Make bootstrap use train via chef_core/TargetHost This commit implements usage of TargetHost instead of knife_ssh. TargetHost is a platform-independent representation of a Train connection. It abstracts common operations (such as file upload/download, permissions, temp directories, command executation, etc) and connection error handling. Moving to TargetHost and train gives us the ability to execute commands on the bootstrap target; instead of running sh -c 'long-command-string-containing-secrets', we'll now upload the bootstrap script to a temporary directory on the bootstrap target and execute it there. Incorporating WinRM support directly (allowing core support for Windows bootstraps, without the knife-windows plugin) will follow. THis also improves unit test coverage of the bootstrap module, and clarifies functional versus behavior tests in that space Signed-off-by: Marc A. Paradise <marc.paradise@gmail.com>
Diffstat (limited to 'spec')
-rw-r--r--spec/unit/knife/bootstrap_spec.rb1583
1 files changed, 1373 insertions, 210 deletions
diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb
index 258d193cf1..1c6943f2fe 100644
--- a/spec/unit/knife/bootstrap_spec.rb
+++ b/spec/unit/knife/bootstrap_spec.rb
@@ -22,33 +22,36 @@ Chef::Knife::Bootstrap.load_deps
require "net/ssh"
describe Chef::Knife::Bootstrap do
- before do
- allow(ChefConfig).to receive(:windows?) { false }
- end
+ let(:bootstrap_template) { nil }
+ let(:stderr) { StringIO.new }
+ let(:bootstrap_cli_options) { [ ] }
+ let(:base_os) { :linux }
+ let(:target_host) { double("TargetHost") }
+
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)
- k.merge_configs
-
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(:target_host).and_return target_host
+ k.merge_configs
k
end
- let(:stderr) { StringIO.new }
-
- let(:bootstrap_template) { nil }
-
- let(:bootstrap_cli_options) { [ ] }
+ before do
+ allow(target_host).to receive(:base_os).and_return base_os
+ end
- it "should use chef-full as default template" do
- expect(knife.bootstrap_template).to be_a_kind_of(String)
- expect(File.basename(knife.bootstrap_template)).to eq("chef-full")
+ 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 "when using the chef-full default template" do
+ context "#render_template - when using the chef-full default template" do
let(:rendered_template) do
knife.merge_configs
knife.render_template
@@ -284,14 +287,14 @@ describe Chef::Knife::Bootstrap do
jsonfile.close
end
- context "when --json-attributes and --json-attribute-file were both passed" do
- 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
- expect { knife.run }.to raise_error(Chef::Exceptions::BootstrapCommandInputError)
- 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.run }.to raise_error(Chef::Exceptions::BootstrapCommandInputError)
+ jsonfile.close
end
end
end
@@ -317,13 +320,16 @@ describe Chef::Knife::Bootstrap do
subject(:knife) do
k = described_class.new
Chef::Config[:knife][:bootstrap_template] = template_file
+ allow(k).to receive(:target_host).and_return target_host
k.parse_options(options)
k.merge_configs
k
end
let(:options) { ["--bootstrap-no-proxy", setting, "-s", "foo"] }
+
let(:template_file) { File.expand_path(File.join(CHEF_SPEC_DATA, "bootstrap", "no_proxy.erb")) }
+
let(:rendered_template) do
knife.render_template
end
@@ -506,48 +512,153 @@ describe Chef::Knife::Bootstrap do
context "when client_d_dir is set" do
let(:client_d_dir) do
Chef::Util::PathHelper.cleanpath(
- File.join(File.dirname(__FILE__), "../../data/client.d_00")) end
+ File.join(File.dirname(__FILE__), "../../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(File.dirname(__FILE__), "../../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
+ 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 "creates /etc/chef/client.d" do
- expect(rendered_template).to match("mkdir -p /etc/chef/client\.d")
+ it "returns the value provided by the host argument" do
+ expect(knife.connection_protocol).to eq "ssh"
end
+ 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'\\\\''")
+ 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
- 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
+
+ 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
- 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
+
+ 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 "a nested directory structure" do
- let(:client_d_dir) do
- Chef::Util::PathHelper.cleanpath(
- File.join(File.dirname(__FILE__), "../../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")
+ 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 "handling policyfile options" do
+ 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_options! }.to raise_error(SystemExit)
+ 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
@@ -558,7 +669,7 @@ describe Chef::Knife::Bootstrap 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_options! }.to raise_error(SystemExit)
+ 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
@@ -569,7 +680,7 @@ describe Chef::Knife::Bootstrap 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_options! }.to raise_error(SystemExit)
+ expect { knife.validate_policy_options! }.to raise_error(SystemExit)
expect(stderr.string).to include("ERROR: Policyfile options and --run-list are exclusive")
end
@@ -580,7 +691,7 @@ describe Chef::Knife::Bootstrap do
let(:bootstrap_cli_options) { %w{ --policy-name my-app --policy-group staging } }
it "passes options validation" do
- expect { knife.validate_options! }.to_not raise_error
+ expect { knife.validate_policy_options! }.to_not raise_error
end
it "passes them into the bootstrap context" do
@@ -598,267 +709,1319 @@ describe Chef::Knife::Bootstrap do
# 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_options! }.to_not raise_error
+ 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) {
+ { base_opts: true,
+ ssh_identity_opts: true,
+ ssh_opts: true,
+ gateway_opts: true,
+ host_verify_opts: true,
+ sudo_opts: true,
+ winrm_opts: true }
+ }
+
+ 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
+ Chef::Config[:knife][:max_wait] = 9999
+ 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][:winrm_session_timeout] = 9999
+ Chef::Config[:knife][:winrm_ssl] = true
+ Chef::Config[:knife][:winrm_ssl_peer_fingerprint] = "ABCDEF"
+ 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][:winrm_password] = "blah"
+ end
+ it "does not include the corresponding option in the connection options" do
+ expect(knife.connection_opts.key?(:password)).to eq false
+ end
+ end
+
+ context "and no CLI options have been given" do
+ before do
+ knife.config = {}
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ ca_trust_file: "trust.me",
+ max_wait_until_ready: 9999,
+ 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"
+ }
+ }
+
+ it "generates a config hash using the Chef::Config values" do
+ expect(knife.connection_opts).to match expected_result
+ end
+
+ end
+
+ context "and some CLI options have been given" do
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ ca_trust_file: "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"
+ }
+ }
+
+ 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[:password] = "lobster"
+ end
+
+ it "generates a config hash using the CLI options when available and falling back to Chef::Config values" do
+ 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[:password] = "blue"
+ knife.config[:max_wait] = 1000
+ 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[:winrm_session_timeout] = 1000
+ knife.config[:winrm_ssl] = false
+ knife.config[:winrm_ssl_peer_fingerprint] = "FEDCBA"
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ ca_trust_file: "trust.the.internet",
+ max_wait_until_ready: 1000,
+ 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"
+ }
+ }
+ it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do
+ 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
+ knife.config = {}
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log,
+ operation_timeout: 60,
+ self_signed: false,
+ ssl: false,
+ ssl_peer_fingerprint: nil,
+ winrm_basic_auth_only: false,
+ winrm_transport: "negotiate"
+ }
+ }
+ it "populates appropriate defaults" do
+ 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
+ Chef::Config[:knife][:max_wait] = 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
+ before do
+ knife.config = {}
+ end
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ max_wait_until_ready: 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: false,
+ port: 9999
+ }
+ }
+
+ it "generates a correct config hash using the Chef::Config values" do
+ 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
+ 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[: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[:password] = "feta cheese"
+ knife.config[:max_wait] = 150
+ knife.config[:use_sudo] = true
+ knife.config[:use_sudo_pasword] = true
+ knife.config[:ssh_forward_agent] = true
+ end
+
+ let(:expected_result) {
+ {
+ logger: Chef::Log, # not configurable
+ max_wait_until_ready: 150, #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: false, # Config
+ port: 12 # cli
+ }
+ }
+
+
+ it "generates a config hash using the CLI options when available and falling back to Chef::Config values" do
+ expect(knife.connection_opts).to match expected_result
+ end
+ end
+
+ context "and all CLI options have been given" do
+ before do
+ knife.config[:max_wait] = 150
+ knife.config[:connection_user] = "sshroot"
+ knife.config[:connection_port] = 1000
+ knife.config[: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) {
+ {
+ logger: Chef::Log, # not configurable
+ max_wait_until_ready: 150,
+ 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,
+ }
+ }
+ it "generates a config hash using the CLI options and pulling nothing from Chef::Config" do
+ 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
+ knife.config = {}
+ end
+ let(:expected_result) {
+ {
+ forward_agent: false,
+ key_files: [],
+ logger: Chef::Log,
+ keys_only: false,
+ sudo: false,
+ verify_host_key: true
+ }
+ }
+ it "populates appropriate defaults" do
+ 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 "when determining knife config keys for user and port" do
+ let(:connection_protocol) { "fake" }
+ it "uses the protocol name to resolve the knife config keys" do
+ allow(knife).to receive(:config_value).with(:max_wait)
+
+ expect(knife).to receive(:config_value).with(:connection_port, :fake_port)
+ expect(knife).to receive(:config_value).with(:connection_user, :fake_user)
+ knife.base_opts
+ end
+ 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[:password] = "opscode"
+ end
+
+ let(:expected_opts) {
+ {
+ port: 250,
+ user: "test",
+ logger: Chef::Log,
+ password: "opscode"
+ }
+ }
+ 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) {
+ {
+ port: 250,
+ user: "test",
+ logger: Chef::Log
+ }
+ }
+ it "generates the correct options" do
+ expect(knife.base_opts).to eq expected_opts
+ end
+ end
+ end
end
- describe "when configuring the underlying knife ssh command" do
- context "from the command line" do
- let(:knife_ssh) do
- knife.name_args = ["foo.example.com"]
- knife.config[:ssh_user] = "rooty"
- knife.config[:ssh_port] = "4001"
- knife.config[:ssh_password] = "open_sesame"
- Chef::Config[:knife][:ssh_user] = nil
- Chef::Config[:knife][:ssh_port] = nil
- knife.config[:forward_agent] = true
- knife.config[:ssh_identity_file] = "~/.ssh/me.rsa"
- allow(knife).to receive(:render_template).and_return("")
- knife.knife_ssh
+ 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
- it "configures the hostname" do
- expect(knife_ssh.name_args.first).to eq("foo.example.com")
+ 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: true } )
+ 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
- it "configures the ssh user" do
- expect(knife_ssh.config[:ssh_user]).to eq("rooty")
+ 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[: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
- it "configures the ssh password" do
- expect(knife_ssh.config[:ssh_password]).to eq("open_sesame")
+ 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
- it "configures the ssh port" do
- expect(knife_ssh.config[:ssh_port]).to eq("4001")
+ 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
- it "configures the ssh agent forwarding" do
- expect(knife_ssh.config[:forward_agent]).to eq(true)
+ 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
- it "configures the ssh identity file" do
- expect(knife_ssh.config[:ssh_identity_file]).to eq("~/.ssh/me.rsa")
+ 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 "validating use_sudo_password" do
- before do
- knife.config[:ssh_password] = "password"
- allow(knife).to receive(:render_template).and_return("")
+ 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
- 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(""))
+ context "when use_sudo_password is also set" do
+ before do
+ knife.config[:use_sudo_password] = true
+ knife.config[:password] = "opscode"
+ end
+ it "includes :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
- it "uses the password from --ssh-password for sudo when --use-sudo-password is set" do
- knife.config[:use_sudo] = true
- knife.config[:use_sudo_password] = true
- expect(knife.ssh_command).to include("echo \'#{knife.config[:ssh_password]}\' | sudo -S")
+ 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
- it "should not honor --use-sudo-password when --use-sudo is not set" do
- knife.config[:use_sudo] = false
- knife.config[:use_sudo_password] = true
- expect(knife.ssh_command).not_to include("echo #{knife.config[:ssh_password]} | sudo -S")
+ context "for ssh" do
+ let(:connection_protocol) { "ssh" }
+ context "when ssh_forward_agent has a value" do
+ before do
+ knife.config[:ssh_forward_agent] = true
+ end
+ it "returns a configuration hash with forward_agent set to true" do
+ expect(knife.ssh_opts).to eq({ forward_agent: true })
+ end
+ end
+ context "when ssh_forward_agent is not set" do
+ it "returns a configuration hash with forward_agent set to false" do
+ expect(knife.ssh_opts).to eq({ forward_agent: false })
+ end
end
end
- context "from the knife config file" do
- let(:knife_ssh) do
- knife.name_args = ["config.example.com"]
- Chef::Config[:knife][:ssh_user] = "curiosity"
- Chef::Config[:knife][:ssh_port] = "2430"
- Chef::Config[:knife][:forward_agent] = true
- Chef::Config[:knife][:ssh_identity_file] = "~/.ssh/you.rsa"
- Chef::Config[:knife][:ssh_gateway] = "towel.blinkenlights.nl"
- Chef::Config[:knife][:ssh_gateway_identity] = "~/.ssh/gateway.rsa"
- Chef::Config[:knife][:host_key_verify] = true
- allow(knife).to receive(:render_template).and_return("")
- knife.config = {}
- knife.merge_configs
- knife.knife_ssh
+ 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
- it "configures the ssh user" do
- expect(knife_ssh.config[:ssh_user]).to eq("curiosity")
+ 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) { {
+ winrm_transport: "negotiate",
+ winrm_basic_auth_only: false,
+ ssl: false,
+ ssl_peer_fingerprint: nil,
+ operation_timeout: 60,
+ }}
+
+ it "generates a correct configuration hash with expected defaults" do
+ expect(knife.winrm_opts).to eq expected
end
- it "configures the ssh port" do
- expect(knife_ssh.config[:ssh_port]).to eq("2430")
+ context "with ssl_peer_fingerprint" do
+ let(:ssl_peer_fingerprint_expected) {
+ expected.merge({ ssl_peer_fingerprint: "ABCD"})
+ }
+
+ 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
- it "configures the ssh agent forwarding" do
- expect(knife_ssh.config[:forward_agent]).to eq(true)
+ context "with winrm_ssl" do
+ let(:ssl_expected) {
+ expected.merge({ ssl: true })
+ }
+ 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
- it "configures the ssh identity file" do
- expect(knife_ssh.config[:ssh_identity_file]).to eq("~/.ssh/you.rsa")
+ context "with winrm_auth_method" do
+ let(:winrm_auth_method_expected) {
+ expected.merge({ winrm_transport: "freeaccess" })
+ }
+
+ 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
- it "configures the ssh gateway" do
- expect(knife_ssh.config[:ssh_gateway]).to eq("towel.blinkenlights.nl")
+ context "with ca_trust_file" do
+ let(:ca_trust_expected) {
+ expected.merge({ ca_trust_file: "/trust.me"})
+ }
+ 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
- it "configures the ssh gateway identity" do
- expect(knife_ssh.config[:ssh_gateway_identity]).to eq("~/.ssh/gateway.rsa")
+ context "with kerberos auth" do
+ let(:kerberos_expected) {
+ expected.merge({
+ kerberos_service: "testsvc",
+ kerberos_realm: "TESTREALM",
+ winrm_transport: "kerberos"
+ })
+ }
+
+ 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
- it "configures the host key verify mode" do
- expect(knife_ssh.config[:host_key_verify]).to eq(true)
+ context "with winrm_basic_auth_only" do
+ before do
+ knife.config[:winrm_basic_auth_only] = true
+ end
+ let(:basic_auth_expected) {
+ expected.merge( { winrm_basic_auth_only: true } )
+ }
+ 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
- describe "when falling back to password auth when host key auth fails" do
- let(:knife_ssh_with_password_auth) do
- knife.name_args = ["foo.example.com"]
- knife.config[:ssh_user] = "rooty"
- knife.config[:ssh_identity_file] = "~/.ssh/me.rsa"
- allow(knife).to receive(:render_template).and_return("")
- k = knife.knife_ssh
- allow(k).to receive(:get_password).and_return("typed_in_password")
- allow(knife).to receive(:knife_ssh).and_return(k)
- knife.knife_ssh_with_password_auth
+ 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
+ before do
+ allow(knife.client_builder).to receive(:client_path).and_return("/key.pem")
+ end
+
+ it "performs the steps we expect to run a bootstrap" do
+ 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(:register_client).ordered
+ expect(knife).to receive(:connect!).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(target_host).to receive(:del_file) # Make sure cleanup happens
+
+ knife.run
+
+ # Post-run verify expected state changes (not many directly in #run)
+ expect(knife.bootstrap_context.client_pem).to eq "/key.pem"
+ 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"
+ 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
+ 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("/blah").and_return false
+ end
+ it_behaves_like "creating the client locally"
+ end
- it "prompts the user for a password " do
- expect(knife_ssh_with_password_auth.config[:ssh_password]).to eq("typed_in_password")
+ 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("/blah").and_return true
+ allow(vault_handler_mock).to receive(:doing_chef_vault?).and_return false
end
- it "configures knife not to use the identity file that didn't work previously" do
- expect(knife_ssh_with_password_auth.config[:ssh_identity_file]).to be_nil
+ it "shows a message" do
+ expect(knife.ui).to receive(:info)
+ knife.register_client
end
end
end
- it "verifies that a server to bootstrap was given as a command line arg" do
- knife.name_args = nil
- expect { knife.run }.to raise_error(SystemExit)
- expect(stderr.string).to match(/ERROR:.+FQDN or ip/)
+ describe "#perform_bootstrap" do
+ let(:exit_status) { 0 }
+ let(:result_mock) { double("result", exit_status: exit_status, stderr: "A message") }
+
+ before do
+ allow(target_host).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(target_host).
+ to receive(:run_command).
+ with("sh /path.sh").
+ and_yield("output here").
+ 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(target_host).to receive(:run_command).with("sh /path.sh").and_return result_mock
+ expect{knife.perform_bootstrap("/path.sh")}.to raise_error(SystemExit)
+ end
+ end
end
- describe "when running the bootstrap" do
- let(:knife_ssh) do
- knife.name_args = ["foo.example.com"]
- knife.config[:chef_node_name] = "foo.example.com"
- knife.config[:ssh_user] = "rooty"
- knife.config[:ssh_identity_file] = "~/.ssh/me.rsa"
- allow(knife).to receive(:render_template).and_return("")
- knife_ssh = knife.knife_ssh
- allow(knife).to receive(:knife_ssh).and_return(knife_ssh)
- knife_ssh
+
+ describe "#connect!" do
+ 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 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
- let(:client) { Chef::ApiClient.new }
- context "when running with a configured and present validation key" do
+ context "when an auth failure occurs" do
+ let(:expected_error) {
+ # TODO This is awkward and ugly. Requires some refactor of chef_core/error
+ # to make it not so. See comment in rescue block of connect! for details.
+ e = RuntimeError.new
+ interim = RuntimeError.new
+ actual = Net::SSH::AuthenticationFailed.new
+ allow(interim).to receive(:cause).and_return(actual)
+ allow(e).to receive(:cause).and_return(interim)
+ e
+ }
+
before do
- # this tests runs the old code path where we have a validation key, so we need to pass that check
- allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true)
+ require 'net/ssh'
end
- it "configures the underlying ssh command and then runs it" do
- expect(knife_ssh).to receive(:run)
- knife.run
+ context "and password auth was used" do
+ before do
+ knife.config[:password] = "tryme"
+ 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
- it "falls back to password based auth when auth fails the first time" do
- allow(knife).to receive(:puts)
+ context "and password auth was not used" do
+ before do
+ knife.config[:password] = nil
+ allow(target_host).to receive(:user).and_return "testuser"
+ end
- fallback_knife_ssh = knife_ssh.dup
- expect(knife_ssh).to receive(:run).and_raise(Net::SSH::AuthenticationFailed.new("no ssh for you"))
- allow(knife).to receive(:knife_ssh_with_password_auth).and_return(fallback_knife_ssh)
- expect(fallback_knife_ssh).to receive(:run)
- knife.run
+ it "warns, prompts for password, then reconnects with a password-enabled configuration using the new password" do
+ question_mock = double("question")
+ expect(knife).to receive(:do_connect).and_raise(expected_error)
+ expect(knife.ui).to receive(:warn).with(/Failed to auth.*/)
+ expect(knife.ui).to receive(:ask).and_yield(question_mock).and_return("newpassword")
+ # Ensure that we set echo off to prevent showing password on the screen
+ expect(question_mock).to receive(:echo=).with false
+ expect(knife).to receive(:do_connect) do |opts|
+ expect(opts[:password]).to eq "newpassword"
+ end
+ knife.connect!
+ 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.run }.to raise_error(SystemExit)
+ expect(stderr.string).to match(/ERROR:.+FQDN or ip/)
+ end
- it "raises the exception if config[:ssh_password] is set and an authentication exception is raised" do
- knife.config[:ssh_password] = "password"
- expect(knife_ssh).to receive(:run).and_raise(Net::SSH::AuthenticationFailed)
- expect { knife.run }.to raise_error(Net::SSH::AuthenticationFailed)
+ describe "#bootstrap_context" do
+ context "under Windows" do
+ let(:base_os) { :windows }
+ 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
- it "creates the client and adds chef-vault items if vault_list is set" do
- knife.config[:bootstrap_vault_file] = "/not/our/responsibility/to/check/if/this/exists"
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).to receive(:run)
- expect(knife.client_builder).to receive(:client).and_return(client)
- expect(knife.chef_vault_handler).to receive(:run).with(client)
- knife.run
+ context "under linux" do
+ let(:base_os) { :linux }
+ 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
- it "creates the client and adds chef-vault items if vault_items is set" do
- knife.config[:bootstrap_vault_json] = '{ "vault" => "item" }'
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).to receive(:run)
- expect(knife.client_builder).to receive(:client).and_return(client)
- expect(knife.chef_vault_handler).to receive(:run).with(client)
- knife.run
+
+ 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"
+ end
+
+ it "returns CLI value when key is only provided by the CLI" do
+ expect(knife.config_value(:test_key_b)).to eq "b from cli"
+ end
+
+ it "returns CLI value when key is provided by CLI and Chef::Config" do
+ expect(knife.config_value(:test_key_a)).to eq "a from cli"
+ end
+
+ it "returns Chef::Config value whent he key is only provided by Chef::Config" do
+ expect(knife.config_value(:test_key_c)).to eq "c from Chef::Config"
+ 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_c, :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(target_host).to receive(:temp_dir).and_return(temp_dir)
+ allow(target_host).to receive(:normalize_path) { |a| a }
+ end
+
+ let(:content) { "bootstrap script content" }
+ context "under Windows" do
+ let(:base_os) { :windows }
+ let(:temp_dir) { "C:/Temp/bootstrap" }
+ it "creates a bat file in the temp dir provided by target_host, using given content" do
+ expect(target_host).to receive(:save_as_remote_file).with(content, "C:/Temp/bootstrap/bootstrap.bat")
+ expect(knife.upload_bootstrap(content)).to eq "C:/Temp/bootstrap/bootstrap.bat"
end
+ end
- it "does old-style validation without creating a client key if vault_list+vault_items is not set" do
- expect(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(true)
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).not_to receive(:run)
- expect(knife.chef_vault_handler).not_to receive(:run)
- knife.run
+ context "under Linux" do
+ let(:base_os) { :linux }
+ let(:temp_dir) { "/tmp/bootstrap" }
+ it "creates a 'sh file in the temp dir provided by target_host, using given content" do
+ expect(target_host).to receive(:save_as_remote_file).with(content, "/tmp/bootstrap/bootstrap.sh")
+ expect(knife.upload_bootstrap(content)).to eq "/tmp/bootstrap/bootstrap.sh"
end
+ end
+ end
- it "raises an exception if the config[:chef_node_name] is not present" do
- knife.config[:chef_node_name] = nil
+ describe "#bootstrap_command" do
+ context "under Windows" do
+ let(:base_os) { :windows }
+ it "prefixes the command to run under cmd.exe" do
+ expect(knife.bootstrap_command("autoexec.bat")).to eq "cmd.exe /C autoexec.bat"
+ end
- expect { knife.run }.to raise_error(SystemExit)
+ end
+ context "under Linux" do
+ let(:base_os) { :linux }
+ it "prefixes the command to run under sh" do
+ expect(knife.bootstrap_command("bootstrap")).to eq "sh bootstrap"
end
end
+ end
- context "when the validation key is not present" do
- before do
- # this tests runs the old code path where we have a validation key, so we need to pass that check
- allow(File).to receive(:exist?).with(File.expand_path(Chef::Config[:validation_key])).and_return(false)
+
+ describe "#default_bootstrap_template" do
+ context "under Windows" do
+ let(:base_os) { :windows }
+ it "is windows-chef-client-msi" do
+ expect(knife.default_bootstrap_template).to eq "windows-chef-client-msi"
end
- it "creates the client (and possibly adds chef-vault items)" do
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).to receive(:run)
- expect(knife.client_builder).to receive(:client).and_return(client)
- expect(knife.chef_vault_handler).to receive(:run).with(client)
- knife.run
+ end
+ context "under Linux" do
+ let(:base_os) { :linux }
+ 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(:target_host) { double("TargetHost") }
+ let(:resolver_mock) { double("TargetResolver", targets: [ target_host ]) }
+ before do
+ allow(knife).to receive(:host_descriptor).and_return host_descriptor
+ end
- it "raises an exception if the config[:chef_node_name] is not present" do
- knife.config[:chef_node_name] = nil
+ it "resolves the target and connects it" do
+ expect(ChefCore::TargetResolver).to receive(:new).and_return resolver_mock
+ expect(target_host).to receive(:connect!)
+ knife.do_connect({})
+ end
+ end
- expect { knife.run }.to raise_error(SystemExit)
+ 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
- context "when the validation_key is nil" do
- before do
- # this tests runs the old code path where we have a validation key, so we need to pass that check for some plugins
- Chef::Config[:validation_key] = nil
+ 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
- it "creates the client and does not run client_builder or the chef_vault_handler" do
- expect(knife_ssh).to receive(:run)
- expect(knife.client_builder).not_to receive(:run)
- expect(knife.chef_vault_handler).not_to receive(:run)
- knife.run
+ 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 "specifying ssl verification" do
+ 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
end
+
+