From 7af794d7694dfdc2ccd80d96990e67b19cf57171 Mon Sep 17 00:00:00 2001 From: Boris Odnopozov Date: Thu, 18 Oct 2018 16:43:24 +0300 Subject: [PATCH] Add support for using run_role_async Add the option to use the run_role_async using AnsibleWorkflow --- .../providers/ansible_playbook_workflow.rb | 44 +++++ .../providers/ansible_role_workflow.rb | 46 +++++ .../providers/ansible_runner_workflow.rb | 47 ++--- ...c.rb => ansible_playbook_workflow_spec.rb} | 6 +- .../providers/ansible_role_workflow_spec.rb | 174 ++++++++++++++++++ 5 files changed, 281 insertions(+), 36 deletions(-) create mode 100644 app/models/manageiq/providers/ansible_playbook_workflow.rb create mode 100644 app/models/manageiq/providers/ansible_role_workflow.rb rename spec/models/manageiq/providers/{ansible_runner_workflow_spec.rb => ansible_playbook_workflow_spec.rb} (95%) create mode 100644 spec/models/manageiq/providers/ansible_role_workflow_spec.rb diff --git a/app/models/manageiq/providers/ansible_playbook_workflow.rb b/app/models/manageiq/providers/ansible_playbook_workflow.rb new file mode 100644 index 00000000000..beabef8726c --- /dev/null +++ b/app/models/manageiq/providers/ansible_playbook_workflow.rb @@ -0,0 +1,44 @@ +class ManageIQ::Providers::AnsiblePlaybookWorkflow < ManageIQ::Providers::AnsibleRunnerWorkflow + def self.job_options(env_vars, extra_vars, playbook_options, timeout, poll_interval) + { + :env_vars => env_vars, + :extra_vars => extra_vars, + :playbook_path => playbook_options[:playbook_path], + :timeout => timeout, + :poll_interval => poll_interval, + } + end + + def pre_playbook + # A step before running the playbook for any optional setup tasks + queue_signal(:run_playbook) + end + + def run_playbook + env_vars, extra_vars, playbook_path = options.values_at(:env_vars, :extra_vars, :playbook_path) + + response = Ansible::Runner.run_async(env_vars, extra_vars, playbook_path) + if response.nil? + queue_signal(:abort, "Failed to run ansible playbook", "error") + else + context[:ansible_runner_response] = response.dump + + started_on = Time.now.utc + update_attributes!(:context => context, :started_on => started_on) + miq_task.update_attributes!(:started_on => started_on) + + queue_signal(:poll_runner) + end + end + + def load_transitions + super.tap do |transactions| + transactions.merge!( + :start => {'waiting_to_start' => 'pre_playbook'}, + :run_playbook => {'pre_playbook' => 'running'}, + ) + end + end + + alias start pre_playbook +end diff --git a/app/models/manageiq/providers/ansible_role_workflow.rb b/app/models/manageiq/providers/ansible_role_workflow.rb new file mode 100644 index 00000000000..79b40f3a219 --- /dev/null +++ b/app/models/manageiq/providers/ansible_role_workflow.rb @@ -0,0 +1,46 @@ +class ManageIQ::Providers::AnsibleRoleWorkflow < ManageIQ::Providers::AnsibleRunnerWorkflow + def self.job_options(env_vars, extra_vars, role_options, timeout, poll_interval) + { + :env_vars => env_vars, + :extra_vars => extra_vars, + :role_name => role_options[:role_name], + :roles_path => role_options[:roles_path], + :role_skip_facts => role_options[:role_skip_facts], + :timeout => timeout, + :poll_interval => poll_interval + } + end + + def pre_role + # A step before running the playbook for any optional setup tasks + queue_signal(:run_role) + end + + def run_role + env_vars, extra_vars, role_name, roles_path, role_skip_facts = options.values_at(:env_vars, :extra_vars, :role_name, :roles_path, :role_skip_facts) + role_skip_facts = true if role_skip_facts.nil? + response = Ansible::Runner.run_role_async(env_vars, extra_vars, role_name, :roles_path => roles_path, :role_skip_facts => role_skip_facts) + if response.nil? + queue_signal(:abort, "Failed to run ansible role", "error") + else + context[:ansible_runner_response] = response.dump + + started_on = Time.now.utc + update_attributes!(:context => context, :started_on => started_on) + miq_task.update_attributes!(:started_on => started_on) + + queue_signal(:poll_runner) + end + end + + def load_transitions + super.tap do |transactions| + transactions.merge!( + :start => {'waiting_to_start' => 'pre_role'}, + :run_role => {'pre_role' => 'running' }, + ) + end + end + + alias start pre_role +end diff --git a/app/models/manageiq/providers/ansible_runner_workflow.rb b/app/models/manageiq/providers/ansible_runner_workflow.rb index f5ddf6ab84e..5ceffb845c6 100644 --- a/app/models/manageiq/providers/ansible_runner_workflow.rb +++ b/app/models/manageiq/providers/ansible_runner_workflow.rb @@ -1,36 +1,14 @@ class ManageIQ::Providers::AnsibleRunnerWorkflow < Job - def self.create_job(env_vars, extra_vars, playbook_path, timeout: 1.hour, poll_interval: 1.second) - options = { - :env_vars => env_vars, - :extra_vars => extra_vars, - :playbook_path => playbook_path, - :timeout => timeout, - :poll_interval => poll_interval, - } - - super(name, options) + def self.create_job(env_vars, extra_vars, role_or_playbook_options, timeout: 1.hour, poll_interval: 1.second) + super(name, job_options(env_vars, extra_vars, role_or_playbook_options, timeout, poll_interval)) end - def pre_playbook - # A step before running the playbook for any optional setup tasks - queue_signal(:run_playbook) + def current_job_timeout(_timeout_adjustment = 1) + options[:timeout] || super end - def run_playbook - env_vars, extra_vars, playbook_path = options.values_at(:env_vars, :extra_vars, :playbook_path) - - response = Ansible::Runner.run_async(env_vars, extra_vars, playbook_path) - if response.nil? - queue_signal(:abort, "Failed to run ansible playbook", "error") - else - context[:ansible_runner_response] = response.dump - - started_on = Time.now.utc - update_attributes!(:context => context, :started_on => started_on) - miq_task.update_attributes!(:started_on => started_on) - - queue_signal(:poll_runner) - end + def job_options(options) + raise(NotImplementedError, 'abstract') end def poll_runner @@ -52,19 +30,24 @@ def poll_runner if result.return_code != 0 set_status("Playbook failed", "error") _log.warn("Playbook failed:\n#{result.parsed_stdout.join("\n")}") + else + set_status("Playbook completed with no errors", "ok") end - queue_signal(:post_playbook) end end def post_playbook # A step after running the playbook for any optional cleanup tasks - queue_signal(:finish) + queue_signal(:finish, message, status) + end + + def fail_unimplamented + raise(NotImplementedError, "this is an abstract class, use a subclass that implaments a 'start' method") end alias initializing dispatch_start - alias start pre_playbook + alias start fail_unimplamented alias finish process_finished alias abort_job process_abort alias cancel process_cancel @@ -99,8 +82,6 @@ def load_transitions { :initializing => {'initialize' => 'waiting_to_start'}, - :start => {'waiting_to_start' => 'pre_playbook'}, - :run_playbook => {'pre_playbook' => 'running'}, :poll_runner => {'running' => 'running'}, :post_playbook => {'running' => 'post_playbook'}, :finish => {'*' => 'finished'}, diff --git a/spec/models/manageiq/providers/ansible_runner_workflow_spec.rb b/spec/models/manageiq/providers/ansible_playbook_workflow_spec.rb similarity index 95% rename from spec/models/manageiq/providers/ansible_runner_workflow_spec.rb rename to spec/models/manageiq/providers/ansible_playbook_workflow_spec.rb index 3fa0860bf32..19ce99ec525 100644 --- a/spec/models/manageiq/providers/ansible_runner_workflow_spec.rb +++ b/spec/models/manageiq/providers/ansible_playbook_workflow_spec.rb @@ -1,6 +1,6 @@ -describe ManageIQ::Providers::AnsibleRunnerWorkflow do +describe ManageIQ::Providers::AnsiblePlaybookWorkflow do let(:job) { described_class.create_job(*options).tap { |job| job.state = state } } - let(:options) { [{"ENV" => "VAR"}, %w(arg1 arg2), "/path/to/playbook"] } + let(:options) { [{"ENV" => "VAR"}, %w(arg1 arg2), {:playbook_path => "/path/to/playbook"}] } let(:state) { "waiting_to_start" } context ".create_job" do @@ -151,7 +151,7 @@ end context ".deliver_on" do - let(:options) { [{"ENV" => "VAR"}, %w(arg1 arg2), "/path/to/playbook", :poll_interval => 5.minutes] } + let(:options) { [{"ENV" => "VAR"}, %w(arg1 arg2), {:playbook_path => "/path/to/playbook"}, :poll_interval => 5.minutes] } it "uses the option to queue poll_runner" do now = Time.now.utc diff --git a/spec/models/manageiq/providers/ansible_role_workflow_spec.rb b/spec/models/manageiq/providers/ansible_role_workflow_spec.rb new file mode 100644 index 00000000000..6ccbe19afc5 --- /dev/null +++ b/spec/models/manageiq/providers/ansible_role_workflow_spec.rb @@ -0,0 +1,174 @@ +describe ManageIQ::Providers::AnsibleRoleWorkflow do + let(:job) { described_class.create_job(*options).tap { |job| job.state = state } } + let(:role_options) { {:role_name => 'role_name', :roles_path => 'path/role', :role_skip_facts => true } } + let(:options) { [{"ENV" => "VAR"}, %w(arg1 arg2), role_options] } + let(:state) { "waiting_to_start" } + + context ".create_job" do + it "leaves job waiting to start" do + expect(job.state).to eq("waiting_to_start") + end + end + + context ".signal" do + %w(start pre_role run_role poll_runner post_playbook finish abort_job cancel error).each do |signal| + shared_examples_for "allows #{signal} signal" do + it signal.to_s do + expect(job).to receive(signal.to_sym) + job.signal(signal.to_sym) + end + end + end + + %w(start pre_role run_role poll_runner post_playbook).each do |signal| + shared_examples_for "doesn't allow #{signal} signal" do + it signal.to_s do + expect { job.signal(signal.to_sym) }.to raise_error(RuntimeError, /#{signal} is not permitted at state #{job.state}/) + end + end + end + + context "waiting_to_start" do + let(:state) { "waiting_to_start" } + + it_behaves_like "allows start signal" + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + + it_behaves_like "doesn't allow run_role signal" + it_behaves_like "doesn't allow poll_runner signal" + it_behaves_like "doesn't allow post_playbook signal" + end + + context "per_role" do + let(:state) { "pre_role" } + + it_behaves_like "allows run_role signal" + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + + it_behaves_like "doesn't allow start signal" + it_behaves_like "doesn't allow poll_runner signal" + it_behaves_like "doesn't allow post_playbook signal" + end + + context "running" do + let(:state) { "running" } + + it_behaves_like "allows poll_runner signal" + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + + it_behaves_like "doesn't allow start signal" + it_behaves_like "doesn't allow pre_role signal" + end + + context "post_playbook" do + let(:state) { "post_playbook" } + + it_behaves_like "allows finish signal" + it_behaves_like "allows abort_job signal" + it_behaves_like "allows cancel signal" + it_behaves_like "allows error signal" + + it_behaves_like "doesn't allow start signal" + it_behaves_like "doesn't allow pre_role signal" + it_behaves_like "doesn't allow run_role signal" + it_behaves_like "doesn't allow poll_runner signal" + it_behaves_like "doesn't allow post_playbook signal" + end + end + + context ".run_role" do + let(:state) { "pre_role" } + let(:response_async) { Ansible::Runner::ResponseAsync.new(:base_dir => "/path/to/results") } + + it "ansible-runner succeeds" do + response_async = Ansible::Runner::ResponseAsync.new(:base_dir => "/path/to/results") + + expect(Ansible::Runner).to receive(:run_role_async) + .with({"ENV"=>"VAR"}, %w(arg1 arg2), "role_name", :roles_path => "path/role", :role_skip_facts => true).and_return(response_async) + expect(job).to receive(:queue_signal).with(:poll_runner) + + job.signal(:run_role) + + expect(job.context[:ansible_runner_response]).to eq(response_async.dump) + end + + it "ansible-runner fails" do + expect(Ansible::Runner).to receive(:run_role_async).and_return(nil) + expect(job).to receive(:queue_signal).with(:abort, "Failed to run ansible role", "error") + + job.signal(:run_role) + end + end + + context "#current_job_timeout" do + it "sets the job current timeout" do + expect(job.current_job_timeout).to eq(1.hour) + end + end + + context ".poll_runner" do + let(:state) { "running" } + let(:response_async) { Ansible::Runner::ResponseAsync.new(:base_dir => "/path/to/results") } + + before do + allow(Ansible::Runner::ResponseAsync).to receive(:new).and_return(response_async) + + job.context[:ansible_runner_response] = response_async.dump + job.started_on = Time.now.utc + job.save! + end + + it "ansible-runner completed" do + expect(response_async).to receive(:running?).and_return(false) + + response = Ansible::Runner::Response.new(response_async.dump.merge(:return_code => 0)) + expect(response_async).to receive(:response).and_return(response) + expect(job).to receive(:queue_signal).with(:post_playbook) + + job.signal(:poll_runner) + end + + it "ansible-runner still running" do + now = Time.now.utc + allow(Time).to receive(:now).and_return(now) + expect(response_async).to receive(:running?).and_return(true) + expect(job).to receive(:queue_signal).with(:poll_runner, :deliver_on => now + 1.second) + + job.signal(:poll_runner) + end + + it "fails if the playbook has been running too long" do + time = job.started_on + job.options[:timeout] + 5.minutes + + Timecop.travel(time) do + expect(response_async).to receive(:running?).and_return(true) + expect(response_async).to receive(:stop) + expect(job).to receive(:queue_signal).with(:abort, "Playbook has been running longer than timeout", "error") + + job.signal(:poll_runner) + end + end + + context ".deliver_on" do + let(:options) { [{"ENV" => "VAR"}, %w(arg1 arg2), {:playbook_path => "/path/to/playbook"}, :poll_interval => 5.minutes] } + + it "uses the option to queue poll_runner" do + now = Time.now.utc + allow(Time).to receive(:now).and_return(now) + expect(response_async).to receive(:running?).and_return(true) + expect(job).to receive(:queue_signal).with(:poll_runner, :deliver_on => now + 5.minutes) + + job.signal(:poll_runner) + end + end + end +end