theforeman/foreman

View on GitHub
app/services/foreman_chef/fact_parser.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module ForemanChef
  class FactParser < ::FactParser
    VIRTUAL = /\A([a-z0-9]+)[:.](\d+)\Z/
    VIRTUAL_NAMES = /#{VIRTUAL}|#{BRIDGES}|#{BONDS}/

    def operatingsystem
      os_name = facts['lsb::id'] || facts['platform']
      release = facts['lsb::release'] || facts['platform_version']
      # if we have no release information we can't assign OS properly (e.g. missing redhat-lsb)
      if release.nil?
        major, minor = 1, nil
      elsif os_name == 'Ubuntu'
        major, minor = release, nil
      else
        major, minor = release.split('.')
      end

      if facts['platform'] == 'windows'
        release_name = facts['kernel::name']
        os_name = os_name.capitalize
      else
        # to keep OS names consistent with other cfgmgt agents we skip description importing
        # description = facts['lsb::description']
        release_name = facts['lsb::codename']
      end

      # convert OHAI names to Foreman common OS names
      case os_name
        when 'RedHatEnterpriseServer'
          os_name = 'RedHat'
      end

      # So far only CentOS needs exception
      case os_name
        when 'CentOS'
          if major.to_i >= 7
            # get the minor.build number, e.g. 7.2.1511 -> 2.1511
            minor = release[2..-1]
          end
        when 'centos'
          # Centos Stream doesn't have minor version on need to replace blank spaces due to name restriction
          # Also normalize CentOS / CentOS Linux
          os_name = minor ? 'CentOS' : facts.dig(:os_release, :name).tr(' ', '_')
      end

      begin
        klass = os_name.constantize
      rescue NameError => _
        logger.debug "unknown operating system #{os_name}, fallback to generic type"
        klass = Operatingsystem
      end

      args = { :name => os_name, :major => major.to_s, :minor => minor.to_s }
      klass.where(args).first || klass.new(args.merge(:release_name => release_name)).tap(&:save!)
    end

    # we don't want to parse puppet environment, foreman_chef already has its own environment
    def environment
      nil
    end

    def architecture
      name = facts['kernel::machine']
      name = "x86_64" if name == "amd64"
      Architecture.where(:name => name).first_or_create if name.present?
    end

    def model
      if facts['virtualization'].present?
        name = facts['virtualization']['system']
      else
        name = facts['dmi::system::product_name']
      end
      Model.where(:name => name.strip).first_or_create if name.present?
    end

    def domain
      name = facts['domain']
      Domain.where(:name => name).first_or_create if name.present?
    end

    def ipmi_interface
      if facts['ipmi::mac_address'].present?
        { 'ipaddress' => facts['ipmi::address'], 'macaddress' => facts['ipmi::mac_address'] }.with_indifferent_access
      end
    end

    def certname
      facts['chef_node_name']
    end

    def support_interfaces_parsing?
      true
    end

    def parse_interfaces?
      support_interfaces_parsing? && !Setting['ignore_puppet_facts_for_provisioning']
    end

    def boot_timestamp
      Time.zone.now.to_i - facts['system_uptime::seconds'].to_i
    end

    def bios
      {
        :vendor => facts['dmi::bios::all_records::vendor'],
        :version => facts['dmi::bios::all_records::version'],
        :release_date => facts['dmi::bios::all_records::release_date'],
      }
    end

    private

    def logger
      Foreman::Logging.logger('background')
    end

    # meant to be implemented in inheriting classes
    # should return hash with indifferent access in following format:
    # { 'link': 'true',
    #   'macaddress': '00:00:00:00:00:FF',
    #   'ipaddress': nil,
    #   'any_other_fact': 'value' }
    #
    # note that link and macaddress are mandatory
    def get_facts_for_interface(interface)
      facts = interfaces_hash[interface]
      hash = {
        'link' => facts['state'] == 'up',
        'macaddress' => get_address_by_family(facts['addresses'], 'lladdr'),
        'ipaddress' => get_address_by_family(facts['addresses'], 'inet'),
      }
      hash['tag'] = facts['vlan']['id'] if facts['vlan'].present?
      hash.with_indifferent_access
    end

    # meant to be implemented in inheriting classes
    # should return array of interfaces names, e.g.
    #   ['eth0', 'eth0.0', 'eth1']
    def get_interfaces
      interfaces_hash.keys
    end

    def get_address_by_family(addresses, family)
      addresses.detect { |address, attributes| attributes['family'] == family }.try(:first)
    end

    def network_hash
      @network_hash ||= ForemanChef::FactImporter::Sparser.new.unsparse(facts.select { |k, v| k.start_with?('network::interfaces::') }).try(:[], 'network') || {}
    end

    def interfaces_hash
      @interfaces_hash ||= network_hash.try(:[], 'interfaces') || {}
    end

    # adds attributes like virtual
    def set_additional_attributes(attributes, name)
      if name =~ VIRTUAL_NAMES
        attributes[:virtual] = true
        if Regexp.last_match(1).nil? && name =~ BRIDGES
          attributes[:bridge] = true
        else
          attributes[:attached_to] = Regexp.last_match(1)
        end
      else
        attributes[:virtual] = false
      end
      attributes
    end
  end
end