In some daemonized tasks, we can restart the process since it is idempotent New command is added to the daemonizer2 which allows us to easily restart services like this: vm.sshable.d_restart("/path/to/executable")
194 lines
6.8 KiB
Ruby
194 lines
6.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "spec_helper"
|
|
|
|
RSpec.describe Sshable do
|
|
# Avoid using excessive entropy by using one generated key for all
|
|
# tests.
|
|
key = SshKey.generate.keypair.freeze
|
|
|
|
subject(:sa) {
|
|
described_class.new(
|
|
id: described_class.generate_uuid,
|
|
host: "test.localhost",
|
|
unix_user: "testuser",
|
|
raw_private_key_1: key
|
|
)
|
|
}
|
|
|
|
it "can encrypt and decrypt a field" do
|
|
sa.save_changes
|
|
|
|
expect(sa.values[:raw_private_key_1] =~ /\AA[AgQ]..A/).not_to be_nil
|
|
expect(sa.raw_private_key_1).to eq(key)
|
|
end
|
|
|
|
describe "caching" do
|
|
# The cache is thread local, so re-set the thread state by boxing
|
|
# each test in a new thread.
|
|
around do |ex|
|
|
Thread.new {
|
|
ex.run
|
|
}.join
|
|
end
|
|
|
|
it "can cache SSH connections" do
|
|
expect(Net::SSH).to receive(:start) do
|
|
instance_double(Net::SSH::Connection::Session, close: nil)
|
|
end
|
|
|
|
expect(Thread.current[:clover_ssh_cache]).to be_nil
|
|
first_time = sa.connect
|
|
expect(Thread.current[:clover_ssh_cache].size).to eq(1)
|
|
second_time = sa.connect
|
|
expect(first_time).to equal(second_time)
|
|
|
|
expect(described_class.reset_cache).to eq []
|
|
expect(Thread.current[:clover_ssh_cache]).to be_empty
|
|
end
|
|
|
|
it "does not crash if a cache has never been made" do
|
|
expect {
|
|
sa.invalidate_cache_entry
|
|
}.not_to raise_error
|
|
end
|
|
|
|
it "can invalidate a single cache entry" do
|
|
sess = instance_double(Net::SSH::Connection::Session, close: nil)
|
|
expect(Net::SSH).to receive(:start).and_return sess
|
|
sa.connect
|
|
expect {
|
|
sa.invalidate_cache_entry
|
|
}.to change { Thread.current[:clover_ssh_cache] }.from({["test.localhost", "testuser"] => sess}).to({})
|
|
end
|
|
|
|
it "can reset caches when has cached connection" do
|
|
sess = instance_double(Net::SSH::Connection::Session, close: nil)
|
|
expect(Net::SSH).to receive(:start).and_return sess
|
|
sa.connect
|
|
expect {
|
|
described_class.reset_cache
|
|
}.to change { Thread.current[:clover_ssh_cache] }.from({["test.localhost", "testuser"] => sess}).to({})
|
|
end
|
|
|
|
it "can reset caches when has no cached connection" do
|
|
expect(described_class.reset_cache).to eq([])
|
|
end
|
|
|
|
it "can reset caches even if session fails while closing" do
|
|
sess = instance_double(Net::SSH::Connection::Session)
|
|
expect(sess).to receive(:close).and_raise Sshable::SshError.new("bogus", "", "", nil, nil)
|
|
expect(Net::SSH).to receive(:start).and_return sess
|
|
sa.connect
|
|
|
|
expect(described_class.reset_cache.first).to be_a Sshable::SshError
|
|
expect(Thread.current[:clover_ssh_cache]).to eq({})
|
|
end
|
|
end
|
|
|
|
describe "#cmd" do
|
|
let(:session) { instance_double(Net::SSH::Connection::Session) }
|
|
|
|
before do
|
|
expect(sa).to receive(:connect).and_return(session).at_least(:once)
|
|
end
|
|
|
|
def simulate(cmd:, exit_status:, exit_signal:, stdout:, stderr:)
|
|
expect(session).to receive(:open_channel) do |&blk|
|
|
chan = instance_spy(Net::SSH::Connection::Channel)
|
|
expect(chan).to receive(:exec).with(cmd) do |&blk|
|
|
chan2 = instance_spy(Net::SSH::Connection::Channel)
|
|
expect(chan2).to receive(:on_request).with("exit-status") do |&blk|
|
|
buf = instance_double(Net::SSH::Buffer)
|
|
expect(buf).to receive(:read_long).and_return(exit_status)
|
|
blk.call(nil, buf)
|
|
end
|
|
|
|
expect(chan2).to receive(:on_request).with("exit-signal") do |&blk|
|
|
buf = instance_double(Net::SSH::Buffer)
|
|
expect(buf).to receive(:read_long).and_return(exit_signal)
|
|
blk.call(nil, buf)
|
|
end
|
|
expect(chan2).to receive(:on_data).and_yield(instance_double(Net::SSH::Connection::Channel), stdout)
|
|
expect(chan2).to receive(:on_extended_data).and_yield(nil, 1, stderr)
|
|
|
|
blk.call(chan2, true)
|
|
end
|
|
blk.call(chan, true)
|
|
chan
|
|
end
|
|
end
|
|
|
|
it "can run a command" do
|
|
[false, true].each do |repl_value|
|
|
[false, true].each do |log_value|
|
|
allow(described_class).to receive(:repl?).and_return(repl_value)
|
|
if repl_value
|
|
# Note that in the REPL, stdout and stderr get multiplexed
|
|
# into stderr in real time, packet by packet.
|
|
expect($stderr).to receive(:write).with("hello")
|
|
expect($stderr).to receive(:write).with("world")
|
|
end
|
|
|
|
if log_value
|
|
sa.instance_variable_set(:@connect_duration, 1.1)
|
|
expect(Clog).to receive(:emit).with("ssh cmd execution") do |&blk|
|
|
dat = blk.call
|
|
if repl_value
|
|
expect(dat[:ssh].slice(:stdout, :stderr)).to be_empty
|
|
else
|
|
expect(dat[:ssh].slice(:stdout, :stderr)).to eq({stdout: "hello", stderr: "world"})
|
|
end
|
|
end
|
|
end
|
|
simulate(cmd: "echo hello", exit_status: 0, exit_signal: nil, stdout: "hello", stderr: "world")
|
|
expect(sa.cmd("echo hello", log: log_value)).to eq("hello")
|
|
end
|
|
end
|
|
end
|
|
|
|
it "raises an error with a non-zero exit status" do
|
|
simulate(cmd: "exit 1", exit_status: 1, exit_signal: 127, stderr: "", stdout: "")
|
|
expect { sa.cmd("exit 1") }.to raise_error Sshable::SshError, "command exited with an error: exit 1"
|
|
end
|
|
|
|
it "invalidates the cache if the session raises an error" do
|
|
err = IOError.new("the party is over")
|
|
expect(session).to receive(:open_channel).and_raise err
|
|
expect(sa).to receive(:invalidate_cache_entry)
|
|
expect { sa.cmd("irrelevant") }.to raise_error err
|
|
end
|
|
end
|
|
|
|
describe "daemonizer methods" do
|
|
let(:unit_name) { "test_unit" }
|
|
let(:run_command) { "sudo host/bin/setup-vm prep test_unit" }
|
|
let(:stdin_data) { "secret_data" }
|
|
|
|
it "calls cmd with the correct check command" do
|
|
expect(sa).to receive(:cmd).with("common/bin/daemonizer2 check test_unit")
|
|
sa.d_check(unit_name)
|
|
end
|
|
|
|
it "calls cmd with the correct clean command" do
|
|
expect(sa).to receive(:cmd).with("common/bin/daemonizer2 clean test_unit")
|
|
sa.d_clean(unit_name)
|
|
end
|
|
|
|
it "calls cmd with the correct restart command" do
|
|
expect(sa).to receive(:cmd).with("common/bin/daemonizer2 restart test_unit")
|
|
sa.d_restart(unit_name)
|
|
end
|
|
|
|
it "calls cmd with the correct run command and no stdin" do
|
|
expect(sa).to receive(:cmd).with("common/bin/daemonizer2 run test_unit sudo\\ host/bin/setup-vm\\ prep\\ test_unit", stdin: nil, log: true)
|
|
sa.d_run(unit_name, run_command)
|
|
end
|
|
|
|
it "calls cmd with the correct run command and passes stdin" do
|
|
expect(sa).to receive(:cmd).with("common/bin/daemonizer2 run test_unit sudo\\ host/bin/setup-vm\\ prep\\ test_unit", stdin: stdin_data, log: true)
|
|
sa.d_run(unit_name, run_command, stdin: stdin_data)
|
|
end
|
|
end
|
|
end
|