Files
ubicloud/spec/model/private_subnet_spec.rb
Jeremy Evans d60112b212 Use recursive CTE instead of recursive method with multiple queries per method for PrivateSubnet#find_all_connected_nics
To avoid cycles, use the CYCLE support added in PostgreSQL 14.

Unfortunately, this change breaks a number of specs that use mocking,
for both PrivateSubnet and Prog::Vnet::SubnetNexus.  Change those
specs to avoid mocking the related methods.

Avoid N+1 queries for SubnetNexus#active_nics and #nics_to_rekey
to check the label for the related strands.  Instead, join to the
strand table and do the filtering in the database. This also
allows nics_to_rekey to use a single query for the two labels
(wait and wait_setup), instead of a separate query per label.

Avoid N+1 query for SubnetNexus#rekeying_nics by eager loading
the strands, and do the rekey_payload filter in the database
instead of in Ruby.

Add SubnetNexus#nics_with_strand_label and #all_connected_nics
to DRY up the related code.

Remove SubnetNexus#to_be_added_nics, which was only called
internally by #nics_to_rekey, and no longer needed there, as
it does a single query instead of 2 queries.
2025-06-25 04:47:48 +09:00

239 lines
9.6 KiB
Ruby

# frozen_string_literal: true
require_relative "spec_helper"
RSpec.describe PrivateSubnet do
subject(:private_subnet) {
described_class.new(
net6: NetAddr.parse_net("fd1b:9793:dcef:cd0a::/64"),
net4: NetAddr.parse_net("10.9.39.0/26"),
location_id: Location::HETZNER_FSN1_ID,
state: "waiting",
name: "ps",
project_id: Project.create(name: "test").id
)
}
let(:nic) { instance_double(Nic, id: "0a9a166c-e7e7-4447-ab29-7ea442b5bb0e") }
let(:existing_nic) {
instance_double(Nic,
id: "46ca6ded-b056-4723-bd91-612959f52f6f",
private_ipv4: "10.9.39.5/32",
private_ipv6: "fd1b:9793:dcef:cd0a:c::/79")
}
it "disallows VM ubid format as name" do
ps = described_class.new(name: described_class.generate_ubid.to_s)
ps.validate
expect(ps.errors[:name]).to eq ["cannot be exactly 26 numbers/lowercase characters starting with ps to avoid overlap with id format"]
end
it "allows inference endpoint ubid format as name" do
ps = described_class.new(name: InferenceEndpoint.generate_ubid.to_s)
ps.validate
expect(ps.errors[:name]).to be_nil
end
describe "random ip generation" do
it "returns random private ipv4" do
private_subnet
expect(SecureRandom).to receive(:random_number).with(59).and_return(5)
expect(private_subnet.random_private_ipv4.to_s).to eq "10.9.39.9/32"
end
it "returns random private ipv6" do
private_subnet
expect(SecureRandom).to receive(:random_number).with(32766).and_return(5)
expect(private_subnet.random_private_ipv6.to_s).to eq "fd1b:9793:dcef:cd0a:c::/79"
end
it "returns random private ipv4 when ip exists" do
private_subnet
expect(SecureRandom).to receive(:random_number).with(59).and_return(1, 2)
expect(private_subnet).to receive(:nics).and_return([existing_nic]).twice
expect(private_subnet.random_private_ipv4.to_s).to eq "10.9.39.6/32"
end
it "returns random private ipv6 when ip exists" do
private_subnet
expect(SecureRandom).to receive(:random_number).with(32766).and_return(5, 6)
expect(private_subnet).to receive(:nics).and_return([existing_nic]).twice
expect(private_subnet.random_private_ipv6.to_s).to eq "fd1b:9793:dcef:cd0a:e::/79"
end
end
describe ".[]" do
let(:private_subnet) {
subnet = super()
subnet.net6 = subnet.net6.to_s
subnet.net4 = subnet.net4.to_s
subnet.id = described_class.generate_ubid.to_uuid.to_s
subnet.save_changes
}
it "looks up by ubid object" do
expect(described_class[UBID.parse(private_subnet.ubid)].id).to eq private_subnet.id
end
it "looks up by ubid string" do
expect(described_class[private_subnet.ubid].id).to eq private_subnet.id
end
it "looks up by uuid string" do
expect(described_class[private_subnet.id].id).to eq private_subnet.id
end
it "looks up by hash" do
expect(described_class[id: private_subnet.id].id).to eq private_subnet.id
end
it "doesn't raise if given something that looks like a ubid but isn't" do
expect(described_class["a" * 26]).to be_nil
end
end
describe "#inspect" do
it "includes ubid if id is available" do
ubid = described_class.generate_ubid
private_subnet.id = ubid.to_uuid.to_s
expect(private_subnet.inspect).to eq "#<PrivateSubnet[\"#{ubid}\"] @values={net6: \"fd1b:9793:dcef:cd0a::/64\", net4: \"10.9.39.0/26\", location_id: \"10saktg1sprp3mxefj1m3kppq2\", state: \"waiting\", name: \"ps\", project_id: \"#{private_subnet.project.ubid}\"}>"
end
it "does not includes ubid if id is missing" do
expect(private_subnet.inspect).to eq "#<PrivateSubnet @values={net6: \"fd1b:9793:dcef:cd0a::/64\", net4: \"10.9.39.0/26\", location_id: \"10saktg1sprp3mxefj1m3kppq2\", state: \"waiting\", name: \"ps\", project_id: \"#{private_subnet.project.ubid}\"}>"
end
end
describe "uuid to name" do
it "returns the name" do
expect(described_class.ubid_to_name("psetv2ff83xj6h3prt2jwavh0q")).to eq "psetv2ff"
end
end
describe "ui utility methods" do
it "returns path" do
expect(private_subnet.path).to eq "/location/eu-central-h1/private-subnet/ps"
end
end
describe "display_state" do
it "returns available when waiting" do
expect(private_subnet.display_state).to eq "available"
end
it "returns state if not waiting" do
private_subnet.state = "failed"
expect(private_subnet.display_state).to eq "failed"
end
end
describe "destroy" do
it "destroys firewalls private subnets" do
project_id = Project.create(name: "test").id
ps = described_class.create_with_id(name: "test-ps", location_id: Location::HETZNER_FSN1_ID, net6: "2001:db8::/64", net4: "10.0.0.0/24", project_id:)
ps.add_firewall(project_id:, location_id: Location::HETZNER_FSN1_ID)
expect(ps.firewalls_dataset.count).to eq 1
ps.destroy
expect(ps.firewalls_dataset.count).to eq 0
end
end
describe ".create_tunnels" do
let(:src_nic) {
instance_double(Nic, id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b")
}
let(:dst_nic) {
instance_double(Nic, id: "6a187cc1-291b-8eac-bdfc-96801fa3118d")
}
it "creates tunnels if doesn't exist" do
expect(IpsecTunnel).to receive(:create).with(src_nic_id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b", dst_nic_id: "6a187cc1-291b-8eac-bdfc-96801fa3118d").and_return(true)
expect(IpsecTunnel).to receive(:create).with(src_nic_id: "6a187cc1-291b-8eac-bdfc-96801fa3118d", dst_nic_id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b").and_return(true)
private_subnet.create_tunnels([src_nic, dst_nic], dst_nic)
end
it "skips existing tunnels" do
expect(IpsecTunnel).to receive(:[]).with(src_nic_id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b", dst_nic_id: "6a187cc1-291b-8eac-bdfc-96801fa3118d").and_return(true)
expect(IpsecTunnel).to receive(:[]).with(src_nic_id: "6a187cc1-291b-8eac-bdfc-96801fa3118d", dst_nic_id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b").and_return(false)
expect(IpsecTunnel).to receive(:create).with(src_nic_id: "6a187cc1-291b-8eac-bdfc-96801fa3118d", dst_nic_id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b").and_return(true)
private_subnet.create_tunnels([src_nic, dst_nic], dst_nic)
end
it "skips existing tunnels - 2" do
expect(IpsecTunnel).to receive(:[]).with(src_nic_id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b", dst_nic_id: "6a187cc1-291b-8eac-bdfc-96801fa3118d").and_return(false)
expect(IpsecTunnel).to receive(:[]).with(src_nic_id: "6a187cc1-291b-8eac-bdfc-96801fa3118d", dst_nic_id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b").and_return(true)
expect(IpsecTunnel).to receive(:create).with(src_nic_id: "8ce8a85c-c3d6-86ac-bfdf-022bad69440b", dst_nic_id: "6a187cc1-291b-8eac-bdfc-96801fa3118d").and_return(true)
private_subnet.create_tunnels([src_nic, dst_nic], dst_nic)
end
end
describe "connected subnets related methods" do
let(:prj) {
Project.create_with_id(name: "test-prj")
}
let(:ps1) {
Prog::Vnet::SubnetNexus.assemble(prj.id, name: "test-ps1", location_id: Location::HETZNER_FSN1_ID).subject
}
it ".connected_subnets" do
ps2 = Prog::Vnet::SubnetNexus.assemble(prj.id, name: "test-ps2", location_id: Location::HETZNER_FSN1_ID).subject
expect(ps1.connected_subnets).to eq []
ps1.connect_subnet(ps2)
expect(ps1.connected_subnets.map(&:id)).to eq [ps2.id]
expect(ps2.connected_subnets.map(&:id)).to eq [ps1.id]
ps3 = Prog::Vnet::SubnetNexus.assemble(prj.id, name: "test-ps3", location_id: Location::HETZNER_FSN1_ID).subject
ps2.connect_subnet(ps3)
expect(ps1.connected_subnets.map(&:id)).to eq [ps2.id]
expect(ps2.connected_subnets.map(&:id).sort).to eq [ps1.id, ps3.id].sort
expect(ps3.connected_subnets.map(&:id)).to eq [ps2.id]
ps1.disconnect_subnet(ps2)
expect(ps1.connected_subnets.map(&:id)).to eq []
expect(ps2.connected_subnets.map(&:id).sort).to eq [ps3.id].sort
expect(ps3.connected_subnets.map(&:id)).to eq [ps2.id]
end
it ".all_nics" do
ps2 = Prog::Vnet::SubnetNexus.assemble(prj.id, name: "test-ps2", location_id: Location::HETZNER_FSN1_ID).subject
ps1_nic = Prog::Vnet::NicNexus.assemble(ps1.id, name: "test-ps1-nic1").subject
ps2_nic = Prog::Vnet::NicNexus.assemble(ps2.id, name: "test-ps2-nic1").subject
expect(ps1.all_nics.map(&:id)).to eq [ps1_nic.id]
expect(ps1).to receive(:create_tunnels).with([ps2_nic], ps1_nic).and_call_original
ps1.connect_subnet(ps2)
expect(ps1.all_nics.map(&:id).sort).to eq [ps1_nic.id, ps2_nic.id].sort
ps1.disconnect_subnet(ps2)
expect(ps1.all_nics.map(&:id)).to eq [ps1_nic.id]
end
it "disconnect_subnet does not destroy in subnet tunnels" do
ps2 = Prog::Vnet::SubnetNexus.assemble(prj.id, name: "test-ps2", location_id: Location::HETZNER_FSN1_ID).subject
ps1_nic = Prog::Vnet::NicNexus.assemble(ps1.id, name: "test-ps1-nic1").subject
ps1_nic2 = Prog::Vnet::NicNexus.assemble(ps1.id, name: "test-ps1-nic2").subject
ps1.create_tunnels([ps1_nic], ps1_nic2)
ps2_nic = Prog::Vnet::NicNexus.assemble(ps2.id, name: "test-ps2-nic1").subject
ps1.connect_subnet(ps2)
expect(ps1.find_all_connected_nics.map(&:id).sort).to eq [ps1_nic.id, ps1_nic2.id, ps2_nic.id].sort
expect(IpsecTunnel.count).to eq 6
ps1.disconnect_subnet(ps2)
expect(ps1.find_all_connected_nics.map(&:id).sort).to eq [ps1_nic.id, ps1_nic2.id].sort
tunnels = ps1_nic.src_ipsec_tunnels + ps1_nic.dst_ipsec_tunnels
expect(IpsecTunnel.all.map(&:id).sort).to eq(tunnels.map(&:id).sort)
expect(IpsecTunnel.count).to eq 2
end
end
end