diff --git a/hooks/cleanup-lxc-config b/hooks/cleanup-lxc-config index f0831d3..c8c452a 100755 --- a/hooks/cleanup-lxc-config +++ b/hooks/cleanup-lxc-config @@ -1,27 +1,20 @@ #!/usr/bin/env ruby -require 'json' -require 'fileutils' +require_relative "lib/lxc" -abort "Must run as root" unless Process.uid == 0 - -CONTAINER_DATA = "/etc/lxc/container.json" - -data = JSON.load(File.open(CONTAINER_DATA)) +registry = Lxc::Registry.new containers = `lxc-ls -1`.split(/\n/) modified = false -data["network"].each do |host, value| +network = registry.data["network"] || {} +network.each do |host, value| unless containers.include?(host) or value["lxc"] == false - data["network"].delete(host) + network.delete(host) modified = true end end if modified - FileUtils.cp(CONTAINER_DATA, CONTAINER_DATA + ".backup") - File.open(CONTAINER_DATA, "w+") do |f| - f.puts JSON.pretty_generate(data) - end + registry.write else puts "Unchanged" end diff --git a/hooks/create-lxc-config b/hooks/create-lxc-config index 38f923b..061fed2 100755 --- a/hooks/create-lxc-config +++ b/hooks/create-lxc-config @@ -1,11 +1,9 @@ #!/usr/bin/env ruby +require "ostruct" +require "optparse" require "json" -require "pathname" -require 'ostruct' -require 'optparse' -require 'json' -require 'netaddr' -require_relative 'lib/container' + +require_relative "lib/lxc" def try_env(key) ENV[key] or abort("environment variable '#{key}' not set") @@ -47,32 +45,16 @@ OptionParser.new do |opts| end end end.parse! -CONFIG_PATH = Pathname.new("/etc/lxc/") -container_data = CONFIG_PATH.join("container.json") -data = if File.exists?(container_data) - JSON.load(File.open(container_data)) - else - {} - end - -data["network"] ||= {} -data["network"][options.container_name] = {} -network = data["network"][options.container_name] -network["ipv4"] = NetAddr::CIDR.create(ipv4_address).to_s(Short: true) -network["ipv6"] = NetAddr::CIDR.create(ipv6_address).to_s(Short: true) -network["group"] = options.group -network["vars"] = options.vars - -open(container_data, File::CREAT|File::TRUNC|File::RDWR) do |f| - f.write(JSON.pretty_generate(data)) -end - -container = Container.new(data, - name: options.container_name, - ipv4: ipv4_address, - ipv6: ipv6_address, - rootfs: options.rootfs, - dn42_ipv4: options.dn42_ipv4, - dn42_ipv6: options.dn42_ipv4) +registry = Lxc::Registry.new +container = Lxc::Container.new(registry.data, + name: options.container_name, + ipv4: options.ipv4, + ipv6: options.ipv6, + rootfs: options.rootfs, + dn42_ipv4: options.dn42_ipv4, + dn42_ipv6: options.dn42_ipv4, + group: options.group, + vars: options.vars) container.write_config(options.container_config) +registry.save diff --git a/hooks/lib/container.rb b/hooks/lib/container.rb deleted file mode 100644 index 77e3979..0000000 --- a/hooks/lib/container.rb +++ /dev/null @@ -1,69 +0,0 @@ -require "erb" -ROOT_PATH = Pathname.new(File.expand_path("../../..", __FILE__)) - -class TemplateContext - def initialize(hash) - hash.each do |key, value| - singleton_class.send(:define_method, key) { value } - end - end - - def get_binding - binding - end -end - -class Container - def initialize(data, ip4:, ip6:, dn42_ipv4:, dn42_ipv6:, **options) - network = @data["network"] || {} - zone = data["zone"] || {} - - ipv4_subnet = NetAddr::CIDR.create(zone["v4_subnet"] || "192.168.10.0/24") - ipv6_subnet = NetAddr::CIDR.create(zone["v6_subnet"] || "fd7d:aed0:18aa::/48") - - if subnet = zone["dn42_ipv4_subnet"] - dn42_ipv4_netmask = NetAddr::CIDR.create(subnet).to_i(:netmask) - else - dn42_ipv4_netmask = 24 - end - - if subnet = zone["dn42_ipv6_subnet"] - dn42_ipv6_netmask = NetAddr::CIDR.create(subnet).to_i(:netmask) - else - dn42_ipv6_netmask = 48 - end - - ip4 ||= find_address(ipv4_subnet, collect_subnets(network, "ipv4")) - ip6 ||= find_address(ipv6_subnet, collect_subnets(network, "ipv6")) - @opts = options.merge(ipv4: format_address(ip4, ipv4_subnet.to_i(:netmask)), - ipv6: format_address(ip6, ipv6_subnet.to_i(:netmask))) - @opts[:dn42_ipv4] = format_address(dn42_ipv6, dn42_ipv4_netmask) - @opts[:dn42_ipv6] = format_address(dn42_ipv4, dn42_ipv6_netmask) - end - - def write_config(config_path) - context = TemplateContext.new(@opts) - erb = ERB.new(File.read(ROOT_PATH.join("templates", "config.erb"))) - open(config_path, "w+") do |f| - f.write(erb.result(context.get_binding)) - end - end - - private - def format_address(address, netmask) - NetAddr::CIDR.create(address, Mask: netmask).desc(IP: true, Short: true) - end - - def collect_subnets(network, type) - network.map do |k,v| - NetAddr::CIDR.create(v[type]) if v[type] - end.compact! - end - - def find_address(subnet, assigned_subnets) - subnet.enumerate(Limit: 1E4, Short: true)[1..1E4].each do |cidr| - assigned = assigned_subnets.find { |s| s.contains?(cidr) || s == cidr } - return cidr unless assigned - end - end -end diff --git a/hooks/lib/lxc.rb b/hooks/lib/lxc.rb new file mode 100644 index 0000000..2576b82 --- /dev/null +++ b/hooks/lib/lxc.rb @@ -0,0 +1,10 @@ +require_relative "lxc/container" +require_relative "lxc/registry" +require_relative "lxc/template" +require_relative "lxc/hetzner" +require_relative "lxc/rdns" +require_relative "lxc/utils" + +module Lxc + CONFIG_ROOT = Pathname.new(File.expand_path("../../..", __FILE__)) +end diff --git a/hooks/lib/lxc/container.rb b/hooks/lib/lxc/container.rb new file mode 100644 index 0000000..2d9aa26 --- /dev/null +++ b/hooks/lib/lxc/container.rb @@ -0,0 +1,80 @@ +require "erb" +require "netaddr" +require "pathname" + +module Lxc + class Container + def initialize(data, name:, ipv4: nil, ipv6: nil, + dn42_ipv4: nil, dn42_ipv6: nil, + **options) + @data = data + @data["network"] ||= {} + @data["network"][name] = {} + + zone = @data["zone"] || {} + @ipv4_subnet = NetAddr::CIDR.create(zone["v4_subnet"] || "192.168.10.0/24") + @ipv6_subnet = NetAddr::CIDR.create(zone["v6_subnet"] || "fd7d:aed0:18aa::/48") + + if subnet = zone["dn42_ipv4_subnet"] + @dn42_ipv4_netmask = NetAddr::CIDR.create(subnet).to_i(:netmask) + else + @dn42_ipv4_netmask = 24 + end + + if subnet = zone["dn42_ipv6_subnet"] + @dn42_ipv6_netmask = NetAddr::CIDR.create(subnet).to_i(:netmask) + else + @dn42_ipv6_netmask = 48 + end + + network = data["network"] + @name = name + @ipv4 = ipv4 ||= ipv4 || find_address(@ipv4_subnet, collect_subnets(network, "ipv4")) + @ipv6 = ipv6 ||= find_address(@ipv6_subnet, collect_subnets(network, "ipv6")) + @dn42_ipv4 = dn42_ipv4 + @dn42_ipv6 = dn42_ipv6 + @options = options + end + + def write_config(config_path) + c = @data["network"][@name] || {} + c["ipv4"] = NetAddr::CIDR.create(@ipv4).to_s(Short: true) + c["ipv6"] = NetAddr::CIDR.create(@ipv6).to_s(Short: true) + c["group"] = @options[:group] + c["vars"] = @options[:vars] + opts = @options.merge(name: @name, + ipv4: format_address(@ipv4, @ipv4_subnet.to_i(:netmask)), + ipv6: format_address(@ipv6, @ipv6_subnet.to_i(:netmask))) + if @dn42_ipv4 + opts[:dn42_ipv4] = format_address(dn42_ipv6, dn42_ipv4_netmask) + c["dn42_ipv4"] = NetAddr::CIDR.create(@dn42_ipv4).to_s(Short: true) + end + + if @dn42_ipv6 + opts[:dn42_ipv6] = format_address(dn42_ipv4, dn42_ipv6_netmask) + c["dn42_ipv6"] = NetAddr::CIDR.create(@dn42_ipv6).to_s(Short: true) + end + + templ = Template.new(CONFIG_ROOT.join("templates", "config.erb")) + templ.write(config_path, opts) + end + + private + def format_address(address, netmask) + NetAddr::CIDR.create(address, Mask: netmask).desc(IP: true, Short: true) + end + + def collect_subnets(network, type) + network.map do |k,v| + NetAddr::CIDR.create(v[type]) if v[type] + end.compact + end + + def find_address(subnet, assigned_subnets) + subnet.enumerate(Limit: 1E4, Short: true)[1..1E4].each do |cidr| + assigned = assigned_subnets.find { |s| s.contains?(cidr) || s == cidr } + return cidr unless assigned + end + end + end +end diff --git a/hooks/lib/lxc/hetzner.rb b/hooks/lib/lxc/hetzner.rb new file mode 100644 index 0000000..f40a0f4 --- /dev/null +++ b/hooks/lib/lxc/hetzner.rb @@ -0,0 +1,56 @@ +require "net/http" +require "json" + +module Lxc + class Hetzner + BASE_URI = URI("https://robot-ws.your-server.de") + def initialize(user, password) + @user = user + @password = password + end + + def get(path) + resp = perform_request(Net::HTTP::Get.new(uri_for(path))) + JSON.parse(resp.body) + end + + def post(path, params={}) + req = Net::HTTP::Post.new(uri_for(path)) + req.set_form_data(params) + resp = perform_request(req) + JSON.parse(resp.body) + end + + def put(path, params={}) + req = Net::HTTP::Put.new(uri_for(path)) + req.set_form_data(params) + resp = perform_request(req) + JSON.parse(resp.body) + end + + def delete(path) + perform_request(Net::HTTP::Delete.new(uri_for(path))) + end + + private + def uri_for(path) + u = BASE_URI.clone + u.path = path + u + end + + def perform_request(req) + req.basic_auth(@user, @password) + resp = Net::HTTP.start(BASE_URI.hostname, + BASE_URI.port, + use_ssl: true) do |http| + http.request(req) + end + if resp.code.start_with? "2" + return resp + else + raise StandardError.new("failed to perform request: #{resp.inspect}") + end + end + end +end diff --git a/hooks/lib/lxc/rdns.rb b/hooks/lib/lxc/rdns.rb new file mode 100644 index 0000000..f81e5db --- /dev/null +++ b/hooks/lib/lxc/rdns.rb @@ -0,0 +1,36 @@ +module Lxc + class RdnsZone + def initialize(data, subnet) + @data = data + @subnet = NetAddr::CIDR.create(subnet) + end + + attr_reader :data + + def [](key) + (data["zone"] || {})[key] + end + + def pointers(&blk) + version = @subnet.version + + @data["network"].each do |name, host| + ip = host["ipv#{version}"] + next unless ip + arpa = NetAddr::CIDR.create(ip).arpa + next unless arpa.end_with?(@subnet.arpa) + host_part = arpa[0, arpa.size - @subnet.arpa.size - 1] + yield name, host_part + end + end + + def name + @subnet.arpa.gsub(/\.$/, "") + end + + def write_zone_file(path) + zone_template = Template.new(CONFIG_ROOT.join("templates/rdns-zone.erb")) + zone_template.write(path.join("zones", name), zone: self, data: data) + end + end +end diff --git a/hooks/lib/lxc/registry.rb b/hooks/lib/lxc/registry.rb new file mode 100644 index 0000000..a773108 --- /dev/null +++ b/hooks/lib/lxc/registry.rb @@ -0,0 +1,15 @@ +require "json" +require "pathname" + +module Lxc + class Registry + PATH = Pathname.new(File.expand_path("../../../../container.json", __FILE__)) + def initialize + @data = JSON.load(File.open(Registry::PATH)) + end + attr_accessor :data + def save + Utils.safe_write(Registry::PATH, JSON.pretty_generate(@data)) + end + end +end diff --git a/hooks/lib/lxc/template.rb b/hooks/lib/lxc/template.rb new file mode 100644 index 0000000..c6cb5cc --- /dev/null +++ b/hooks/lib/lxc/template.rb @@ -0,0 +1,25 @@ +require "erb" + +module Lxc + class TemplateContext < OpenStruct + def get_binding + binding + end + end + + class Template + def initialize(path) + @path = path + @erb = ERB.new(File.read(path), nil, "-") + end + def render(params={}) + @erb.result(TemplateContext.new(params).get_binding) + rescue => e + raise StandardError.new("fail to render '#{@path}': #{e}") + end + + def write(path, options={}) + Utils.safe_write(path, render(options)) + end + end +end diff --git a/hooks/lib/lxc/utils.rb b/hooks/lib/lxc/utils.rb new file mode 100644 index 0000000..b1f92f2 --- /dev/null +++ b/hooks/lib/lxc/utils.rb @@ -0,0 +1,22 @@ +require "fileutils" + +module Lxc + module Utils + def self.safe_write(path, content) + dir = File.dirname(path) + unless Dir.exist?(dir) + FileUtils.mkdir_p(dir) + end + temp_path = path.to_s + ".tmp" + File.open(temp_path, 'w+') do |f| + f.write(content) + end + + FileUtils.mv(temp_path, path) + end + def self.sh(cmd, *args) + puts "$ #{cmd} " + args.map {|a| "'#{a}'" }.join(" ") + system(cmd, *args) + end + end +end diff --git a/hooks/update-hetzner-rdns b/hooks/update-hetzner-rdns index 68a51d2..39be4b2 100755 --- a/hooks/update-hetzner-rdns +++ b/hooks/update-hetzner-rdns @@ -1,65 +1,9 @@ #!/usr/bin/env ruby -require 'net/http' -require 'netaddr' -require 'json' -require 'pathname' +require "netaddr" +require_relative "lib/lxc" -LXC_ROOT = Pathname.new("/etc/lxc") -CONTAINER_DATA = LXC_ROOT.join("container.json") - -class Hetzner - BASE_URI = URI("https://robot-ws.your-server.de") - def initialize(user, password) - @user = user - @password = password - end - - def get(path) - resp = perform_request(Net::HTTP::Get.new(uri_for(path))) - JSON.parse(resp.body) - end - - def post(path, params={}) - req = Net::HTTP::Post.new(uri_for(path)) - req.set_form_data(params) - resp = perform_request(req) - JSON.parse(resp.body) - end - - def put(path, params={}) - req = Net::HTTP::Put.new(uri_for(path)) - req.set_form_data(params) - resp = perform_request(req) - JSON.parse(resp.body) - end - - def delete(path) - perform_request(Net::HTTP::Delete.new(uri_for(path))) - end - -private - def uri_for(path) - u = BASE_URI.clone - u.path = path - u - end - - def perform_request(req) - req.basic_auth(@user, @password) - resp = Net::HTTP.start(BASE_URI.hostname, - BASE_URI.port, - use_ssl: true) do |http| - http.request(req) - end - if resp.code.start_with? "2" - return resp - else - raise StandardError.new("failed to perform request: #{resp.inspect}") - end - end -end -def update_hetzner_rdns6(user, password, json) - api = Hetzner.new(user, password) +def update_hetzner_rdns6(user, password, domain, network) + api = Lxc::Hetzner.new(user, password) rdns = api.get("/rdns") records = {} rdns.each do |val| @@ -68,11 +12,11 @@ def update_hetzner_rdns6(user, password, json) next if cidr.version == 4 records[cidr.ip] = rec["ptr"] end - json["network"].each do |host, data| + network.each do |host, data| cidr = data["ipv6"] next if cidr.nil? ipv6 = NetAddr::CIDR.create(cidr).ip - hostname = data["rdns6"] || "#{host}.higgsboson.tk" + hostname = data["rdns6"] || "#{host}.#{domain}" ptr = records.delete(ipv6) if ptr.nil? or ptr != hostname api.post("/rdns/#{ipv6}", ptr: hostname) @@ -83,8 +27,9 @@ def update_hetzner_rdns6(user, password, json) end end -credentials = File.read("/etc/lxc/hetzner.key") -json = JSON.load(File.open(CONTAINER_DATA)) +credentials = File.read(Lxc::CONFIG_ROOT.join("hetzner.key")) user, password = credentials.split(":") -update_hetzner_rdns6(user, password, json) -puts("ok") +registry = Lxc::Registry.new +registry.data["zone"] ||= {} +domain = registry.data["zone"]["domain"] +update_hetzner_rdns6(user, password, domain, registry.data["network"] || {}) diff --git a/hooks/update-zone b/hooks/update-zone index 6bb604e..866b7c3 100755 --- a/hooks/update-zone +++ b/hooks/update-zone @@ -1,77 +1,32 @@ #!/usr/bin/env ruby -require 'json' -require 'erb' -require 'netaddr' -require 'fileutils' -require 'pathname' -require 'ostruct' +require "json" +require "pathname" +require_relative "lib/lxc" -LXC_ROOT = Pathname.new("/etc/lxc") -ZONE_PATH = LXC_ROOT.join("zones") -TEMPLATE_PATH = LXC_ROOT.join("templates") -CONTAINER_DATA = LXC_ROOT.join("container.json") -LXC_ZONE = ZONE_PATH.join("eve.higgsboson.tk.zone") DNS_CONTAINER = "dns" -def atomic_write(path, content) - dir = File.dirname(path) - unless Dir.exist?(dir) - FileUtils.mkdir_p(dir) - end - temp_path = path.to_s + ".tmp" - File.open(temp_path, 'w+') do |f| - f.write(content) - end - - FileUtils.mv(temp_path, path) -end - -class ZoneData < OpenStruct - def get_binding - binding - end - - def ip(subnet) - NetAddr::CIDR.create(subnet).ip(Short: true) - end - - def pointers(&block) - subnet_arpa = subnet.arpa - version = subnet.version - - data["network"].each do |name, data| - next unless data["ipv#{version}"] - arpa = NetAddr::CIDR.create(data["ipv#{version}"]).arpa - addr = arpa[0, arpa.size - subnet_arpa.size - 1] - yield addr, name - end - end -end - -def reverse_zone(data, subnet) - subnet = NetAddr::CIDR.create(subnet) - zone_data = ZoneData.new(data: data, subnet: subnet).get_binding - rdns_zone_template = File.read(TEMPLATE_PATH.join("rdns-zone.erb")) - rdns_path = ZONE_PATH.join(subnet.arpa.gsub(/\.$/, "")) - template = ERB.new(rdns_zone_template, nil, '-').result(zone_data) - [rdns_path, template] -end - def main - json = JSON.load(File.open(CONTAINER_DATA)) - json["zone"]["serial"] += 1 + registry = Lxc::Registry.new + registry.data["zone"] ||= {} + registry.data["zone"]["serial"] += 1 + registry.save - zone_data = ZoneData.new(data: json) + root_path = Lxc::CONFIG_ROOT + if subnet = registry.data["zone"]["v4_subnet"] + Lxc::RdnsZone.new(registry.data, subnet).write_zone_file(root_path) + end - lxc_zone_template = File.read(TEMPLATE_PATH.join("lxc-zone.erb")) - zone = ERB.new(lxc_zone_template, nil, '-').result(zone_data.get_binding) + if subnet = registry.data["zone"]["v6_subnet"] + Lxc::RdnsZone.new(registry.data, subnet).write_zone_file(root_path) + end - atomic_write(LXC_ZONE, zone) - atomic_write(*reverse_zone(json, json["zone"]["v4_subnet"])) - atomic_write(*reverse_zone(json, json["zone"]["v6_subnet"])) - atomic_write(CONTAINER_DATA, JSON.pretty_generate(json)) + root_path = Pathname.new(File.expand_path("../..", __FILE__)) + zone_template = Lxc::Template.new(root_path.join("templates/lxc-zone.erb")) + zone = registry.data["zone"] || {} + zone_name = registry.data["zone"]["domain"] || "lxc" + zone_template.write(root_path.join("zones", "#{zone_name}.zone"), data: registry.data, zone: zone) - system("lxc-attach", "-n", DNS_CONTAINER, "--", "rndc", "reload") + Lxc::Utils.sh("lxc-attach", "-n", DNS_CONTAINER, "--", "rndc", "reload") end main diff --git a/templates/lxc-zone.erb b/templates/lxc-zone.erb index d4be75e..7afda15 100644 --- a/templates/lxc-zone.erb +++ b/templates/lxc-zone.erb @@ -18,11 +18,11 @@ <%= name %> SRV <%= value["srv"] %> <% end -%> <% if value["ipv4"] -%> -<%= name %> A <%= ip(value["ipv4"]) %> -ipv4.<%= name %> A <%= ip(value["ipv4"]) %> +<%= name %> A <%= NetAddr::CIDR.create(value["ipv4"]).ip(Short: true) %> +ipv4.<%= name %> A <%= NetAddr::CIDR.create(value["ipv4"]).ip(Short: true) %> <% end -%> <% if value["ipv6"] -%> -<%= name %> AAAA <%= ip(value["ipv6"]) %> -ipv6.<%= name %> AAAA <%= ip(value["ipv6"]) %> +<%= name %> AAAA <%= NetAddr::CIDR.create(value["ipv6"]).ip(Short: true) %> +ipv6.<%= name %> AAAA <%= NetAddr::CIDR.create(value["ipv6"]).ip(Short: true) %> <% end -%> <% end -%> diff --git a/templates/rdns-zone.erb b/templates/rdns-zone.erb index 318e89a..bcccd7e 100644 --- a/templates/rdns-zone.erb +++ b/templates/rdns-zone.erb @@ -13,10 +13,10 @@ <% data["network"].each do |name, value| -%> <% if value["ns"] -%> <% if value["ipv4"] -%> -<%= name %> A <%= ip(value["ipv4"]) %> +<%= name %> A <%= NetAddr::CIDR.create(value["ipv4"]).ip(Short: true) %> <% end -%> <% if value["ipv6"] -%> -<%= name %> AAAA <%= ip(value["ipv6"]) %> +<%= name %> AAAA <%= NetAddr::CIDR.create(value["ipv6"]).ip(Short: true) %> <% end -%> <% end -%> <% end -%>