#!/usr/bin/env ruby require "erb" require "pathname" require 'ostruct' require 'optparse' require 'json' require 'netaddr' def try_env(key) ENV[key] or abort("enviroment variable '#{key}' not set") 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 options = OpenStruct.new options.container_name = try_env("LXC_NAME") options.container_config = try_env("LXC_CONFIG_FILE") options.rootfs = try_env("LXC_ROOTFS_PATH") OptionParser.new do |opts| opts.banner = "Usage: create-lxc-config [options]" opts.on("-4", "--ipv4", "private Ipv4 subnet") do |v| options.ipv4 = v end opts.on("-6", "--ipv6", "public Ipv6 subnet") do |v| options.ipv6 = v end opts.on("--group GROUP", String, "set ansible group (default NONE)") do |group| options.group = group end opts.on("--vars FILE", String, "set json file for ansible variables") do |vars| begin options.vars = JSON.load(File.open(vars)) unless options.vars.is_a? Hash abort "vars: Should be a json object" end rescue SystemCallError => e abort "Failed to open '#{vars}': #{e.message}" rescue JSON::ParserError => e abort "Failed to parse ansible variables: #{e.message}" 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 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") data["network"] ||= {} #if data["network"][options.container_name] # abort "container name '#{options.container_name}' in '#{container_data}' already in use!" #end data["network"][options.container_name] = {} ipv4_subnets = data["network"].map {|k,v| NetAddr::CIDR.create(v["ipv4"]) if v["ipv4"]}.compact! ipv6_subnets = data["network"].map {|k,v| NetAddr::CIDR.create(v["ipv6"]) if v["ipv6"]}.compact! ipv4_address = if options.ipv4.nil? find_address(IPV4_SUBNET, ipv4_subnets) elsif address_free?(ipv4_subnets, options.ipv4) abort "The address #{ipv4} is already assigned" else options.ipv4 end ipv6_address = if options.ipv6.nil? find_address(IPV6_SUBNET, ipv6_subnets) elsif address_free?(ipv6_subnets, options.ipv6) abort "The address #{ipv4} is already assigned" else options.ipv6 end network_config = data["network"][options.container_name] network_config["ipv4"] = NetAddr::CIDR.create(ipv4_address).to_s(Short: true) network_config["ipv6"] = NetAddr::CIDR.create(ipv6_address).to_s(Short: true) network_config["group"] = options.group network_config["vars"] = options.vars open(container_data, File::CREAT|File::TRUNC|File::RDWR) do |f| f.write(JSON.pretty_generate(data)) end 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 context = TemplateContext.new(container_name: options.container_name, ipv4: NetAddr::CIDR.create(ipv4_address, Mask: IPV4_SUBNET.to_i(:netmask)).desc(IP: true, Short: true), ipv6: NetAddr::CIDR.create(ipv6_address, Mask: IPV6_SUBNET.to_i(:netmask)).desc(IP: true, Short: true), rootfs: options.rootfs) erb = ERB.new(File.read(CONFIG_PATH.join("templates", "config.erb"))) open(options.container_config, "w+") do |f| f.write(erb.result(context.get_binding)) end