# coding: UTF-8

require "membrane"

module Warden
  class Config
    def self.server_defaults
      {
        "unix_domain_path"        => "/tmp/warden.sock",
        "unix_domain_permissions" => 0755,
        "container_klass"         => "Warden::Container::Insecure",
        "container_grace_time"    => (5 * 60), # 5 minutes
        "job_output_limit"        => (10 * 1024 * 1024), # 10 megabytes
        "quota" => {
          "disk_quota_enabled" => true,
        },
        "allow_nested_warden" => false,
      }
    end

    def self.health_check_server_defaults
      {
        "port" => 2345,
      }
    end

    def self.server_schema
      ::Membrane::SchemaParser.parse do
        {
          "unix_domain_path"        => String,
          "unix_domain_permissions" => Integer,

          optional("container_rootfs_path") => String,
          optional("container_depot_path")  => String,

          "container_klass"       => String,
          "container_grace_time"  => enum(nil, Integer),

          # See getrlimit(2) for details. Integer values are passed verbatim.
          optional("container_rlimits") => {
            optional("as")         => Integer,
            optional("core")       => Integer,
            optional("cpu")        => Integer,
            optional("data")       => Integer,
            optional("fsize")      => Integer,
            optional("locks")      => Integer,
            optional("memlock")    => Integer,
            optional("msgqueue")   => Integer,
            optional("nice")       => Integer,
            optional("nofile")     => Integer,
            optional("nproc")      => Integer,
            optional("rss")        => Integer,
            optional("rtprio")     => Integer,
            optional("sigpending") => Integer,
            optional("stack")      => Integer,
          },

          "job_output_limit" => Integer,

          "quota" => {
            optional("disk_quota_enabled") => bool,
          },

          "allow_nested_warden" => bool,

          optional("pidfile") => enum(nil, String),

          optional("syslog_socket") => enum(nil, String),
        }
      end
    end

    def self.logging_defaults
      {
        "level" => "debug2",
      }
    end

    def self.logging_schema
      ::Membrane::SchemaParser.parse do
        {
          "level"            => String,
          optional("file")   => String,
          optional("syslog") => String,
        }
      end
    end

    def self.network_defaults
      {
        "pool_network"   => "10.254.0.0/24",
        "allow_networks" => [],
        "allow_host_access" => false,
        "mtu"            => 1500,
        "dns_servers" => [],
      }
    end

    def self.network_schema
      ::Membrane::SchemaParser.parse do
        {
          # Preferred way to specify networks to pool
          optional("pool_network") => String,

          # Present for Backwards compatibility
          optional("pool_start_address") => String,
          optional("pool_size")          => Integer,
          optional("release_delay")          => Integer,
          optional("mtu")                => Integer,

          "allow_networks"     => [String],
          optional("allow_host_access") => bool,
          optional("dns_servers") => [String],
        }
      end
    end

    def self.ip_local_port_range
      # if no ip_local_port_range found, make some guess"
      if File.exist?("/proc/sys/net/ipv4/ip_local_port_range")
        File.read("/proc/sys/net/ipv4/ip_local_port_range").split.map(&:to_i)
      else
        return 32768, 61000
      end
    end

    def self.port_defaults
      _, ephemeral_stop = self.ip_local_port_range
      start = ephemeral_stop + 1
      stop = 65000 + 1
      count = stop - start

      {
        "pool_start_port" => start,
        "pool_size"       => count,
      }
    end

    def self.port_schema
      ::Membrane::SchemaParser.parse do
        {
          "pool_start_port" => Integer,
          "pool_size"       => Integer,
        }
      end
    end

    def self.user_defaults
      {
        "pool_start_uid" => 10000,
        "pool_size"      => 64,
      }
    end

    def self.user_schema
      ::Membrane::SchemaParser.parse do
        {
          "pool_start_uid" => Integer,
          "pool_size"      => Integer,
        }
      end
    end

    attr_reader :config

    attr_reader :server
    attr_reader :health_check_server
    attr_reader :logging
    attr_reader :network
    attr_reader :port
    attr_reader :user

    def initialize(config)
      @config = config

      populate
      validate
      transform
    end

    def populate
      @server = self.class.server_defaults.merge(config["server"] || {})
      @health_check_server = self.class.health_check_server_defaults.
        merge(config["health_check_server"] || {})
      @logging = self.class.logging_defaults.merge(config["logging"] || {})
      @network = self.class.network_defaults.merge(config["network"] || {})
      @port = self.class.port_defaults.merge(config["port"] || {})
      @user = self.class.user_defaults.merge(config["user"] || {})
    end

    def validate
      self.class.server_schema.validate(@server)
      self.class.logging_schema.validate(@logging)
      self.class.network_schema.validate(@network)
      self.class.port_schema.validate(@port)
      self.class.user_schema.validate(@user)
    end

    def transform
      @server["container_klass"] = @server["container_klass"].
        split("::").
        inject(Kernel) { |prev, cur| prev.const_get(cur) }

      @network["allow_networks"] = @network["allow_networks"].compact

      # Transform pool_start_address/pool_size into pool_network if needed
      if @network.has_key?("pool_start_address") && @network.has_key?("pool_size")
        pool_start_address = @network.delete("pool_start_address")
        pool_size = @network.delete("pool_size").to_i

        # Determine number of fixed bits in netmask
        fixed_bits = Math.log2(pool_size).ceil + 2

        @network["pool_network"] = "%s/%d" % [pool_start_address, 32-fixed_bits]
      end
    end

    def rlimits
      @server["container_rlimits"] || {}
    end

    def allow_nested_warden?
      !!@server["allow_nested_warden"]
    end

    def to_hash
      {
        "server"  => server,
        "logging" => logging,
        "network" => network,
        "port"    => port,
        "user"    => user,
      }
    end
  end
end
