class Proxy::Ansible::Runner::AnsibleRunner

Attributes

command_pid[R]
execution_timeout_interval[R]

Public Class Methods

new(input, suspended_action:) click to toggle source
Calls superclass method
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 13
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]
  @tags = action_input[:tags]
  @tags_flag = action_input[:tags_flag]
  @passphrase = action_input['secrets']['key_passphrase']
  @execution_timeout_interval = action_input[:execution_timeout_interval]
  @cleanup_working_dirs = action_input.fetch(:cleanup_working_dirs, true)
end

Public Instance Methods

close() click to toggle source
Calls superclass method
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 72
def close
  super
  FileUtils.remove_entry(@root) if @tmp_working_dir && Dir.exist?(@root) && @cleanup_working_dirs
end
kill() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 64
def kill
  ::Process.kill('SIGTERM', @command_pid)
  publish_exit_status(2)
  @inventory['all']['hosts'].each { |hostname| @exit_statuses[hostname] = 2 }
  broadcast_data('Timeout for execution passed, stopping the job', 'stderr')
  close
end
refresh() click to toggle source
Calls superclass method
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 37
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/smart_proxy_ansible/runner/ansible_runner.rb, line 29
def start
  prepare_directory_structure
  write_inventory
  write_playbook
  write_ssh_key if !@passphrase.nil? && !@passphrase.empty?
  start_ansible_runner
end
timeout() click to toggle source
Calls superclass method
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 55
def timeout
  logger.debug('job timed out')
  super
end
timeout_interval() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 60
def timeout_interval
  execution_timeout_interval
end

Private Instance Methods

check_cmd() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 178
def check_cmd
  check_mode? ? '"--check"' : ''
end
check_mode?() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 190
def check_mode?
  @check_mode == true && @rex_command == false
end
cmdline() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 167
def cmdline
  cmd_args = [tags_cmd, check_cmd].reject(&:empty?)
  return nil unless cmd_args.any?
  cmd_args.join(' ')
end
handle_broadcast_data(event) click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 108
def handle_broadcast_data(event)
  log_event("broadcast", event)
  if event['event'] == 'playbook_on_stats'
    failures = event.dig('event_data', 'failures') || {}
    unreachable = event.dig('event_data', 'dark') || {}
    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
      if @exit_statuses[host].to_i != 0 && failures[host].to_i <= 0 && unreachable[host].to_i <= 0
        publish_exit_status_for(host, 0)
      end
    end
  else
    broadcast_data(event['stdout'] + "\n", 'stdout')
  end
end
handle_event_file(event_file) click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 79
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/smart_proxy_ansible/runner/ansible_runner.rb, line 95
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/smart_proxy_ansible/runner/ansible_runner.rb, line 201
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/smart_proxy_ansible/runner/ansible_runner.rb, line 194
def prepare_directory_structure
  inner = %w[inventory project env].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/smart_proxy_ansible/runner/ansible_runner.rb, line 210
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] ||= Proxy::RemoteExecution::Ssh::Plugin.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/smart_proxy_ansible/runner/ansible_runner.rb, line 237
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/smart_proxy_ansible/runner/ansible_runner.rb, line 157
def start_ansible_runner
  env = {}
  env['FOREMAN_CALLBACK_DISABLE'] = '1' if @rex_command
  command = [env, 'ansible-runner', 'run', @root, '-p', 'playbook.yml']
  command << '--cmdline' << cmdline unless cmdline.nil?
  command << verbosity if verbose?
  initialize_command(*command)
  logger.debug("[foreman_ansible] - Running command '#{command.join(' ')}'")
end
tags_cmd() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 173
def tags_cmd
  flag = @tags_flag == 'include' ? '--tags' : '--skip-tags'
  @tags.empty? ? '' : "#{flag} '#{Array(@tags).join(',')}'"
end
verbose?() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 186
def verbose?
  @verbosity_level.to_i.positive?
end
verbosity() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 182
def verbosity
  '-' + 'v' * @verbosity_level.to_i
end
working_dir() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 226
def working_dir
  return @root if @root
  dir = Proxy::Ansible::Plugin.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/smart_proxy_ansible/runner/ansible_runner.rb, line 128
      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/smart_proxy_ansible/runner/ansible_runner.rb, line 140
def write_playbook
  File.write(File.join(@root, 'project', 'playbook.yml'), @playbook)
end
write_ssh_key() click to toggle source
# File lib/smart_proxy_ansible/runner/ansible_runner.rb, line 144
def write_ssh_key
  key_path = File.join(@root, 'env', 'ssh_key')
  File.symlink(File.expand_path(Proxy::RemoteExecution::Ssh::Plugin.settings[:ssh_identity_key_file]), key_path)

  passwords_path = File.join(@root, 'env', 'passwords')
  # here we create a secrets file for ansible-runner, which uses the key as regexp
  # to match line asking for password, given the limitation to match only first 100 chars
  # and the fact the line contains dynamically created temp directory, the regexp
  # mentions only things that are always there, such as artifacts directory and the key name
  secrets = YAML.dump({ "for.*/artifacts/.*/ssh_key_data:" => @passphrase })
  File.write(passwords_path, secrets, perm: 0o600)
end