ubicloud/spec/model/vm_host_spec.rb
Jeremy Evans 45a66b2195 Support testing with frozen Database and models
This makes the testing more like the production environment.

This renames the previous frozen_{,p}spec rake task to
frozen_core_{,p}spec, and adds:

* frozen_db_model_{,p}spec for running the specs with just a frozen
  Database and models
* frozen_{,p}spec for running the specs with a frozen core, Database,
  and models (the most similar to the production environment, but
  the most skipped tests).

This adds rspec and turbo_tests lambdas to DRY up the related rake
task code. It sets RAKE_ENV explicitly for turbo_tests, instead
of relying on the Rakefile setting it implicitly due to how the
spec tasks is currently defined.

This adds a skip_if_frozen_models method to the specs, which does
nothing when running without frozen Database and models, but skips
the spec if running with frozen Database and models.  It then
adds a call to skip_if_frozen_models to all specs that try to mock
DB or the models (109 callsites, 124 specs).

To avoid pages of output of skipped specs, this uses the approach
recommended by the rspec maintainers to suppress skipped specs.
The approach is stored in a separate spec/supress_pending file
so it is usable by both the normal and parallel specs.

I tested this with the implicit_subquery commit
(0085befc4a), and the frozen model
tests fail with it, showing that this approach would have caught
that failure before the change was put into production.
2024-10-28 14:31:00 -07:00

372 lines
16 KiB
Ruby

# frozen_string_literal: true
require_relative "spec_helper"
require_relative "../../model/address"
RSpec.describe VmHost do
subject(:vh) {
described_class.new(
net6: NetAddr.parse_net("2a01:4f9:2b:35a::/64"),
ip6: NetAddr.parse_ip("2a01:4f9:2b:35a::2")
)
}
let(:cidr) { NetAddr::IPv4Net.parse("0.0.0.0/30") }
let(:address) {
Address.new(
cidr: cidr,
routed_to_host_id: "46683a25-acb1-4371-afe9-d39f303e44b4"
)
}
let(:assigned_host_address) {
AssignedHostAddress.new(
ip: cidr,
address_id: address.id,
host_id: "46683a25-acb1-4371-afe9-d39f303e44b4"
)
}
let(:hetzner_ips) {
[
["1.1.1.0/30", "1.1.1.1", true],
["1.1.1.2/32", "1.1.0.0", true],
["1.1.1.3/32", "1.1.1.1", false],
["2a01:4f8:10a:128b::/64", "1.1.1.1", true]
].map {
Hosting::HetznerApis::IpInfo.new(ip_address: _1, source_host_ip: _2, is_failover: _3)
}
}
it "requires an Sshable too" do
expect {
sa = Sshable.create_with_id(host: "test.localhost", raw_private_key_1: SshKey.generate.keypair)
described_class.create(location: "test-location") { _1.id = sa.id }
}.not_to raise_error
end
it "can generate random ipv6 subnets" do
expect(vh.ip6_random_vm_network.contains(vh.ip6)).to be false
end
it "crashes if the prefix length for a VM is shorter than the host's prefix" do
expect {
vh.ip6_reserved_network(1)
}.to raise_error RuntimeError, "BUG: host prefix must be is shorter than reserved prefix"
end
it "has no ipv6 reserved network when vendor used NDP" do
expect(vh).to receive(:ip6).and_return(nil)
expect(vh.ip6_reserved_network).to be_nil
end
it "tries to get another random network if the proposal matches the reserved nework" do
expect(SecureRandom).to receive(:random_number).and_return(0)
expect(SecureRandom).to receive(:random_number).and_call_original
expect(vh.ip6_random_vm_network.to_s).not_to eq(vh.ip6_reserved_network)
end
it "can generate ipv6 for hosts with smaller than /64 prefix with two bytes" do
vh.net6 = NetAddr.parse_net("2a01:4f9:2b:35a::/68")
expect(SecureRandom).to receive(:random_number).with(2...2**16).and_return(5)
expect(vh.ip6_random_vm_network.to_s).to eq("2a01:4f9:2b:35a:0:4000:0:0/83")
expect(SecureRandom).to receive(:random_number).with(2...2**16).and_return(2)
expect(vh.ip6_random_vm_network.to_s).to eq("2a01:4f9:2b:35a:0:2000:0:0/83")
expect(SecureRandom).to receive(:random_number).with(2...2**16).and_return(2**16 - 1)
expect(vh.ip6_random_vm_network.to_s).to eq("2a01:4f9:2b:35a:fff:e000::/83")
end
it "can generate the mask properly" do
vh.net6 = NetAddr.parse_net("::/64")
expect(SecureRandom).to receive(:random_number).with(2...2**16).and_return(5001)
expect(vh.ip6_random_vm_network.to_s).to eq("::1388:0:0:0/79")
expect(SecureRandom).to receive(:random_number).with(2...2**16).and_return(2)
expect(vh.ip6_random_vm_network.to_s).to eq("::2:0:0:0/79")
expect(SecureRandom).to receive(:random_number).with(2...2**16).and_return(2**16 - 1)
expect(vh.ip6_random_vm_network.to_s).to eq("::fffe:0:0:0/79")
expect(SecureRandom).to receive(:random_number).with(2...2**16).and_return(2**15)
expect(vh.ip6_random_vm_network.to_s).to eq("::8000:0:0:0/79")
end
it "has a shortcut to install Rhizome" do
skip_if_frozen_models
vh.id = "46683a25-acb1-4371-afe9-d39f303e44b4"
expect(Strand).to receive(:create) do |args|
expect(args[:prog]).to eq("InstallRhizome")
expect(args[:stack]).to eq([subject_id: vh.id, target_folder: "host", install_specs: false])
end
vh.install_rhizome
end
it "has a shortcut to download a new boot image" do
skip_if_frozen_models
vh.id = "46683a25-acb1-4371-afe9-d39f303e44b4"
expect(Strand).to receive(:create) do |args|
expect(args[:prog]).to eq("DownloadBootImage")
expect(args[:stack]).to eq([subject_id: vh.id, image_name: "my-image", custom_url: "https://example.com/my-image.raw", version: "20230303"])
end
vh.download_boot_image("my-image", custom_url: "https://example.com/my-image.raw", version: "20230303")
end
it "has a shortcut to download a new firmware for x64" do
skip_if_frozen_models
vh.id = "46683a25-acb1-4371-afe9-d39f303e44b4"
vh.arch = "x64"
expect(Strand).to receive(:create) do |args|
expect(args[:prog]).to eq("DownloadFirmware")
expect(args[:stack]).to eq([subject_id: vh.id, version: "202405", sha256: "sha-1"])
end
vh.download_firmware(version_x64: "202405", sha256_x64: "sha-1")
end
it "has a shortcut to download a new firmware for arm64" do
skip_if_frozen_models
vh.id = "46683a25-acb1-4371-afe9-d39f303e44b4"
vh.arch = "arm64"
expect(Strand).to receive(:create) do |args|
expect(args[:prog]).to eq("DownloadFirmware")
expect(args[:stack]).to eq([subject_id: vh.id, version: "202406", sha256: "sha-2"])
end
vh.download_firmware(version_arm64: "202406", sha256_arm64: "sha-2")
end
it "requires version and sha256 to download a new firmware" do
vh.arch = "x64"
expect { vh.download_firmware(sha256_x64: "thesha") }.to raise_error(ArgumentError, "No version provided")
expect { vh.download_firmware(version_x64: "202405") }.to raise_error(ArgumentError, "No SHA-256 digest provided")
vh.arch = "arm64"
expect { vh.download_firmware(sha256_arm64: "thesha") }.to raise_error(ArgumentError, "No version provided")
expect { vh.download_firmware(version_arm64: "202406") }.to raise_error(ArgumentError, "No SHA-256 digest provided")
end
it "has a shortcut to download a new version of cloud hypervisor for x64" do
skip_if_frozen_models
vh.id = "46683a25-acb1-4371-afe9-d39f303e44b4"
vh.arch = "x64"
expect(Strand).to receive(:create) do |args|
expect(args[:prog]).to eq("DownloadCloudHypervisor")
expect(args[:stack]).to eq([subject_id: vh.id, version: "35.1", sha256_ch_bin: "sha-1", sha256_ch_remote: "sha-2"])
end
vh.download_cloud_hypervisor(version_x64: "35.1", sha256_ch_bin_x64: "sha-1", sha256_ch_remote_x64: "sha-2")
end
it "has a shortcut to download a new version of cloud hypervisor for arm64" do
skip_if_frozen_models
vh.id = "46683a25-acb1-4371-afe9-d39f303e44b4"
vh.arch = "arm64"
expect(Strand).to receive(:create) do |args|
expect(args[:prog]).to eq("DownloadCloudHypervisor")
expect(args[:stack]).to eq([subject_id: vh.id, version: "35.1", sha256_ch_bin: "sha-3", sha256_ch_remote: "sha-4"])
end
vh.download_cloud_hypervisor(version_arm64: "35.1", sha256_ch_bin_arm64: "sha-3", sha256_ch_remote_arm64: "sha-4")
end
it "requires version to download a new version of cloud hypervisor" do
vh.arch = "x64"
expect { vh.download_cloud_hypervisor(sha256_ch_bin_x64: "ch_sha", sha256_ch_remote_x64: "remote_sha") }.to raise_error(ArgumentError, "No version provided")
vh.arch = "arm64"
expect { vh.download_cloud_hypervisor(sha256_ch_bin_arm64: "ch_sha", sha256_ch_remote_arm64: "remote_sha") }.to raise_error(ArgumentError, "No version provided")
vh.arch = "unexpectedarch"
expect { vh.download_cloud_hypervisor(version_x64: "35.1", version_arm64: "35.1") }.to raise_error("BUG: unexpected architecture")
end
it "assigned_subnets returns the assigned subnets" do
expect(vh).to receive(:assigned_subnets).and_return([address])
expect(vh).to receive(:vm_addresses).and_return([])
expect(SecureRandom).to receive(:random_number).with(4).and_return(0)
expect(vh).to receive(:sshable).and_return(instance_double(Sshable, host: "0.0.0.2")).at_least(:once)
ip4, r_address = vh.ip4_random_vm_network
expect(ip4.to_s).to eq("0.0.0.0")
expect(r_address).to eq(address)
end
it "returns nil if there is no available subnet" do
expect(vh).to receive(:assigned_subnets).and_return([address])
expect(address.assigned_vm_addresses).to receive(:count).and_return(4)
expect(vh).to receive(:sshable).and_return(instance_double(Sshable, host: "0.0.0.2")).at_least(:once)
ip4, address = vh.ip4_random_vm_network
expect(ip4).to be_nil
expect(address).to be_nil
end
it "finds another address if it's already assigned" do
expect(vh).to receive(:assigned_subnets).and_return([address]).at_least(:once)
expect(vh).to receive(:vm_addresses).and_return([instance_double(AssignedVmAddress, ip: NetAddr::IPv4Net.parse("0.0.0.0"))]).at_least(:once)
expect(vh).to receive(:sshable).and_return(instance_double(Sshable, host: "0.0.0.2")).at_least(:once)
expect(SecureRandom).to receive(:random_number).with(4).and_return(0, 1)
ip4, r_address = vh.ip4_random_vm_network
expect(ip4.to_s).to eq("0.0.0.1")
expect(r_address).to eq(address)
end
it "returns vm_addresses" do
vm = instance_double(Vm, assigned_vm_address: address)
expect(vh).to receive(:vms).and_return([vm])
expect(vh.vm_addresses).to eq([address])
end
it "sshable_address returns the sshable address" do
expect(vh).to receive(:assigned_host_addresses).and_return([assigned_host_address])
expect(vh.sshable_address).to eq(assigned_host_address)
end
it "hetznerifies a host" do
skip_if_frozen_models
expect(vh).to receive(:create_addresses).at_least(:once)
expect(HetznerHost).to receive(:create).with(server_identifier: "12").and_return(true)
vh.hetznerify("12")
end
it "reset server fails for non development" do
expect(Config).to receive(:development?).and_return(false)
expect {
vh.reset
}.to raise_error(RuntimeError, "BUG: reset is only allowed in development")
end
it "resets the server in development" do
expect(Config).to receive(:development?).and_return(true)
expect(Hosting::Apis).to receive(:reset_server).with(vh)
vh.reset
end
it "create_addresses fails if a failover ip of non existent server is being added" do
expect(Hosting::Apis).to receive(:pull_ips).and_return(hetzner_ips)
expect(vh).to receive(:id).and_return("46683a25-acb1-4371-afe9-d39f303e44b4").at_least(:once)
Sshable.create(host: "test.localhost") { _1.id = vh.id }
described_class.create(location: "test-location") { _1.id = vh.id }
expect(vh).to receive(:assigned_subnets).and_return([]).at_least(:once)
expect { vh.create_addresses }.to raise_error(RuntimeError, "BUG: source host 1.1.1.1 isn't added to the database")
end
it "create_addresses creates given addresses and doesn't make an api call when ips given" do
expect(vh).to receive(:id).and_return("46683a25-acb1-4371-afe9-d39f303e44b4").at_least(:once)
Sshable.create(host: "1.1.0.0") { _1.id = vh.id }
Sshable.create_with_id(host: "1.1.1.1")
described_class.create(location: "test-location") { _1.id = vh.id }
expect(vh).to receive(:assigned_subnets).and_return([]).at_least(:once)
vh.create_addresses(ip_records: hetzner_ips)
expect(Address.where(routed_to_host_id: vh.id).count).to eq(4)
end
it "create_addresses creates addresses" do
expect(Hosting::Apis).to receive(:pull_ips).and_return(hetzner_ips)
expect(vh).to receive(:id).and_return("46683a25-acb1-4371-afe9-d39f303e44b4").at_least(:once)
Sshable.create(host: "1.1.0.0") { _1.id = vh.id }
Sshable.create_with_id(host: "1.1.1.1")
described_class.create(location: "test-location") { _1.id = vh.id }
expect(vh).to receive(:assigned_subnets).and_return([]).at_least(:once)
vh.create_addresses
expect(Address.where(routed_to_host_id: vh.id).count).to eq(4)
end
it "create_addresses returns immediately if there are no addresses to create" do
expect(Hosting::Apis).to receive(:pull_ips).and_return(nil)
vh.create_addresses
expect(Address.where(routed_to_host_id: vh.id).count).to eq(0)
end
it "skips already assigned subnets" do
expect(Hosting::Apis).to receive(:pull_ips).and_return(hetzner_ips)
expect(vh).to receive(:id).and_return("46683a25-acb1-4371-afe9-d39f303e44b4").at_least(:once)
Sshable.create(host: "1.1.0.0") { _1.id = vh.id }
Sshable.create_with_id(host: "1.1.1.1")
described_class.create(location: "test-location") { _1.id = vh.id }
expect(vh).to receive(:assigned_subnets).and_return([Address.new(cidr: NetAddr::IPv4Net.parse("1.1.1.0/30".shellescape))]).at_least(:once)
vh.create_addresses
expect(Address.where(routed_to_host_id: vh.id).count).to eq(3)
end
it "updates the routed_to_host_id if the address is reassigned to another host and there is no vm using the ip range" do
skip_if_frozen_models
hetzner_ips = [
Hosting::HetznerApis::IpInfo.new(ip_address: "1.1.1.0/30", source_host_ip: "1.1.1.1", is_failover: true)
]
old_id = "4c5dc171-a116-4a05-9e6d-381a4b382b71"
new_id = "46683a25-acb1-4371-afe9-d39f303e44b4"
expect(vh).to receive(:id).and_return(new_id).at_least(:once)
expect(Hosting::Apis).to receive(:pull_ips).and_return(hetzner_ips)
Sshable.create(host: "1.1.0.0") { _1.id = old_id }
described_class.create(location: "test-location") { _1.id = old_id }
Sshable.create_with_id(host: "1.1.1.1")
adr = Address.create_with_id(cidr: "1.1.1.0/30", routed_to_host_id: old_id)
expect(Address).to receive(:where).with(cidr: "1.1.1.0/30").and_return([adr]).once
expect(adr).to receive(:update).with(routed_to_host_id: new_id).and_return(true)
vh.create_addresses
end
it "fails if the ip range is already assigned to a vm" do
skip_if_frozen_models
hetzner_ips = [
Hosting::HetznerApis::IpInfo.new(ip_address: "1.1.1.0/30", source_host_ip: "1.1.1.1", is_failover: true)
]
old_id = "4c5dc171-a116-4a05-9e6d-381a4b382b71"
expect(Hosting::Apis).to receive(:pull_ips).and_return(hetzner_ips)
Sshable.create(host: "1.1.0.0") { _1.id = old_id }
described_class.create(location: "test-location") { _1.id = old_id }
adr = Address.create_with_id(cidr: "1.1.1.0/30", routed_to_host_id: old_id)
expect(Address).to receive(:where).with(cidr: "1.1.1.0/30").and_return([adr]).once
expect(adr).to receive(:assigned_vm_addresses).and_return([instance_double(Vm)]).at_least(:once)
expect {
vh.create_addresses
}.to raise_error RuntimeError, "BUG: failover ip 1.1.1.0/30 is already assigned to a vm"
end
it "finds local ip to assign to veth* devices" do
expect(SecureRandom).to receive(:random_number).with(32767).and_return(5)
expect(vh.veth_pair_random_ip4_addr.network.to_s).to eq("169.254.0.10")
end
it "finds local ip to assign to veth* devices and eliminates already assigned" do
expect(vh).to receive(:vms).and_return([instance_double(Vm, local_vetho_ip: "169.254.0.10")]).at_least(:once)
expect(SecureRandom).to receive(:random_number).with(32767).and_return(5, 10)
expect(vh.veth_pair_random_ip4_addr.network.to_s).to eq("169.254.0.20")
end
it "initiates a new health monitor session" do
sshable = instance_double(Sshable)
expect(vh).to receive(:sshable).and_return(sshable)
expect(sshable).to receive(:start_fresh_session)
vh.init_health_monitor_session
end
it "checks pulse" do
session = {
ssh_session: instance_double(Net::SSH::Connection::Session)
}
pulse = {
reading: "down",
reading_rpt: 5,
reading_chg: Time.now - 30
}
expect(session[:ssh_session]).to receive(:exec!).and_return("true\n")
expect(vh.check_pulse(session: session, previous_pulse: pulse)[:reading]).to eq("up")
expect(session[:ssh_session]).to receive(:exec!).and_raise Sshable::SshError
expect(vh).to receive(:reload).and_return(vh)
expect(vh).to receive(:incr_checkup)
expect(vh.check_pulse(session: session, previous_pulse: pulse)[:reading]).to eq("down")
end
it "#render_arch errors on an unexpected architecture" do
expect(vh).to receive(:arch).and_return("nope")
expect { vh.render_arch(arm64: "a", x64: "x") }.to raise_error RuntimeError, "BUG: inexhaustive render code"
end
end