# type for managing persistent interface config options
# Inspired by puppet-network module. Adrien, thanks.

require 'ipaddr'

Puppet::Type.newtype(:l23_stored_config) do
  @doc = "Manage lines in interface config file"
  desc @doc

  feature :provider_options, <<-EOD
    The provider can accept a hash of arbitrary options. The semantics of
    these options will depend on the provider.
  EOD

  ensurable

  newparam(:name) do
    isnamevar
    desc "The name of the physical or logical network device"
  end

  newproperty(:method) do
    desc "The method for determining an IP address for the interface"
    # static -- assign IP address in config
    # manual -- UP interface without IP address
    newvalues(:static, :absent, :manual, :dhcp, :loopback, :none, :undef, :nil)
    aliasvalue(:none,  :manual)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :manual
  end

  # newproperty(:port_type) do
  #   desc "port_type fake RO property"
  # end

  newproperty(:if_type) do
    desc "Device type. Service property, shouldn't be setting by puppet"
    newvalues(:ethernet, :bridge, :bond, :patch, :vport)
  end

  newproperty(:if_provider) do
    desc "Device provider. Service property, shouldn't be setting by puppet"
  end

  newproperty(:bridge, :array_matching => :all) do
    # Array_matching for this property required for very complicated cases
    # ex. patchcord for connectind two bridges or bridge and network namesspace
    desc "Name of bridge, including this port"
    newvalues(/^[\w+\-]+$/, :none, :undef, :nil, :absent)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :absent
  end

  newproperty(:jacks, :array_matching => :all) do
    desc "Name of jacks for patchcord"
    newvalues(/^[\w+\-]+$/)
  end

  newproperty(:bridge_ports, :array_matching => :all) do
    desc "Ports, member of bridge, service property, do not use directly."
  end

  newproperty(:bridge_stp) do
    desc "Whether stp enable"
    newvalues(:true, :absent, :yes, :on, :false, :no, :off)
    aliasvalue(:yes, :true)
    aliasvalue(:on,  :true)
    aliasvalue(:no,  :false)
    aliasvalue(:off, :false)
    defaultto :absent
  end

  newproperty(:onboot) do
    desc "Whether to bring the interface up on boot"
    newvalues(:true, :yes, :on, :false, :no, :off)
    aliasvalue(:yes, :true)
    aliasvalue(:on,  :true)
    aliasvalue(:no,  :false)
    aliasvalue(:off, :false)
    defaultto :true

    def insync?(value)
      value.to_s.downcase == should.to_s.downcase
    end
  end

  newproperty(:mtu) do
    desc "The Maximum Transmission Unit size to use for the interface"
    newvalues(/^\d+$/, :absent, :none, :undef, :nil)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    aliasvalue(0,      :absent)
    defaultto :absent   # MTU value should be undefined by default, because some network resources (bridges, subinterfaces)
    validate do |value| #     inherits it from a parent interface
      # Intel 82598 & 82599 chips support MTUs up to 16110; is there any
      # hardware in the wild that supports larger frames?
      #
      # It appears loopback devices routinely have large MTU values; Eg. 65536
      #
      # Frames small than 64bytes are discarded as runts.  Smallest valid MTU
      # is 42 with a 802.1q header and 46 without.
      min_mtu = 42
      max_mtu = 65536
      if ! (value.to_s == 'absent' or (min_mtu .. max_mtu).include?(value.to_i))
        raise ArgumentError, "'#{value}' is not a valid mtu (must be a positive integer in range (#{min_mtu} .. #{max_mtu})"
      end
    end
    munge do |val|
      ((val == :absent)  ?  :absent  :  val.to_i)
    end
  end

  newproperty(:vlan_dev) do
    desc "802.1q vlan base device"
  end

  newproperty(:vlan_id) do
    desc "802.1q vlan ID"
    newvalues(/^\d+$/, :absent, :none, :undef, :nil)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    aliasvalue(0,      :absent)
    defaultto :absent
    validate do |val|
      min_vid = 0
      max_vid = 4094
      if ! (val.to_s == 'absent' or (min_vid .. max_vid).include?(val.to_i))
        raise ArgumentError, "'#{val}' is not a valid 802.1q NALN_ID (must be a integer value in range (#{min_vid} .. #{max_vid})"
      end
    end
    munge do |val|
      ((val == :absent or val.to_s == '0')  ?  :absent  :  val.to_i)
    end
  end

  newproperty(:vlan_mode) do
    desc "802.1q vlan interface naming model"
    #newvalues(:ethernet, :bridge, :bond)
    #defaultto :ethernet
  end


  newproperty(:ipaddr) do
    desc "Primary IP address for interface"
    newvalues(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/, :absent, :none, :undef, :nil, :dhcp)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :absent
  end

  newproperty(:ipaddr_aliases, :array_matching => :all) do
    desc "Additional IP addresses for interface"
    newvalues(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/\d{1,2}$/, :absent, :none, :undef, :nil)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :absent

    def is_absent(value)
      if value.is_a?(Hash) or value.is_a?(Array)
        ( value.empty?  ?  nil  :  value )
      else
        ( ['', 'nil', 'none', 'absent'].include?(value.to_s.downcase)  ?  nil  :  value.to_s )
      end
    end

    def should_to_s(value)
      (is_absent(value).nil?  ?  ''  :  "#{value.join(':')}")
    end

    def is_to_s(value)
      (is_absent(value).nil?  ?  ''  :  "#{value.join(':')}")
    end

    def insync?(value)
      val  = (is_absent(value).nil?  ?  ''  :  should_to_s(value.sort) )
      shou = (is_absent(value).nil?  ?  ''  :  should_to_s(should.sort) )
      val == shou
    end
  end

  newproperty(:gateway) do
    desc "Default gateway"
    newvalues(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, :absent, :none, :undef, :nil)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :absent
  end

  newproperty(:gateway_metric) do
    desc "Default gateway metric"
    newvalues(/^\d+$/, :absent, :none, :undef, :nil)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    aliasvalue(0,      :absent)
    defaultto :absent
    validate do |val|
      min_metric = 0
      max_metric = 65535
      if ! (val.to_s == 'absent' or (min_metric .. max_metric).include?(val.to_i))
        raise ArgumentError, "'#{val}' is not a valid metric (must be a integer value in range (#{min_metric} .. #{max_metric})"
      end
    end
    munge do |val|
      ((val == :absent)  ?  :absent  :  val.to_i)
    end
  end

  newproperty(:delay_while_up) do
    desc "Delay while interface stay UP"
    newvalues(/^\d+$/, :absent, :none, :undef, :nil)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    aliasvalue(0,      :absent)
    defaultto :absent
    munge do |val|
      ((val == :absent)  ?  :absent  :  [val.to_i])
    end
  end

  newproperty(:bond_master) do
    desc "bond name for bonded interface"
    newvalues(/^\w[\w+\-]*\w$/, :none, :undef, :nil, :absent)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :absent
  end

  newproperty(:bond_slaves, :array_matching => :all) do
    desc "slave ports for bond interface"
    newvalues(/^\w[\w+\-\.]*\w$/, :false, :none, :undef, :nil, :absent)
    #aliasvalue(:absent, :none)  # none is a valid config value
    aliasvalue(:false, :none)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :absent
  end

  newproperty(:bond_mode)
  newproperty(:bond_miimon)
  newproperty(:bond_use_carrier)
  newproperty(:bond_lacp)
  newproperty(:bond_lacp_rate)
  newproperty(:bond_xmit_hash_policy)

  newproperty(:bond_updelay) do
    newvalues(/^\d+$/)
  end

  newproperty(:bond_downdelay) do
    newvalues(/^\d+$/)
  end

  newproperty(:bond_ad_select) do
    validate do |val|
      allowed_values = ['0','1','2','stable','bandwidth','count']
      if ! allowed_values.include? val.to_s
        raise ArgumentError, "'#{val}' is not a valid bond_ad_select. Only #{allowed_values.join(', ')} allowed.)"
      end
    end
  end

  # # `:options` provides an arbitrary passthrough for provider properties, so
  # # that provider specific behavior doesn't clutter up the main type but still
  # # allows for more powerful actions to be taken.
  # newproperty(:options, :required_features => :provider_options) do
  #   desc "Provider specific options to be passed to the provider"

  #   def is_to_s(hash = @is)
  #     hash.keys.sort.map {|key| "#{key} => #{hash[key]}"}.join(", ")
  #   end

  #   def should_to_s(hash = @should)
  #     hash.keys.sort.map {|key| "#{key} => #{hash[key]}"}.join(", ")
  #   end

  #   defaultto {}

  #   validate do |value|
  #     raise ArgumentError, "#{self.class} requires a hash for the options property" unless value.is_a? Hash
  #     #provider.validate
  #   end
  # end

  newproperty(:routes) do
    desc "routes, corresponded to this interface. This-a R/O property, that autofill from L3_route resource"
    #defaultto {}  # no default value should be!!!
    validate do |val|
      if ! val.is_a? Hash
        fail("routes should be a hash!")
      end
    end

    munge do |value|
      (value.empty?  ?  nil  :  L23network.reccursive_sanitize_hash(value))
    end

    def should_to_s(value)
      "\n#{value.to_yaml.gsub('!ruby/sym','')}\n"
    end

    def is_to_s(value)
      "\n#{value.to_yaml.gsub('!ruby/sym','')}\n"
    end
  end

  newproperty(:ethtool) do
    desc "ethtool addition configuration for this interface"
    #defaultto {}  # no default value should be!!!
    validate do |val|
      if ! val.is_a? Hash
        fail("ethtool commands should be a hash!")
      end
    end

    munge do |value|
      (value.empty?  ?  nil  :  L23network.reccursive_sanitize_hash(value))
    end

    def should_to_s(value)
      "\n#{value.to_yaml.gsub('!ruby/sym','')}\n"
    end

    def is_to_s(value)
      "\n#{value.to_yaml.gsub('!ruby/sym','')}\n"
    end
  end

  newproperty(:datapath_type) do
    desc "OVS datapath type"
    newvalues(/^\w+$/, "", :none, :undef, :nil, :absent)
    aliasvalue("",  :absent)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :absent
  end

  newproperty(:sriov_numvfs) do
    desc "SR-IOV VFs number"
    newvalues(/^\d+$/, 0, "", :none, :undef, :nil, :absent)
    aliasvalue(0,  :absent)
    aliasvalue("",  :absent)
    aliasvalue(:none,  :absent)
    aliasvalue(:undef, :absent)
    aliasvalue(:nil,   :absent)
    defaultto :absent
  end

  newproperty(:vendor_specific) do
    desc "Hash of vendor specific properties"
    #defaultto {}  # no default value should be!!!
    # provider-specific properties, can be validating only by provider.
    validate do |val|
      if ! val.is_a? Hash
        fail("Vendor_specific should be a hash!")
      end
    end

    munge do |value|
      (value.empty?  ?  nil  :  L23network.reccursive_sanitize_hash(value))
    end

    def should_to_s(value)
      "\n#{value.to_yaml.gsub('!ruby/sym ','')}\n"
    end

    def is_to_s(value)
      "\n#{value.to_yaml.gsub('!ruby/sym ','')}\n"
    end

    def insync?(value)
      should_to_s(value) == should_to_s(should)
    end
  end


  def generate
    # if_type = :ethernet is the same as if_type = nil
    if (!([:absent, :none, :nil, :undef] & self[:bridge]).any? and ([:ethernet, :bond, :vport, :patch].include? self[:if_type] or self[:if_type].nil?))
      self[:bridge].each do |bridge|
        br = self.catalog.resource('L23_stored_config', bridge)
        fail("Stored_config resource for bridge '#{bridge}' not found for port '#{self[:name]}'!") if ! br
        br[:bridge_ports] ||= []
        ports = br[:bridge_ports]
        return if ! ports.is_a? Array
        if ! ports.include? self[:name]
          ports << self[:name].to_s
          br[:bridge_ports] = ports.reject{|a| a=='none'}.sort
        end
      end
    end
    # find routes, that should be applied while this interface UP
    if !['', 'none', 'absent'].include?(self[:ipaddr].to_s.downcase)
      l3_routes = self.catalog.resources.reject{|nnn| nnn.ref.split('[')[0]!='L3_route'}
      my_network = IPAddr.new(self[:ipaddr].to_s.downcase) # only primary IP ADDR use !!!
      my_route = {}
      l3_routes.each do |rou|
        if my_network.include? rou[:gateway]
          #self[:routes] = {} if self[:routes].nil?
          my_route[rou[:name]] = {
            :gateway     => rou[:gateway],
            :destination => rou[:destination]
          }
          my_route[rou[:name]][:metric] = rou[:metric] if !['', 'absent'].include? rou[:metric].to_s.downcase
          debug("+++My route: #{my_route}")
        end
      end
      if ! my_route.empty?
        if self[:routes].nil?
          self[:routes] = my_route
        else
          self[:routes].merge!(my_route)
        end
      end
    end
    nil
  end
end
# vim: set ts=2 sw=2 et :
