#!/usr/bin/env ruby require "optparse" require "shellwords" require_relative "utils" OPTION_PARSER = OptionParser.new do |opts| opts.banner = "Usage: jails [options] [subcommand [options]]" opts.separator "" opts.separator < 192.168.67.0 addr =~ /\|?([^|]+)$/ $1 end end end class JailRegistry < Registry def add(name) data["jails"] ||= {} data["settings"] ||= {} ip4 = next_address("ip4") ip6 = next_address("ip6") data["jails"][name] = { "ip4" => [ip4], "ip6" => [ip6] } ipconfig = "#{ip4},#{ip6}" flavour = [] if settings["flavour"] flavour = ["-f", settings["flavour"]] end sh("ezjail-admin", "create", *flavour, name, ipconfig) end def env(name) jail_data = data["jails"][name] or die("no jail with name #{name} found") jail_data = default_jail_conf.merge(jail_data) templ = Template.new(ROOT_PATH.join("templates/jail_env.erb")) properties = jail_properties(name, jail_data) puts(templ.render(name: name, properties: properties)) end def update_pf_vars templ = Template.new(ROOT_PATH.join("templates/pf.erb")) path = ROOT_PATH.join("pf_vars.conf") atomic_write(path, templ.render(jails: jails)) end def update_config_symlinks conf_path = ROOT_PATH.join("scripts/jail_conf") FileUtils.mkdir_p(EZJAIL_CONFIG_PATH) jails.each do |jail| FileUtils.ln_sf(conf_path, EZJAIL_CONFIG_PATH.join(jail.name)) end end def update_fstabs templ = Template.new(ROOT_PATH.join("templates/fstab.erb")) jails.each do |jail| fstab = settings["fstab"].dup fstab.concat(jail.properties["fstab"] || []) fstab.map! do |entry| entry % { name: jail.name } end path = "/etc/fstab.#{jail.name}" atomic_write(path, templ.render(fstab: fstab)) end end private def settings { "ip4_subnet" => "192.168.10.0/24", "ip6_subnet" => "fd7d:aed0:18aa::/48", "fstab" => [ "/usr/jails/basejail /usr/jails/%{name}/basejail nullfs ro 0 0", ], }.merge(data["settings"]) end def default_jail_conf { "exec_start" => "/bin/sh /etc/rc", "exec_stop" => nil, "hostname" => "%{name}", "rootdir" => "/usr/jails/%{name}", "mount_enable" => true, "devfs_ruleset" => "devfsrules_jails", "procfs_enable" => true, "fdescfs_enable" => true, "image" => nil, "imagetype" => nil, "attachparams" => nil, "attachblocking" => nil, "forceblocking" => nil, "zfs_datasets" => nil, "cpuset" => nil, "fib" => nil, "parentzfs" => nil, "parameters" => nil, "post_start_script" => nil, "retention_policy" => nil }.merge(data["default_jail_conf"]) end def jail_properties(name, properties) props = properties.dup ips = props.delete("ip4") || [] ips.concat(props.delete("ip6") || []) unless ips.empty? props["ip"] = ips.join(",") end props.each do |prop, value| props[prop] = serialize_property(name, value) end props end def serialize_property(name, value) str = case value when TrueClass return value ? "YES" : "NO" when String value % { name: name } else value end Shellwords.escape(str) end def jails data["jails"].map do |name, properties| Jail.new(name, properties) end end def next_address(type) subnets = [] data["jails"].each do |k,v| if v[type].is_a? Array v[type].each do |subnet| subnets << NetAddr::CIDR.create(subnet) end end end subnet = settings["#{type}_subnet"] next_free_subnet(NetAddr::CIDR.create(subnet), subnets) end end def main(args) OPTION_PARSER.order!(args) usage if args.size < 1 registry = JailRegistry.new case command = args.shift when "add" name = args.first or die "name required" registry.add(name) registry.save when nil usage when "regenerate" when "env" name = args.first or die "name required" registry.env(name) else die "unknown subcommand #{command}" end registry.update_pf_vars registry.update_fstabs registry.update_config_symlinks end main(ARGV)