# frozen_string_literal: true
#
# Author:: Adam Jacob (<adam@chef.io>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

Ohai.plugin(:NetworkAddresses) do
  require_relative "../mixin/network_helper"
  include Ohai::Mixin::NetworkHelper

  provides "ipaddress", "ip6address", "macaddress"

  depends "network/interfaces"

  # from interface data create array of hashes with ipaddress, scope, and iface
  # sorted by scope, prefixlen and then ipaddress where longest prefixes first
  def sorted_ips(family = "inet")
    raise "bad family #{family}" unless %w{inet inet6}.include? family

    # priority of ipv6 link scopes to sort by later
    scope_prio = [ "global", "site", "link", "host", "node", nil ]

    # grab ipaddress, scope, and iface for sorting later
    ipaddresses = []
    Mash[network["interfaces"]].each do |iface, iface_v|
      next if iface_v.nil? || !iface_v.key?("addresses")

      iface_v["addresses"].each do |addr, addr_v|
        next if addr_v.nil? || (!addr_v.key? "family") || addr_v["family"] != family

        ipaddresses << {
          ipaddress: addr_v["prefixlen"] ? IPAddress("#{addr}/#{addr_v["prefixlen"]}") : IPAddress("#{addr}/#{addr_v["netmask"]}"),
          scope: addr_v["scope"].nil? ? nil : addr_v["scope"].downcase,
          iface: iface,
        }
      end
    end

    # sort ip addresses by scope, by prefixlen and then by ip address
    # 128 - prefixlen: longest prefixes first
    ipaddresses.sort_by do |v|
      [ ( scope_prio.index(v[:scope]) || 999999 ),
        128 - v[:ipaddress].prefix.to_i,
        v[:ipaddress].to_i,
      ]
    end
  end

  # finds ip address / interface for interface with default route based on
  # passed in family.  returns [ipaddress, interface] uses 1st ip if no default
  # route is found
  def find_ip(family = "inet")
    ips = sorted_ips(family)

    # return if there aren't any #{family} addresses!
    return [ nil, nil ] if ips.empty?

    # shortcuts to access default #{family} interface and gateway
    int_attr = Ohai::Mixin::NetworkHelper::FAMILIES[family] + "_interface"
    gw_attr = Ohai::Mixin::NetworkHelper::FAMILIES[family] + "_gateway"

    if network[int_attr]
      # working with the address(es) of the default network interface
      gw_if_ips = ips.select do |v|
        v[:iface] == network[int_attr]
      end
      if gw_if_ips.empty?
        logger.warn("Plugin Network: [#{family}] no ip address on #{network[int_attr]}")
      elsif network[gw_attr] &&
          network["interfaces"][network[int_attr]] &&
          network["interfaces"][network[int_attr]]["addresses"]
        if [ "0.0.0.0", "::", /^fe80:/ ].any? { |pat| pat === network[gw_attr] }
          # link level default route
          logger.trace("Plugin Network: link level default #{family} route, picking ip from #{network[gw_attr]}")
          r = gw_if_ips.first
        else
          # checking network masks
          r = gw_if_ips.find do |v|
            network_contains_address(network[gw_attr], v[:ipaddress], v[:iface])
          end
          if r.nil?
            r = gw_if_ips.first
            logger.trace("Plugin Network: [#{family}] no ipaddress/mask on #{network[int_attr]} matching the gateway #{network[gw_attr]}, picking #{r[:ipaddress]}")
          else
            logger.trace("Plugin Network: [#{family}] Using default interface #{network[int_attr]} and default gateway #{network[gw_attr]} to set the default ip to #{r[:ipaddress]}")
          end
        end
      else
        # return the first ip address on network[int_attr]
        r = gw_if_ips.first
      end
    else
      r = ips.first
      logger.trace("Plugin Network: [#{family}] no default interface, picking the first ipaddress")
    end

    return [ nil, nil ] if r.nil? || r.empty?

    [ r[:ipaddress].to_s, r[:iface] ]
  end

  # select mac address of first interface with family of lladdr
  def find_mac_from_iface(iface)
    r = network["interfaces"][iface]["addresses"].select { |k, v| v["family"] == "lladdr" }
    r.nil? || r.first.nil? ? nil : r.first.first
  end

  # address_to_match: String
  # ipaddress: IPAddress
  # iface: String
  def network_contains_address(address_to_match, ipaddress, iface)
    if ( peer = network["interfaces"][iface]["addresses"][ipaddress.to_s][:peer] )
      IPAddress(peer) == IPAddress(address_to_match)
    else
      ipaddress.include? IPAddress(address_to_match)
    end
  end

  # ipaddress, ip6address and macaddress are set for each interface by the
  # #{os}::network plugin. atm it is expected macaddress is set at the same
  # time as ipaddress. if ipaddress is set and macaddress is nil, that means
  # the interface ipaddress is bound to has the NOARP flag
  collect_data do
    require "ipaddress" unless defined?(IPAddress)

    results = {}

    network Mash.new unless network
    network[:interfaces] ||= Mash.new
    counters Mash.new unless counters
    counters[:network] ||= Mash.new

    # inet family is processed before inet6 to give ipv4 precedence
    Ohai::Mixin::NetworkHelper::FAMILIES.keys.sort.each do |family|
      r = {}
      # find the ip/interface with the default route for this family
      (r["ip"], r["iface"]) = find_ip(family)
      r["mac"] = find_mac_from_iface(r["iface"]) unless r["iface"].nil?
      # don't overwrite attributes if they've already been set by the "#{os}::network" plugin
      if (family == "inet") && ipaddress.nil?
        if r["ip"].nil?
          logger.warn("Plugin Network: unable to detect ipaddress")
        else
          ipaddress r["ip"]
        end
      elsif (family == "inet6") && ip6address.nil?
        if r["ip"].nil?
          logger.trace("Plugin Network: unable to detect ip6address")
        else
          ip6address r["ip"]
        end
      end

      # set the macaddress [only if we haven't already]. this allows the #{os}::network plugin to set macaddress
      # otherwise we set macaddress on a first-found basis (and we started with ipv4)
      if macaddress.nil?
        if r["mac"]
          logger.trace("Plugin Network: setting macaddress to '#{r["mac"]}' from interface '#{r["iface"]}' for family '#{family}'")
          macaddress r["mac"]
        else
          logger.trace("Plugin Network: unable to detect macaddress for family '#{family}'")
        end
      end

      results[family] = r
    end

    if results["inet"]["iface"] && results["inet6"]["iface"] &&
        (results["inet"]["iface"] != results["inet6"]["iface"])
      logger.trace("Plugin Network: ipaddress and ip6address are set from different interfaces (#{results["inet"]["iface"]} & #{results["inet6"]["iface"]})")
    end
  end
end
