Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ansible runner async method #17763

Merged
merged 5 commits into from
Jul 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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