diff --git a/lib/ansible/runner.rb b/lib/ansible/runner.rb index 4a49ad55c0b..43674395197 100644 --- a/lib/ansible/runner.rb +++ b/lib/ansible/runner.rb @@ -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 @@ -95,11 +131,13 @@ 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, @@ -107,12 +145,35 @@ def run_via_cli(env_vars, extra_vars, playbook_path: nil, role_name: nil, roles_ :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 @@ -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 @@ -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 @@ -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 diff --git a/lib/ansible/runner/response.rb b/lib/ansible/runner/response.rb index 33b5bfb8b51..d13e86e397d 100644 --- a/lib/ansible/runner/response.rb +++ b/lib/ansible/runner/response.rb @@ -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] 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 @@ -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 diff --git a/lib/ansible/runner/response_async.rb b/lib/ansible/runner/response_async.rb new file mode 100644 index 00000000000..47a25045cef --- /dev/null +++ b/lib/ansible/runner/response_async.rb @@ -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