Skip to content

Commit

Permalink
Merge pull request #17763 from Ladas/ansible_runner_async_method
Browse files Browse the repository at this point in the history
Ansible runner async method
  • Loading branch information
agrare authored Jul 27, 2018
2 parents 1893e95 + c6571ea commit 12b109c
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 24 deletions.
98 changes: 77 additions & 21 deletions lib/ansible/runner.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
module Ansible
class Runner
class << self
# Runs a playbook via ansible-runner, see: https://ansible-runner.readthedocs.io/en/latest/standalone.html#running-playbooks
#
# @param env_vars [Hash] Hash with key/value pairs that will be passed as environment variables to the
# ansible-runner run
# @param extra_vars [Hash] Hash with key/value pairs that will be passed as extra_vars to the ansible-runner run
# @param playbook_path [String] Path to the playbook we will want to run
# @return [Ansible::Runner::ResponseAsync] Response object that we can query for .running?, providing us the
# Ansible::Runner::Response object, when the job is finished.
def run_async(env_vars, extra_vars, playbook_path)
run_via_cli(env_vars,
extra_vars,
:playbook_path => playbook_path,
:ansible_runner_method => "start")
end

# Runs a role directly via ansible-runner, a simple playbook is then automatically created,
# see: https://ansible-runner.readthedocs.io/en/latest/standalone.html#running-roles-directly
#
# @param env_vars [Hash] Hash with key/value pairs that will be passed as environment variables to the
# ansible-runner run
# @param extra_vars [Hash] Hash with key/value pairs that will be passed as extra_vars to the ansible-runner run
# @param role_name [String] Ansible role name
# @param roles_path [String] Path to the directory with roles
# @param role_skip_facts [Boolean] Whether we should skip facts gathering, equals to 'gather_facts: False' in a
# playbook. True by default.
# @return [Ansible::Runner::ResponseAsync] Response object that we can query for .running?, providing us the
# Ansible::Runner::Response object, when the job is finished.
def run_role_async(env_vars, extra_vars, role_name, roles_path:, role_skip_facts: true)
run_via_cli(env_vars,
extra_vars,
:role_name => role_name,
:roles_path => roles_path,
:role_skip_facts => role_skip_facts,
:ansible_runner_method => "start")
end

# Runs a playbook via ansible-runner, see: https://ansible-runner.readthedocs.io/en/latest/standalone.html#running-playbooks
#
# @param env_vars [Hash] Hash with key/value pairs that will be passed as environment variables to the
Expand Down Expand Up @@ -95,24 +131,49 @@ def run_in_queue(method_name, user_id, queue_opts, args)
# @param role_skip_facts [Boolean] Whether we should skip facts gathering, equals to 'gather_facts: False' in a
# playbook. True by default.
# @return [Ansible::Runner::Response] Response object with all details about the ansible run
def run_via_cli(env_vars, extra_vars, playbook_path: nil, role_name: nil, roles_path: nil, role_skip_facts: true)
def run_via_cli(env_vars, extra_vars, playbook_path: nil, role_name: nil, roles_path: nil, role_skip_facts: true,
ansible_runner_method: "run")
validate_params!(env_vars, extra_vars, playbook_path, roles_path)

Dir.mktmpdir("ansible-runner") do |base_dir|
result = AwesomeSpawn.run(ansible_command(base_dir),
base_dir = Dir.mktmpdir("ansible-runner")
begin
result = AwesomeSpawn.run(ansible_command(base_dir, ansible_runner_method),
:env => env_vars,
:params => params(:extra_vars => extra_vars,
:playbook_path => playbook_path,
:role_name => role_name,
:roles_path => roles_path,
:role_skip_facts => role_skip_facts))

Ansible::Runner::Response.new(:return_code => return_code(base_dir),
:stdout => result.output,
:stderr => result.error)
response(base_dir, ansible_runner_method, result)
ensure
# Clean up the tmp dir for the sync method, for async we will clean it up after the job is finished and we've
# read the output, that will be written into this directory.
FileUtils.remove_entry(base_dir) unless async?(ansible_runner_method)
end
end

# @param base_dir [String] ansible-runner private_data_dir parameter
# @param ansible_runner_method [String] Method we will use to run the ansible-runner. It can be either "run",
# which is sync call, or "start" which is async call
# @param result [AwesomeSpawn::CommandResult] Result object of AwesomeSpawn.run
# @return [Ansible::Runner::ResponseAsync, Ansible::Runner::Response] response or ResponseAsync based on the
# ansible_runner_method
def response(base_dir, ansible_runner_method, result)
if async?(ansible_runner_method)
Ansible::Runner::ResponseAsync.new(:base_dir => base_dir)
else
Ansible::Runner::Response.new(:base_dir => base_dir,
:stdout => result.output,
:stderr => result.error)
end
end

# @return [Bollean] True if ansible-runner will run on background
def async?(ansible_runner_method)
ansible_runner_method == "start"
end

# Generate correct params, depending if we've passed :playbook_path or a :role_name
#
# @param extra_vars [Hash] Hash with key/value pairs that will be passed as extra_vars to the ansible-runner run
Expand Down Expand Up @@ -150,9 +211,9 @@ def playbook_params(playbook_path:)
# playbook.
# @return [Hash] Partial arguments for the ansible-runner run
def role_params(role_name:, roles_path:, role_skip_facts:)
role_params = {:role => role_name}
role_params = {:role => role_name}

role_params[:"roles-path"] = roles_path if roles_path
role_params[:"roles-path"] = roles_path if roles_path
role_params[:"role-skip-facts"] = nil if role_skip_facts

role_params
Expand All @@ -166,16 +227,6 @@ def shared_params(extra_vars:)
{:cmdline => "--extra-vars '#{JSON.dump(extra_vars)}'"}
end

# Reads a return code from a file used by ansible-runner
#
# @return [Integer] Return code of the ansible-runner run, 0 == ok, others mean failure
def return_code(base_dir)
File.read(File.join(base_dir, "artifacts/result/rc")).to_i
rescue
_log.warn("Couldn't find ansible-runner return code")
1
end

# Asserts passed parameters are correct, if not throws an exception.
#
# @param env_vars [Hash] Hash with key/value pairs that will be passed as environment variables to the
Expand Down Expand Up @@ -210,9 +261,14 @@ def assert_path!(path)
end
end

# @return [String] ansible_runner executable command
def ansible_command(base_dir)
"ansible-runner run #{base_dir} --json -i result --hosts localhost"
# Returns ansible-runner executable command
#
# @param base_dir [String] ansible-runner private_data_dir parameter
# @param ansible_runner_method [String] Method we will use to run the ansible-runner. It can be either "run",
# which is sync call, or "start" which is async call
# @return [String] ansible-runner executable command
def ansible_command(base_dir, ansible_runner_method)
"ansible-runner #{ansible_runner_method} #{base_dir} --json -i result --hosts localhost"
end
end
end
Expand Down
52 changes: 49 additions & 3 deletions lib/ansible/runner/response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,48 @@ class Runner
class Response
include Vmdb::Logging

attr_reader :return_code, :stdout, :stderr, :parsed_stdout
attr_reader :base_dir, :ident

# Response object designed for holding full response from ansible-runner
#
# @param base_dir [String] ansible-runner private_data_dir parameter
# @param return_code [Integer] Return code of the ansible-runner run, 0 == ok, others mean failure
# @param stdout [String] Stdout from ansible-runner run
# @param stderr [String] Stderr from ansible-runner run
def initialize(return_code:, stdout:, stderr:)
# @param ident [String] ansible-runner ident parameter
def initialize(base_dir:, return_code: nil, stdout: nil, stderr: nil, ident: "result")
@base_dir = base_dir
@ident = ident
@return_code = return_code
@stdout = stdout
@parsed_stdout = parse_stdout(stdout)
@parsed_stdout = parse_stdout(stdout) if stdout
@stderr = stderr
end

# @return [Integer] Return code of the ansible-runner run, 0 == ok, others mean failure
def return_code
@return_code ||= load_return_code
end

# @return [String] Stdout that is text, where each line should be JSON encoded object
def stdout
@stdout ||= load_stdout
end

# @return [Array<Hash>] Array of hashes as individual Ansible plays
def parsed_stdout
@parsed_stdout ||= parse_stdout(stdout)
end

# Loads needed data from the filesystem and deletes the ansible-runner base dir
def cleanup_filesystem!
# Load all needed files, before we cleanup the dir
return_code
stdout

FileUtils.remove_entry(base_dir)
end

private

# Parses stdout to array of hashes
Expand All @@ -40,6 +68,24 @@ def parse_stdout(stdout)

parsed_stdout
end

# Reads a return code from a file used by ansible-runner
#
# @return [Integer] Return code of the ansible-runner run, 0 == ok, others mean failure
def load_return_code
File.read(File.join(base_dir, "artifacts", ident, "rc")).to_i
rescue
_log.warn("Couldn't find ansible-runner return code in #{base_dir}")
1
end

# @return [String] Stdout that is text, where each line should be JSON encoded object
def load_stdout
File.read(File.join(base_dir, "artifacts", ident, "stdout"))
rescue
_log.warn("Couldn't find ansible-runner stdout in #{base_dir}")
""
end
end
end
end
61 changes: 61 additions & 0 deletions lib/ansible/runner/response_async.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module Ansible
class Runner
class ResponseAsync
include Vmdb::Logging

attr_reader :base_dir, :ident

# Response object designed for holding full response from ansible-runner
#
# @param base_dir [String] Base directory containing Runner metadata (project, inventory, etc). ansible-runner
# refers to it as 'private_data_dir'
# @param ident [String] An identifier that will be used when generating the artifacts directory and can be used to
# uniquely identify a playbook run. We use unique base dir per run, so this identifier can be static for
# most cases.
def initialize(base_dir:, ident: "result")
@base_dir = base_dir
@ident = ident
end

# @return [Boolean] true if the ansible job is still running, false when it's finished
def running?
AwesomeSpawn.run("ansible-runner is-alive #{base_dir} --json -i result").exit_status.zero?
end

# Stops the running Ansible job
def stop
AwesomeSpawn.run("ansible-runner stop #{base_dir} --json -i result")
end

# @return [Ansible::Runner::Response, NilClass] Response object with all details about the Ansible run, or nil
# if the Ansible is still running
def response
return if running?
return @response if @response

@response = Ansible::Runner::Response.new(:base_dir => base_dir, :ident => ident)
@response.cleanup_filesystem!

@response
end

# Dumps the Ansible::Runner::ResponseAsync into the hash
#
# @return [Hash] Dumped Ansible::Runner::ResponseAsync object
def dump
{
:base_dir => base_dir,
:ident => ident
}
end

# Creates the Ansible::Runner::ResponseAsync object from hash data
#
# @param [Hash] Dumped Ansible::Runner::ResponseAsync object
# @return [Ansible::Runner::ResponseAsync] Ansible::Runner::ResponseAsync Object created from hash data
def self.load(kwargs)
new(kwargs)
end
end
end
end

0 comments on commit 12b109c

Please sign in to comment.