Files
ubicloud/spec/prog/vnet/subnet_nexus_spec.rb
Jeremy Evans 4b819d3cb2 Change all create_with_id to create
It hasn't been necessary to use create_with_id since
ebc79622df, in December 2024.

I have plans to introduce:

```ruby
def create_with_id(id, values)
  obj = new(values)
  obj.id = id
  obj.save_changes
end
```

This will make it easier to use the same id when creating
multiple objects.  The first step is removing the existing
uses of create_with_id.
2025-08-06 01:55:51 +09: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(name: "default") }
let(:ps) {
PrivateSubnet.create(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(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(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(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(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(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(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(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(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(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(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(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(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(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