Files
ubicloud/spec/model/sshable_spec.rb
mohi-kalantari 9bef74cab4 Add restart command to daemonizer2
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")
2025-05-22 11:55:47 +02:00

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