SPDK supported max ios per sec for read/write combined, so we added support for it. However, cgroup v2 (which we’re migrating to for I/O rate limiting) does not support combined IOPS limits — only separate `IOReadIOPSMax` and `IOWriteIOPSMax`. This patch removes the unused `max_ios_per_sec` knob. It was never used in production, and our current rate limiting relies solely on read/write bandwidth, which is fully supported by cgroup v2.
352 lines
13 KiB
Ruby
352 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "spec_helper"
|
|
|
|
RSpec.describe Vm do
|
|
subject(:vm) { described_class.new(display_state: "creating", created_at: Time.now) }
|
|
|
|
describe "#display_state" do
|
|
it "returns deleting if destroy semaphore increased" do
|
|
expect(vm).to receive(:semaphores).and_return([instance_double(Semaphore, name: "destroy")]).at_least(:once)
|
|
expect(vm.display_state).to eq("deleting")
|
|
end
|
|
|
|
it "returns restarting if restart semaphore increased" do
|
|
expect(vm).to receive(:semaphores).and_return([instance_double(Semaphore, name: "restart")]).at_least(:once)
|
|
expect(vm.display_state).to eq("restarting")
|
|
end
|
|
|
|
it "returns stopped if stop semaphore increased" do
|
|
expect(vm).to receive(:semaphores).and_return([instance_double(Semaphore, name: "stop")]).at_least(:once)
|
|
expect(vm.display_state).to eq("stopped")
|
|
end
|
|
|
|
it "returns waiting for capacity if semaphore increased" do
|
|
expect(vm).to receive(:semaphores).and_return([instance_double(Semaphore, name: "waiting_for_capacity")]).at_least(:once)
|
|
expect(vm.display_state).to eq("waiting for capacity")
|
|
end
|
|
|
|
it "returns no capacity available if it's waiting capacity more than 15 minutes" do
|
|
expect(vm).to receive(:created_at).and_return(Time.now - 16 * 60)
|
|
expect(vm).to receive(:semaphores).and_return([instance_double(Semaphore, name: "waiting_for_capacity")]).at_least(:once)
|
|
expect(vm.display_state).to eq("no capacity available")
|
|
end
|
|
|
|
it "return same if semaphores not increased" do
|
|
expect(vm.display_state).to eq("creating")
|
|
end
|
|
end
|
|
|
|
describe "#cloud_hypervisor_cpu_topology" do
|
|
it "scales a single-socket hyperthreaded system" do
|
|
vm.family = "standard"
|
|
vm.vcpus = 4
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 12,
|
|
total_cores: 6,
|
|
total_dies: 1,
|
|
total_sockets: 1
|
|
)).at_least(:once)
|
|
expect(vm.cloud_hypervisor_cpu_topology.to_s).to eq("2:2:1:1")
|
|
end
|
|
|
|
it "scales a dual-socket hyperthreaded system" do
|
|
vm.family = "standard"
|
|
vm.vcpus = 4
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 24,
|
|
total_cores: 12,
|
|
total_dies: 2,
|
|
total_sockets: 2
|
|
)).at_least(:once)
|
|
expect(vm.cloud_hypervisor_cpu_topology.to_s).to eq("2:2:1:1")
|
|
end
|
|
|
|
it "crashes if total_cpus is not multiply of total_cores" do
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 3,
|
|
total_cores: 2
|
|
)).at_least(:once)
|
|
|
|
expect { vm.cloud_hypervisor_cpu_topology }.to raise_error RuntimeError, "BUG"
|
|
end
|
|
|
|
it "crashes if total_dies is not a multiple of total_sockets" do
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 24,
|
|
total_cores: 12,
|
|
total_dies: 3,
|
|
total_sockets: 2
|
|
)).at_least(:once)
|
|
|
|
expect { vm.cloud_hypervisor_cpu_topology }.to raise_error RuntimeError, "BUG"
|
|
end
|
|
|
|
it "crashes if cores allocated per die is not uniform number" do
|
|
vm.family = "standard"
|
|
vm.vcpus = 4
|
|
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 1,
|
|
total_cores: 1,
|
|
total_dies: 1,
|
|
total_sockets: 1
|
|
)).at_least(:once)
|
|
|
|
expect { vm.cloud_hypervisor_cpu_topology }.to raise_error RuntimeError, "BUG: need uniform number of cores allocated per die"
|
|
end
|
|
|
|
it "crashes if the vcpus is an odd number" do
|
|
vm.family = "burstable"
|
|
vm.vcpus = 5
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 12,
|
|
total_cores: 6,
|
|
total_dies: 1,
|
|
total_sockets: 1
|
|
)).at_least(:once)
|
|
|
|
expect { vm.cloud_hypervisor_cpu_topology }.to raise_error RuntimeError, "BUG: need uniform number of cores allocated per die"
|
|
end
|
|
|
|
it "scales a single-socket non-hyperthreaded system" do
|
|
vm.family = "standard"
|
|
vm.vcpus = 4
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 12,
|
|
total_cores: 12,
|
|
total_dies: 1,
|
|
total_sockets: 1
|
|
)).at_least(:once)
|
|
expect(vm.cloud_hypervisor_cpu_topology.to_s).to eq("1:4:1:1")
|
|
end
|
|
|
|
it "scales a single-socket hyperthreaded system for burstable family for 2 vcpus" do
|
|
vm.family = "burstable"
|
|
vm.vcpus = 2
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 12,
|
|
total_cores: 6,
|
|
total_dies: 1,
|
|
total_sockets: 1
|
|
)).at_least(:once)
|
|
expect(vm.cloud_hypervisor_cpu_topology.to_s).to eq("2:1:1:1")
|
|
end
|
|
|
|
it "scales a single-socket non-hyperthreaded system for burstable family for 2 vcpus" do
|
|
vm.family = "burstable"
|
|
vm.vcpus = 2
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 12,
|
|
total_cores: 12,
|
|
total_dies: 1,
|
|
total_sockets: 1
|
|
)).at_least(:once)
|
|
expect(vm.cloud_hypervisor_cpu_topology.to_s).to eq("1:2:1:1")
|
|
end
|
|
|
|
it "scales a single-socket hyperthreaded system for burstable family for 1 vcpu" do
|
|
vm.family = "burstable"
|
|
vm.vcpus = 1
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 12,
|
|
total_cores: 6,
|
|
total_dies: 1,
|
|
total_sockets: 1
|
|
)).at_least(:once)
|
|
expect(vm.cloud_hypervisor_cpu_topology.to_s).to eq("1:1:1:1")
|
|
end
|
|
|
|
it "scales a double-socket hyperthreaded system for burstable family for 1 vcpu" do
|
|
vm.family = "burstable"
|
|
vm.vcpus = 1
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 24,
|
|
total_cores: 12,
|
|
total_dies: 2,
|
|
total_sockets: 2
|
|
)).at_least(:once)
|
|
expect(vm.cloud_hypervisor_cpu_topology.to_s).to eq("1:1:1:1")
|
|
end
|
|
|
|
it "scales a single-socket non-hyperthreaded system for burstable family for 1 vcpu" do
|
|
vm.family = "burstable"
|
|
vm.vcpus = 1
|
|
expect(vm).to receive(:vm_host).and_return(instance_double(
|
|
VmHost,
|
|
total_cpus: 12,
|
|
total_cores: 12,
|
|
total_dies: 1,
|
|
total_sockets: 1
|
|
)).at_least(:once)
|
|
expect(vm.cloud_hypervisor_cpu_topology.to_s).to eq("1:1:1:1")
|
|
end
|
|
end
|
|
|
|
describe "#update_spdk_version" do
|
|
let(:vmh) { create_vm_host }
|
|
|
|
before do
|
|
expect(vm).to receive(:vm_host).and_return(vmh)
|
|
end
|
|
|
|
it "can update spdk version" do
|
|
spdk_installation = SpdkInstallation.create(version: "b", allocation_weight: 100, vm_host_id: vmh.id) { it.id = vmh.id }
|
|
volume_dataset = instance_double(Sequel::Dataset)
|
|
expect(vm).to receive(:vm_storage_volumes_dataset).and_return(volume_dataset)
|
|
expect(volume_dataset).to receive(:update).with(spdk_installation_id: spdk_installation.id)
|
|
expect(vm).to receive(:incr_update_spdk_dependency)
|
|
|
|
vm.update_spdk_version("b")
|
|
end
|
|
|
|
it "fails if spdk installation not found" do
|
|
expect { vm.update_spdk_version("b") }.to raise_error RuntimeError, "SPDK version b not found on host"
|
|
end
|
|
end
|
|
|
|
describe "#utility functions" do
|
|
it "can compute the ipv4 addresses" do
|
|
as_ad = instance_double(AssignedVmAddress, ip: NetAddr::IPv4Net.new(NetAddr.parse_ip("1.1.1.0"), NetAddr::Mask32.new(32)))
|
|
expect(vm).to receive(:assigned_vm_address).and_return(as_ad).at_least(:once)
|
|
expect(vm.ephemeral_net4.to_s).to eq("1.1.1.0")
|
|
expect(vm.ip4.to_s).to eq("1.1.1.0/32")
|
|
end
|
|
|
|
it "can compute nil if ipv4 is not assigned" do
|
|
expect(vm.ephemeral_net4).to be_nil
|
|
end
|
|
|
|
it "can compute the ipv6 addresses" do
|
|
expect(vm).to receive(:location).and_return(instance_double(Location, provider: "hetzner")).twice
|
|
expect(vm).to receive(:ephemeral_net6).and_return(NetAddr::IPv6Net.parse("2001:db8::/64"))
|
|
expect(vm.ip6.to_s).to eq("2001:db8::2")
|
|
|
|
expect(vm).to receive(:ephemeral_net6).and_return(nil)
|
|
expect(vm.ip6).to be_nil
|
|
|
|
expect(vm).to receive(:location).and_return(instance_double(Location, provider: "aws"))
|
|
expect(vm).to receive(:ephemeral_net6).and_return(NetAddr::IPv6Net.parse("2001:db8::/128"))
|
|
expect(vm.ip6.to_s).to eq("2001:db8::")
|
|
|
|
expect(vm).to receive(:location).and_return(instance_double(Location, provider: "aws"))
|
|
expect(vm).to receive(:ephemeral_net6).and_return(nil)
|
|
expect(vm.ip6).to be_nil
|
|
end
|
|
|
|
it "returns the right private_ipv4 based on the netmask" do
|
|
nic = instance_double(Nic, private_ipv4: NetAddr::IPv4Net.parse("192.168.12.13/32"))
|
|
expect(vm).to receive(:nics).and_return([nic])
|
|
expect(vm.private_ipv4.to_s).to eq("192.168.12.13")
|
|
|
|
nic = instance_double(Nic, private_ipv4: NetAddr.parse_net("10.10.240.0/24"))
|
|
expect(vm).to receive(:nics).and_return([nic])
|
|
expect(vm.private_ipv4.to_s).to eq("10.10.240.1")
|
|
end
|
|
end
|
|
|
|
it "initiates a new health monitor session" do
|
|
vh = instance_double(VmHost, sshable: instance_double(Sshable))
|
|
expect(vm).to receive(:vm_host).and_return(vh).at_least(:once)
|
|
expect(vh.sshable).to receive(:start_fresh_session)
|
|
vm.init_health_monitor_session
|
|
end
|
|
|
|
it "checks underlying enum value when validating" do
|
|
vm = create_vm
|
|
expect(vm.valid?).to be true
|
|
def vm.display_state
|
|
"invalid"
|
|
end
|
|
expect(vm.valid?).to be true
|
|
end
|
|
|
|
it "disallows VM ubid format as name" do
|
|
vm = described_class.new(name: described_class.generate_ubid.to_s)
|
|
vm.validate
|
|
expect(vm.errors[:name]).to eq ["cannot be exactly 26 numbers/lowercase characters starting with vm to avoid overlap with id format"]
|
|
end
|
|
|
|
it "allows postgres server ubid format as name" do
|
|
vm = described_class.new(name: PostgresServer.generate_ubid.to_s)
|
|
vm.validate
|
|
expect(vm.errors[:name]).to be_nil
|
|
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(vm).to receive(:inhost_name).and_return("vmxxxx").at_least(:once)
|
|
expect(session[:ssh_session]).to receive(:exec!).and_return("active\nactive\n")
|
|
expect(vm.check_pulse(session: session, previous_pulse: pulse)[:reading]).to eq("up")
|
|
|
|
expect(session[:ssh_session]).to receive(:exec!).and_return("active\ninactive\n")
|
|
expect(vm).to receive(:reload).and_return(vm)
|
|
expect(vm).to receive(:incr_checkup)
|
|
expect(vm.check_pulse(session: session, previous_pulse: pulse)[:reading]).to eq("down")
|
|
|
|
expect(session[:ssh_session]).to receive(:exec!).and_raise Sshable::SshError
|
|
expect(vm).to receive(:reload).and_return(vm)
|
|
expect(vm).to receive(:incr_checkup)
|
|
expect(vm.check_pulse(session: session, previous_pulse: pulse)[:reading]).to eq("down")
|
|
end
|
|
|
|
it "returns storage volumes hash list" do
|
|
boot_image = instance_double(BootImage, name: "boot_image", version: "1")
|
|
storage_device = instance_double(StorageDevice, name: "default")
|
|
volumes = [
|
|
instance_double(VmStorageVolume, disk_index: 0, device_id: "dev1",
|
|
size_gib: 1, boot: true, boot_image: boot_image,
|
|
key_encryption_key_1: "key", spdk_version: "spdk1",
|
|
use_bdev_ubi: false, skip_sync: false,
|
|
storage_device: storage_device,
|
|
max_read_mbytes_per_sec: nil, max_write_mbytes_per_sec: nil,
|
|
vhost_block_backend_version: nil, num_queues: 1, queue_size: 256),
|
|
instance_double(VmStorageVolume, disk_index: 1, device_id: "dev2",
|
|
size_gib: 100, boot: false, boot_image: nil,
|
|
key_encryption_key_1: nil, spdk_version: "spdk2",
|
|
use_bdev_ubi: true, skip_sync: true,
|
|
storage_device: storage_device,
|
|
max_read_mbytes_per_sec: 200, max_write_mbytes_per_sec: 300,
|
|
vhost_block_backend_version: "v0.1-5", num_queues: 4, queue_size: 64)
|
|
]
|
|
expect(vm).to receive(:vm_storage_volumes).and_return(volumes)
|
|
expect(vm.storage_volumes).to eq([
|
|
{"boot" => true, "image" => "boot_image", "image_version" => "1", "size_gib" => 1,
|
|
"device_id" => "dev1", "disk_index" => 0, "encrypted" => true,
|
|
"spdk_version" => "spdk1", "use_bdev_ubi" => false, "skip_sync" => false,
|
|
"storage_device" => "default", "read_only" => false,
|
|
"max_read_mbytes_per_sec" => nil,
|
|
"max_write_mbytes_per_sec" => nil,
|
|
"vhost_block_backend_version" => nil, "num_queues" => 1, "queue_size" => 256,
|
|
"copy_on_read" => false, "slice_name" => "system.slice"},
|
|
{"boot" => false, "image" => nil, "image_version" => nil, "size_gib" => 100,
|
|
"device_id" => "dev2", "disk_index" => 1, "encrypted" => false,
|
|
"spdk_version" => "spdk2", "use_bdev_ubi" => true, "skip_sync" => true,
|
|
"storage_device" => "default", "read_only" => false,
|
|
"max_read_mbytes_per_sec" => 200,
|
|
"max_write_mbytes_per_sec" => 300,
|
|
"vhost_block_backend_version" => "v0.1-5", "num_queues" => 4, "queue_size" => 64,
|
|
"copy_on_read" => false, "slice_name" => "system.slice"}
|
|
])
|
|
end
|
|
end
|