class ForemanAnsibleCore::Runner::AnsibleRunner

Public Class Methods

new(input, suspended_action:) click to toggle source
Calls superclass method
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 8
def initialize(input, suspended_action:)
  super input, :suspended_action => suspended_action
  @inventory = rebuild_secrets(rebuild_inventory(input), input)
  action_input = input.values.first[:input][:action_input]
  @playbook = action_input[:script]
  @root = working_dir
  @verbosity_level = action_input[:verbosity_level]
  @rex_command = action_input[:remote_execution_command]
  @check_mode = action_input[:check_mode]
end

Public Instance Methods

close() click to toggle source
Calls superclass method
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 44
def close
  super
  FileUtils.remove_entry(@root) if @tmp_working_dir
end
refresh() click to toggle source
Calls superclass method
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 26
def refresh
  return unless super
  @counter ||= 1
  @uuid ||= File.basename(Dir["#{@root}/artifacts/*"].first)
  job_event_dir = File.join(@root, 'artifacts', @uuid, 'job_events')
  loop do
    files = Dir["#{job_event_dir}/*.json"].map do |file|
      num = File.basename(file)[/\A\d+/].to_i unless file.include?('partial')
      [file, num]
    end
    files_with_nums = files.select { |(_, num)| num && num >= @counter }.sort_by(&:last)
    break if files_with_nums.empty?
    logger.debug("[foreman_ansible] - processing event files: #{files_with_nums.map(&:first).inspect}}")
    files_with_nums.map(&:first).each { |event_file| handle_event_file(event_file) }
    @counter = files_with_nums.last.last + 1
  end
end
start() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 19
def start
  prepare_directory_structure
  write_inventory
  write_playbook
  start_ansible_runner
end

Private Instance Methods

check_mode?() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 131
def check_mode?
  @check_mode == true
end
handle_broadcast_data(event) click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 80
def handle_broadcast_data(event)
  log_event("broadcast", event)
  if event['event'] == 'playbook_on_stats'
    failures = event.dig('event_data', 'failures') || {}
    header, *rows = event['stdout'].strip.lines.map(&:chomp)
    @outputs.keys.select { |key| key.is_a? String }.each do |host|
      line = rows.find { |row| row =~ /#{host}/ }
      publish_data_for(host, [header, line].join("\n"), 'stdout')

      # If the task has been rescued, it won't consider a failure
      publish_exit_status_for(host, 0) if @exit_statuses[host].to_i != 0 && failures[host].to_i <= 0
    end
  else
    broadcast_data(event['stdout'] + "\n", 'stdout')
  end
end
handle_event_file(event_file) click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 51
def handle_event_file(event_file)
  logger.debug("[foreman_ansible] - parsing event file #{event_file}")
  begin
    event = JSON.parse(File.read(event_file))
    if (hostname = event.dig('event_data', 'host'))
      handle_host_event(hostname, event)
    else
      handle_broadcast_data(event)
    end
    true
  rescue JSON::ParserError => e
    logger.error("[foreman_ansible] - Error parsing runner event at #{event_file}: #{e.class}: #{e.message}")
    logger.debug(e.backtrace.join("\n"))
  end
end
handle_host_event(hostname, event) click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 67
def handle_host_event(hostname, event)
  log_event("for host: #{hostname.inspect}", event)
  publish_data_for(hostname, event['stdout'] + "\n", 'stdout') if event['stdout']
  case event['event']
  when 'runner_on_ok'
    publish_exit_status_for(hostname, 0) if @exit_statuses[hostname].nil?
  when 'runner_on_unreachable'
    publish_exit_status_for(hostname, 1)
  when 'runner_on_failed'
    publish_exit_status_for(hostname, 2) if event.dig('event_data', 'ignore_errors').nil?
  end
end
log_event(description, event) click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 142
def log_event(description, event)
  # TODO: replace this ugly code with block variant once https://github.com/Dynflow/dynflow/pull/323
  # arrives in production
  logger.debug("[foreman_ansible] - handling event #{description}: #{JSON.pretty_generate(event)}") if logger.level <= ::Logger::DEBUG
end
prepare_directory_structure() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 135
def prepare_directory_structure
  inner = %w[inventory project].map { |part| File.join(@root, part) }
  ([@root] + inner).each do |path|
    FileUtils.mkdir_p path
  end
end
rebuild_inventory(input) click to toggle source

Each per-host task has inventory only for itself, we must collect all the partial inventories into one large inventory containing all the hosts.

# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 151
def rebuild_inventory(input)
  action_inputs = input.values.map { |hash| hash[:input][:action_input] }
  hostnames = action_inputs.map { |hash| hash[:name] }
  inventories = action_inputs.map { |hash| hash[:ansible_inventory] }
  host_vars = inventories.map { |i| i['_meta']['hostvars'] }.reduce({}) do |acc, hosts|
    hosts.reduce(acc) do |inner_acc, (hostname, vars)|
      vars[:ansible_ssh_private_key_file] ||= ForemanRemoteExecutionCore.settings[:ssh_identity_key_file]
      inner_acc.merge(hostname => vars)
    end
  end

  { '_meta' => { 'hostvars' => host_vars },
    'all' => { 'hosts' => hostnames,
               'vars' => inventories.first['all']['vars'] } }
end
rebuild_secrets(inventory, input) click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 178
def rebuild_secrets(inventory, input)
  input.each do |host, host_input|
    secrets = host_input['input']['action_input']['secrets']
    per_host = secrets['per-host'][host]

    new_secrets = {
      'ansible_password' => inventory['ssh_password'] || per_host['ansible_password'],
      'ansible_become_password' => inventory['effective_user_password'] || per_host['ansible_become_password']
    }
    inventory['_meta']['hostvars'][host].update(new_secrets)
  end

  inventory
end
start_ansible_runner() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 113
def start_ansible_runner
  env = {}
  env['FOREMAN_CALLBACK_DISABLE'] = '1' if @rex_command
  command = [env, 'ansible-runner', 'run', @root, '-p', 'playbook.yml']
  command << '--cmdline' << '"--check"' if check_mode?
  command << verbosity if verbose?
  initialize_command(*command)
  logger.debug("[foreman_ansible] - Running command '#{command.join(' ')}'")
end
verbose?() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 127
def verbose?
  @verbosity_level.to_i.positive?
end
verbosity() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 123
def verbosity
  '-' + 'v' * @verbosity_level.to_i
end
working_dir() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 167
def working_dir
  return @root if @root
  dir = ForemanAnsibleCore.settings[:working_dir]
  @tmp_working_dir = true
  if dir.nil?
    Dir.mktmpdir
  else
    Dir.mktmpdir(nil, File.expand_path(dir))
  end
end
write_inventory() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 97
      def write_inventory
        path = File.join(@root, 'inventory', 'hosts')
        data_path = File.join(@root, 'data')
        inventory_script = <<~INVENTORY_SCRIPT
          #!/bin/sh
          cat #{::Shellwords.escape data_path}
        INVENTORY_SCRIPT
        File.write(path, inventory_script)
        File.write(data_path, JSON.dump(@inventory))
        File.chmod(0o0755, path)
      end
write_playbook() click to toggle source
# File lib/foreman_ansible_core/runner/ansible_runner.rb, line 109
def write_playbook
  File.write(File.join(@root, 'project', 'playbook.yml'), @playbook)
end