217 lines
5.0 KiB
Ruby
Executable File
217 lines
5.0 KiB
Ruby
Executable File
#!/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 <<HELP
|
|
Available subcommands:
|
|
add [options] NAME [ipaddress[,ipaddress,...]]: add jail
|
|
regenerate: regenerate configuration
|
|
|
|
See 'jail COMMAND --help' for more information on a specific command.
|
|
HELP
|
|
end
|
|
|
|
def usage
|
|
puts(OPTION_PARSER.help)
|
|
exit(0)
|
|
end
|
|
|
|
ROOT_PATH = Pathname.new(File.expand_path("../.." ,__FILE__))
|
|
EZJAIL_CONFIG_PATH = Pathname.new("/usr/local/etc/ezjail/")
|
|
|
|
class Jail
|
|
def initialize(name, properties={})
|
|
@name = name
|
|
@properties = properties
|
|
end
|
|
attr_accessor :name, :properties
|
|
|
|
def ip4
|
|
extract_ip("ip4")
|
|
end
|
|
def ip6
|
|
extract_ip("ip6")
|
|
end
|
|
|
|
private
|
|
def extract_ip(type)
|
|
ips = @properties[type] || []
|
|
ips.map do |addr|
|
|
# example: em0|192.168.67.0 -> 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)
|