Skip to content

Commit

Permalink
Move Firewalls from VM to Subnet
Browse files Browse the repository at this point in the history
  • Loading branch information
furkansahin committed Apr 10, 2024
1 parent 375f7c2 commit 91df86a
Show file tree
Hide file tree
Showing 17 changed files with 88 additions and 48 deletions.
10 changes: 8 additions & 2 deletions lib/validation.rb
Expand Up @@ -116,9 +116,15 @@ def self.validate_postgres_superuser_password(original_password, repeat_password
end

def self.validate_cidr(cidr)
NetAddr::IPv4Net.parse(cidr)
if cidr.include?(".")
NetAddr::IPv4Net.parse(cidr)
elsif cidr.include?(":")
NetAddr::IPv6Net.parse(cidr)
else
fail ValidationFailed.new({cidr: "Invalid CIDR"})
end
rescue NetAddr::ValidationError
fail ValidationFailed.new({CIDR: "Invalid CIDR"})
fail ValidationFailed.new({cidr: "Invalid CIDR"})
end

def self.validate_port_range(port_range)
Expand Down
24 changes: 24 additions & 0 deletions migrate/20240409_move_firewall_to_subnet.rb
@@ -0,0 +1,24 @@
# frozen_string_literal: true

Sequel.migration do
up do
alter_table(:firewall) do
drop_constraint :firewall_vm_id_fkey
rename_column :vm_id, :private_subnet_id
end

run <<~SQL
UPDATE firewall f
SET private_subnet_id = (
SELECT n.private_subnet_id
FROM nic n
WHERE n.vm_id = f.private_subnet_id
);
SQL

alter_table(:firewall) do
add_foreign_key [:private_subnet_id], :private_subnet
set_column_allow_null :private_subnet_id, true
end
end
end
4 changes: 2 additions & 2 deletions model/firewall.rb
Expand Up @@ -4,7 +4,7 @@

class Firewall < Sequel::Model
one_to_many :firewall_rules, key: :firewall_id
many_to_one :vm, key: :vm_id
many_to_one :private_subnet, key: :private_subnet_id

plugin :association_dependencies, firewall_rules: :destroy

Expand All @@ -17,7 +17,7 @@ def insert_firewall_rule(cidr, port_range)
port_range: port_range
)

vm&.incr_update_firewall_rules
private_subnet&.incr_update_firewall_rules
fwr
end
end
2 changes: 1 addition & 1 deletion model/postgres/postgres_server.rb
Expand Up @@ -165,7 +165,7 @@ def health_monitor_socket_path
end

def create_resource_firewall_rules
fw = Firewall.create_with_id(vm_id: vm.id, name: ubid.to_s, description: "Postgres default firewall")
fw = Firewall.create_with_id(private_subnet_id: vm.private_subnets.first.id, name: ubid.to_s, description: "Postgres default firewall")
resource.firewall_rules.each do |pg_fwr|
fw.insert_firewall_rule(pg_fwr.cidr.to_s, Sequel.pg_range(5432..5432))
end
Expand Down
6 changes: 4 additions & 2 deletions model/private_subnet.rb
Expand Up @@ -6,14 +6,16 @@ class PrivateSubnet < Sequel::Model
many_to_many :vms, join_table: Nic.table_name, left_key: :private_subnet_id, right_key: :vm_id
one_to_many :nics, key: :private_subnet_id
one_to_one :strand, key: :id
one_to_many :firewall_rules
one_to_many :firewalls

PRIVATE_SUBNET_RANGES = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16"
].freeze

plugin :association_dependencies, firewalls: :destroy

dataset_module Pagination
dataset_module Authorization::Dataset
include Authorization::HyperTagMethods
Expand All @@ -38,7 +40,7 @@ def display_state
end

include SemaphoreMethods
semaphore :destroy, :refresh_keys, :add_new_nic
semaphore :destroy, :refresh_keys, :add_new_nic, :update_firewall_rules

def self.random_subnet
PRIVATE_SUBNET_RANGES.sample
Expand Down
7 changes: 5 additions & 2 deletions model/vm.rb
Expand Up @@ -11,9 +11,8 @@ class Vm < Sequel::Model
one_to_one :assigned_vm_address, key: :dst_vm_id, class: :AssignedVmAddress
one_to_many :vm_storage_volumes, key: :vm_id, order: Sequel.desc(:boot)
one_to_one :active_billing_record, class: :BillingRecord, key: :resource_id do |ds| ds.active end
one_to_many :firewalls, key: :vm_id

plugin :association_dependencies, sshable: :destroy, assigned_vm_address: :destroy, vm_storage_volumes: :destroy, firewalls: :destroy
plugin :association_dependencies, sshable: :destroy, assigned_vm_address: :destroy, vm_storage_volumes: :destroy

dataset_module Pagination
dataset_module Authorization::Dataset
Expand All @@ -31,6 +30,10 @@ def hyper_tag_name(project)

include Authorization::TaggableMethods

def firewalls
private_subnets.flat_map(&:firewalls)
end

def path
"/location/#{location}/vm/#{name}"
end
Expand Down
1 change: 0 additions & 1 deletion prog/postgres/postgres_server_nexus.rb
Expand Up @@ -291,7 +291,6 @@ def before_run

# create a new set of firewall rules
postgres_server.create_resource_firewall_rules
vm.incr_update_firewall_rules

hop_wait
end
Expand Down
6 changes: 1 addition & 5 deletions prog/vm/nexus.rb
Expand Up @@ -72,7 +72,7 @@ def self.assemble(public_key, project_id, name: nil, size: "standard-2",
raise "Given subnet doesn't exist with the id #{private_subnet_id}" unless subnet
raise "Given subnet is not available in the given project" unless project.private_subnets.any? { |ps| ps.id == subnet.id }
else
subnet_s = Prog::Vnet::SubnetNexus.assemble(project_id, name: "#{name}-subnet", location: location)
subnet_s = Prog::Vnet::SubnetNexus.assemble(project_id, name: "#{name}-subnet", location: location, allow_only_ssh: allow_only_ssh)
subnet = PrivateSubnet[subnet_s.id]
end
nic_s = Prog::Vnet::NicNexus.assemble(subnet.id, name: "#{name}-nic")
Expand All @@ -90,10 +90,6 @@ def self.assemble(public_key, project_id, name: nil, size: "standard-2",
boot_image: boot_image, ip4_enabled: enable_ip4, pool_id: pool_id, arch: arch) { _1.id = ubid.to_uuid }
nic.update(vm_id: vm.id)

port_range = allow_only_ssh ? 22..22 : 0..65535
fw = Firewall.create_with_id(vm_id: vm.id, name: "#{name}-default")
["0.0.0.0/0", "::/0"].each { |cidr| FirewallRule.create_with_id(firewall_id: fw.id, cidr: cidr, port_range: Sequel.pg_range(port_range)) }

vm.associate_with_project(project)

Strand.create(
Expand Down
13 changes: 11 additions & 2 deletions prog/vnet/subnet_nexus.rb
Expand Up @@ -2,9 +2,9 @@

class Prog::Vnet::SubnetNexus < Prog::Base
subject_is :private_subnet
semaphore :destroy, :refresh_keys, :add_new_nic
semaphore :destroy, :refresh_keys, :add_new_nic, :update_firewall_rules

def self.assemble(project_id, name: nil, location: "hetzner-hel1", ipv6_range: nil, ipv4_range: nil)
def self.assemble(project_id, name: nil, location: "hetzner-hel1", ipv6_range: nil, ipv4_range: nil, allow_only_ssh: false)
unless (project = Project[project_id])
fail "No existing project"
end
Expand All @@ -20,6 +20,10 @@ def self.assemble(project_id, name: nil, location: "hetzner-hel1", ipv6_range: n
DB.transaction do
ps = PrivateSubnet.create(name: name, location: location, net6: ipv6_range, net4: ipv4_range, state: "waiting") { _1.id = ubid.to_uuid }
ps.associate_with_project(project)
port_range = allow_only_ssh ? 22..22 : 0..65535
fw = Firewall.create_with_id(private_subnet_id: ubid.to_uuid, name: "#{name}-default")
["0.0.0.0/0", "::/0"].each { |cidr| FirewallRule.create_with_id(firewall_id: fw.id, cidr: cidr, port_range: Sequel.pg_range(port_range)) }

Strand.create(prog: "Vnet::SubnetNexus", label: "wait") { _1.id = ubid.to_uuid }
end
end
Expand All @@ -44,6 +48,11 @@ def before_run
hop_add_new_nic
end

when_update_firewall_rules_set? do
private_subnet.vms.map(&:incr_update_firewall_rules)
decr_update_firewall_rules
end

if private_subnet.last_rekey_at < Time.now - 60 * 60 * 24
private_subnet.incr_refresh_keys
end
Expand Down
3 changes: 2 additions & 1 deletion routes/web/project/location/postgres.rb
Expand Up @@ -27,11 +27,12 @@ class CloverWeb
r.on "firewall-rule" do
r.post true do
Authorization.authorize(@current_user.id, "Postgres:Firewall:edit", pg.id)
parsed_cidr = Validation.validate_cidr(r.params["cidr"])

DB.transaction do
PostgresFirewallRule.create_with_id(
postgres_resource_id: pg.id,
cidr: r.params["cidr"]
cidr: parsed_cidr.to_s
)
pg.incr_update_firewall_rules
end
Expand Down
3 changes: 2 additions & 1 deletion routes/web/project/location/vm.rb
Expand Up @@ -30,9 +30,10 @@ class CloverWeb
Validation.validate_port_range(r.params["port_range"])
end

parsed_cidr = Validation.validate_cidr(r.params["cidr"])
pg_range = Sequel.pg_range(port_range.first..port_range.last)

vm.firewalls.first.insert_firewall_rule(r.params["cidr"], pg_range)
vm.firewalls.first.insert_firewall_rule(parsed_cidr.to_s, pg_range)
flash["notice"] = "Firewall rule is created"

r.redirect "#{@project.path}#{vm.path}"
Expand Down
7 changes: 7 additions & 0 deletions spec/lib/validation_spec.rb
Expand Up @@ -222,13 +222,20 @@
expect { described_class.validate_cidr("0.0.0.0/1") }.not_to raise_error
expect { described_class.validate_cidr("192.168.1.0/24") }.not_to raise_error
expect { described_class.validate_cidr("255.255.255.255/0") }.not_to raise_error

expect { described_class.validate_cidr("::/0") }.not_to raise_error
expect { described_class.validate_cidr("::1/128") }.not_to raise_error
expect { described_class.validate_cidr("2001:db8::/32") }.not_to raise_error
end

it "invalid cidr" do
expect { described_class.validate_cidr("192.168.1.256/24") }.to raise_error described_class::ValidationFailed
expect { described_class.validate_cidr("10.256.0.0/8") }.to raise_error described_class::ValidationFailed
expect { described_class.validate_cidr("172.16.0.0/33") }.to raise_error described_class::ValidationFailed
expect { described_class.validate_cidr("not_a_cidr") }.to raise_error described_class::ValidationFailed

expect { described_class.validate_cidr("::1/129") }.to raise_error described_class::ValidationFailed
expect { described_class.validate_cidr("::1/::1") }.to raise_error described_class::ValidationFailed
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions spec/model/firewall_spec.rb
Expand Up @@ -18,9 +18,9 @@
end

it "increments VMs update_firewall_rules if there is a VM" do
vm = instance_double(Vm)
expect(fw).to receive(:vm).and_return(vm)
expect(vm).to receive(:incr_update_firewall_rules)
private_subnet = instance_double(PrivateSubnet)
expect(fw).to receive(:private_subnet).and_return(private_subnet)
expect(private_subnet).to receive(:incr_update_firewall_rules)
fw.insert_firewall_rule("0.0.0.0/0", nil)
end
end
Expand Down
4 changes: 2 additions & 2 deletions spec/prog/postgres/postgres_server_nexus_spec.rb
Expand Up @@ -19,7 +19,8 @@
Vm,
id: "1c7d59ee-8d46-8374-9553-6144490ecec5",
sshable: sshable,
ephemeral_net4: "1.1.1.1"
ephemeral_net4: "1.1.1.1",
private_subnets: [instance_double(PrivateSubnet)]
)
)
}
Expand Down Expand Up @@ -472,7 +473,6 @@
expect(postgres_server.vm).to receive(:firewalls).and_return([fw])
expect(fw).to receive(:destroy)
expect(postgres_server).to receive(:create_resource_firewall_rules)
expect(postgres_server.vm).to receive(:incr_update_firewall_rules)

expect { nx.update_firewall_rules }.to hop("wait")
end
Expand Down
36 changes: 14 additions & 22 deletions spec/prog/vnet/subnet_nexus_spec.rb
Expand Up @@ -24,43 +24,27 @@
end

it "uses ipv6_addr if passed and creates entities" do
ps = instance_double(PrivateSubnet, id: "57afa8a7-2357-4012-9632-07fbe13a3133")
expect(ps).to receive(:associate_with_project).with(prj).and_return(true)
expect(PrivateSubnet).to receive(:create).with(
name: "default-ps",
location: "hetzner-hel1",
net6: "fd10:9b0b:6b4b:8fbb::/64",
net4: "10.0.0.0/26",
state: "waiting"
).and_return(ps)
expect(described_class).to receive(:random_private_ipv4).and_return("10.0.0.0/26")
expect(Strand).to receive(:create).with(prog: "Vnet::SubnetNexus", label: "wait").and_yield(Strand.new).and_return(Strand.new)
described_class.assemble(
ps = described_class.assemble(
prj.id,
name: "default-ps",
location: "hetzner-hel1",
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
ps = instance_double(PrivateSubnet, id: "57afa8a7-2357-4012-9632-07fbe13a3133")
expect(ps).to receive(:associate_with_project).with(prj).and_return(true)
expect(PrivateSubnet).to receive(:create).with(
name: "default-ps",
location: "hetzner-hel1",
net6: "fd10:9b0b:6b4b:8fbb::/64",
net4: "10.0.0.0/26",
state: "waiting"
).and_return(ps)
expect(described_class).to receive(:random_private_ipv6).and_return("fd10:9b0b:6b4b:8fbb::/64")
expect(Strand).to receive(:create).with(prog: "Vnet::SubnetNexus", label: "wait").and_yield(Strand.new).and_return(Strand.new)
described_class.assemble(
ps = described_class.assemble(
prj.id,
name: "default-ps",
location: "hetzner-hel1",
ipv4_range: "10.0.0.0/26"
)

expect(ps.subject.net4.to_s).to eq("10.0.0.0/26")
end
end

Expand Down Expand Up @@ -134,6 +118,14 @@
expect { nx.wait }.to nap(30)
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(30)
end

it "naps if nothing to do" do
expect { nx.wait }.to nap(30)
end
Expand Down
2 changes: 1 addition & 1 deletion spec/routes/api/project/location/postgres_spec.rb
Expand Up @@ -227,7 +227,7 @@
}.to_json

expect(last_response.status).to eq(400)
expect(JSON.parse(last_response.body)["error"]["details"]["CIDR"]).to eq("Invalid CIDR")
expect(JSON.parse(last_response.body)["error"]["details"]["cidr"]).to eq("Invalid CIDR")
end

it "restore" do
Expand Down
2 changes: 1 addition & 1 deletion spec/routes/web/vm_spec.rb
Expand Up @@ -332,7 +332,7 @@
expect(page).to have_content "12.12.12.0/26"
expect(page).to have_content "443"

expect(SemSnap.new(vm.id).set?("update_firewall_rules")).to be true
expect(SemSnap.new(vm.private_subnets.first.id).set?("update_firewall_rules")).to be true
end
end

Expand Down

0 comments on commit 91df86a

Please sign in to comment.