From a12d80d353187079bb00666b8d8e6127f74a1631 Mon Sep 17 00:00:00 2001 From: Ladislav Smola Date: Thu, 26 Jul 2018 15:14:05 +0200 Subject: [PATCH 1/5] Add a possibility to start async ansible-runner Add a possibility to start async ansible-runner --- lib/ansible/runner.rb | 68 +++++++++++++++++++++++++--- lib/ansible/runner/response_async.rb | 46 +++++++++++++++++++ 2 files changed, 107 insertions(+), 7 deletions(-) create mode 100644 lib/ansible/runner/response_async.rb diff --git a/lib/ansible/runner.rb b/lib/ansible/runner.rb index 4a49ad55c0b..2ab31876baf 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,28 @@ 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)) + 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 + + 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(:return_code => return_code(base_dir), :stdout => result.output, :stderr => result.error) end end + 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 +204,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 @@ -211,8 +265,8 @@ def assert_path!(path) end # @return [String] ansible_runner executable command - def ansible_command(base_dir) - "ansible-runner run #{base_dir} --json -i result --hosts localhost" + 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_async.rb b/lib/ansible/runner/response_async.rb new file mode 100644 index 00000000000..2c14109d6a7 --- /dev/null +++ b/lib/ansible/runner/response_async.rb @@ -0,0 +1,46 @@ +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. sWe use unique base dir per run, so this idrntifier 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? + + end + + # @return [Ansible::Runner::Response] Response object with all details about the ansible run + def response + response = nil # TODO(lsmola) add Ansible::Runner::Response.new(...) + + FileUtils.remove_entry(base_dir) # Clean up the temp dir, when the response is generated + + response + end + + def dump + { + :base_dir => base_dir, + :ident => ident + } + end + + def self.load(kwargs) + self.new(kwargs) + end + end + end +end From b285902e8e1b3c4d54e939f5e67aec12455c8b4a Mon Sep 17 00:00:00 2001 From: Ladislav Smola Date: Thu, 26 Jul 2018 15:58:32 +0200 Subject: [PATCH 2/5] Make Response object work also for the async run Make Response object work also for the async run --- lib/ansible/runner.rb | 16 +++--------- lib/ansible/runner/response.rb | 39 +++++++++++++++++++++++++--- lib/ansible/runner/response_async.rb | 11 +++++--- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/lib/ansible/runner.rb b/lib/ansible/runner.rb index 2ab31876baf..53d9c76322d 100644 --- a/lib/ansible/runner.rb +++ b/lib/ansible/runner.rb @@ -157,9 +157,9 @@ 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(:return_code => return_code(base_dir), - :stdout => result.output, - :stderr => result.error) + Ansible::Runner::Response.new(:base_dir => base_dir, + :stdout => result.output, + :stderr => result.error) end end @@ -220,16 +220,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 diff --git a/lib/ansible/runner/response.rb b/lib/ansible/runner/response.rb index 33b5bfb8b51..298a1555ef2 100644 --- a/lib/ansible/runner/response.rb +++ b/lib/ansible/runner/response.rb @@ -3,20 +3,36 @@ 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 + def return_code + @return_code ||= load_return_code + end + + def stdout + @stdout ||= load_stdout + end + + def parsed_stdout + @parsed_stdout ||= parse_stdout(stdout) + end + private # Parses stdout to array of hashes @@ -40,6 +56,23 @@ 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 + + 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 index 2c14109d6a7..85881cf651f 100644 --- a/lib/ansible/runner/response_async.rb +++ b/lib/ansible/runner/response_async.rb @@ -19,16 +19,19 @@ def initialize(base_dir:, ident: "result") # @return [Boolean] true if the ansible job is still running, false when it's finished def running? - + false # TODO(lsmola) can't get running? from ansible-runner https://github.com/ansible/ansible-runner/issues/99 end # @return [Ansible::Runner::Response] Response object with all details about the ansible run def response - response = nil # TODO(lsmola) add Ansible::Runner::Response.new(...) + return if running? + return @response if @response + + @response = Ansible::Runner::Response.new(:base_dir => base_dir, :ident => ident) FileUtils.remove_entry(base_dir) # Clean up the temp dir, when the response is generated - response + @response end def dump @@ -39,7 +42,7 @@ def dump end def self.load(kwargs) - self.new(kwargs) + new(kwargs) end end end From ab7eb28dee5476172224bb9cd26d9982655afbf3 Mon Sep 17 00:00:00 2001 From: Ladislav Smola Date: Fri, 27 Jul 2018 14:41:44 +0200 Subject: [PATCH 3/5] Implement stop and running? methods with proper response and cleanup Implement stop and running? methods with proper response and cleanup --- lib/ansible/runner/response.rb | 8 ++++++++ lib/ansible/runner/response_async.rb | 20 ++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/ansible/runner/response.rb b/lib/ansible/runner/response.rb index 298a1555ef2..e8f9148dcfe 100644 --- a/lib/ansible/runner/response.rb +++ b/lib/ansible/runner/response.rb @@ -33,6 +33,14 @@ def parsed_stdout @parsed_stdout ||= parse_stdout(stdout) end + 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 diff --git a/lib/ansible/runner/response_async.rb b/lib/ansible/runner/response_async.rb index 85881cf651f..13a3e9bf402 100644 --- a/lib/ansible/runner/response_async.rb +++ b/lib/ansible/runner/response_async.rb @@ -19,21 +19,29 @@ def initialize(base_dir:, ident: "result") # @return [Boolean] true if the ansible job is still running, false when it's finished def running? - false # TODO(lsmola) can't get running? from ansible-runner https://github.com/ansible/ansible-runner/issues/99 + AwesomeSpawn.run("ansible-runner is-alive #{base_dir} --json -i result").exit_status.zero? end - # @return [Ansible::Runner::Response] Response object with all details about the ansible run + # 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) - - FileUtils.remove_entry(base_dir) # Clean up the temp dir, when the response is generated + @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, @@ -41,6 +49,10 @@ def dump } 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 From 0b86e0d9e7382ccf0345c17aef98a44bfab5ac1b Mon Sep 17 00:00:00 2001 From: Ladislav Smola Date: Fri, 27 Jul 2018 15:29:41 +0200 Subject: [PATCH 4/5] Add missing YARD docs Add missing YARD docs --- lib/ansible/runner.rb | 14 +++++++++++++- lib/ansible/runner/response.rb | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/ansible/runner.rb b/lib/ansible/runner.rb index 53d9c76322d..43674395197 100644 --- a/lib/ansible/runner.rb +++ b/lib/ansible/runner.rb @@ -153,6 +153,12 @@ def run_via_cli(env_vars, extra_vars, playbook_path: nil, role_name: nil, roles_ 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) @@ -163,6 +169,7 @@ def response(base_dir, ansible_runner_method, result) end end + # @return [Bollean] True if ansible-runner will run on background def async?(ansible_runner_method) ansible_runner_method == "start" end @@ -254,7 +261,12 @@ def assert_path!(path) end end - # @return [String] ansible_runner executable command + # 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 diff --git a/lib/ansible/runner/response.rb b/lib/ansible/runner/response.rb index e8f9148dcfe..d13e86e397d 100644 --- a/lib/ansible/runner/response.rb +++ b/lib/ansible/runner/response.rb @@ -21,18 +21,22 @@ def initialize(base_dir:, return_code: nil, stdout: nil, stderr: nil, ident: "re @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 @@ -75,6 +79,7 @@ def load_return_code 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 From c6571ea45f83940b7311b5a130e567f2c17b854a Mon Sep 17 00:00:00 2001 From: Ladislav Smola Date: Fri, 27 Jul 2018 15:44:48 +0200 Subject: [PATCH 5/5] Fix YARD typos Fix YARD typos --- lib/ansible/runner/response_async.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ansible/runner/response_async.rb b/lib/ansible/runner/response_async.rb index 13a3e9bf402..47a25045cef 100644 --- a/lib/ansible/runner/response_async.rb +++ b/lib/ansible/runner/response_async.rb @@ -10,7 +10,7 @@ class ResponseAsync # @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. sWe use unique base dir per run, so this idrntifier can be static for + # 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