Files
ubicloud/spec/prog/vnet/subnet_nexus_spec.rb
Furkan Sahin 7ed27c3c4e Don't perform any work for subnet and nics in wait if aws?
If the location is aws? we don't need to do anything in SubnetNexus and
NicNexus except for provisioning and destroy. One exception is the
distribution of firewall update to the VMs which we move into its own
method and call.
2025-07-07 11:15:08 +02:00

493 lines
21 KiB
Ruby

# frozen_string_literal: true
RSpec.describe Prog::Vnet::SubnetNexus do
subject(:nx) {
described_class.new(st)
}
let(:st) { Strand.new }
let(:prj) { Project.create_with_id(name: "default") }
let(:ps) {
PrivateSubnet.create_with_id(name: "ps", location_id: Location::HETZNER_FSN1_ID, net6: "fd10:9b0b:6b4b:8fbb::/64",
net4: "1.1.1.0/26", state: "waiting", project_id: prj.id)
}
let(:ps2) {
PrivateSubnet.create_with_id(name: "ps2", location_id: Location::HETZNER_FSN1_ID, net6: "fd10:9b0b:6b4b:8fcc::/64",
net4: "1.1.1.128/26", state: "waiting", project_id: prj.id)
}
before do
nx.instance_variable_set(:@private_subnet, ps)
end
describe ".assemble" do
it "fails if project doesn't exist" do
expect {
described_class.assemble(nil)
}.to raise_error RuntimeError, "No existing project"
end
it "fails if location doesn't exist" do
expect {
described_class.assemble(prj.id, location_id: nil)
}.to raise_error RuntimeError, "No existing location"
end
it "uses ipv6_addr if passed and creates entities" do
expect(described_class).to receive(:random_private_ipv4).and_return("10.0.0.0/26")
ps = described_class.assemble(
prj.id,
name: "default-ps",
location_id: Location::HETZNER_FSN1_ID,
ipv6_range: "fd10:9b0b:6b4b:8fbb::/64"
)
expect(ps.subject.net6.to_s).to eq("fd10:9b0b:6b4b:8fbb::/64")
end
it "uses ipv4_addr if passed and creates entities" do
expect(described_class).to receive(:random_private_ipv6).and_return("fd10:9b0b:6b4b:8fbb::/64")
ps = described_class.assemble(
prj.id,
name: "default-ps",
location_id: Location::HETZNER_FSN1_ID,
ipv4_range: "10.0.0.0/26"
)
expect(ps.subject.net4.to_s).to eq("10.0.0.0/26")
end
it "uses firewall if provided" do
fw = Firewall.create_with_id(name: "default-firewall", location_id: Location::HETZNER_FSN1_ID, project_id: prj.id)
ps = described_class.assemble(prj.id, firewall_id: fw.id)
expect(ps.subject.firewalls.count).to eq(1)
expect(ps.subject.firewalls.first).to eq(fw)
end
it "fails if provided firewall does not exist" do
expect {
described_class.assemble(prj.id, firewall_id: "550e8400-e29b-41d4-a716-446655440000")
}.to raise_error RuntimeError, "Firewall with id 550e8400-e29b-41d4-a716-446655440000 and location hetzner-fsn1 does not exist"
end
it "fails if firewall is not in the project" do
fw = Firewall.create_with_id(name: "default-firewall", location_id: Location::HETZNER_FSN1_ID, project_id: Project.create(name: "t2").id)
expect {
described_class.assemble(prj.id, firewall_id: fw.id)
}.to raise_error RuntimeError, "Firewall with id #{fw.id} and location hetzner-fsn1 does not exist"
end
it "fails if both allow_only_ssh and firewall_id are specified" do
fw = Firewall.create_with_id(name: "default-firewall", location_id: Location::HETZNER_FSN1_ID, project_id: prj.id)
expect {
described_class.assemble(prj.id, firewall_id: fw.id, allow_only_ssh: true)
}.to raise_error RuntimeError, "Cannot specify both allow_only_ssh and firewall_id"
end
end
describe ".gen_spi" do
it "generates a random spi" do
expect(SecureRandom).to receive(:bytes).with(4).and_return("e3af3a04")
expect(nx.gen_spi).to eq("0x6533616633613034")
end
end
describe ".gen_reqid" do
it "generates a random reqid" do
expect(SecureRandom).to receive(:random_number).with(100000).and_return(10)
expect(nx.gen_reqid).to eq(11)
end
end
describe ".gen_encryption_key" do
it "generates a random encryption key" do
expect(SecureRandom).to receive(:bytes).with(36).and_return("e3af3a04")
expect(nx.gen_encryption_key).to eq("0x6533616633613034")
end
end
describe ".nics_to_rekey" do
it "returns nics that need rekeying" do
nic1 = Prog::Vnet::NicNexus.assemble(ps.id, name: "a").subject
nic2 = Prog::Vnet::NicNexus.assemble(ps.id, name: "b").subject
expect(nx.nics_to_rekey).to eq([])
nic1.strand.update(label: "wait")
expect(nx.nics_to_rekey.map(&:name)).to eq(["a"])
nic2.strand.update(label: "wait_setup")
expect(nx.nics_to_rekey.map(&:name).sort).to eq(["a", "b"])
end
end
describe "#before_run" do
it "hops to destroy if when_destroy_set?" do
expect(nx).to receive(:when_destroy_set?).and_yield
expect { nx.before_run }.to hop("destroy")
end
it "hops to destroy if when_destroy_set? from wait_fw_rules" do
expect(nx).to receive(:when_destroy_set?).and_yield
expect(nx.strand).to receive(:label).and_return("wait_fw_rules").at_least(:once)
expect { nx.before_run }.to hop("destroy")
end
it "does not hop to destroy if strand is destroy" do
expect(nx).to receive(:when_destroy_set?).and_yield
expect(nx.strand).to receive(:label).and_return("destroy")
expect { nx.before_run }.not_to hop("destroy")
end
end
describe "#start" do
it "creates a vpc if location is aws and starts to wait for it" do
loc = Location.create_with_id(name: "aws-us-west-2", provider: "aws", project_id: prj.id, display_name: "aws-us-west-2", ui_name: "AWS US East 1", visible: true)
expect(ps).to receive(:location).and_return(loc).at_least(:once)
expect(nx).to receive(:bud).with(Prog::Aws::Vpc, {"subject_id" => ps.id}, :create_vpc)
expect { nx.start }.to hop("wait_vpc_created")
end
it "does not create the PrivateSubnetAwsResource if it already exists" do
loc = Location.create_with_id(name: "aws-us-west-2", provider: "aws", project_id: prj.id, display_name: "aws-us-west-2", ui_name: "AWS US East 1", visible: true)
expect(ps).to receive(:location).and_return(loc).at_least(:once)
expect(ps).to receive(:private_subnet_aws_resource).and_return(instance_double(PrivateSubnetAwsResource, id: "123")).at_least(:once)
expect(nx).to receive(:bud).with(Prog::Aws::Vpc, {"subject_id" => ps.id}, :create_vpc)
expect { nx.start }.to hop("wait_vpc_created")
expect(PrivateSubnetAwsResource.count).to eq(0)
end
it "hops to wait if location is not aws" do
expect { nx.start }.to hop("wait")
end
end
describe "#wait_vpc_created" do
it "reaps and hops to wait if leaf" do
st.update(prog: "Vnet::SubnetNexus", label: "wait_vpc_created", stack: [{}])
expect { nx.wait_vpc_created }.to hop("wait")
end
it "naps if not leaf" do
st.update(prog: "Vnet::SubnetNexus", label: "wait_vpc_created", stack: [{}])
Strand.create(parent_id: st.id, prog: "Aws::Vpc", label: "create_vpc", stack: [{}], lease: Time.now + 10)
# Cover case where reap without reaper argument has results in reapable children
Strand.create(parent_id: st.id, prog: "Aws::Vpc", label: "create_vpc", stack: [{}]).this.update(exitval: '"subnet created"')
expect { nx.wait_vpc_created }.to nap(2)
end
end
describe "#wait" do
it "naps if location is aws" do
expect(ps.location).to receive(:aws?).and_return(true)
expect(ps).to receive(:semaphores).and_return([])
expect { nx.wait }.to nap(60 * 60 * 24 * 365)
end
it "hops to refresh_keys if when_refresh_keys_set?" do
expect(nx).to receive(:when_refresh_keys_set?).and_yield
expect(ps).to receive(:update).with(state: "refreshing_keys").and_return(true)
expect { nx.wait }.to hop("refresh_keys")
end
it "hops to add_new_nic if when_add_new_nic_set?" do
expect(nx).to receive(:when_add_new_nic_set?).and_yield
expect(ps).to receive(:update).with(state: "adding_new_nic").and_return(true)
expect { nx.wait }.to hop("add_new_nic")
end
it "increments refresh_keys if it passed more than a day" do
expect(ps).to receive(:last_rekey_at).and_return(Time.now - 60 * 60 * 24 - 1)
expect(ps).to receive(:incr_refresh_keys).and_return(true)
expect { nx.wait }.to nap(10 * 60)
end
it "triggers update_firewall_rules if when_update_firewall_rules_set?" do
expect(nx).to receive(:when_update_firewall_rules_set?).and_yield
expect(ps).to receive(:vms).and_return([instance_double(Vm, id: "vm1")]).at_least(:once)
expect(ps.vms.first).to receive(:incr_update_firewall_rules).and_return(true)
expect(nx).to receive(:decr_update_firewall_rules).and_return(true)
expect { nx.wait }.to nap(10 * 60)
end
it "naps if nothing to do" do
expect { nx.wait }.to nap(10 * 60)
end
end
describe "#add_new_nic" do
it "adds new nics and creates tunnels" do
st = instance_double(Strand, label: "wait_setup")
nic_to_add = instance_double(Nic, id: "57afa8a7-2357-4012-9632-07fbe13a3133", rekey_payload: {}, strand: st, lock_set?: false)
st = instance_double(Strand, label: "wait")
added_nic = instance_double(Nic, id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b", rekey_payload: {}, strand: st, lock_set?: false)
nics_to_rekey = [added_nic, nic_to_add]
expect(nx).to receive(:decr_add_new_nic)
expect(nic_to_add).to receive(:incr_lock)
expect(added_nic).to receive(:incr_lock)
expect(nic_to_add).to receive(:incr_start_rekey)
expect(added_nic).to receive(:incr_start_rekey)
expect(nx).to receive(:nics_to_rekey).and_return(nics_to_rekey)
expect(nx).to receive(:gen_spi).and_return("0xe3af3a04").at_least(:once)
expect(nx).to receive(:gen_reqid).and_return(86879).at_least(:once)
expect(nx).to receive(:gen_encryption_key).and_return("0x0a0b0c0d0e0f10111213141516171819").at_least(:once)
expect(nx.private_subnet).to receive(:create_tunnels).and_return(true).at_least(:once)
expect(added_nic).to receive(:update).with(encryption_key: "0x0a0b0c0d0e0f10111213141516171819", rekey_payload:
{
spi4: "0xe3af3a04",
spi6: "0xe3af3a04",
reqid: 86879
}).and_return(true)
expect(nic_to_add).to receive(:update).with(encryption_key: "0x0a0b0c0d0e0f10111213141516171819", rekey_payload:
{
spi4: "0xe3af3a04",
spi6: "0xe3af3a04",
reqid: 86879
}).and_return(true)
expect { nx.add_new_nic }.to hop("wait_inbound_setup")
end
it "naps if the nics are locked" do
st = instance_double(Strand, label: "wait_setup")
nic_to_add = instance_double(Nic, id: "57afa8a7-2357-4012-9632-07fbe13a3133", rekey_payload: {}, strand: st, lock_set?: false)
st = instance_double(Strand, label: "wait")
added_nic = instance_double(Nic, id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b", rekey_payload: {}, strand: st, lock_set?: false)
nics_to_rekey = [added_nic, nic_to_add]
expect(added_nic).to receive(:lock_set?).and_return(true)
expect(nx).to receive(:nics_to_rekey).and_return(nics_to_rekey)
expect { nx.add_new_nic }.to nap(10)
end
end
describe "#refresh_keys" do
let(:nic) {
Prog::Vnet::NicNexus.assemble(ps.id, name: "a").update(label: "wait").subject
}
let(:nx) {
described_class.new(Strand.create(prog: "Vnet::SubnetNexus", label: "refresh_keys", id: ps.id))
}
it "refreshes keys and hops to wait_refresh_keys" do
expect(nx).to receive(:gen_spi).and_return("0xe3af3a04").at_least(:once)
expect(nx).to receive(:gen_reqid).and_return(86879)
expect(nx).to receive(:gen_encryption_key).and_return("0x0a0b0c0d0e0f10111213141516171819")
expect(nic.start_rekey_set?).to be false
expect(nic.lock_set?).to be false
expect { nx.refresh_keys }.to hop("wait_inbound_setup")
nic.refresh
expect(nic.encryption_key).to eq "0x0a0b0c0d0e0f10111213141516171819"
expect(nic.rekey_payload).to eq("spi4" => "0xe3af3a04", "spi6" => "0xe3af3a04", "reqid" => 86879)
expect(nic.start_rekey_set?).to be true
expect(nic.lock_set?).to be true
end
it "naps if the nics are locked" do
nic.incr_lock
expect { nx.refresh_keys }.to nap(10)
end
end
describe "#wait_inbound_setup" do
let(:nic) {
Prog::Vnet::NicNexus.assemble(ps.id, name: "a").subject.update(rekey_payload: {})
}
let(:nx) {
described_class.new(Strand.create(prog: "Vnet::SubnetNexus", label: "wait_inbound_setup", id: ps.id))
}
it "naps 5 if state creation is ongoing" do
nic
expect { nx.wait_inbound_setup }.to nap(5)
end
it "hops to wait_policy_updated if state creation is done" do
nic.strand.update(label: "wait_rekey_outbound_trigger")
expect(nic.trigger_outbound_update_set?).to be false
expect { nx.wait_inbound_setup }.to hop("wait_outbound_setup")
nic.refresh
expect(nic.trigger_outbound_update_set?).to be true
end
end
describe "#wait_outbound_setup" do
let(:nic) {
Prog::Vnet::NicNexus.assemble(ps.id, name: "a").subject.update(rekey_payload: {})
}
let(:nx) {
described_class.new(Strand.create(prog: "Vnet::SubnetNexus", label: "wait_outbound_setup", id: ps.id))
}
it "donates if policy update is ongoing" do
nic
expect { nx.wait_outbound_setup }.to nap(5)
end
it "hops to wait_state_dropped if policy update is done" do
nic.strand.update(label: "wait_rekey_old_state_drop_trigger")
expect(nic.old_state_drop_trigger_set?).to be false
expect { nx.wait_outbound_setup }.to hop("wait_old_state_drop")
nic.refresh
expect(nic.old_state_drop_trigger_set?).to be true
end
end
describe "#wait_old_state_drop" do
let(:nic) {
Prog::Vnet::NicNexus.assemble(ps.id, name: "a").subject.update(rekey_payload: {})
}
let(:nx) {
described_class.new(Strand.create(prog: "Vnet::SubnetNexus", label: "wait_old_state_drop", id: ps.id))
}
it "donates if policy update is ongoing" do
nic
expect { nx.wait_old_state_drop }.to nap(5)
end
it "hops to wait if all is done" do
nic.strand.update(label: "wait")
nic.incr_lock
ps.update(last_rekey_at: Time.now - 100)
expect { nx.wait_old_state_drop }.to hop("wait")
ps.refresh
expect(ps.state).to eq "waiting"
expect(ps.last_rekey_at > Time.now - 10).to be true
nic.refresh
expect(nic.encryption_key).to be_nil
expect(nic.rekey_payload).to be_nil
expect(nic.lock_set?).to be false
end
end
describe ".random_private_ipv4" do
it "returns a random private ipv4 range" do
expect(described_class.random_private_ipv4(Location[name: "hetzner-fsn1"], prj)).to be_a NetAddr::IPv4Net
end
it "finds a new subnet if the one it found is taken" do
expect(PrivateSubnet).to receive(:random_subnet).and_return("10.0.0.0/8").at_least(:once)
project = Project.create_with_id(name: "test-project")
described_class.assemble(project.id, location_id: Location::HETZNER_FSN1_ID, name: "test-subnet", ipv4_range: "10.0.0.128/26")
allow(SecureRandom).to receive(:random_number).with(2**(26 - 8) - 1).and_return(1, 2)
expect(described_class.random_private_ipv4(Location[name: "hetzner-fsn1"], project).to_s).to eq("10.0.0.192/26")
end
it "finds a new subnet if the one it found is banned" do
expect(PrivateSubnet).to receive(:random_subnet).and_return("172.16.0.0/16", "10.0.0.0/8")
project = Project.create_with_id(name: "test-project")
allow(SecureRandom).to receive(:random_number).with(2**(26 - 16) - 1).and_return(1)
allow(SecureRandom).to receive(:random_number).with(2**(26 - 8) - 1).and_return(1)
expect(described_class.random_private_ipv4(Location[name: "hetzner-fsn1"], project).to_s).to eq("10.0.0.128/26")
end
it "finds a new subnet if the initial range is smaller than the requested cidr range" do
expect(PrivateSubnet).to receive(:random_subnet).and_return("172.16.0.0/16", "10.0.0.0/8")
project = Project.create_with_id(name: "test-project")
expect(SecureRandom).not_to receive(:random_number).with(2**(16 - 16) - 1)
allow(SecureRandom).to receive(:random_number).with(2**(16 - 8) - 1).and_return(15)
expect(described_class.random_private_ipv4(Location[name: "hetzner-fsn1"], project, 16).to_s).to eq("10.16.0.0/16")
end
it "raises an error when invalid CIDR is given" do
project = Project.create_with_id(name: "test-project")
expect { described_class.random_private_ipv4(Location[name: "hetzner-fsn1"], project, 33) }.to raise_error(ArgumentError)
end
end
describe ".random_private_ipv6" do
it "returns a random private ipv6 range" do
expect(described_class.random_private_ipv6(Location[name: "hetzner-fsn1"], prj)).to be_a NetAddr::IPv6Net
end
it "finds a new subnet if the one it found is taken" do
project = Project.create_with_id(name: "test-project")
described_class.assemble(project.id, location_id: Location::HETZNER_FSN1_ID, name: "test-subnet", ipv6_range: "fd61:6161:6161:6161::/64")
expect(SecureRandom).to receive(:bytes).with(7).and_return("a" * 7, "b" * 7)
expect(described_class.random_private_ipv6(Location[name: "hetzner-fsn1"], project).to_s).to eq("fd62:6262:6262:6262::/64")
end
end
describe "#destroy" do
let(:nic) {
instance_double(Nic, vm_id: nil)
}
it "extends deadline if a vm prevents destroy" do
vm = Vm.new(family: "standard", cores: 1, name: "dummy-vm", location_id: Location::HETZNER_FSN1_ID).tap {
it.id = "788525ed-d6f0-4937-a844-323d4fd91946"
}
expect(ps).to receive(:nics).and_return([nic]).twice
expect(nic).to receive(:vm_id).and_return("vm-id")
expect(nic).to receive(:vm).and_return(vm)
expect(vm).to receive(:prevent_destroy_set?).and_return(true)
expect(nx).to receive(:register_deadline).with(nil, 10 * 60, allow_extension: true)
expect { nx.destroy }.to nap(5)
end
it "fails if there are active resources" do
expect(ps).to receive(:nics).and_return([nic]).twice
expect(nic).to receive(:vm_id).and_return("vm-id")
expect(nic).to receive(:vm).and_return(nil)
expect(Clog).to receive(:emit).with("Cannot destroy subnet with active nics, first clean up the attached resources").and_call_original
expect { nx.destroy }.to nap(5)
end
it "hops to wait_aws_vpc_destroyed if location is aws" do
expect(ps).to receive(:location).and_return(Location.create_with_id(name: "aws-us-west-2", provider: "aws", project_id: prj.id, display_name: "aws-us-west-2", ui_name: "AWS US East 1", visible: true))
expect(nx).to receive(:private_subnet).and_return(ps).at_least(:once)
expect(ps).to receive(:nics).and_return([nic]).at_least(:once)
expect(nic).to receive(:incr_destroy)
expect(nx).to receive(:bud).with(Prog::Aws::Vpc, {"subject_id" => ps.id}, :destroy)
expect { nx.destroy }.to hop("wait_aws_vpc_destroyed")
end
it "increments the destroy semaphore of nics" do
expect(ps).to receive(:nics).and_return([nic]).at_least(:once)
expect(nic).to receive(:incr_destroy).and_return(true)
expect { nx.destroy }.to nap(1)
end
it "deletes and pops if nics are destroyed" do
expect(ps).to receive(:destroy).and_return(true)
expect(ps).to receive(:nics).and_return([]).at_least(:once)
expect { nx.destroy }.to exit({"msg" => "subnet destroyed"})
end
it "disconnects all subnets" do
prj = Project.create_with_id(name: "test-project")
ps1 = described_class.assemble(prj.id, name: "ps1").subject
ps2 = described_class.assemble(prj.id, name: "ps2").subject
ps1.connect_subnet(ps2)
expect(ps1.connected_subnets.map(&:id)).to eq [ps2.id]
expect(ps2.connected_subnets.map(&:id)).to eq [ps1.id]
expect(nx).to receive(:private_subnet).and_return(ps1).at_least(:once)
expect(ps1).to receive(:disconnect_subnet).with(ps2).and_call_original
expect { nx.destroy }.to exit({"msg" => "subnet destroyed"})
end
end
describe "#wait_aws_vpc_destroyed" do
it "naps if there are nics" do
st.update(prog: "Vnet::SubnetNexus", label: "wait_aws_vpc_destroyed", stack: [{}])
expect(nx).to receive(:private_subnet).and_return(ps).at_least(:once)
expect(ps).to receive(:nics).and_return([1]).at_least(:once)
expect { nx.wait_aws_vpc_destroyed }.to nap(5)
end
it "deletes the vpc and pops if leaf" do
st.update(prog: "Vnet::SubnetNexus", label: "wait_aws_vpc_destroyed", stack: [{}])
expect(nx).to receive(:private_subnet).and_return(ps).at_least(:once)
expect(ps).to receive(:destroy).and_return(true)
expect(ps).to receive(:private_subnet_aws_resource).and_return(instance_double(PrivateSubnetAwsResource, id: "123", destroy: true))
expect { nx.wait_aws_vpc_destroyed }.to exit({"msg" => "vpc destroyed"})
end
it "naps if not leaf" do
st.update(prog: "Vnet::SubnetNexus", label: "wait_aws_vpc_destroyed", stack: [{}])
Strand.create(parent_id: st.id, prog: "Aws::Vpc", label: "destroy", stack: [{}])
expect { nx.wait_aws_vpc_destroyed }.to nap(10)
end
end
end