Puppet::Type.type(:zone).provide(:solaris) do
  desc "Provider for Solaris Zones."

  commands :adm => "/usr/sbin/zoneadm", :cfg => "/usr/sbin/zonecfg"
  defaultfor :osfamily => :solaris

  mk_resource_methods

  # Convert the output of a list into a hash
  def self.line2hash(line)
    fields = [:id, :name, :ensure, :path, :uuid, :brand, :iptype]
    properties = Hash[fields.zip(line.split(':'))]

    del_id = [:brand, :uuid]
    # Configured but not installed zones do not have IDs
    del_id << :id if properties[:id] == "-"
    del_id.each { |p| properties.delete(p) }

    properties[:ensure] = properties[:ensure].intern
    properties[:iptype] = 'exclusive' if properties[:iptype] == 'excl'

    properties
  end

  def self.instances
    adm(:list, "-cp").split("\n").collect do |line|
      new(line2hash(line))
    end
  end

  def multi_conf(name, should, &action)
    has = properties[name]
    has = [] if !has || has == :absent
    rms = has - should
    adds = should - has
    (rms.map{|o| action.call(:rm,o)} + adds.map{|o| action.call(:add,o)}).join("\n")
  end

  def self.def_prop(var, str)
    define_method('%s_conf' % var.to_s) do |v|
      str % v
    end
    define_method('%s=' % var.to_s) do |v|
      setconfig self.send( ('%s_conf'% var).intern, v)
    end
  end

  def self.def_multiprop(var, &conf)
    define_method(var.to_s) do |v|
      o = properties[var]
      return '' if o.nil? or o == :absent
      o.join(' ')
    end
    define_method('%s=' % var.to_s) do |v|
      setconfig self.send( ('%s_conf'% var).intern, v)
    end
    define_method('%s_conf' % var.to_s) do |v|
      multi_conf(var, v, &conf)
    end
  end

  def_prop :iptype, "set ip-type=%s"
  def_prop :autoboot, "set autoboot=%s"
  def_prop :path, "set zonepath=%s"
  def_prop :pool, "set pool=%s"
  def_prop :shares, "add rctl\nset name=zone.cpu-shares\nadd value (priv=privileged,limit=%s,action=none)\nend"

  def_multiprop :ip do |action, str|
    interface, ip, defrouter = str.split(':')
    case action
    when :add
      cmd = ["add net"]
      cmd << "set physical=#{interface}" if interface
      cmd << "set address=#{ip}" if ip
      cmd << "set defrouter=#{defrouter}" if defrouter
      cmd << "end"
      cmd.join("\n")
    when :rm
      if ip
        "remove net address=#{ip}"
      elsif interface
        "remove net physical=#{interface}"
      else
        raise ArgumentError, _("can not remove network based on default router")
      end
    else self.fail action
    end
  end

  def_multiprop :dataset do |action, str|
    case action
    when :add; ['add dataset',"set name=#{str}",'end'].join("\n")
    when :rm; "remove dataset name=#{str}"
    else self.fail action
    end
  end

  def_multiprop :inherit do |action, str|
    case action
    when :add; ['add inherit-pkg-dir', "set dir=#{str}",'end'].join("\n")
    when :rm; "remove inherit-pkg-dir dir=#{str}"
    else self.fail action
    end
  end

  def my_properties
    [:path, :iptype, :autoboot, :pool, :shares, :ip, :dataset, :inherit]
  end

  # Perform all of our configuration steps.
  def configure
    self.fail "Path is required" unless @resource[:path]
    arr = ["create -b #{@resource[:create_args]}"]

    # Then perform all of our configuration steps.  It's annoying
    # that we need this much internal info on the resource.
    self.resource.properties.each do |property|
      next unless my_properties.include? property.name
      method = (property.name.to_s + '_conf').intern
      arr << self.send(method ,@resource[property.name]) unless property.safe_insync?(properties[property.name])
    end
    setconfig(arr.join("\n"))
  end

  def destroy
    zonecfg :delete, "-F"
  end

  def add_cmd(cmd)
    @cmds = [] if @cmds.nil?
    @cmds << cmd
  end

  def exists?
    properties[:ensure] != :absent
  end

  # We cannot use the execpipe in util because the pipe is not opened in
  # read/write mode.
  def exec_cmd(var)
    # In bash, the exit value of the last command is the exit value of the
    # entire pipeline
    out = execute("echo \"#{var[:input]}\" | #{var[:cmd]}", :failonfail => false, :combine => true)
    st = $?.exitstatus
    {:out => out, :exit => st}
  end

  # Clear out the cached values.
  def flush
    return if @cmds.nil? || @cmds.empty?
    str = (@cmds << "commit" << "exit").join("\n")
    @cmds = []
    @property_hash.clear

    command = "#{command(:cfg)} -z #{@resource[:name]} -f -"
    r = exec_cmd(:cmd => command, :input => str)
    if r[:exit] != 0 or r[:out] =~ /not allowed/
      raise ArgumentError, _("Failed to apply configuration")
    end
  end

  def install
    if @resource[:clone] # TODO: add support for "-s snapshot"
      zoneadm :clone, @resource[:clone]
    elsif @resource[:install_args]
      zoneadm :install, @resource[:install_args].split(" ")
    else
      zoneadm :install
    end
  end

  # Look up the current status.
  def properties
    if @property_hash.empty?
      @property_hash = status || {}
      if @property_hash.empty?
        @property_hash[:ensure] = :absent
      else
        @resource.class.validproperties.each do |name|
          @property_hash[name] ||= :absent
        end
      end
    end
    @property_hash.dup
  end

  # We need a way to test whether a zone is in process.  Our 'ensure'
  # property models the static states, but we need to handle the temporary ones.
  def processing?
    hash = status
    return false unless hash
    ["incomplete", "ready", "shutting_down"].include? hash[:ensure]
  end

  # Collect the configuration of the zone. The output looks like:
  # zonename: z1
  # zonepath: /export/z1
  # brand: native
  # autoboot: true
  # bootargs:
  # pool:
  # limitpriv:
  # scheduling-class:
  # ip-type: shared
  # hostid:
  # net:
  #         address: 192.168.1.1
  #         physical: eg0001
  #         defrouter not specified
  # net:
  #         address: 192.168.1.3
  #         physical: eg0002
  #         defrouter not specified
  #
  def getconfig
    output = zonecfg :info

    name = nil
    current = nil
    hash = {}
    output.split("\n").each do |line|
      case line
      when /^(\S+):\s*$/
        name = $1
        current = nil # reset it
      when /^(\S+):\s*(\S+)$/
        hash[$1.intern] = $2
      when /^\s+(\S+):\s*(.+)$/
        if name
          hash[name] ||= []
          unless current
            current = {}
            hash[name] << current
          end
          current[$1.intern] = $2
        else
          err "Ignoring '#{line}'"
        end
      else
        debug "Ignoring zone output '#{line}'"
      end
    end

    hash
  end

  # Execute a configuration string.  Can't be private because it's called
  # by the properties.
  def setconfig(str)
    add_cmd str
  end

  def start
    # Check the sysidcfg stuff
    if cfg = @resource[:sysidcfg]
      self.fail "Path is required" unless @resource[:path]
      zoneetc = File.join(@resource[:path], "root", "etc")
      sysidcfg = File.join(zoneetc, "sysidcfg")

      # if the zone root isn't present "ready" the zone
      # which makes zoneadmd mount the zone root
      zoneadm :ready unless File.directory?(zoneetc)

      unless Puppet::FileSystem.exist?(sysidcfg)
        begin
          # For compatibility reasons use System encoding for this OS file
          # the manifest string is UTF-8 so this could result in conversion errors
          # which should propagate to users
          Puppet::FileSystem.open(sysidcfg, 0600, "w:#{Encoding.default_external.name}") do |f|
            f.puts cfg
          end
        rescue => detail
          puts detail.stacktrace if Puppet[:debug]
          raise Puppet::Error, "Could not create sysidcfg: #{detail}", detail.backtrace
        end
      end
    end

    zoneadm :boot
  end

  # Return a hash of the current status of this zone.
  def status
    begin
      output = adm "-z", @resource[:name], :list, "-p"
    rescue Puppet::ExecutionFailure
      return nil
    end

    main = self.class.line2hash(output.chomp)

    # Now add in the configuration information
    config_status.each do |name, value|
      main[name] = value
    end

    main
  end

  def ready
    zoneadm :ready
  end

  def stop
    zoneadm :halt
  end

  def unconfigure
    zonecfg :delete, "-F"
  end

  def uninstall
    zoneadm :uninstall, "-F"
  end

  private

  # Turn the results of getconfig into status information.
  def config_status
    config = getconfig
    result = {}

    result[:autoboot] = config[:autoboot] ? config[:autoboot].intern : :true
    result[:pool] = config[:pool]
    result[:shares] = config[:shares]
    if dir = config["inherit-pkg-dir"]
      result[:inherit] = dir.collect { |dirs| dirs[:dir] }
    end
    if datasets = config["dataset"]
      result[:dataset] = datasets.collect { |dataset| dataset[:name] }
    end
    result[:iptype] = config[:'ip-type'] if config[:'ip-type']
    if net = config["net"]
      result[:ip] = net.collect do |params|
        if params[:defrouter]
          "#{params[:physical]}:#{params[:address]}:#{params[:defrouter]}"
        elsif params[:address]
          "#{params[:physical]}:#{params[:address]}"
        else
          params[:physical]
        end
      end
    end

    result
  end

  def zoneadm(*cmd)
    adm("-z", @resource[:name], *cmd)
  rescue Puppet::ExecutionFailure => detail
    self.fail Puppet::Error, "Could not #{cmd[0]} zone: #{detail}", detail
  end

  def zonecfg(*cmd)
    # You apparently can't get the configuration of the global zone (strictly in solaris11)
    return "" if self.name == "global"
    begin
      cfg("-z", self.name, *cmd)
    rescue Puppet::ExecutionFailure => detail
      self.fail Puppet::Error, "Could not #{cmd[0]} zone: #{detail}", detail
    end
  end
end

