theforeman/foreman_ansible

View on GitHub
app/services/foreman_ansible/variables_importer.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# frozen_string_literal: true

module ForemanAnsible
  # Methods to transform variable names coming from the proxy into
  # Foreman AnsibleVariable objects
  class VariablesImporter
    include ::ForemanAnsible::ProxyAPI

    VARIABLE_TYPES = {
      'TrueClass' => 'boolean',
      'FalseClass' => 'boolean',
      'Integer' => 'integer',
      'Fixnum' => 'integer',
      'Float' => 'real',
      'Array' => 'array',
      'Hash' => 'hash',
      'String' => 'string'
    }.freeze

    def initialize(proxy = nil)
      @ansible_proxy = proxy
    end

    def import_variable_names(new_roles)
      return import_variables(remote_variables, new_roles) if @ansible_proxy
      import_variables(local_variables, new_roles)
    end

    def get_variables_names(role_name)
      remote_variables[role_name]
    end

    def import_variables(role_variables, new_roles)
      detect_changes(role_variables.map do |role_name, variables|
        next if variables.blank?
        role = import_new_role(role_name, new_roles)
        next if role.blank?
        initialize_variables(variables, role)
      end.select(&:present?).flatten.compact)
    end

    def import_variables_roles(roles)
      if roles['new']
        imported_roles = roles['new'].keys
        variables_to_import = {}
        remote_variables.each do |role, variables|
          next unless imported_roles.include? role
          role_obj = AnsibleRole.find_by(:name => role)
          variables_to_import[role] = {}
          values = initialize_variables(variables, role_obj)
          values.each do |value|
            variables_to_import[role][value.key] = value.attributes.to_json
          end
        end
        create_new_variables(variables_to_import)
      end
      return if roles['old'].empty?
      variables_to_update = { 'new' => {}, 'obsolete' => {}, 'update' => {} }
      import_variable_names([]).each do |kind, variables|
        variables.each do |variable|
          next unless roles['old'].values.map { |role| role['id'] }.include?(variable.ansible_role_id)
          role_name = variable.ansible_role.name
          variables_to_update[kind][role_name] ||= {}
          variables_to_update[kind][role_name][variable.key] = variable.attributes.to_json
        end
      end
      finish_import(variables_to_update['new'], variables_to_update['obsolete'], variables_to_update['update'])
    end

    def import_new_role(role_name, new_roles)
      role = AnsibleRole.find_by(:name => role_name)
      if role.blank? && new_roles.map(&:name).include?(role_name)
        role = new_roles.find { |r| r.name == role_name }
      end
      role
    end

    def initialize_variables(variables, role)
      variables.map do |variable_name, variable_default|
        variable = AnsibleVariable.find_or_initialize_by(
          :key => variable_name,
          :ansible_role_id => role.id
        )
        variable.assign_attributes(:hidden_value => false,
                                   :default_value => variable_default,
                                   :key_type => infer_key_type(variable_default))
        variable.imported = true if variable.new_record?
        variable.valid? ? variable : nil
      end
    end

    def detect_changes(imported)
      changes = {}.with_indifferent_access
      persisted, changes[:new] = imported.partition { |var| var.id.present? }
      changed, _old = persisted.partition(&:changed?)
      _overriden, changes[:update] = changed.partition(&:override?)
      changes[:obsolete] = AnsibleVariable.where.not(id: persisted.pluck(:id)).
                           where.not(imported: false)

      changes
    end

    def finish_import(new, obsolete, update)
      results = { :added => [], :obsolete => [], :updated => [] }
      results[:added] = create_new_variables(new) if new.present?
      results[:obsolete] = delete_old_variables(obsolete) if obsolete.present?
      results[:updated] = update_variables(update) if update.present?
      results
    end

    def create_new_variables(variables)
      iterate_over_variables(variables) do |role, memo, attrs|
        variable = AnsibleVariable.new(
          JSON.parse(attrs)
        )
        # Make sure, newly created variables are not hidden
        variable.hidden_value = false
        variable.ansible_role = ::AnsibleRole.find_by(:name => role)
        variable.save
        memo << variable
      end
    end

    def update_variables(variables)
      iterate_over_variables(variables) do |_role, memo, attrs|
        attributes = JSON.parse(attrs)
        var = AnsibleVariable.find attributes['id']
        var.update(attributes)
        memo << var
      end
    end

    def delete_old_variables(variables)
      iterate_over_variables(variables) do |_role, memo, attrs|
        variable = AnsibleVariable.find(
          JSON.parse(attrs)['id']
        )
        memo << variable.key
        variable.destroy
      end
    end

    private

    def local_variables
      ::AnsibleVariable.all
    end

    def remote_variables
      proxy_api.all_variables
    end

    def infer_key_type(value)
      VARIABLE_TYPES[value.class.to_s] || 'string'
    end

    def iterate_over_variables(variables)
      variables.reduce([]) do |memo, (role, vars)|
        vars.map do |_key, attrs|
          yield role, memo, attrs if block_given?
          memo
        end
      end
    end
  end
end