From a754fa8d7a9455dbca12fe50bbaee0880d29bf69 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 12 Oct 2017 09:51:04 -0400 Subject: [PATCH 01/12] Don't use python to generate the ansible secret key --- lib/embedded_ansible/appliance_embedded_ansible.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/embedded_ansible/appliance_embedded_ansible.rb b/lib/embedded_ansible/appliance_embedded_ansible.rb index cab236c98a8..85129b58be1 100644 --- a/lib/embedded_ansible/appliance_embedded_ansible.rb +++ b/lib/embedded_ansible/appliance_embedded_ansible.rb @@ -113,12 +113,11 @@ def with_inventory_file def configure_secret_key key = miq_database.ansible_secret_key - if key.present? - File.write(SECRET_KEY_FILE, key) - else - AwesomeSpawn.run!("/usr/bin/python -c \"import uuid; file('#{SECRET_KEY_FILE}', 'wb').write(uuid.uuid4().hex)\"") - miq_database.ansible_secret_key = File.read(SECRET_KEY_FILE) + unless key.present? + key = SecureRandom.hex(16) + miq_database.ansible_secret_key = key end + File.write(SECRET_KEY_FILE, key) end def update_proxy_settings From fbc3d3d18edc4ae0a34d7b57068bcc90222c48b3 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 12 Oct 2017 09:55:34 -0400 Subject: [PATCH 02/12] Refactor generating the ansible secret key into a method This will make it easier to move this method into a shared location --- lib/embedded_ansible/appliance_embedded_ansible.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/embedded_ansible/appliance_embedded_ansible.rb b/lib/embedded_ansible/appliance_embedded_ansible.rb index 85129b58be1..c99bd6395a6 100644 --- a/lib/embedded_ansible/appliance_embedded_ansible.rb +++ b/lib/embedded_ansible/appliance_embedded_ansible.rb @@ -112,11 +112,7 @@ def with_inventory_file end def configure_secret_key - key = miq_database.ansible_secret_key - unless key.present? - key = SecureRandom.hex(16) - miq_database.ansible_secret_key = key - end + key = miq_database.ansible_secret_key || generate_secret_key File.write(SECRET_KEY_FILE, key) end @@ -134,6 +130,10 @@ def update_proxy_settings File.write(SETTINGS_FILE, new_contents) end + def generate_secret_key + miq_database.ansible_secret_key = SecureRandom.hex(16) + end + def generate_admin_authentication miq_database.set_ansible_admin_authentication(:password => generate_password) end From 28b87db4bc9fca82caa509571794518431ad7d6c Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 12 Oct 2017 11:14:44 -0400 Subject: [PATCH 03/12] Refactor password generation methods into shared find_or_create methods This allows the "fetch from the database or generate and save" behavior to be shared across different embedded ansible platforms --- lib/embedded_ansible.rb | 32 +++++++++++++++++ .../appliance_embedded_ansible.rb | 35 +++---------------- .../appliance_embedded_ansible_spec.rb | 27 ++------------ spec/lib/embedded_ansible_spec.rb | 28 +++++++++++++++ 4 files changed, 66 insertions(+), 56 deletions(-) diff --git a/lib/embedded_ansible.rb b/lib/embedded_ansible.rb index 165adba9ca3..1f136438d11 100644 --- a/lib/embedded_ansible.rb +++ b/lib/embedded_ansible.rb @@ -43,9 +43,41 @@ def api_connection_raw(host, port) ) end + def find_or_create_secret_key + miq_database.ansible_secret_key ||= SecureRandom.hex(16) + end + + def find_or_create_admin_authentication + miq_database.ansible_admin_authentication || miq_database.set_ansible_admin_authentication(:password => generate_password) + end + + def find_or_create_rabbitmq_authentication + miq_database.ansible_rabbitmq_authentication || miq_database.set_ansible_rabbitmq_authentication(:password => generate_password) + end + + def find_or_create_database_authentication + auth = miq_database.ansible_database_authentication + return auth if auth + + auth = miq_database.set_ansible_database_authentication(:password => generate_password) + + database_connection.select_value("CREATE ROLE #{database_connection.quote_column_name(auth.userid)} WITH LOGIN PASSWORD #{database_connection.quote(auth.password)}") + database_connection.select_value("CREATE DATABASE awx OWNER #{database_connection.quote_column_name(auth.userid)} ENCODING 'utf8'") + + auth + end + + def generate_password + SecureRandom.base64(18).tr("+/", "-_") + end + def miq_database MiqDatabase.first end + + def database_connection + ActiveRecord::Base.connection + end end Dir.glob(File.join(File.dirname(__FILE__), "embedded_ansible/*.rb")).each { |f| require_dependency f } diff --git a/lib/embedded_ansible/appliance_embedded_ansible.rb b/lib/embedded_ansible/appliance_embedded_ansible.rb index c99bd6395a6..2184cb69b29 100644 --- a/lib/embedded_ansible/appliance_embedded_ansible.rb +++ b/lib/embedded_ansible/appliance_embedded_ansible.rb @@ -112,7 +112,7 @@ def with_inventory_file end def configure_secret_key - key = miq_database.ansible_secret_key || generate_secret_key + key = find_or_create_secret_key File.write(SECRET_KEY_FILE, key) end @@ -130,29 +130,10 @@ def update_proxy_settings File.write(SETTINGS_FILE, new_contents) end - def generate_secret_key - miq_database.ansible_secret_key = SecureRandom.hex(16) - end - - def generate_admin_authentication - miq_database.set_ansible_admin_authentication(:password => generate_password) - end - - def generate_rabbitmq_authentication - miq_database.set_ansible_rabbitmq_authentication(:password => generate_password) - end - - def generate_database_authentication - auth = miq_database.set_ansible_database_authentication(:password => generate_password) - database_connection.select_value("CREATE ROLE #{database_connection.quote_column_name(auth.userid)} WITH LOGIN PASSWORD #{database_connection.quote(auth.password)}") - database_connection.select_value("CREATE DATABASE awx OWNER #{database_connection.quote_column_name(auth.userid)} ENCODING 'utf8'") - auth - end - def inventory_file_contents - admin_auth = miq_database.ansible_admin_authentication || generate_admin_authentication - rabbitmq_auth = miq_database.ansible_rabbitmq_authentication || generate_rabbitmq_authentication - database_auth = miq_database.ansible_database_authentication || generate_database_authentication + admin_auth = find_or_create_admin_authentication + rabbitmq_auth = find_or_create_rabbitmq_authentication + database_auth = find_or_create_database_authentication db_config = Rails.configuration.database_configuration[Rails.env] <<-EOF.strip_heredoc @@ -181,14 +162,6 @@ def inventory_file_contents EOF end - def generate_password - SecureRandom.base64(18).tr("+/", "-_") - end - - def database_connection - ActiveRecord::Base.connection - end - def local_tower_version File.read(TOWER_VERSION_FILE).strip end diff --git a/spec/lib/embedded_ansible/appliance_embedded_ansible_spec.rb b/spec/lib/embedded_ansible/appliance_embedded_ansible_spec.rb index fd63fc1bd18..13667752157 100644 --- a/spec/lib/embedded_ansible/appliance_embedded_ansible_spec.rb +++ b/spec/lib/embedded_ansible/appliance_embedded_ansible_spec.rb @@ -242,7 +242,7 @@ it "generates new passwords with no passwords set" do expect(subject).to receive(:alive?).and_return(true) - expect(subject).to receive(:generate_database_authentication).and_return(double(:userid => "awx", :password => "databasepassword")) + expect(subject).to receive(:find_or_create_database_authentication).and_return(double(:userid => "awx", :password => "databasepassword")) expect(AwesomeSpawn).to receive(:run!) do |script_path, options| params = options[:params] inventory_file_contents = File.read(params[:inventory=]) @@ -292,36 +292,13 @@ it "removes the secret key from the database when setup fails" do miq_database.ansible_secret_key = "supersecretkey" - expect(subject).to receive(:generate_database_authentication).and_return(double(:userid => "awx", :password => "databasepassword")) + expect(subject).to receive(:find_or_create_database_authentication).and_return(double(:userid => "awx", :password => "databasepassword")) expect(AwesomeSpawn).to receive(:run!).and_raise(AwesomeSpawn::CommandResultError.new("error", 1)) expect { subject.start }.to raise_error(AwesomeSpawn::CommandResultError) expect(miq_database.reload.ansible_secret_key).not_to be_present end end - - describe "#generate_database_authentication (private)" do - let(:password) { "secretpassword" } - let(:quoted_password) { ActiveRecord::Base.connection.quote(password) } - let(:connection) { double(:quote => quoted_password) } - - before do - allow(connection).to receive(:quote_column_name) do |name| - ActiveRecord::Base.connection.quote_column_name(name) - end - end - - it "creates the database" do - allow(subject).to receive(:database_connection).and_return(connection) - expect(subject).to receive(:generate_password).and_return(password) - expect(connection).to receive(:select_value).with("CREATE ROLE \"awx\" WITH LOGIN PASSWORD #{quoted_password}") - expect(connection).to receive(:select_value).with("CREATE DATABASE awx OWNER \"awx\" ENCODING 'utf8'") - - auth = subject.send(:generate_database_authentication) - expect(auth.userid).to eq("awx") - expect(auth.password).to eq(password) - end - end end describe "#update_proxy_settings (private)" do diff --git a/spec/lib/embedded_ansible_spec.rb b/spec/lib/embedded_ansible_spec.rb index 242a2d547e7..aa7b3361d8a 100644 --- a/spec/lib/embedded_ansible_spec.rb +++ b/spec/lib/embedded_ansible_spec.rb @@ -128,5 +128,33 @@ end end end + + describe "#find_or_create_database_authentication (private)" do + let(:password) { "secretpassword" } + let(:quoted_password) { ActiveRecord::Base.connection.quote(password) } + let(:connection) { double(:quote => quoted_password) } + + before do + allow(connection).to receive(:quote_column_name) do |name| + ActiveRecord::Base.connection.quote_column_name(name) + end + end + + it "creates the database" do + allow(subject).to receive(:database_connection).and_return(connection) + expect(subject).to receive(:generate_password).and_return(password) + expect(connection).to receive(:select_value).with("CREATE ROLE \"awx\" WITH LOGIN PASSWORD #{quoted_password}") + expect(connection).to receive(:select_value).with("CREATE DATABASE awx OWNER \"awx\" ENCODING 'utf8'") + + auth = subject.send(:find_or_create_database_authentication) + expect(auth).to have_attributes(:userid => "awx", :password => password) + end + + it "returns the saved authentication" do + miq_database.set_ansible_database_authentication(:password => "mypassword") + auth = subject.send(:find_or_create_database_authentication) + expect(auth).to have_attributes(:userid => "awx", :password => "mypassword") + end + end end end From 3d740c4754cc804c257301c1aaf94c98c84a3814 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 12 Oct 2017 17:52:51 -0400 Subject: [PATCH 04/12] Add the docker-api gem --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 8cbb4ee2fb0..9fccc09066b 100644 --- a/Gemfile +++ b/Gemfile @@ -31,6 +31,7 @@ gem "color", "~>1.8" gem "config", "~>1.3.0", :require => false gem "dalli", "~>2.7.4", :require => false gem "default_value_for", "~>3.0.3" +gem "docker-api", "~>1.33.6", :require => false gem "elif", "=0.1.0", :require => false gem "fast_gettext", "~>1.2.0" gem "gettext_i18n_rails", "~>1.7.2" From e194cca6fd924acda485aaec466835352500bc48 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 12 Oct 2017 17:54:19 -0400 Subject: [PATCH 05/12] Extract the database configuration into a method on EmbeddedAnsible --- lib/embedded_ansible.rb | 4 ++++ lib/embedded_ansible/appliance_embedded_ansible.rb | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/embedded_ansible.rb b/lib/embedded_ansible.rb index 1f136438d11..18088296ca4 100644 --- a/lib/embedded_ansible.rb +++ b/lib/embedded_ansible.rb @@ -78,6 +78,10 @@ def miq_database def database_connection ActiveRecord::Base.connection end + + def database_configuration + @db_config ||= ActiveRecord::Base.configurations[Rails.env] + end end Dir.glob(File.join(File.dirname(__FILE__), "embedded_ansible/*.rb")).each { |f| require_dependency f } diff --git a/lib/embedded_ansible/appliance_embedded_ansible.rb b/lib/embedded_ansible/appliance_embedded_ansible.rb index 2184cb69b29..d559c457319 100644 --- a/lib/embedded_ansible/appliance_embedded_ansible.rb +++ b/lib/embedded_ansible/appliance_embedded_ansible.rb @@ -134,7 +134,6 @@ def inventory_file_contents admin_auth = find_or_create_admin_authentication rabbitmq_auth = find_or_create_rabbitmq_authentication database_auth = find_or_create_database_authentication - db_config = Rails.configuration.database_configuration[Rails.env] <<-EOF.strip_heredoc [tower] @@ -145,8 +144,8 @@ def inventory_file_contents [all:vars] admin_password='#{admin_auth.password}' - pg_host='#{db_config["host"] || "localhost"}' - pg_port='#{db_config["port"] || "5432"}' + pg_host='#{database_configuration["host"] || "localhost"}' + pg_port='#{database_configuration["port"] || "5432"}' pg_database='awx' pg_username='#{database_auth.userid}' From e3caa6cb25442fd1b38c727d1d8d2f13c44e6361 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Thu, 12 Oct 2017 17:54:28 -0400 Subject: [PATCH 06/12] Add DockerEmbeddedAnsible This will run the containers which make up AWX (https://github.com/ansible/awx) and configure our app to use that for the embedded ansible feature. This class uses the docker-api gem to communicate with the locally running docker daemon to pull and launch the containers. We use port 54321 as the host port so that this can be used seamlessly in place of ApplianceEmbeddedAnsible when ansible tower is not installed locally --- config/settings.yml | 10 + .../docker_embedded_ansible.rb | 201 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 lib/embedded_ansible/docker_embedded_ansible.rb diff --git a/config/settings.yml b/config/settings.yml index d71129b9e45..56ba48a2314 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -94,6 +94,16 @@ :history: :keep_drift_states: 6.months :purge_window_size: 10000 +:embedded_ansible: + :docker: + :task_image_name: ansible/awx_task + :task_image_tag: latest + :web_image_name: ansible/awx_web + :web_image_tag: latest + :rabbitmq_image_name: rabbitmq + :rabbitmq_image_tag: 3 + :memcached_image_name: memcached + :memcached_image_tag: alpine :ems: # provider specific settings are nested here, but they are in the provider repos # e.g.: diff --git a/lib/embedded_ansible/docker_embedded_ansible.rb b/lib/embedded_ansible/docker_embedded_ansible.rb new file mode 100644 index 00000000000..bcb1807e76c --- /dev/null +++ b/lib/embedded_ansible/docker_embedded_ansible.rb @@ -0,0 +1,201 @@ +class DockerEmbeddedAnsible < EmbeddedAnsible + AWX_WEB_PORT = "54321".freeze + + def self.available? + require 'docker' + Docker.validate_version! + rescue RuntimeError + false + end + + def initialize + super + require 'docker' + end + + def start + run_rabbitmq_container + run_memcached_container + sleep(15) + run_web_container + run_task_container + + loop do + break if alive? + + _log.info("Waiting for Ansible container to respond") + sleep WAIT_FOR_ANSIBLE_SLEEP + end + rescue RuntimeError + stop + end + + def stop + container_names.each { |c| stop_container(c) } + end + + alias disable stop + + def running? + container_names.all? { |c| container_running?(c) } + end + + def configured? + self.class.available? + end + + def api_connection + api_connection_raw("localhost", AWX_WEB_PORT) + end + + private + + def run_rabbitmq_container + rabbitmq_auth = find_or_create_rabbitmq_authentication + run_container( + rabbitmq_image_name, + 'name' => rabbitmq_container_name, + 'ExposedPorts' => {"25672/tcp" => {}, + "4369/tcp" => {}, + "5671/tcp" => {}, + "5672/tcp" => {}}, + 'Env' => ["RABBITMQ_DEFAULT_VHOST=awx", + "RABBITMQ_DEFAULT_USER=#{rabbitmq_auth.userid}", + "RABBITMQ_DEFAULT_PASS=#{rabbitmq_auth.password}"] + ) + end + + def run_memcached_container + run_container( + memcached_image_name, + 'name' => memcached_container_name, + 'ExposedPorts' => {"11211/tcp" => {}} + ) + end + + def run_web_container + run_container( + awx_web_image_name, + 'name' => awx_web_container_name, + 'Env' => awx_env, + 'Hostname' => "awxweb", + 'User' => "root", + 'ExposedPorts' => {"8052/tcp" => {}}, + 'HostConfig' => { + 'Links' => [rabbitmq_container_name, memcached_container_name], + 'PortBindings' => { + '8052/tcp' => [{ 'HostPort' => AWX_WEB_PORT, 'HostIp' => "0.0.0.0" }] + } + } + ) + end + + def run_task_container + run_container( + awx_task_image_name, + 'name' => awx_task_container_name, + 'Env' => awx_env, + 'Hostname' => "awx", + 'User' => "root", + 'ExposedPorts' => {"8052/tcp" => {}}, + 'HostConfig' => { + 'Links' => [rabbitmq_container_name, + memcached_container_name, + awx_web_container_name] + } + ) + end + + def run_container(image, create_params = {}) + Docker::Image.create('fromImage' => image) + container = Docker::Container.create(create_params.merge('Image' => image)) + container.start + end + + def stop_container(name) + Docker::Container.get(name).kill.delete + rescue Docker::Error::NotFoundError + nil + end + + def container_running?(name) + Docker::Container.get(name) + true + rescue Docker::Error::NotFoundError + false + end + + def awx_env + admin_auth = find_or_create_admin_authentication + database_auth = find_or_create_database_authentication + rabbitmq_auth = find_or_create_rabbitmq_authentication + + [ + "SECRET_KEY=#{find_or_create_secret_key}", + "DATABASE_NAME=awx", + "DATABASE_USER=#{database_auth.userid}", + "DATABASE_PASSWORD=#{database_auth.password}", + "DATABASE_PORT=#{database_configuration["port"] || 5432}", + "DATABASE_HOST=#{database_configuration["host"] || docker_bridge_gateway}", + "RABBITMQ_USER=#{rabbitmq_auth.userid}", + "RABBITMQ_PASSWORD=#{rabbitmq_auth.password}", + "RABBITMQ_HOST=#{rabbitmq_container_name}", + "RABBITMQ_PORT=5672", + "RABBITMQ_VHOST=awx", + "MEMCACHED_HOST=#{memcached_container_name}", + "MEMCACHED_PORT=11211", + "AWX_ADMIN_USER=#{admin_auth.userid}", + "AWX_ADMIN_PASSWORD=#{admin_auth.password}" + ] + end + + def docker_bridge_gateway + br = Docker::Network.get("bridge") + br.info["IPAM"]["Config"].first["Gateway"] + end + + def container_names + [ + awx_task_container_name, + awx_web_container_name, + memcached_container_name, + rabbitmq_container_name + ] + end + + def awx_task_container_name + "awx_task" + end + + def awx_task_image_name + "#{settings.task_image_name}:#{settings.task_image_tag}" + end + + def awx_web_container_name + "awx_web" + end + + def awx_web_image_name + "#{settings.web_image_name}:#{settings.web_image_tag}" + end + + def rabbitmq_container_name + "rabbitmq" + end + + def rabbitmq_image_name + "#{settings.rabbitmq_image_name}:#{settings.rabbitmq_image_tag}" + end + + def memcached_container_name + "memcached" + end + + def memcached_image_name + "#{settings.memcached_image_name}:#{settings.memcached_image_tag}" + end + + def settings + ::Settings.embedded_ansible.docker + end +end From 6b22cb98209d5a8665882757413d8dbe49c330b7 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Mon, 16 Oct 2017 18:49:18 -0400 Subject: [PATCH 07/12] Don't try to use "localhost" for the database host When we have "localhost" in our database configuration, we have to change that to the local machine's IP on the docker NIC --- lib/embedded_ansible/docker_embedded_ansible.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/embedded_ansible/docker_embedded_ansible.rb b/lib/embedded_ansible/docker_embedded_ansible.rb index bcb1807e76c..36e89c4862b 100644 --- a/lib/embedded_ansible/docker_embedded_ansible.rb +++ b/lib/embedded_ansible/docker_embedded_ansible.rb @@ -130,13 +130,16 @@ def awx_env database_auth = find_or_create_database_authentication rabbitmq_auth = find_or_create_rabbitmq_authentication + db_host = database_configuration["host"] || docker_bridge_gateway + db_host = docker_bridge_gateway if db_host == "localhost" + [ "SECRET_KEY=#{find_or_create_secret_key}", "DATABASE_NAME=awx", "DATABASE_USER=#{database_auth.userid}", "DATABASE_PASSWORD=#{database_auth.password}", "DATABASE_PORT=#{database_configuration["port"] || 5432}", - "DATABASE_HOST=#{database_configuration["host"] || docker_bridge_gateway}", + "DATABASE_HOST=#{db_host}", "RABBITMQ_USER=#{rabbitmq_auth.userid}", "RABBITMQ_PASSWORD=#{rabbitmq_auth.password}", "RABBITMQ_HOST=#{rabbitmq_container_name}", From 481bf42d0df0454affe0e0766674be1d58a95409 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Fri, 17 Nov 2017 16:39:51 -0500 Subject: [PATCH 08/12] Add some specs for DockerEmbeddedAnsible This also adds stubs for all of the subclass availability in each of the specs to avoid sporadic test failures depending on the order the subclasses are evaluated for availability. --- .../appliance_embedded_ansible_spec.rb | 2 ++ .../container_embedded_ansible_spec.rb | 3 ++ .../docker_embedded_ansible_spec.rb | 22 ++++++++++++++ spec/lib/embedded_ansible_spec.rb | 29 +++++++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 spec/lib/embedded_ansible/docker_embedded_ansible_spec.rb diff --git a/spec/lib/embedded_ansible/appliance_embedded_ansible_spec.rb b/spec/lib/embedded_ansible/appliance_embedded_ansible_spec.rb index 13667752157..df3238beabf 100644 --- a/spec/lib/embedded_ansible/appliance_embedded_ansible_spec.rb +++ b/spec/lib/embedded_ansible/appliance_embedded_ansible_spec.rb @@ -1,10 +1,12 @@ require 'linux_admin' +require 'docker' require_dependency 'embedded_ansible' describe ApplianceEmbeddedAnsible do before do allow(MiqEnvironment::Command).to receive(:is_appliance?).and_return(true) allow(ContainerOrchestrator).to receive(:available?).and_return(false) + allow(Docker).to receive(:validate_version!).and_raise(RuntimeError) installed_rpms = { "ansible-tower-server" => "1.0.1", diff --git a/spec/lib/embedded_ansible/container_embedded_ansible_spec.rb b/spec/lib/embedded_ansible/container_embedded_ansible_spec.rb index 9876f9a2fe8..0efabb04c07 100644 --- a/spec/lib/embedded_ansible/container_embedded_ansible_spec.rb +++ b/spec/lib/embedded_ansible/container_embedded_ansible_spec.rb @@ -1,3 +1,4 @@ +require 'docker' require_dependency 'embedded_ansible' describe ContainerEmbeddedAnsible do @@ -5,6 +6,8 @@ before do allow(ContainerOrchestrator).to receive(:available?).and_return(true) + allow(MiqEnvironment::Command).to receive(:is_appliance?).and_return(false) + allow(Docker).to receive(:validate_version!).and_raise(RuntimeError) FactoryGirl.create(:miq_region, :region => ApplicationRecord.my_region_number) MiqDatabase.seed diff --git a/spec/lib/embedded_ansible/docker_embedded_ansible_spec.rb b/spec/lib/embedded_ansible/docker_embedded_ansible_spec.rb new file mode 100644 index 00000000000..8a2b7e2b8f1 --- /dev/null +++ b/spec/lib/embedded_ansible/docker_embedded_ansible_spec.rb @@ -0,0 +1,22 @@ +require 'docker' +require_dependency 'embedded_ansible' + +describe DockerEmbeddedAnsible do + before do + allow(Docker).to receive(:validate_version!).and_return(true) + allow(MiqEnvironment::Command).to receive(:is_appliance?).and_return(false) + allow(ContainerOrchestrator).to receive(:available?).and_return(false) + end + + describe "subject" do + it "is an instance of DockerEmbeddedAnsible" do + expect(subject).to be_an_instance_of(described_class) + end + end + + describe ".available?" do + it "returns true when the docker gem is usable" do + expect(described_class.available?).to be true + end + end +end diff --git a/spec/lib/embedded_ansible_spec.rb b/spec/lib/embedded_ansible_spec.rb index aa7b3361d8a..be32c10a32c 100644 --- a/spec/lib/embedded_ansible_spec.rb +++ b/spec/lib/embedded_ansible_spec.rb @@ -1,8 +1,11 @@ +require 'docker' + describe EmbeddedAnsible do context "with no available subclass" do before do expect(MiqEnvironment::Command).to receive(:is_appliance?).and_return(false) expect(ContainerOrchestrator).to receive(:available?).and_return(false) + expect(Docker).to receive(:validate_version!).and_raise(RuntimeError) end describe ".new" do @@ -21,6 +24,8 @@ context "in an appliance" do before do allow(MiqEnvironment::Command).to receive(:is_appliance?).and_return(true) + allow(ContainerOrchestrator).to receive(:available?).and_return(false) + allow(Docker).to receive(:validate_version!).and_raise(RuntimeError) installed_rpms = { "ansible-tower-server" => "1.0.1", @@ -46,6 +51,8 @@ context "in Kubernetes/OpenShift" do before do expect(ContainerOrchestrator).to receive(:available?).and_return(true) + allow(MiqEnvironment::Command).to receive(:is_appliance?).and_return(false) + allow(Docker).to receive(:validate_version!).and_raise(RuntimeError) end describe ".new" do @@ -61,7 +68,29 @@ end end + context "when using docker" do + before do + allow(ContainerOrchestrator).to receive(:available?).and_return(false) + allow(MiqEnvironment::Command).to receive(:is_appliance?).and_return(false) + allow(Docker).to receive(:validate_version!).and_return(true) + end + + describe ".new" do + it "returns an instance of DockerEmbeddedAnsible" do + expect(described_class.new).to be_an_instance_of(DockerEmbeddedAnsible) + end + end + + describe ".available?" do + it "returns true" do + expect(described_class.available?).to be true + end + end + end + context "with an miq_databases row" do + subject { NullEmbeddedAnsible.new } + let(:miq_database) { MiqDatabase.first } before do From e72b52ee694b5ac822ec9bf446e41893e12b721d Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Tue, 28 Nov 2017 16:20:07 -0500 Subject: [PATCH 09/12] Add priority to the EmbeddedAnsible subclasses This sorts the subclasses and instantiates the first available one --- lib/embedded_ansible.rb | 10 +++++++++- lib/embedded_ansible/appliance_embedded_ansible.rb | 4 ++++ lib/embedded_ansible/container_embedded_ansible.rb | 4 ++++ lib/embedded_ansible/docker_embedded_ansible.rb | 4 ++++ spec/lib/embedded_ansible_spec.rb | 6 ++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/embedded_ansible.rb b/lib/embedded_ansible.rb index 18088296ca4..48a6a42830d 100644 --- a/lib/embedded_ansible.rb +++ b/lib/embedded_ansible.rb @@ -10,7 +10,7 @@ def self.new end def self.detect_available_platform - subclasses.detect(&:available?) || NullEmbeddedAnsible + subclasses.sort.detect(&:available?) || NullEmbeddedAnsible end def self.available? @@ -21,6 +21,14 @@ def self.enabled? MiqServer.my_server(true).has_active_role?(ANSIBLE_ROLE) end + def self.priority + 0 + end + + def self.<=>(other_embedded_ansible) + other_embedded_ansible.priority <=> priority + end + def alive? return false unless configured? && running? begin diff --git a/lib/embedded_ansible/appliance_embedded_ansible.rb b/lib/embedded_ansible/appliance_embedded_ansible.rb index d559c457319..b13febb1353 100644 --- a/lib/embedded_ansible/appliance_embedded_ansible.rb +++ b/lib/embedded_ansible/appliance_embedded_ansible.rb @@ -18,6 +18,10 @@ def self.available? required_rpms.subset?(LinuxAdmin::Rpm.list_installed.keys.to_set) end + def self.priority + 30 + end + def initialize require "linux_admin" end diff --git a/lib/embedded_ansible/container_embedded_ansible.rb b/lib/embedded_ansible/container_embedded_ansible.rb index 19855fdafe0..2d4b4136af5 100644 --- a/lib/embedded_ansible/container_embedded_ansible.rb +++ b/lib/embedded_ansible/container_embedded_ansible.rb @@ -5,6 +5,10 @@ def self.available? ContainerOrchestrator.available? end + def self.priority + 20 + end + def start miq_database.set_ansible_admin_authentication(:password => ENV["ANSIBLE_ADMIN_PASSWORD"]) ContainerOrchestrator.new.scale(ANSIBLE_DC_NAME, 1) diff --git a/lib/embedded_ansible/docker_embedded_ansible.rb b/lib/embedded_ansible/docker_embedded_ansible.rb index 36e89c4862b..2e5ba598643 100644 --- a/lib/embedded_ansible/docker_embedded_ansible.rb +++ b/lib/embedded_ansible/docker_embedded_ansible.rb @@ -8,6 +8,10 @@ def self.available? false end + def self.priority + 10 + end + def initialize super require 'docker' diff --git a/spec/lib/embedded_ansible_spec.rb b/spec/lib/embedded_ansible_spec.rb index be32c10a32c..84334c485d1 100644 --- a/spec/lib/embedded_ansible_spec.rb +++ b/spec/lib/embedded_ansible_spec.rb @@ -1,6 +1,12 @@ require 'docker' describe EmbeddedAnsible do + describe ".<=>" do + it "allows classes to be sorted by priority" do + expect(EmbeddedAnsible.subclasses.sort).to eq([ApplianceEmbeddedAnsible, ContainerEmbeddedAnsible, DockerEmbeddedAnsible, NullEmbeddedAnsible]) + end + end + context "with no available subclass" do before do expect(MiqEnvironment::Command).to receive(:is_appliance?).and_return(false) From 19c8f63fbb3ad9197fbcbc76730a7e6ef6b3ea25 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Wed, 29 Nov 2017 11:32:39 -0500 Subject: [PATCH 10/12] Rescue JSON::ParserError in DockerEmbeddedAnsible.alive? This error will be raised when the containers are just started. Every API end point during the initial migration will return an html page rather than a json payload. This accounts for that specific situation by assuming if we don't get a valid json response the service is not ready to serve requests --- lib/embedded_ansible/docker_embedded_ansible.rb | 6 ++++++ .../docker_embedded_ansible_spec.rb | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/embedded_ansible/docker_embedded_ansible.rb b/lib/embedded_ansible/docker_embedded_ansible.rb index 2e5ba598643..a24143d6676 100644 --- a/lib/embedded_ansible/docker_embedded_ansible.rb +++ b/lib/embedded_ansible/docker_embedded_ansible.rb @@ -52,6 +52,12 @@ def api_connection api_connection_raw("localhost", AWX_WEB_PORT) end + def alive? + super + rescue JSON::ParserError + false + end + private def run_rabbitmq_container diff --git a/spec/lib/embedded_ansible/docker_embedded_ansible_spec.rb b/spec/lib/embedded_ansible/docker_embedded_ansible_spec.rb index 8a2b7e2b8f1..6afb20d3adb 100644 --- a/spec/lib/embedded_ansible/docker_embedded_ansible_spec.rb +++ b/spec/lib/embedded_ansible/docker_embedded_ansible_spec.rb @@ -19,4 +19,17 @@ expect(described_class.available?).to be true end end + + describe "#alive?" do + let(:connection) { double("APIConnection", :api => api) } + let(:api) { double("AnsibleAPI") } + + it "returns false if the api raises a JSON::ParserError" do + expect(subject).to receive(:running?).and_return(true) + expect(subject).to receive(:api_connection).and_return(connection) + expect(api).to receive(:verify_credentials).and_raise(JSON::ParserError) + + expect(subject.alive?).to be false + end + end end From 1ed4128076d60c40d630495dcfb3414338b1e4c5 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Wed, 29 Nov 2017 13:51:07 -0500 Subject: [PATCH 11/12] Start the docker daemon when we start DockerEmbeddedAnsible --- .../docker_embedded_ansible.rb | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/embedded_ansible/docker_embedded_ansible.rb b/lib/embedded_ansible/docker_embedded_ansible.rb index a24143d6676..7b589387ead 100644 --- a/lib/embedded_ansible/docker_embedded_ansible.rb +++ b/lib/embedded_ansible/docker_embedded_ansible.rb @@ -1,10 +1,30 @@ class DockerEmbeddedAnsible < EmbeddedAnsible AWX_WEB_PORT = "54321".freeze + class DockerDaemon + def initialize + require 'linux_admin' + end + + def start + return unless Rails.env.production? + LinuxAdmin::Service.new("docker").start.enable + end + + def stop + return unless Rails.env.production? + LinuxAdmin::Service.new("docker").stop.disable + end + end + + private_constant :DockerDaemon + def self.available? require 'docker' + + DockerDaemon.new.start Docker.validate_version! - rescue RuntimeError + rescue false end @@ -18,6 +38,7 @@ def initialize end def start + DockerDaemon.new.start run_rabbitmq_container run_memcached_container sleep(15) @@ -36,6 +57,7 @@ def start def stop container_names.each { |c| stop_container(c) } + DockerDaemon.new.stop end alias disable stop From b0157233f35486899ccca12b07cf90ac1dceac91 Mon Sep 17 00:00:00 2001 From: Nick Carboni Date: Fri, 13 Oct 2017 18:10:30 -0400 Subject: [PATCH 12/12] Add hack to get EmbeddedAnsible provider working on dev machines This really just assumes that a dev environment isn't multi-appliance and isn't fronted by our httpd configuration. This means that we always go to localhost, use http over https and hardcode the port and path. --- app/models/embedded_ansible_worker/runner.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/models/embedded_ansible_worker/runner.rb b/app/models/embedded_ansible_worker/runner.rb index d7e48dea6b8..28c1756f464 100644 --- a/app/models/embedded_ansible_worker/runner.rb +++ b/app/models/embedded_ansible_worker/runner.rb @@ -66,17 +66,18 @@ def message_sync_config(*_args); end private def provider_url - server = MiqServer.my_server(true) + URI::Generic.build(provider_uri_hash).to_s + end + def provider_uri_hash if MiqEnvironment::Command.is_container? - host = ENV["ANSIBLE_SERVICE_HOST"] - path = "/api/v1" + {:scheme => "https", :host => ENV["ANSIBLE_SERVICE_HOST"], :path => "/api/v1"} + elsif Rails.env.development? + {:scheme => "http", :host => "localhost", :path => "/api/v1", :port => 54321} else - host = server.hostname || server.ipaddress - path = "/ansibleapi/v1" + server = MiqServer.my_server(true) + {:scheme => "https", :host => server.hostname || server.ipaddress, :path => "/ansibleapi/v1"} end - - URI::HTTPS.build(:host => host, :path => path).to_s end def raise_role_notification(notification_type)