Files
ubicloud/prog/dns_zone/setup_dns_server_vm.rb
Eren Başak c8f46268b9 Add Prog to Create a new VM for a DNS Server
We didn't have any automation for creation of VMs to server DNS servers
and the process consisted of many manual error-prone steps.

In this commit, we introduce SetupDnsServerVM prog, which makes it
straightforward to create fully-functional DNS servers from scratch
(which could be useful for development environments for now and prod in
the future) as well as adding VMs to existing DNS servers.

While working on this, I noticed that we don't have a config variable
for the project holding the DNS service resources we use in prod, so I
created that. You also need to populate that in the dev env. DnsServer
class itself doesn't hold that value (but DnsZone does) so that addition
could be another beginner-friendly task.

Usage is like this:
```ruby
ds = DnsServer.first || DnsServer.create_with_id(name: "elephant")

dz1 = DnsZone.create_with_id(
    project_id: Config.dns_service_project_id,
    name: "erentest1.ubicloud.com",
    last_purged_at: Time.now
)
dz2 = DnsZone.create_with_id(
    project_id: Config.dns_service_project_id,
    name: "erentest2.ubicloud.com",
    last_purged_at: Time.now
)

Strand.create(prog: "DnsZone::DnsZoneNexus", label: "wait") { _1.id =
dz1.id }
Strand.create(prog: "DnsZone::DnsZoneNexus", label: "wait") { _1.id =
dz2.id }

dz1.add_dns_server DnsServer.first
dz2.add_dns_server DnsServer.first

["hebele", "hubele", "asdasd-asdasd-asd", "123123"].each do |r|
dz1.insert_record(record_name: "#{r}.#{dz1.name}", type: "A", ttl:
10, data: "0.1.1.0")
dz2.insert_record(record_name: "#{r}.#{dz2.name}", type: "A", ttl:
10, data: "0.1.1.0")
end

st = Prog::DnsZone::SetupDnsServerVm.assemble(DnsServer.first)

```

Note that concurrency between the VM creation, zone operations and
records are not handled in the best way. So it's advised to double-check
the results after the VM is created in prod environment.

We have a ton more to do for a proper DNS service lifecycle. A few
ideas:
- Health check for DNS servers and VMs
- Check-ups for consistency among the VMs of a DNS server
- Ability to add/remove DNS zones
- More validation about multiple DNS servers
- DnsServer belongs to a project
2025-01-14 07:46:45 +02:00

145 lines
3.9 KiB
Ruby

# frozen_string_literal: true
class Prog::DnsZone::SetupDnsServerVm < Prog::Base
subject_is :vm, :sshable
def self.assemble(dns_server_id, name: nil, vm_size: "standard-2", storage_size_gib: 30, location: "hetzner-fsn1")
unless (dns_server = DnsServer[dns_server_id])
fail "No existing Dns Server"
end
unless Project[Config.dns_service_project_id]
fail "No existing Project"
end
name ||= "#{dns_server.ubid}-#{SecureRandom.alphanumeric(8).downcase}"
DB.transaction do
vm_st = Prog::Vm::Nexus.assemble_with_sshable(
"ubi",
Config.dns_service_project_id,
location: location,
name: name,
size: vm_size,
storage_volumes: [
{encrypted: true, size_gib: storage_size_gib}
],
boot_image: "ubuntu-jammy",
enable_ip4: true
)
Strand.create_with_id(prog: "DnsZone::SetupDnsServerVm", label: "start", stack: [{subject_id: vm_st.id, dns_server_id: dns_server_id}])
end
end
def ds
@ds ||= DnsServer[frame["dns_server_id"]]
end
label def start
nap 5 unless vm.strand.label == "wait"
register_deadline(nil, 15 * 60)
hop_prepare
end
label def prepare
sshable.cmd <<~SH
sudo sed -i 's/#DNSStubListener=yes/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo sed -i ':a;N;$!ba;s/127.0.0.1 localhost\\n\\n#/127.0.0.1 localhost\\n127.0.0.1 #{vm.inhost_name}\\n\\n#/' /etc/hosts
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
sudo apt-get update
sudo apt-get -y install apt-transport-https ca-certificates wget
sudo wget -O /usr/share/keyrings/cznic-labs-pkg.gpg https://pkg.labs.nic.cz/gpg
echo "deb [signed-by=/usr/share/keyrings/cznic-labs-pkg.gpg] https://pkg.labs.nic.cz/knot-dns jammy main" | sudo tee /etc/apt/sources.list.d/cznic-labs-knot-dns.list
sudo apt-get update
sudo systemctl reboot
SH
hop_setup_knot
end
label def setup_knot
nap 5 unless sshable.available?
sshable.cmd <<~SH
sudo apt-get -y install knot
echo "KNOTD_ARGS="-C /var/lib/knot/confdb"" | sudo tee -a /etc/default/knot
SH
knot_config = <<-CONF
server:
rundir: "/run/knot"
user: "knot:knot"
listen: [ "0.0.0.0@53", "::@53" ]
log:
- target: "syslog"
any: "info"
database:
storage: "/var/lib/knot"
acl:
- id: "allow_dynamic_updates"
address: "127.0.0.1/32"
action: "update"
template:
- id: "default"
storage: "/var/lib/knot"
file: "%s.zone"
acl: "allow_dynamic_updates"
zonefile-sync: "60"
zonefile-load: "difference"
journal-content: "all"
zone:
#{ds.dns_zones.map { |dz| "- domain: \"#{dz.name}.\"" }.join("\n ")}
CONF
sshable.cmd("sudo tee /etc/knot/knot.conf > /dev/null", stdin: knot_config)
hop_sync_zones
end
label def sync_zones
nap 5 if ds.dns_zones.any?(&:refresh_dns_servers_set?)
ds.dns_zones.each do |dz|
zone_config = <<-CONF
#{dz.name}. 3600 SOA ns.#{dz.name}. #{dz.name}. 37 86400 7200 1209600 3600
#{dz.name}. 3600 NS #{ds.name}.
CONF
sshable.cmd("sudo -u knot tee /var/lib/knot/#{dz.name}.zone > /dev/null", stdin: zone_config)
end
sshable.cmd "sudo systemctl restart knot"
ds.dns_zones.each(&:purge_obsolete_records)
commands = ds.dns_zones.flat_map do |dz|
["zone-abort #{dz.name}", "zone-begin #{dz.name}"] +
dz.records.map do |r|
"zone-set #{dz.name} #{r.name} #{r.ttl} #{r.type} #{r.data}"
end + ["zone-commit #{dz.name}", "zone-flush #{dz.name}"]
end
# Put records
sshable.cmd("sudo -u knot knotc", stdin: commands.join("\n"))
hop_validate
end
label def validate
outputs = (ds.vms + [vm]).map do |vm|
vm.sshable.cmd("sudo -u knot knotc", stdin: "zone-read --").split("\n").to_set
end
hop_sync_zones unless outputs.uniq.count == 1
ds.add_vm vm unless ds.vms.map(&:id).include? vm.id
pop "created VM for DnsServer"
end
end