From 34847034ac757f32b652fb96bf551ceb5926d73e Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 16 Mar 2017 15:33:24 -0400 Subject: [PATCH 1/4] Make EmbeddedAnsible.api_connection public for the worker --- lib/embedded_ansible.rb | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/embedded_ansible.rb b/lib/embedded_ansible.rb index 42ac36c13dc..dff4d7a6ca0 100644 --- a/lib/embedded_ansible.rb +++ b/lib/embedded_ansible.rb @@ -64,6 +64,15 @@ def self.services AwesomeSpawn.run!("source /etc/sysconfig/ansible-tower; echo $TOWER_SERVICES").output.split end + def self.api_connection + admin_auth = miq_database.ansible_admin_authentication + AnsibleTowerClient::Connection.new( + :base_url => URI::HTTP.build(:host => "localhost", :path => "/api/v1", :port => HTTP_PORT).to_s, + :username => admin_auth.userid, + :password => admin_auth.password + ) + end + def self.run_setup_script(exclude_tags) json_extra_vars = { :minimum_var_space => 0, @@ -171,14 +180,4 @@ def self.database_connection ActiveRecord::Base.connection end private_class_method :database_connection - - def self.api_connection - admin_auth = miq_database.ansible_admin_authentication - AnsibleTowerClient::Connection.new( - :base_url => URI::HTTP.build(:host => "localhost", :path => "/api/v1", :port => HTTP_PORT).to_s, - :username => admin_auth.userid, - :password => admin_auth.password - ) - end - private_class_method :api_connection end From 8322c6635b38ee5b4c9e3543b9b584d6f783823a Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 16 Mar 2017 16:31:31 -0400 Subject: [PATCH 2/4] Fix up the initial embedded ansible environment from the EmbeddedAnsibleWorker::Runner This creates a concern around creating and removing objects in the embedded ansible instance. In particular we remove all of the demo data that is initialized for us during setup and we create just the objects that we need and save the ids to the provider. --- app/models/embedded_ansible_worker.rb | 1 + .../object_management.rb | 56 +++++++ app/models/embedded_ansible_worker/runner.rb | 4 + spec/models/embedded_ansible_worker_spec.rb | 138 ++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 app/models/embedded_ansible_worker/object_management.rb create mode 100644 spec/models/embedded_ansible_worker_spec.rb diff --git a/app/models/embedded_ansible_worker.rb b/app/models/embedded_ansible_worker.rb index 54fa385a6f0..e30aa34f019 100644 --- a/app/models/embedded_ansible_worker.rb +++ b/app/models/embedded_ansible_worker.rb @@ -1,5 +1,6 @@ class EmbeddedAnsibleWorker < MiqWorker require_nested :Runner + include_concern 'ObjectManagement' self.required_roles = ['embedded_ansible'] diff --git a/app/models/embedded_ansible_worker/object_management.rb b/app/models/embedded_ansible_worker/object_management.rb new file mode 100644 index 00000000000..800c24f53f5 --- /dev/null +++ b/app/models/embedded_ansible_worker/object_management.rb @@ -0,0 +1,56 @@ +module EmbeddedAnsibleWorker::ObjectManagement + extend ActiveSupport::Concern + + def ensure_initial_objects(provider, connection) + ensure_organization(provider, connection) + ensure_credential(provider, connection) + ensure_inventory(provider, connection) + ensure_host(provider, connection) + end + + def remove_demo_data(connection) + connection.api.credentials.all(:name => "Demo Credential").each(&:destroy!) + connection.api.inventories.all(:name => "Demo Inventory").each(&:destroy!) + connection.api.job_templates.all(:name => "Demo Job Template").each(&:destroy!) + connection.api.projects.all(:name => "Demo Project").each(&:destroy!) + connection.api.organizations.all(:name => "Default").each(&:destroy!) + end + + def ensure_organization(provider, connection) + return if provider.default_organization + + provider.default_organization = connection.api.organizations.create!( + :name => I18n.t("product.name"), + :description => "#{I18n.t("product.name")} Default Organization" + ).id + end + + def ensure_credential(provider, connection) + return if provider.default_credential + + provider.default_credential = connection.api.credentials.create!( + :name => "#{I18n.t("product.name")} Default Credential", + :kind => "ssh", + :organization => provider.default_organization + ).id + end + + def ensure_inventory(provider, connection) + return if provider.default_inventory + + provider.default_inventory = connection.api.inventories.create!( + :name => "#{I18n.t("product.name")} Default Inventory", + :organization => provider.default_organization + ).id + end + + def ensure_host(provider, connection) + return if provider.default_host + + provider.default_host = connection.api.hosts.create!( + :name => "localhost", + :inventory => provider.default_inventory, + :variables => {'ansible_connection' => "local"}.to_yaml + ).id + end +end diff --git a/app/models/embedded_ansible_worker/runner.rb b/app/models/embedded_ansible_worker/runner.rb index 089276b6a23..bdc917faa8c 100644 --- a/app/models/embedded_ansible_worker/runner.rb +++ b/app/models/embedded_ansible_worker/runner.rb @@ -49,6 +49,10 @@ def update_embedded_ansible_provider provider.save! + api_connection = EmbeddedAnsible.api_connection + worker.remove_demo_data(api_connection) + worker.ensure_initial_objects(provider, api_connection) + admin_auth = MiqDatabase.first.ansible_admin_authentication provider.update_authentication(:default => {:userid => admin_auth.userid, :password => admin_auth.password}) diff --git a/spec/models/embedded_ansible_worker_spec.rb b/spec/models/embedded_ansible_worker_spec.rb new file mode 100644 index 00000000000..d28bf87cdd2 --- /dev/null +++ b/spec/models/embedded_ansible_worker_spec.rb @@ -0,0 +1,138 @@ +describe EmbeddedAnsibleWorker do + subject { FactoryGirl.create(:embedded_ansible_worker) } + + context "ObjectManagement concern" do + let(:provider) { FactoryGirl.create(:provider_embedded_ansible) } + let(:api_connection) { double("AnsibleAPIConnection", :api => tower_api) } + let(:tower_api) do + methods = { + :organizations => org_collection, + :credentials => cred_collection, + :inventories => inv_collection, + :hosts => host_collection, + :job_templates => job_templ_collection, + :projects => proj_collection + + } + double("TowerAPI", methods) + end + + let(:org_collection) { double("AnsibleOrgCollection", :all => [org_resource]) } + let(:cred_collection) { double("AnsibleCredCollection", :all => [cred_resource]) } + let(:inv_collection) { double("AnsibleInvCollection", :all => [inv_resource]) } + let(:host_collection) { double("AnsibleHostCollection", :all => [host_resource]) } + let(:job_templ_collection) { double("AnsibleJobTemplCollection", :all => [job_templ_resource]) } + let(:proj_collection) { double("AnsibleProjCollection", :all => [proj_resource]) } + + let(:org_resource) { double("AnsibleOrgResource", :id => 12) } + let(:cred_resource) { double("AnsibleCredResource", :id => 13) } + let(:inv_resource) { double("AnsibleInvResource", :id => 14) } + let(:host_resource) { double("AnsibleHostResource", :id => 15) } + let(:job_templ_resource) { double("AnsibleJobTemplResource", :id => 16) } + let(:proj_resource) { double("AnsibleProjResource", :id => 17) } + + describe "#ensure_initial_objects" do + it "creates the expected objects" do + expect(org_collection).to receive(:create!).and_return(org_resource) + expect(cred_collection).to receive(:create!).and_return(cred_resource) + expect(inv_collection).to receive(:create!).and_return(inv_resource) + expect(host_collection).to receive(:create!).and_return(host_resource) + + subject.ensure_initial_objects(provider, api_connection) + end + end + + describe "#remove_demo_data" do + it "removes the existing data" do + expect(org_resource).to receive(:destroy!) + expect(cred_resource).to receive(:destroy!) + expect(inv_resource).to receive(:destroy!) + expect(job_templ_resource).to receive(:destroy!) + expect(proj_resource).to receive(:destroy!) + + subject.remove_demo_data(api_connection) + end + end + + describe "#ensure_organization" do + it "sets the provider default organization" do + expect(org_collection).to receive(:create!).with( + :name => "ManageIQ", + :description => "ManageIQ Default Organization" + ).and_return(org_resource) + + subject.ensure_organization(provider, api_connection) + expect(provider.default_organization).to eq(12) + end + + it "doesn't recreate the organization if one is already set" do + provider.default_organization = 1 + expect(org_collection).not_to receive(:create!) + + subject.ensure_organization(provider, api_connection) + end + end + + describe "#ensure_credential" do + it "sets the provider default credential" do + provider.default_organization = 123 + expect(cred_collection).to receive(:create!).with( + :name => "ManageIQ Default Credential", + :kind => "ssh", + :organization => 123 + ).and_return(cred_resource) + + subject.ensure_credential(provider, api_connection) + expect(provider.default_credential).to eq(13) + end + + it "doesn't recreate the credential if one is already set" do + provider.default_credential = 2 + expect(cred_collection).not_to receive(:create!) + + subject.ensure_credential(provider, api_connection) + end + end + + describe "#ensure_inventory" do + it "sets the provider default inventory" do + provider.default_organization = 123 + expect(inv_collection).to receive(:create!).with( + :name => "ManageIQ Default Inventory", + :organization => 123 + ).and_return(inv_resource) + + subject.ensure_inventory(provider, api_connection) + expect(provider.default_inventory).to eq(14) + end + + it "doesn't recreate the inventory if one is already set" do + provider.default_inventory = 3 + expect(inv_collection).not_to receive(:create!) + + subject.ensure_inventory(provider, api_connection) + end + end + + describe "#ensure_host" do + it "sets the provider default host" do + provider.default_inventory = 234 + expect(host_collection).to receive(:create!).with( + :name => "localhost", + :inventory => 234, + :variables => "---\nansible_connection: local\n" + ).and_return(host_resource) + + subject.ensure_host(provider, api_connection) + expect(provider.default_host).to eq(15) + end + + it "doesn't recreate the host if one is already set" do + provider.default_host = 1 + expect(host_collection).not_to receive(:create!) + + subject.ensure_host(provider, api_connection) + end + end + end +end From f9ae5770da625d4867b3c8e712d4dd9351e3507b Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Fri, 17 Mar 2017 09:23:55 -0400 Subject: [PATCH 3/4] Correct runner specs for calling new worker methods --- .../models/embedded_ansible_worker/runner_spec.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/spec/models/embedded_ansible_worker/runner_spec.rb b/spec/models/embedded_ansible_worker/runner_spec.rb index e9d5f7beaea..f3c20f37727 100644 --- a/spec/models/embedded_ansible_worker/runner_spec.rb +++ b/spec/models/embedded_ansible_worker/runner_spec.rb @@ -10,7 +10,9 @@ let(:runner) { worker allow_any_instance_of(described_class).to receive(:worker_initialization) - described_class.new(:guid => worker_guid) + r = described_class.new(:guid => worker_guid) + allow(r).to receive(:worker).and_return(worker) + r } it "#do_before_work_loop exits on exceptions" do @@ -21,13 +23,20 @@ end context "#update_embedded_ansible_provider" do + let(:api_connection) { double("AnsibleAPIConnection") } before do EvmSpecHelper.local_guid_miq_server_zone MiqDatabase.seed MiqDatabase.first.set_ansible_admin_authentication(:password => "secret") + + allow(EmbeddedAnsible).to receive(:api_connection).and_return(api_connection) end it "creates initial" do + expect(worker).to receive(:remove_demo_data).with(api_connection) + expect(worker).to receive(:ensure_initial_objects) + .with(instance_of(ManageIQ::Providers::EmbeddedAnsible::Provider), api_connection) + runner.update_embedded_ansible_provider provider = ManageIQ::Providers::EmbeddedAnsible::Provider.first @@ -39,6 +48,10 @@ end it "updates existing" do + expect(worker).to receive(:remove_demo_data).twice.with(api_connection) + expect(worker).to receive(:ensure_initial_objects).twice + .with(instance_of(ManageIQ::Providers::EmbeddedAnsible::Provider), api_connection) + runner.update_embedded_ansible_provider new_zone = FactoryGirl.create(:zone) miq_server.update(:hostname => "boringserver", :zone => new_zone) From 5d2d5dfcb46a346d4e9c947761d6a22905a385ee Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Fri, 17 Mar 2017 16:29:31 -0400 Subject: [PATCH 4/4] Make sure EmbeddedAnsible.alive? == true after setup Without this we were we were getting "502 Bad Gateway" errors when trying to remove the demo data through API calls to the embedded ansible instance. This was because even though the setup playbook finished, the server would take just a bit longer to start to serve requests. --- lib/embedded_ansible.rb | 10 ++++++++ spec/lib/embedded_ansible_spec.rb | 23 ++++++++++++++++++- .../embedded_ansible_worker/runner_spec.rb | 21 ++++++++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/lib/embedded_ansible.rb b/lib/embedded_ansible.rb index dff4d7a6ca0..53a047e065f 100644 --- a/lib/embedded_ansible.rb +++ b/lib/embedded_ansible.rb @@ -12,6 +12,7 @@ class EmbeddedAnsible START_EXCLUDE_TAGS = "packages,migrations,firewall".freeze HTTP_PORT = 54_321 HTTPS_PORT = 54_322 + WAIT_FOR_ANSIBLE_SLEEP = 1.second def self.available? path = ENV["APPLIANCE_ANSIBLE_DIRECTORY"] || APPLIANCE_ANSIBLE_DIRECTORY @@ -50,6 +51,15 @@ def self.configure def self.start configure_secret_key run_setup_script(START_EXCLUDE_TAGS) + + 5.times do + return if alive? + + _log.info("Waiting for EmbeddedAnsible to respond") + sleep WAIT_FOR_ANSIBLE_SLEEP + end + + raise "EmbeddedAnsible service is not responding after setup" end def self.stop diff --git a/spec/lib/embedded_ansible_spec.rb b/spec/lib/embedded_ansible_spec.rb index 36228b50826..d811907403c 100644 --- a/spec/lib/embedded_ansible_spec.rb +++ b/spec/lib/embedded_ansible_spec.rb @@ -275,12 +275,16 @@ end describe ".start" do - it "runs the setup script with the correct args" do + before do miq_database.set_ansible_admin_authentication(:password => "adminpassword") miq_database.set_ansible_rabbitmq_authentication(:userid => "rabbituser", :password => "rabbitpassword") miq_database.set_ansible_database_authentication(:userid => "databaseuser", :password => "databasepassword") expect(described_class).to receive(:configure_secret_key) + stub_const("EmbeddedAnsible::WAIT_FOR_ANSIBLE_SLEEP", 0) + end + + it "runs the setup script with the correct args" do expect(AwesomeSpawn).to receive(:run!) do |script_path, options| params = options[:params] inventory_file_contents = File.read(params[:inventory=]) @@ -296,9 +300,26 @@ expect(inventory_file_contents).to include("pg_username='databaseuser'") expect(inventory_file_contents).to include("pg_password='databasepassword'") end + expect(described_class).to receive(:alive?).and_return(true) + + described_class.start + end + + it "waits for Ansible to respond" do + expect(AwesomeSpawn).to receive(:run!) + + expect(described_class).to receive(:alive?).exactly(3).times.and_return(false, false, true) described_class.start end + + it "raises if Ansible doesn't respond" do + expect(AwesomeSpawn).to receive(:run!) + + expect(described_class).to receive(:alive?).exactly(5).times.and_return(false) + + expect { described_class.start }.to raise_error(RuntimeError) + end end describe ".generate_database_authentication (private)" do diff --git a/spec/models/embedded_ansible_worker/runner_spec.rb b/spec/models/embedded_ansible_worker/runner_spec.rb index f3c20f37727..51221e25b0b 100644 --- a/spec/models/embedded_ansible_worker/runner_spec.rb +++ b/spec/models/embedded_ansible_worker/runner_spec.rb @@ -64,6 +64,25 @@ expect(provider.default_endpoint.url).to eq("https://boringserver/ansibleapi/v1") end end + + context "#setup_ansible" do + it "configures EmbeddedAnsible if it is not configured" do + expect(EmbeddedAnsible).to receive(:start) + + expect(EmbeddedAnsible).to receive(:configured?).and_return(false) + expect(EmbeddedAnsible).to receive(:configure) + + runner.setup_ansible + end + + it "doesn't call configure if EmbeddedAnsible is already configured" do + expect(EmbeddedAnsible).to receive(:start) + + expect(EmbeddedAnsible).to receive(:configured?).and_return(true) + expect(EmbeddedAnsible).not_to receive(:configure) + + runner.setup_ansible + end + end end end -