Skip to content

Commit

Permalink
Cleanup Ansible::Runner
Browse files Browse the repository at this point in the history
The main cleanup in this commit is to properly leverage AwesomeSpawn's
params instead of manually building parts of the command line. Additionally,
this fixes a bug with the :cmdline substitution when an extra_var has a "'"
in it.

Additionally, parameter validation has been lumped together to raise a
single error to the caller, instead of one-by-one, and it also raises
the more specific ArgumentError.
  • Loading branch information
Fryguy committed Aug 6, 2018
1 parent 97b4735 commit 7202f09
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 114 deletions.
162 changes: 50 additions & 112 deletions lib/ansible/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class << self
def run_async(env_vars, extra_vars, playbook_path)
run_via_cli(env_vars,
extra_vars,
:playbook_path => playbook_path,
:ansible_runner_method => "start")
:ansible_runner_method => "start",
:playbook => playbook_path)
end

# Runs a role directly via ansible-runner, a simple playbook is then automatically created,
Expand All @@ -31,10 +31,10 @@ def run_async(env_vars, extra_vars, playbook_path)
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,
:ansible_runner_method => "start",
:role => role_name,
:roles_path => roles_path,
:role_skip_facts => role_skip_facts,
:ansible_runner_method => "start")
:role_skip_facts => role_skip_facts)
end

# Runs a playbook via ansible-runner, see: https://ansible-runner.readthedocs.io/en/latest/standalone.html#running-playbooks
Expand All @@ -45,7 +45,9 @@ def run_role_async(env_vars, extra_vars, role_name, roles_path:, role_skip_facts
# @param playbook_path [String] Path to the playbook we will want to run
# @return [Ansible::Runner::Response] Response object with all details about the ansible run
def run(env_vars, extra_vars, playbook_path)
run_via_cli(env_vars, extra_vars, :playbook_path => playbook_path)
run_via_cli(env_vars,
extra_vars,
:playbook => playbook_path)
end

# Runs a role directly via ansible-runner, a simple playbook is then automatically created,
Expand All @@ -60,7 +62,11 @@ def run(env_vars, extra_vars, playbook_path)
# playbook. True by default.
# @return [Ansible::Runner::Response] Response object with all details about the ansible run
def run_role(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)
run_via_cli(env_vars,
extra_vars,
:role => role_name,
:roles_path => roles_path,
:role_skip_facts => role_skip_facts)
end

# Runs "run" method via queue
Expand Down Expand Up @@ -89,9 +95,7 @@ def run_queue(env_vars, extra_vars, playbook_path, user_id, queue_opts)
# playbook. True by default.
# @return [BigInt] ID of MiqTask record wrapping the task
def run_role_queue(env_vars, extra_vars, role_name, user_id, queue_opts, roles_path:, role_skip_facts: true)
run_in_queue("run_role",
user_id,
queue_opts,
run_in_queue("run_role", user_id, queue_opts,
[env_vars, extra_vars, role_name, {:roles_path => roles_path, :role_skip_facts => role_skip_facts}])
end

Expand Down Expand Up @@ -125,31 +129,23 @@ def run_in_queue(method_name, user_id, queue_opts, args)
# @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
# @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.
# @param ansible_runner_method [String] Optional method we will use to run the ansible-runner. It can be either
# "run", which is sync call, or "start" which is async call. Default is "run"
# @param playbook_or_role_args [Hash] Hash that includes the :playbook key or :role keys
# @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,
ansible_runner_method: "run")
validate_params!(env_vars, extra_vars, playbook_path, roles_path)
def run_via_cli(env_vars, extra_vars, ansible_runner_method: "run", **playbook_or_role_args)
validate_params!(env_vars, extra_vars, ansible_runner_method, playbook_or_role_args)

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))
params = runner_params(base_dir, ansible_runner_method, extra_vars, playbook_or_role_args)

begin
result = AwesomeSpawn.run("ansible-runner", :env => env_vars, :params => params)
res = 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.
res.cleanup_filesystem! unless async?(ansible_runner_method)
res&.cleanup_filesystem! unless async?(ansible_runner_method)
end
end

Expand All @@ -169,106 +165,48 @@ def response(base_dir, ansible_runner_method, result)
end
end

# @return [Bollean] True if ansible-runner will run on background
# @return [Boolean] 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
# @param playbook_path [String] Path to the playbook we will want to 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.
# @return [Array] Arguments for the ansible-runner run
def params(extra_vars:, playbook_path:, role_name:, roles_path:, role_skip_facts:)
role_or_playbook_params = if playbook_path
playbook_params(:playbook_path => playbook_path)
elsif role_name
role_params(:role_name => role_name,
:roles_path => roles_path,
:role_skip_facts => role_skip_facts)
end
def runner_params(base_dir, ansible_runner_method, extra_vars, playbook_or_role_args)
runner_args = playbook_or_role_args.dup

[shared_params(:extra_vars => extra_vars).merge(role_or_playbook_params)]
end
runner_args.delete(:roles_path) if runner_args[:roles_path].nil?
skip_facts = runner_args.delete(:role_skip_facts)
runner_args[:role_skip_facts] = nil if skip_facts

# Generate correct params if using :playbook_path
#
# @param playbook_path [String] Path to the playbook we will want to run
# @return [Hash] Partial arguments for the ansible-runner run
def playbook_params(playbook_path:)
{:playbook => playbook_path}
end
runner_args.merge!(
:ident => "result",
:hosts => "localhost"
)
runner_args[:cmdline] = AwesomeSpawn.build_command_line(nil, [{:extra_vars => extra_vars.to_json}]).lstrip if extra_vars.any?

# Generate correct params if using :role_name
#
# @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.
# @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[:"roles-path"] = roles_path if roles_path
role_params[:"role-skip-facts"] = nil if role_skip_facts

role_params
end

# Generate correct params shared by :playbook_path or a :role_name paths
#
# @param extra_vars [Hash] Hash with key/value pairs that will be passed as extra_vars to the ansible-runner run
# @return [Hash] Partial arguments for the ansible-runner run
def shared_params(extra_vars:)
{:cmdline => "--extra-vars '#{JSON.dump(extra_vars)}'"}
[ansible_runner_method, base_dir, :json, runner_args]
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
# 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
# @param roles_path [String] Path to the directory with roles
def validate_params!(env_vars, extra_vars, playbook_path, roles_path)
assert_hash!(env_vars)
assert_hash!(extra_vars)
assert_path!(playbook_path)
assert_path!(roles_path)
end
def validate_params!(env_vars, extra_vars, ansible_runner_method, playbook_or_role_args)
errors = []

# Asserts that passed argument is a hash
#
# @param hash [Hash] Passed argument we test for being Hash
def assert_hash!(hash)
unless hash.kind_of?(Hash)
raise "Passed parameter must be of type Hash, got: #{hash}"
end
end
errors << "env_vars must be a Hash, got: #{hash.class}" unless env_vars.kind_of?(Hash)
errors << "extra_vars must be a Hash, got: #{hash.class}" unless extra_vars.kind_of?(Hash)

# Asserts that passed path exists
#
# @param path [Hash, NilClass] Passed path we test for existence
def assert_path!(path)
return unless path
unless %w(run start).include?(ansible_runner_method.to_s)
errors << "ansible_runner_method must be 'run' or 'start'"
end

unless File.exist?(path)
raise "File doesn't exist: #{path}"
unless playbook_or_role_args.keys == [:playbook] || playbook_or_role_args.keys.sort == [:role, :role_skip_facts, :roles_path]
errors << "Unexpected playbook/role args: #{args}"
end
end

# 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"
playbook = playbook_or_role_args[:playbook]
errors << "playbook path doesn't exist: #{playbook}" if playbook && !File.exist?(playbook)
role = playbook_or_role_args[:role]
errors << "role path doesn't exist: #{role}" if role && !File.exist?(role)

raise ArgumentError, errors.join("; ") if errors.any?
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/ansible/runner/response_async.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ def initialize(base_dir:, ident: "result")

# @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?
AwesomeSpawn.run("ansible-runner", :params => ["is-alive", base_dir, :json, {:ident => "result"}]).success?
end

# Stops the running Ansible job
def stop
AwesomeSpawn.run("ansible-runner stop #{base_dir} --json -i result")
AwesomeSpawn.run("ansible-runner", :params => ["stop", base_dir, :json, {:ident => "result"}])
end

# @return [Ansible::Runner::Response, NilClass] Response object with all details about the Ansible run, or nil
Expand Down

0 comments on commit 7202f09

Please sign in to comment.