diff options
author | Tim Smith <tsmith@chef.io> | 2021-02-02 14:01:43 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-02 14:01:43 -0800 |
commit | d118631c60569c049abe323e635463e7719758fe (patch) | |
tree | 128d4a1bdfb301e454c34f73b528bfc8b0b931fa | |
parent | 93a4bd6a3206637b32b5248bf82605efb303e2a8 (diff) | |
parent | 36dbbdc8494401e8859aec7e45b468d82454a013 (diff) | |
download | chef-d118631c60569c049abe323e635463e7719758fe.tar.gz |
Merge pull request #10971 from chef/su
handles su - USER session to perform bootstrap
-rw-r--r-- | lib/chef/knife/bootstrap.rb | 58 | ||||
-rw-r--r-- | spec/unit/knife/bootstrap_spec.rb | 45 |
2 files changed, 96 insertions, 7 deletions
diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index 1550c62dc1..340ffaecfd 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -217,6 +217,16 @@ class Chef description: "Execute the bootstrap via sudo with password.", boolean: false + # runtime - su user + option :su_user, + long: "--su-user NAME", + description: "The su - USER name to perform bootstrap command using a non-root user." + + # runtime - su user password + option :su_password, + long: "--su-password PASSWORD", + description: "The su USER password for authentication." + # runtime - client_builder option :chef_node_name, short: "-N NAME", @@ -591,13 +601,31 @@ class Chef def perform_bootstrap(remote_bootstrap_script_path) ui.info("Bootstrapping #{ui.color(server_name, :bold)}") cmd = bootstrap_command(remote_bootstrap_script_path) - r = connection.run_command(cmd) do |data| + bootstrap_run_command(cmd) + end + + # Actual bootstrap command to be run on the node. + # Handles recursive calls if su USER failed to authenticate. + def bootstrap_run_command(cmd) + r = connection.run_command(cmd) do |data, channel| ui.msg("#{ui.color(" [#{connection.hostname}]", :cyan)} #{data}") + channel.send_data("#{config[:su_password] || config[:connection_password]}\n") if data.match?("Password:") end + if r.exit_status != 0 ui.error("The following error occurred on #{server_name}:") - ui.error(r.stderr) - exit 1 + ui.error("#{r.stdout} #{r.stderr}".strip) + exit(r.exit_status) + end + rescue Train::UserError => e + limit ||= 0 + if e.reason == :bad_su_user_password && limit < 3 + limit += 1 + ui.warn("Failed to authenticate su - #{config[:su_user]} to #{server_name}") + config[:su_password] = ui.ask("Enter password for su - #{config[:su_user]}@#{server_name}:", echo: false) + retry + else + raise end end @@ -1082,7 +1110,17 @@ class Chef if connection.windows? "cmd.exe /C #{remote_path}" else - "sh #{remote_path}" + cmd = "sh #{remote_path}" + + if config[:su_user] + # su - USER is subject to required an interactive console + # Otherwise, it will raise: su: must be run from a terminal + set_transport_options(pty: true) + cmd = "su - #{config[:su_user]} -c '#{cmd}'" + cmd = "sudo " << cmd if config[:use_sudo] + end + + cmd end end @@ -1137,6 +1175,18 @@ class Chef timeout.to_i end + + # Train::Transports::SSH::Connection#transport_options + # Append the options to connection transport_options + # + # @param opts [Hash] the opts to be added to connection transport_options. + # @return [Hash] transport_options if the opts contains any option to be set. + # + def set_transport_options(opts) + return unless opts.is_a?(Hash) || !opts.empty? + + connection&.connection&.transport_options&.merge! opts + end end end end diff --git a/spec/unit/knife/bootstrap_spec.rb b/spec/unit/knife/bootstrap_spec.rb index 3797207b6d..294942c229 100644 --- a/spec/unit/knife/bootstrap_spec.rb +++ b/spec/unit/knife/bootstrap_spec.rb @@ -1726,7 +1726,8 @@ describe Chef::Knife::Bootstrap do describe "#perform_bootstrap" do let(:exit_status) { 0 } - let(:result_mock) { double("result", exit_status: exit_status, stderr: "A message") } + 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" @@ -1739,12 +1740,13 @@ describe Chef::Knife::Bootstrap do expect(connection) .to receive(:run_command) .with("sh /path.sh") - .and_yield("output here") + .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 @@ -1756,6 +1758,25 @@ describe Chef::Knife::Bootstrap do 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 @@ -1964,7 +1985,25 @@ describe Chef::Knife::Bootstrap do context "under Linux" do let(:linux_test) { true } it "prefixes the command to run under sh" do - expect(knife.bootstrap_command("bootstrap")).to eq "sh bootstrap" + 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 |