theforeman/foreman

View on GitHub
app/services/structured_fact_importer.rb

Summary

Maintainability
A
3 hrs
Test Coverage
class StructuredFactImporter < FactImporter
  include ActionView::Helpers::NumberHelper

  MAXIMUM_FLAT_FACTS = /^(blockdevice_|ipaddress6?_|macaddress_|mtu_|speed_|auto_negotiation_|duplex_|link_|wol_)/

  def normalize(facts)
    max = Setting[:maximum_structured_facts]
    flat_counts = {}
    dropped = 0
    # Remove empty values first, so nil facts added by flatten_composite imply compose
    # and count flat counts for later removal and also directly remove too large sub-hashes
    facts.delete_if do |k, v|
      match = k.to_s.match(MAXIMUM_FLAT_FACTS)
      if match
        flat_counts[match.captures.first] ||= 0
        flat_counts[match.captures.first] += 1
      end
      v.nil? || (v.is_a?(String) && v.empty?) || ((v.is_a?(Hash) || v.is_a?(Array)) && v.count > max && (dropped += v.count))
    end

    # Remove flat facts exceeding the limit
    flat_counts.each_pair do |string, count|
      if count > max
        facts.delete_if { |k, v| k.to_s.start_with?(string) && (dropped += 1) }
      end
    end

    # Report rough precision of number of dropped facts so it does not change often (e.g. "10 Thousand")
    if dropped > 0
      facts['foreman'] ||= {}
      facts['foreman']['dropped_subtree_facts'] = number_to_human(dropped, precision: 1)
      logger.warn "Some subtrees exceeded #{max} limit of facts, dropped #{dropped} keys"
    end

    facts = flatten_composite({}, facts)

    original_keys = facts.keys.to_a
    original_keys.each do |key|
      fill_hierarchy(facts, key)
    end

    facts
  end

  # expand {'a' => {'b' => 'c'}} to {'a::b' => 'c'}
  def flatten_composite(memo, facts, prefix = '')
    facts.each do |k, v|
      k = prefix.empty? ? k.to_s : prefix + FactName::SEPARATOR + k.to_s

      # skip fact if it is excluded
      next if k.match(excluded_facts)

      if v.is_a?(Hash)
        # skip recursion if current key is excluded. Example:
        # given excluded_facts = macvtap.*, and fact hash: interfaces => macvtap01 => ip => 1.2.3.4
        # do not create nodes that start with: "interfaces::macvtap01"
        flatten_composite(memo, v, k)
      else
        memo[k] = v.to_s
      end
    end
    memo
  end

  # ensures that parent facts already exist in the hash.
  # Example: for fact: "a::b::c", it will make sure that "a" and "a::b" exist in
  # the hash.
  def fill_hierarchy(facts, child_fact_name)
    facts[child_fact_name] = nil unless facts.key?(child_fact_name)

    parent_name = parent_fact_name(child_fact_name)
    fill_hierarchy(facts, parent_name) if parent_name
  end

  def preload_fact_names
    # Also fetch compose values, generating {NAME => [ID, COMPOSE]}, avoiding loading the entire model
    Hash[fact_name_class.where(:type => fact_name_class).reorder('').pluck(:name, :id, :compose).map { |fact| [fact.shift, fact] }]
  end

  def ensure_fact_names
    super

    composite_fact_names = facts.map do |key, value|
      key if value.nil?
    end.compact

    affected_records = fact_name_class.where(:name => composite_fact_names, :compose => false).update_all(:compose => true)

    # reload name records if compose flag was reset.
    initialize_fact_names if affected_records > 0
  end

  def fact_name_attributes(name)
    attributes = super
    fact_value = facts[name]
    parent_fact_record = fact_names[parent_fact_name(name)]

    attributes[:parent] = parent_fact_record
    attributes[:compose] = fact_value.nil?
    attributes
  end

  def parent_fact_name(child_fact_name)
    split = child_fact_name.rindex(FactName::SEPARATOR)
    return nil unless split
    child_fact_name[0, split]
  end
end