puppetlabs/facter

View on GitHub
lib/facter/util/linux/socket_parser.rb

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
# frozen_string_literal: true

module Facter
  module Util
    module Linux
      class SocketParser
        class << self
          def retrieve_interfaces(logger)
            require 'socket'
            @interfaces = {}
            @log = logger
            Socket.getifaddrs.each do |ifaddr|
              populate_interface_info(ifaddr)
            end

            @interfaces
          end

          private

          def populate_interface_info(ifaddr)
            interface_name = ifaddr.name
            @interfaces[interface_name] = {} if @interfaces[interface_name].nil?

            mac(ifaddr)
            ip_info_of(ifaddr)
          end

          def mac(ifaddr)
            return unless @interfaces[ifaddr.name][:mac].nil?

            mac = search_for_mac(ifaddr)
            @interfaces[ifaddr.name][:mac] = mac if mac
          end

          def search_for_mac(ifaddr)
            mac = mac_from_bonded_interface(ifaddr.name)
            mac ||= mac_from(ifaddr)
            mac if !mac.nil? && mac != '00:00:00:00:00:00' && mac =~ /^([0-9A-Fa-f]{2}[:-]){5,19}([0-9A-Fa-f]{2})$/
          end

          def mac_from_bonded_interface(interface_name)
            master = bond_master_of(interface_name)
            return unless master

            output = Facter::Util::FileHelper.safe_read("/proc/net/bonding/#{master}", nil)
            return unless output

            found_match = false
            output.each_line do |line|
              if line.strip == "Slave Interface: #{interface_name}"
                found_match = true
              elsif line.include? 'Slave Interface'
                # if we reached the data block of another interface belonging to the bonded interface
                found_match = false
              end
              return Regexp.last_match(1) if found_match && line =~ /Permanent HW addr: (\S*)/
            end
          end

          def bond_master_of(interface_name)
            content = get_ip_link_show_data(interface_name)
            content&.match(/master (\S*) /)&.captures&.first
          end

          def get_ip_link_show_data(interface_name)
            @ip_link_show_data ||= read_ip_link_show_data
            @ip_link_show_data[interface_name]
          end

          def read_ip_link_show_data
            ip_link_show_data = {}
            output = Facter::Core::Execution.execute('ip -o link show', logger: @log)
            output.each_line do |line|
              interface_name = line.split(':')[1]&.strip if line
              ip_link_show_data[interface_name] = line if interface_name
            end
            ip_link_show_data
          end

          def mac_from(ifaddr)
            if Socket.const_defined? :PF_LINK
              ifaddr.addr&.getnameinfo&.first # sometimes it returns localhost or ip
            elsif Socket.const_defined? :PF_PACKET
              return if ifaddr.addr.nil?

              mac_from_sockaddr_of(ifaddr)
            end
          rescue StandardError => e
            @log.debug("Could not read mac for interface #{ifaddr.name}, got #{e}")
            nil
          end

          def mac_from_sockaddr_of(ifaddr)
            result = ifaddr.addr.inspect_sockaddr
            result&.match(/hwaddr=([\h:]+)/)&.captures&.first
          end

          def ip_info_of(ifaddr)
            return if ifaddr.addr.nil? || ifaddr.netmask.nil?

            add_binding(ifaddr.name, ifaddr)
          rescue StandardError => e
            @log.debug("Could not read binding data, got #{e}")
          end

          def add_binding(interface_name, ifaddr)
            ip, netmask, binding_key = binding_data(ifaddr)
            binding = Facter::Util::Resolvers::Networking.build_binding(ip, netmask)
            return if binding.nil?

            @interfaces[interface_name][binding_key] = [] if @interfaces[interface_name][binding_key].nil?
            @interfaces[interface_name][binding_key] << binding

            @log.debug("Adding to interface #{interface_name}, binding:\n#{binding}")
          end

          def binding_data(ifaddr)
            # ipv6 ips are retrieved as <ip>%<interface_name>
            ip = ifaddr.addr.ip_address.split('%').first
            netmask = ifaddr.netmask.ip_address
            binding_key = ifaddr.addr.ipv4? ? :bindings : :bindings6

            [ip, netmask, binding_key]
          end
        end
      end
    end
  end
end