Files
ubicloud/prog/vnet/subnet_nexus.rb
Jeremy Evans 59621ae323 Use create_with_id in progs and routes
Only changes to the specs in this commit are to fix mocking issues.
2025-08-07 02:13:08 +09:00

275 lines
8.2 KiB
Ruby

# frozen_string_literal: true
class Prog::Vnet::SubnetNexus < Prog::Base
subject_is :private_subnet
def self.assemble(project_id, name: nil, location_id: Location::HETZNER_FSN1_ID, ipv6_range: nil, ipv4_range: nil, allow_only_ssh: false, firewall_id: nil)
unless (project = Project[project_id])
fail "No existing project"
end
unless (location = Location[location_id])
fail "No existing location"
end
if allow_only_ssh && firewall_id
fail "Cannot specify both allow_only_ssh and firewall_id"
end
ubid = PrivateSubnet.generate_ubid
id = ubid.to_uuid
name ||= PrivateSubnet.ubid_to_name(ubid)
Validation.validate_name(name)
ipv6_range ||= random_private_ipv6(location, project).to_s
ipv4_range ||= random_private_ipv4(location, project).to_s
DB.transaction do
ps = PrivateSubnet.create_with_id(id, name:, location_id: location.id, net6: ipv6_range, net4: ipv4_range, state: "waiting", project_id:)
firewall_dataset = project.firewalls_dataset.where(location_id:)
if firewall_id
unless (firewall = firewall_dataset.first(Sequel[:firewall][:id] => firewall_id))
fail "Firewall with id #{firewall_id} and location #{location.name} does not exist"
end
else
port_range = allow_only_ssh ? 22..22 : 0..65535
fw_name = "#{name[0, 55]}-default"
# As is typical when checking before inserting, there is a race condition here with
# a user concurrently manually creating a firewall with the same name. However,
# the worst case scenario is a bogus error message, and the user could try creating
# the private subnet again.
unless firewall_dataset.where(Sequel[:firewall][:name] => fw_name).empty?
fw_name = "#{name[0, 47]}-default-#{Array.new(7) { UBID.from_base32(rand(32)) }.join}"
end
firewall = Firewall.create(name: fw_name, location_id: location.id, project_id:)
["0.0.0.0/0", "::/0"].each { |cidr| FirewallRule.create(firewall_id: firewall.id, cidr: cidr, port_range: Sequel.pg_range(port_range)) }
end
firewall.associate_with_private_subnet(ps, apply_firewalls: false)
Strand.create_with_id(id, prog: "Vnet::SubnetNexus", label: "start")
end
end
def before_run
when_destroy_set? do
if strand.label != "destroy"
register_deadline(nil, 10 * 60)
hop_destroy
end
end
end
label def start
if private_subnet.location.aws?
PrivateSubnetAwsResource.create_with_id(private_subnet.id) unless private_subnet.private_subnet_aws_resource
bud Prog::Aws::Vpc, {"subject_id" => private_subnet.id}, :create_vpc
hop_wait_vpc_created
else
hop_wait
end
end
label def wait_vpc_created
reap(:wait, nap: 2)
end
label def wait
if private_subnet.location.aws?
check_firewall_update
private_subnet.semaphores.each(&:destroy)
nap 60 * 60 * 24 * 365
end
when_refresh_keys_set? do
private_subnet.update(state: "refreshing_keys")
hop_refresh_keys
end
when_add_new_nic_set? do
private_subnet.update(state: "adding_new_nic")
hop_add_new_nic
end
check_firewall_update
if private_subnet.last_rekey_at < Time.now - 60 * 60 * 24
private_subnet.incr_refresh_keys
end
nap 10 * 60
end
def check_firewall_update
when_update_firewall_rules_set? do
private_subnet.vms.each(&:incr_update_firewall_rules)
decr_update_firewall_rules
end
end
def gen_encryption_key
"0x" + SecureRandom.bytes(36).unpack1("H*")
end
def gen_spi
"0x" + SecureRandom.bytes(4).unpack1("H*")
end
def gen_reqid
SecureRandom.random_number(100000) + 1
end
label def add_new_nic
register_deadline("wait", 3 * 60)
nics_snap = nics_to_rekey
nap 10 if nics_snap.any? { |nic| nic.lock_set? }
nics_snap.each do |nic|
nic.update(encryption_key: gen_encryption_key, rekey_payload: {spi4: gen_spi, spi6: gen_spi, reqid: gen_reqid})
nic.incr_start_rekey
nic.incr_lock
private_subnet.create_tunnels(nics_snap, nic)
end
decr_add_new_nic
hop_wait_inbound_setup
end
label def refresh_keys
decr_refresh_keys
nics = active_nics
nap 10 if nics.any? { |nic| nic.lock_set? }
nics.each do |nic|
nic.update(encryption_key: gen_encryption_key, rekey_payload: {spi4: gen_spi, spi6: gen_spi, reqid: gen_reqid})
nic.incr_start_rekey
nic.incr_lock
end
hop_wait_inbound_setup
end
label def wait_inbound_setup
nics = rekeying_nics
if nics.all? { |nic| nic.strand.label == "wait_rekey_outbound_trigger" }
nics.each(&:incr_trigger_outbound_update)
hop_wait_outbound_setup
end
nap 5
end
label def wait_outbound_setup
nics = rekeying_nics
if nics.all? { |nic| nic.strand.label == "wait_rekey_old_state_drop_trigger" }
nics.each(&:incr_old_state_drop_trigger)
hop_wait_old_state_drop
end
nap 5
end
label def wait_old_state_drop
nics = rekeying_nics
if nics.all? { |nic| nic.strand.label == "wait" }
private_subnet.update(state: "waiting", last_rekey_at: Time.now)
nics.each do |nic|
nic.update(encryption_key: nil, rekey_payload: nil)
nic.unlock
end
hop_wait
end
nap 5
end
label def destroy
if private_subnet.nics.any? { |n| !n.vm_id.nil? }
register_deadline(nil, 10 * 60, allow_extension: true) if private_subnet.nics.any? { |n| n.vm&.prevent_destroy_set? }
Clog.emit("Cannot destroy subnet with active nics, first clean up the attached resources") { private_subnet }
nap 5
end
decr_destroy
strand.children.each { it.destroy }
if private_subnet.location.aws?
private_subnet.nics.map(&:incr_destroy)
private_subnet.firewalls.map(&:destroy)
bud Prog::Aws::Vpc, {"subject_id" => private_subnet.id}, :destroy
hop_wait_aws_vpc_destroyed
end
private_subnet.firewalls.map { it.disassociate_from_private_subnet(private_subnet, apply_firewalls: false) }
private_subnet.connected_subnets.each do |subnet|
private_subnet.disconnect_subnet(subnet)
end
if private_subnet.nics.empty? && private_subnet.load_balancers.empty?
private_subnet.destroy
pop "subnet destroyed"
else
private_subnet.nics.map { |n| n.incr_destroy }
private_subnet.load_balancers.map { |lb| lb.incr_destroy }
nap 1
end
end
label def wait_aws_vpc_destroyed
reap(nap: 10) do
nap 5 unless private_subnet.nics.empty?
private_subnet.private_subnet_aws_resource.destroy
private_subnet.destroy
pop "vpc destroyed"
end
end
def self.random_private_ipv6(location, project)
network_address = NetAddr::IPv6.new((SecureRandom.bytes(7) + 0xfd.chr).unpack1("Q<") << 64)
network_mask = NetAddr::Mask128.new(64)
selected_addr = NetAddr::IPv6Net.new(network_address, network_mask)
selected_addr = random_private_ipv6(location, project) if project.private_subnets_dataset[net6: selected_addr.to_s, location_id: location.id]
selected_addr
end
def self.random_private_ipv4(location, project, cidr_size = 26)
raise ArgumentError, "CIDR size must be between 0 and 32" unless cidr_size.between?(0, 32)
private_range = PrivateSubnet.random_subnet
addr = NetAddr::IPv4Net.parse(private_range)
selected_addr = if addr.netmask.prefix_len < cidr_size
addr.nth_subnet(cidr_size, SecureRandom.random_number(2**(cidr_size - addr.netmask.prefix_len) - 1).to_i + 1)
else
random_private_ipv4(location, project, cidr_size)
end
selected_addr = random_private_ipv4(location, project, cidr_size) if PrivateSubnet::BANNED_IPV4_SUBNETS.any? { it.rel(selected_addr) } || project.private_subnets_dataset[net4: selected_addr.to_s, location_id: location.id]
selected_addr
end
def active_nics
nics_with_strand_label("wait").all
end
def nics_to_rekey
nics_with_strand_label(%w[wait wait_setup]).all
end
def rekeying_nics
all_connected_nics.eager(:strand).exclude(rekey_payload: nil).all
end
private
def all_connected_nics
private_subnet.find_all_connected_nics
end
def nics_with_strand_label(label)
all_connected_nics.join(:strand, {id: :id, label:}).select_all(:nic)
end
end