From a28d142f50ca16a3507780fb5600172f233fadc1 Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Wed, 25 Aug 2021 08:27:30 -0400 Subject: [PATCH 001/207] Fix user metric so that poolnames with a dot are replaced by underscore --- lib/vmpooler/api/v1.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/vmpooler/api/v1.rb b/lib/vmpooler/api/v1.rb index 1503100..f7329b3 100644 --- a/lib/vmpooler/api/v1.rb +++ b/lib/vmpooler/api/v1.rb @@ -242,6 +242,7 @@ module Vmpooler backend.hget("vmpooler__vm__#{vmname}", 'token:user') backend.hget("vmpooler__vm__#{vmname}", 'template') jenkins_build_url, user, poolname = backend.exec + poolname = poolname.gsub('.', '_') if user user = user.gsub('.', '_') From c88f5d8b5a939613698f063c7e619a06ef84d835 Mon Sep 17 00:00:00 2001 From: Jake Spain Date: Wed, 25 Aug 2021 09:27:45 -0400 Subject: [PATCH 002/207] Fix missing function when moving metrics from pool_manager to api/v1 Fix dependency function component_to_test for jenkins metrics that was not copied from pool_manager to api/v1 in https://github.com/puppetlabs/vmpooler/pull/455 --- lib/vmpooler/api/v1.rb | 12 +++++++++++ lib/vmpooler/pool_manager.rb | 12 ----------- spec/unit/pool_manager_spec.rb | 37 ---------------------------------- 3 files changed, 12 insertions(+), 49 deletions(-) diff --git a/lib/vmpooler/api/v1.rb b/lib/vmpooler/api/v1.rb index f7329b3..2fffee5 100644 --- a/lib/vmpooler/api/v1.rb +++ b/lib/vmpooler/api/v1.rb @@ -236,6 +236,18 @@ module Vmpooler result end + def component_to_test(match, labels_string) + return if labels_string.nil? + + labels_string_parts = labels_string.split(',') + labels_string_parts.each do |part| + key, value = part.split('=') + next if value.nil? + return value if key == match + end + 'none' + end + def update_user_metrics(operation, vmname) backend.multi backend.hget("vmpooler__vm__#{vmname}", 'tag:jenkins_build_url') diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index 8ad7902..668d1c0 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -478,18 +478,6 @@ module Vmpooler dereference_mutex(vm) end - def component_to_test(match, labels_string) - return if labels_string.nil? - - labels_string_parts = labels_string.split(',') - labels_string_parts.each do |part| - key, value = part.split('=') - next if value.nil? - return value if key == match - end - 'none' - end - def purge_unused_vms_and_folders global_purge = $config[:config]['purge_unconfigured_folders'] providers = $config[:providers].keys diff --git a/spec/unit/pool_manager_spec.rb b/spec/unit/pool_manager_spec.rb index 89ba5da..cc5777b 100644 --- a/spec/unit/pool_manager_spec.rb +++ b/spec/unit/pool_manager_spec.rb @@ -1062,43 +1062,6 @@ EOT end end - describe '#component_to_test' do - let(:matching_key) { 'LABEL_ONE' } - let(:matching_value) { 'test' } - let(:labels_string) { "#{matching_key}=#{matching_value},LABEL_TWO=test2,LABEL_THREE=test3" } - let(:nonmatrix_string) { 'test,stuff,and,things' } - - context 'when string contains a matching key' do - it 'should print the corresponding value' do - expect(subject.component_to_test(matching_key, labels_string)).to eq(matching_value) - end - - context 'when match contains no value' do - it 'should return none' do - expect(subject.component_to_test(matching_key, matching_key)).to eq('none') - end - end - end - - context 'when string contains no key value pairs' do - it 'should return' do - expect(subject.component_to_test(matching_key, nonmatrix_string)).to eq('none') - end - end - - context 'when labels_string is a job number' do - it 'should return nil' do - expect(subject.component_to_test(matching_key, '25')).to eq('none') - end - end - - context 'when labels_string is nil' do - it 'should return nil' do - expect(subject.component_to_test(matching_key, nil)).to be nil - end - end - end - describe '#purge_unused_vms_and_folders' do let(:config) { YAML.load(<<-EOT --- From 5f0d41412c6d28b2ad4d9000080226969d32bd59 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 25 Aug 2021 15:04:35 +0000 Subject: [PATCH 003/207] (GEM) update vmpooler version to 1.1.2 --- lib/vmpooler/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vmpooler/version.rb b/lib/vmpooler/version.rb index 7997dc3..47f7479 100644 --- a/lib/vmpooler/version.rb +++ b/lib/vmpooler/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Vmpooler - VERSION = '1.1.1' + VERSION = '1.1.2' end From 5cd7658ab4f299cce3fd07650c24634da9599a3f Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Tue, 14 Sep 2021 15:01:05 -0400 Subject: [PATCH 004/207] (DIO-2621) Make LDAP encryption configurable Prior to this, the encryption settings for LDAP auth were hard coded to start_tls on port 389 with TLSv1. These are still the defaults, as insecure as they are, so as to not break existing users. This change facilitates replacing the defaults so that simple_tls over port 636 via TLS1.2 can be used. --- lib/vmpooler.rb | 5 ++ lib/vmpooler/api/helpers.rb | 12 +-- spec/unit/api/helpers_spec.rb | 165 ++++++++++++++++++++++------------ vmpooler.yaml.example | 6 +- 4 files changed, 123 insertions(+), 65 deletions(-) diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb index ef497e1..3403896 100644 --- a/lib/vmpooler.rb +++ b/lib/vmpooler.rb @@ -119,6 +119,11 @@ module Vmpooler parsed_config[:auth][:ldap]['port'] = string_to_int(ENV['LDAP_PORT']) if ENV['LDAP_PORT'] parsed_config[:auth][:ldap]['base'] = ENV['LDAP_BASE'] if ENV['LDAP_BASE'] parsed_config[:auth][:ldap]['user_object'] = ENV['LDAP_USER_OBJECT'] if ENV['LDAP_USER_OBJECT'] + if parsed_config[:auth]['provider'] == 'ldap' && parsed_config[:auth][:ldap].key?('encryption') + parsed_config[:auth][:ldap]['encryption'] = parsed_config[:auth][:ldap]['encryption'] + elsif parsed_config[:auth]['provider'] == 'ldap' + parsed_config[:auth][:ldap]['encryption'] = {} + end end # Create an index of pool aliases diff --git a/lib/vmpooler/api/helpers.rb b/lib/vmpooler/api/helpers.rb index e4b2302..c7be9ba 100644 --- a/lib/vmpooler/api/helpers.rb +++ b/lib/vmpooler/api/helpers.rb @@ -56,14 +56,11 @@ module Vmpooler return false end - def authenticate_ldap(port, host, user_object, base, username_str, password_str) + def authenticate_ldap(port, host, encryption_hash, user_object, base, username_str, password_str) ldap = Net::LDAP.new( :host => host, :port => port, - :encryption => { - :method => :start_tls, - :tls_options => { :ssl_version => 'TLSv1' } - }, + :encryption => encryption_hash, :base => base, :auth => { :method => :simple, @@ -86,6 +83,10 @@ module Vmpooler ldap_port = auth[:ldap]['port'] || 389 ldap_user_obj = auth[:ldap]['user_object'] ldap_host = auth[:ldap]['host'] + ldap_encryption_hash = auth[:ldap]['encryption'] || { + :method => :start_tls, + :tls_options => { :ssl_version => 'TLSv1' } + } unless ldap_base.is_a? Array ldap_base = ldap_base.split @@ -100,6 +101,7 @@ module Vmpooler result = authenticate_ldap( ldap_port, ldap_host, + ldap_encryption_hash, search_user_obj, search_base, username_str, diff --git a/spec/unit/api/helpers_spec.rb b/spec/unit/api/helpers_spec.rb index 5c7f0ae..8325baa 100644 --- a/spec/unit/api/helpers_spec.rb +++ b/spec/unit/api/helpers_spec.rb @@ -264,24 +264,48 @@ describe Vmpooler::API::Helpers do } } let(:default_port) { 389 } + let(:default_encryption) do + { + :method => :start_tls, + :tls_options => { :ssl_version => 'TLSv1' } + } + end it 'should attempt ldap authentication' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base, username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base, username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should return true when authentication is successful' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base, username_str, password_str).and_return(true) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base, username_str, password_str).and_return(true) expect(subject.authenticate(auth, username_str, password_str)).to be true end it 'should return false when authentication fails' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base, username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base, username_str, password_str).and_return(false) expect(subject.authenticate(auth, username_str, password_str)).to be false end + context 'with an alternate ssl_version' do + let(:secure_encryption) do + { + :method => :start_tls, + :tls_options => { :ssl_version => 'TLSv1_2' } + } + end + before(:each) do + auth[:ldap]['encryption'] = secure_encryption + end + + it 'should specify the alternate ssl_version when authenticating' do + expect(subject).to receive(:authenticate_ldap).with(default_port, host, secure_encryption, user_object, base, username_str, password_str) + + subject.authenticate(auth, username_str, password_str) + end + end + context 'with an alternate port' do let(:alternate_port) { 636 } before(:each) do @@ -289,7 +313,27 @@ describe Vmpooler::API::Helpers do end it 'should specify the alternate port when authenticating' do - expect(subject).to receive(:authenticate_ldap).with(alternate_port, host, user_object, base, username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(alternate_port, host, default_encryption, user_object, base, username_str, password_str) + + subject.authenticate(auth, username_str, password_str) + end + end + + context 'with simple_tls and port 636' do + let(:secure_port) { 636 } + let(:secure_encryption) do + { + :method => :simple_tls, + :tls_options => { :ssl_version => 'TLSv1_2' } + } + end + before(:each) do + auth[:ldap]['port'] = secure_port + auth[:ldap]['encryption'] = secure_encryption + end + + it 'should specify the secure port and encryption options when authenticating' do + expect(subject).to receive(:authenticate_ldap).with(secure_port, host, secure_encryption, user_object, base, username_str, password_str) subject.authenticate(auth, username_str, password_str) end @@ -307,36 +351,36 @@ describe Vmpooler::API::Helpers do end it 'should attempt to bind with each base' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[0], username_str, password_str) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[0], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[1], username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should not search the second base when the first binds' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[0], username_str, password_str).and_return(true) - expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, user_object, base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[0], username_str, password_str).and_return(true) + expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[1], username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should search the second base when the first bind fails' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[1], username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should return true when any bind succeeds' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[1], username_str, password_str).and_return(true) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[1], username_str, password_str).and_return(true) expect(subject.authenticate(auth, username_str, password_str)).to be true end it 'should return false when all bind attempts fail' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object, base[1], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object, base[1], username_str, password_str).and_return(false) expect(subject.authenticate(auth, username_str, password_str)).to be false end @@ -354,36 +398,36 @@ describe Vmpooler::API::Helpers do end it 'should attempt to bind with each user object' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base, username_str, password_str) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base, username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base, username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base, username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should not search the second user object when the first binds' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base, username_str, password_str).and_return(true) - expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, user_object[1], base, username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base, username_str, password_str).and_return(true) + expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base, username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should search the second user object when the first bind fails' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base, username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base, username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base, username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base, username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should return true when any bind succeeds' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base, username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base, username_str, password_str).and_return(true) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base, username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base, username_str, password_str).and_return(true) expect(subject.authenticate(auth, username_str, password_str)).to be true end it 'should return false when all bind attempts fail' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base, username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base, username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base, username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base, username_str, password_str).and_return(false) expect(subject.authenticate(auth, username_str, password_str)).to be false end @@ -408,64 +452,64 @@ describe Vmpooler::API::Helpers do end it 'should attempt to bind with each user object and base' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[0], username_str, password_str) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[0], username_str, password_str) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[1], username_str, password_str) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[0], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[0], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[1], username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should not continue searching when the first combination binds' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[0], username_str, password_str).and_return(true) - expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, user_object[1], base[0], username_str, password_str) - expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, user_object[0], base[1], username_str, password_str) - expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, user_object[1], base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[0], username_str, password_str).and_return(true) + expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[0], username_str, password_str) + expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[1], username_str, password_str) + expect(subject).to_not receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[1], username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should search the remaining combinations when the first bind fails' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[0], username_str, password_str) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[1], username_str, password_str) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[0], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[1], username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should search the remaining combinations when the first two binds fail' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[1], username_str, password_str) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[1], username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should search the remaining combination when the first three binds fail' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[1], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[1], username_str, password_str) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[1], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[1], username_str, password_str) subject.authenticate(auth, username_str, password_str) end it 'should return true when any bind succeeds' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[1], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[1], username_str, password_str).and_return(true) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[1], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[1], username_str, password_str).and_return(true) expect(subject.authenticate(auth, username_str, password_str)).to be true end it 'should return false when all bind attempts fail' do - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[0], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[0], base[1], username_str, password_str).and_return(false) - expect(subject).to receive(:authenticate_ldap).with(default_port, host, user_object[1], base[1], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[0], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[0], base[1], username_str, password_str).and_return(false) + expect(subject).to receive(:authenticate_ldap).with(default_port, host, default_encryption, user_object[1], base[1], username_str, password_str).and_return(false) expect(subject.authenticate(auth, username_str, password_str)).to be false end @@ -493,16 +537,19 @@ describe Vmpooler::API::Helpers do let(:base) { 'ou=users,dc=example,dc=com' } let(:username_str) { 'admin' } let(:password_str) { 's3cr3t' } + let(:encryption) do + { + :method => :start_tls, + :tls_options => { :ssl_version => 'TLSv1' } + } + end let(:ldap) { double('ldap') } it 'should create a new ldap connection' do allow(ldap).to receive(:bind) expect(Net::LDAP).to receive(:new).with( :host => host, :port => port, - :encryption => { - :method => :start_tls, - :tls_options => { :ssl_version => 'TLSv1' } - }, + :encryption => encryption, :base => base, :auth => { :method => :simple, @@ -511,21 +558,21 @@ describe Vmpooler::API::Helpers do } ).and_return(ldap) - subject.authenticate_ldap(port, host, user_object, base, username_str, password_str) + subject.authenticate_ldap(port, host, encryption, user_object, base, username_str, password_str) end it 'should return true when a bind is successful' do expect(Net::LDAP).to receive(:new).and_return(ldap) expect(ldap).to receive(:bind).and_return(true) - expect(subject.authenticate_ldap(port, host, user_object, base, username_str, password_str)).to be true + expect(subject.authenticate_ldap(port, host, encryption, user_object, base, username_str, password_str)).to be true end it 'should return false when a bind fails' do expect(Net::LDAP).to receive(:new).and_return(ldap) expect(ldap).to receive(:bind).and_return(false) - expect(subject.authenticate_ldap(port, host, user_object, base, username_str, password_str)).to be false + expect(subject.authenticate_ldap(port, host, encryption, user_object, base, username_str, password_str)).to be false end end diff --git a/vmpooler.yaml.example b/vmpooler.yaml.example index a9d15e3..60a9950 100644 --- a/vmpooler.yaml.example +++ b/vmpooler.yaml.example @@ -373,7 +373,11 @@ provider: 'ldap' :ldap: host: 'ldap.example.com' - port: 389 + port: 636 + encryption: + :method: :simple_tls + :tls_options: + :ssl_version: 'TLSv1_2' base: 'ou=users,dc=company,dc=com' user_object: 'uid' From f6eb636dffcb2aedc9f5b077a206c18caf7fb72c Mon Sep 17 00:00:00 2001 From: Jenkins Date: Wed, 15 Sep 2021 13:54:19 +0000 Subject: [PATCH 005/207] (GEM) update vmpooler version to 1.2.0 --- lib/vmpooler/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vmpooler/version.rb b/lib/vmpooler/version.rb index 47f7479..6018812 100644 --- a/lib/vmpooler/version.rb +++ b/lib/vmpooler/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Vmpooler - VERSION = '1.1.2' + VERSION = '1.2.0' end From 9ba85bfa140842f0b2e322286712f60d40ce3ea0 Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Thu, 16 Sep 2021 14:08:16 -0400 Subject: [PATCH 006/207] Ignore .dccache, sort ignore file --- .gitignore | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 0e49c05..6117961 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ +.bundle/ .vagrant/ -results.xml +coverage/ +vendor/ +.dccache .ruby-version -Gemfile.lock Gemfile.local -vendor +Gemfile.lock +results.xml /vmpooler.yaml -.bundle -coverage + From dd1b8167a9c50094a293298a70b7032439f575b2 Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Thu, 16 Sep 2021 14:09:29 -0400 Subject: [PATCH 007/207] (DIO-2186) Add vmp_utils cli tool This adds a thor-based cli under the utils folder. Its initial function is to make it easier to migrate tokens from one redis instance to another. --- utils/vmp_utils | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ vmpooler.gemspec | 1 + 2 files changed, 87 insertions(+) create mode 100755 utils/vmp_utils diff --git a/utils/vmp_utils b/utils/vmp_utils new file mode 100755 index 0000000..613abd7 --- /dev/null +++ b/utils/vmp_utils @@ -0,0 +1,86 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'date' +require 'json' +require 'redis' +require 'thor' + +class VMPUtils < Thor + desc 'export_tokens', 'Export VMPooler tokens' + long_desc <<-LONGDESC + vmp_utils export_tokens will export the tokens stored in redis as JSON + to the specified file. By default it will export all tokens used within + the last year. This can be overridden by setting the "since-date" option. + LONGDESC + option :since_date, default: (DateTime.now - 365).iso8601 + option :redis_host, required: true + option :redis_password + option :redis_port, type: :numeric, default: 6379 + option :output_file, required: true + def export_tokens + redis = Redis.new(host: options[:redis_host], port: options[:redis_port], password: options[:redis_password]) + begin + redis.ping + puts "connection successful to #{options[:redis_host]}" + tokens_to_export = {} + date_filter = DateTime.parse(options[:since_date]) + redis.scan_each(match: 'vmpooler__token__*') do |key| + print('.') + key_data = redis.hgetall(key) + export = false + if key_data.key?('last') && date_filter < DateTime.parse(key_data['last']) + export = true + elsif date_filter < DateTime.parse(key_data['created']) + export = true + end + tokens_to_export[key] = key_data if export + end + puts '' + File.open(options[:output_file], 'w') do |output| + output.puts JSON.pretty_generate(tokens_to_export) + end + rescue Redis::CannotConnectError => e + raise Thor::Error, e + rescue Redis::CommandError => e + raise Thor::Error, e + end + end + + desc 'import_tokens', 'Import previously exported VMPooler tokens' + long_desc <<-LONGDESC + vmp_utils import_tokens will load tokens from a previous export into the + specified redis instance. It expects a JSON file like the one generated by + export_tokens to be provided as input. + LONGDESC + option :redis_host, required: true + option :redis_password + option :redis_port, type: :numeric, default: 6379 + option :input_file, required: true + def import_tokens + raise Thor::Error, "input-file '#{options[:input_file]}' doesn't exist" unless File.exist?(options[:input_file]) + + begin + data_file = File.read(options[:input_file]) + tokens_to_import = JSON.parse(data_file) + rescue JSON::ParserError + raise Thor::Error, "Unable to parse please verify that #{options[:input_file]}. Please make sure it is a valid JSON file" + end + + redis = Redis.new(host: options[:redis_host], port: options[:redis_port], password: options[:redis_password]) + begin + redis.ping + puts "connection successful to #{options[:redis_host]}" + tokens_to_import.each do |key, value| + puts "importing token for #{value['user']}" + redis.hset(key, value) + end + rescue Redis::CannotConnectError => e + raise Thor::Error, e + rescue Redis::CommandError => e + raise Thor::Error, e + end + end +end + +VMPUtils.start(ARGV) diff --git a/vmpooler.gemspec b/vmpooler.gemspec index 5a3dd1e..20e35fe 100644 --- a/vmpooler.gemspec +++ b/vmpooler.gemspec @@ -46,5 +46,6 @@ Gem::Specification.new do |s| s.add_development_dependency 'rspec', '>= 3.2' s.add_development_dependency 'rubocop', '~> 1.1.0' s.add_development_dependency 'simplecov', '>= 0.11.2' + s.add_development_dependency 'thor', '~> 1.0', '>= 1.0.1' s.add_development_dependency 'yarjuf', '>= 2.0' end From 6db71d85890fbca533f11a52588255ee79bec77f Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Wed, 27 Oct 2021 16:19:02 -0400 Subject: [PATCH 008/207] Update Dockerfile_local to rebuild faster This makes it so that cached layers can be used when all that is changing is VMPooler's code, and not its gems. --- docker/Dockerfile_local | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docker/Dockerfile_local b/docker/Dockerfile_local index c1b3f4e..2480e16 100644 --- a/docker/Dockerfile_local +++ b/docker/Dockerfile_local @@ -10,9 +10,6 @@ FROM jruby:9.2-jdk -COPY docker/docker-entrypoint.sh /usr/local/bin/ -COPY ./ ./ - ENV RACK_ENV=production RUN apt-get update -qq && \ @@ -21,9 +18,18 @@ RUN apt-get update -qq && \ apt-get autoremove -y && \ rm -rf /var/lib/apt/lists/* +COPY docker/docker-entrypoint.sh /usr/local/bin/ +COPY ./Gemfile ./ +COPY ./vmpooler.gemspec ./ +COPY ./lib/vmpooler/version.rb ./lib/vmpooler/version.rb + RUN gem install bundler && \ - bundle install && \ - gem build vmpooler.gemspec && \ + bundle config set --local jobs 3 && \ + bundle install + +COPY ./ ./ + +RUN gem build vmpooler.gemspec && \ gem install vmpooler*.gem && \ chmod +x /usr/local/bin/docker-entrypoint.sh From a0caa41a549a6797a7b17ac9f02f1311d0e590bc Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Thu, 14 Oct 2021 15:08:55 -0400 Subject: [PATCH 009/207] (DIO-2675) Undo pool size template overrides This implements a delete method for pooltemplate and poolsize. The API removes the override from Redis and then adds an entry in Redis that causes the pool manager to wake up and process the removal of the override. To facilitate this, a new variable has been created in lib/vmpooler.rb to hold a copy of the original / pre-override config. This supplemental copy of the pools is then indexed for use as a reference. When pool manager wakes up to process an override removal, it looks up the pre-override value from the config via the new variables mentioned above. Just as with entering overrides, no restart is needed. Template and pool size changes are logged so that anyone watching or reviewing the logs can see what happened when. The new API endpoints also return values for both the pre-revert and post-revert value. --- docs/API.md | 44 ++++++++++++ lib/vmpooler.rb | 9 +++ lib/vmpooler/api/v1.rb | 97 ++++++++++++++++++++++++++ lib/vmpooler/pool_manager.rb | 59 ++++++++++++++-- spec/integration/api/v1/config_spec.rb | 95 +++++++++++++++++++++++++ spec/unit/pool_manager_spec.rb | 96 +++++++++++++++++++++++++ spec/unit/vmpooler_spec.rb | 15 ++++ 7 files changed, 409 insertions(+), 6 deletions(-) diff --git a/docs/API.md b/docs/API.md index 038d4e9..ee6fa26 100644 --- a/docs/API.md +++ b/docs/API.md @@ -743,6 +743,28 @@ $ curl -X POST -H "Content-Type: application/json" -d '{"debian-7-i386":"2","deb } ``` +##### DELETE /config/poolsize/<pool> + +Delete an overridden pool size. This results in the values from VMPooler's config being used. + +Return codes: +* 200 - when nothing was changed but no error occurred +* 201 - size reset successful +* 401 - when not authorized +* 404 - pool does not exist +* 405 - The endpoint is disabled because experimental features are disabled + +``` +$ curl -X DELETE -u jdoe --url vmpooler.example.com/api/v1/poolsize/almalinux-8-x86_64 +``` +```json +{ + "ok": true, + "pool_size_before_overrides": 2, + "pool_size_before_reset": 4 +} +``` + ##### POST /config/pooltemplate Change the template configured for a pool, and replenish the pool with instances built from the new template. @@ -775,6 +797,28 @@ $ curl -X POST -H "Content-Type: application/json" -d '{"debian-7-i386":"templat } ``` +##### DELETE /config/pooltemplate/<pool> + +Delete an overridden pool template. This results in the values from VMPooler's config being used. + +Return codes: +* 200 - when nothing was changed but no error occurred +* 201 - template reset successful +* 401 - when not authorized +* 404 - pool does not exist +* 405 - The endpoint is disabled because experimental features are disabled + +``` +$ curl -X DELETE -u jdoe --url vmpooler.example.com/api/v1/pooltemplate/almalinux-8-x86_64 +``` +```json +{ + "ok": true, + "template_before_overrides": "templates/almalinux-8-x86_64-0.0.2", + "template_before_reset": "templates/almalinux-8-x86_64-0.0.3-beta" +} +``` + ##### POST /poolreset Clear all pending and ready instances in a pool, and deploy replacements diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb index 3403896..7d39f18 100644 --- a/lib/vmpooler.rb +++ b/lib/vmpooler.rb @@ -133,8 +133,17 @@ module Vmpooler parsed_config[:pools] = load_pools_from_redis(redis) end + # Marshal.dump is paired with Marshal.load to create a copy that has its own memory space + # so that each can be edited independently + # rubocop:disable Security/MarshalLoad + + # retain a copy of the pools that were observed at startup + serialized_pools = Marshal.dump(parsed_config[:pools]) + parsed_config[:pools_at_startup] = Marshal.load(serialized_pools) + # Create an index of pools by title parsed_config[:pool_index] = pool_index(parsed_config[:pools]) + # rubocop:enable Security/MarshalLoad parsed_config[:pools].each do |pool| parsed_config[:pool_names] << pool['name'] diff --git a/lib/vmpooler/api/v1.rb b/lib/vmpooler/api/v1.rb index 2fffee5..f209de2 100644 --- a/lib/vmpooler/api/v1.rb +++ b/lib/vmpooler/api/v1.rb @@ -28,6 +28,10 @@ module Vmpooler Vmpooler::API.settings.config[:pools] end + def pools_at_startup + Vmpooler::API.settings.config[:pools_at_startup] + end + def pool_exists?(template) Vmpooler::API.settings.config[:pool_names].include?(template) end @@ -289,6 +293,32 @@ module Vmpooler puts 'd', "[!] [#{poolname}] failed while evaluating usage labels on '#{vmname}' with an error: #{e}" end + def reset_pool_size(poolname) + result = { 'ok' => false } + + pool_index = pool_index(pools) + + pools_updated = 0 + sync_pool_sizes + + pool_size_now = pools[pool_index[poolname]]['size'].to_i + pool_size_original = pools_at_startup[pool_index[poolname]]['size'].to_i + result['pool_size_before_reset'] = pool_size_now + result['pool_size_before_overrides'] = pool_size_original + + unless pool_size_now == pool_size_original + pools[pool_index[poolname]]['size'] = pool_size_original + backend.hdel('vmpooler__config__poolsize', poolname) + backend.sadd('vmpooler__pool__undo_size_override', poolname) + pools_updated += 1 + status 201 + end + + status 200 unless pools_updated > 0 + result['ok'] = true + result + end + def update_pool_size(payload) result = { 'ok' => false } @@ -309,6 +339,33 @@ module Vmpooler result end + def reset_pool_template(poolname) + result = { 'ok' => false } + + pool_index_live = pool_index(pools) + pool_index_original = pool_index(pools_at_startup) + + pools_updated = 0 + sync_pool_templates + + template_now = pools[pool_index_live[poolname]]['template'] + template_original = pools_at_startup[pool_index_original[poolname]]['template'] + result['template_before_reset'] = template_now + result['template_before_overrides'] = template_original + + unless template_now == template_original + pools[pool_index_live[poolname]]['template'] = template_original + backend.hdel('vmpooler__config__template', poolname) + backend.sadd('vmpooler__pool__undo_template_override', poolname) + pools_updated += 1 + status 201 + end + + status 200 unless pools_updated > 0 + result['ok'] = true + result + end + def update_pool_template(payload) result = { 'ok' => false } @@ -1375,6 +1432,26 @@ module Vmpooler JSON.pretty_generate(result) end + delete "#{api_prefix}/config/poolsize/:pool/?" do + content_type :json + result = { 'ok' => false } + + if config['experimental_features'] + need_token! if Vmpooler::API.settings.config[:auth] + + if pool_exists?(params[:pool]) + result = reset_pool_size(params[:pool]) + else + metrics.increment('config.invalid.unknown') + status 404 + end + else + status 405 + end + + JSON.pretty_generate(result) + end + post "#{api_prefix}/config/poolsize/?" do content_type :json result = { 'ok' => false } @@ -1406,6 +1483,26 @@ module Vmpooler JSON.pretty_generate(result) end + delete "#{api_prefix}/config/pooltemplate/:pool/?" do + content_type :json + result = { 'ok' => false } + + if config['experimental_features'] + need_token! if Vmpooler::API.settings.config[:auth] + + if pool_exists?(params[:pool]) + result = reset_pool_template(params[:pool]) + else + metrics.increment('config.invalid.unknown') + status 404 + end + else + status 405 + end + + JSON.pretty_generate(result) + end + post "#{api_prefix}/config/pooltemplate/?" do content_type :json result = { 'ok' => false } diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index 668d1c0..1c9551e 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -784,6 +784,10 @@ module Vmpooler # - Fires when a pool reset is requested # - Additional options # :poolname + # :undo_override + # - Fires when a pool override removal is requested + # - Additional options + # :poolname # def sleep_with_wakeup_events(loop_delay, wakeup_period = 5, options = {}) exit_by = Time.now + loop_delay @@ -826,6 +830,11 @@ module Vmpooler break if pending end + if options[:undo_override] + break if redis.sismember('vmpooler__pool__undo_template_override', options[:poolname]) + break if redis.sismember('vmpooler__pool__undo_size_override', options[:poolname]) + end + if options[:pending_vm] pending_vm_count = redis.scard("vmpooler__pending__#{options[:poolname]}") break unless pending_vm_count == 0 @@ -880,7 +889,7 @@ module Vmpooler loop_delay = (loop_delay * loop_delay_decay).to_i loop_delay = loop_delay_max if loop_delay > loop_delay_max end - sleep_with_wakeup_events(loop_delay, loop_delay_min, pool_size_change: true, poolname: pool['name'], pool_template_change: true, clone_target_change: true, pending_vm: true, pool_reset: true) + sleep_with_wakeup_events(loop_delay, loop_delay_min, pool_size_change: true, poolname: pool['name'], pool_template_change: true, clone_target_change: true, pending_vm: true, pool_reset: true, undo_override: true) unless maxloop == 0 break if loop_count >= maxloop @@ -1040,15 +1049,18 @@ module Vmpooler return if mutex.locked? @redis.with_metrics do |redis| - poolsize = redis.hget('vmpooler__config__poolsize', pool['name']) - break if poolsize.nil? + pool_size_requested = redis.hget('vmpooler__config__poolsize', pool['name']) + break if pool_size_requested.nil? - poolsize = Integer(poolsize) - break if poolsize == pool['size'] + pool_size_requested = Integer(pool_size_requested) + pool_size_currently = pool['size'] + break if pool_size_requested == pool_size_currently mutex.synchronize do - pool['size'] = poolsize + pool['size'] = pool_size_requested end + + $logger.log('s', "[*] [#{pool['name']}] size updated from #{pool_size_currently} to #{pool_size_requested}") end end @@ -1066,6 +1078,38 @@ module Vmpooler end end + def undo_override(pool, provider) + poolname = pool['name'] + mutex = pool_mutex(poolname) + return if mutex.locked? + + @redis.with_metrics do |redis| + break unless redis.sismember('vmpooler__pool__undo_template_override', poolname) + + redis.srem('vmpooler__pool__undo_template_override', poolname) + template_now = pool['template'] + template_original = $config[:pools_at_startup][$config[:pool_index][poolname]]['template'] + + mutex.synchronize do + update_pool_template(pool, provider, template_original, template_now, redis) + end + end + + @redis.with_metrics do |redis| + break unless redis.sismember('vmpooler__pool__undo_size_override', poolname) + + redis.srem('vmpooler__pool__undo_size_override', poolname) + pool_size_now = pool['size'] + pool_size_original = $config[:pools_at_startup][$config[:pool_index][poolname]]['size'] + + mutex.synchronize do + pool['size'] = pool_size_original + end + + $logger.log('s', "[*] [#{poolname}] size updated from #{pool_size_now} to #{pool_size_original}") + end + end + def create_inventory(pool, provider, pool_check_response) inventory = {} begin @@ -1300,6 +1344,9 @@ module Vmpooler # Reset a pool when poolreset is requested from the API reset_pool(pool) + # Undo overrides submitted via the api + undo_override(pool, provider) + pool_check_response end diff --git a/spec/integration/api/v1/config_spec.rb b/spec/integration/api/v1/config_spec.rb index 79874c8..62968bd 100644 --- a/spec/integration/api/v1/config_spec.rb +++ b/spec/integration/api/v1/config_spec.rb @@ -26,6 +26,10 @@ describe Vmpooler::API::V1 do {'name' => 'pool1', 'size' => 5, 'template' => 'templates/pool1', 'clone_target' => 'default_cluster'}, {'name' => 'pool2', 'size' => 10} ], + pools_at_startup: [ + {'name' => 'pool1', 'size' => 5, 'template' => 'templates/pool1', 'clone_target' => 'default_cluster'}, + {'name' => 'pool2', 'size' => 10} + ], statsd: { 'prefix' => 'stats_prefix'}, alias: { 'poolone' => 'pool1' }, pool_names: [ 'pool1', 'pool2', 'poolone' ] @@ -45,6 +49,47 @@ describe Vmpooler::API::V1 do create_token('abcdefghijklmnopqrstuvwxyz012345', 'jdoe', current_time) end + describe 'DELETE /config/pooltemplate/:pool' do + it 'resets a pool template' do + post "#{prefix}/config/pooltemplate", '{"pool1":"templates/new_template"}' + delete "#{prefix}/config/pooltemplate/pool1" + expect_json(ok = true, http = 201) + + expected = { + ok: true, + template_before_reset: 'templates/new_template', + template_before_overrides: 'templates/pool1' + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + it 'succeeds when the pool has not been overridden' do + delete "#{prefix}/config/pooltemplate/pool1" + expect_json(ok = true, http = 200) + end + + it 'fails on nonexistent pools' do + delete "#{prefix}/config/pooltemplate/poolpoolpool" + expect_json(ok = false, http = 404) + end + + context 'with experimental features disabled' do + before(:each) do + config[:config]['experimental_features'] = false + end + + it 'should return 405' do + delete "#{prefix}/config/pooltemplate/pool1" + expect_json(ok = false, http = 405) + + expected = { ok: false } + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + end + + end + describe 'POST /config/pooltemplate' do it 'updates a pool template' do post "#{prefix}/config/pooltemplate", '{"pool1":"templates/new_template"}' @@ -142,6 +187,56 @@ describe Vmpooler::API::V1 do end + describe 'DELETE /config/poolsize' do + it 'resets a pool size' do + post "#{prefix}/config/poolsize", '{"pool1":"2"}' + delete "#{prefix}/config/poolsize/pool1" + expect_json(ok = true, http = 201) + + expected = { + ok: true, + pool_size_before_reset: 2, + pool_size_before_overrides: 5 + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + it 'fails when a specified pool does not exist' do + delete "#{prefix}/config/poolsize/pool10" + expect_json(ok = false, http = 404) + expected = { ok: false } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + it 'succeeds when a pool has not been overridden' do + delete "#{prefix}/config/poolsize/pool1" + expect_json(ok = true, http = 200) + expected = { + ok: true, + pool_size_before_reset: 5, + pool_size_before_overrides: 5 + } + + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + + context 'with experimental features disabled' do + before(:each) do + config[:config]['experimental_features'] = false + end + + it 'should return 405' do + delete "#{prefix}/config/poolsize/pool1" + expect_json(ok = false, http = 405) + + expected = { ok: false } + expect(last_response.body).to eq(JSON.pretty_generate(expected)) + end + end + end + describe 'POST /config/poolsize' do it 'changes a pool size' do post "#{prefix}/config/poolsize", '{"pool1":"2"}' diff --git a/spec/unit/pool_manager_spec.rb b/spec/unit/pool_manager_spec.rb index cc5777b..3bc9c68 100644 --- a/spec/unit/pool_manager_spec.rb +++ b/spec/unit/pool_manager_spec.rb @@ -3159,6 +3159,54 @@ EOT end end + describe 'with the undo_override wakeup option' do + let(:wakeup_option) {{ + :undo_override => true, + :poolname => pool + }} + + let(:wakeup_period) { -1 } # A negative number forces the wakeup evaluation to always occur + + context 'when a undoing a template override is requested' do + before(:each) do + redis_connection_pool.with do |redis| + redis.sadd('vmpooler__pool__undo_template_override', pool) + allow(redis).to receive(:hget) + end + end + + it 'should sleep until the undo override request is detected' do + redis_connection_pool.with do |redis| + expect(subject).to receive(:sleep).at_least(2).times + expect(subject).to receive(:sleep).at_most(3).times + expect(redis).to receive(:sismember).with('vmpooler__pool__undo_template_override', pool).and_return(false,false,true) + expect(redis).to receive(:sismember).with('vmpooler__pool__undo_size_override', pool).and_return(false,false) + end + + subject.sleep_with_wakeup_events(loop_delay, wakeup_period, wakeup_option) + end + end + + context 'when a undoing a size override is requested' do + before(:each) do + redis_connection_pool.with do |redis| + redis.sadd('vmpooler__pool__undo_size_override', pool) + allow(redis).to receive(:hget) + end + end + + it 'should sleep until the undo override request is detected' do + redis_connection_pool.with do |redis| + expect(subject).to receive(:sleep).exactly(3).times + expect(redis).to receive(:sismember).with('vmpooler__pool__undo_template_override', pool).and_return(false,false,false) + expect(redis).to receive(:sismember).with('vmpooler__pool__undo_size_override', pool).and_return(false,false,true) + end + + subject.sleep_with_wakeup_events(loop_delay, wakeup_period, wakeup_option) + end + end + end + describe 'with the pending_vm wakeup option' do let(:wakeup_option) {{ :pending_vm => true, @@ -3477,6 +3525,54 @@ EOT end end + describe 'undo_override' do + let(:mutex) { Mutex.new } + let(:original_template) { 'templates/template1' } + let(:override_template) { 'templates/template2' } + let(:original_size) { 2 } + let(:override_size) { 10 } + let(:config) { YAML.load(<<-EOT +--- +:config: + task_limit: 5 +:providers: + :mock: +:pools: + - name: '#{pool}' + size: #{override_size} + template: '#{override_template}' +:pool_index: + '#{pool}': 0 +:pools_at_startup: + - name: '#{pool}' + size: #{original_size} + template: '#{original_template}' +EOT + ) + } + + before(:each) do + redis_connection_pool.with do |redis| + redis.sadd('vmpooler__pool__undo_template_override', pool) + redis.sadd('vmpooler__pool__undo_size_override', pool) + # allow(redis).to receive(:hget) + end + end + + it 'should revert to the original template and pool size' do + redis_connection_pool.with do |redis| + expect(redis).to receive(:sismember).with('vmpooler__pool__undo_template_override', pool).and_return(true) + expect(redis).to receive(:srem).with('vmpooler__pool__undo_template_override', pool).and_return(true) + expect(subject).to receive(:update_pool_template).with(config[:pools][0], provider, original_template, override_template, redis) + + expect(redis).to receive(:sismember).with('vmpooler__pool__undo_size_override', pool).and_return(true) + expect(redis).to receive(:srem).with('vmpooler__pool__undo_size_override', pool).and_return(true) + end + + subject.undo_override(config[:pools][0], provider) + end + end + describe '#create_inventory' do it 'should log an error if one occurs' do diff --git a/spec/unit/vmpooler_spec.rb b/spec/unit/vmpooler_spec.rb index ac9f620..19aed91 100644 --- a/spec/unit/vmpooler_spec.rb +++ b/spec/unit/vmpooler_spec.rb @@ -20,6 +20,21 @@ describe 'Vmpooler' do expect(Vmpooler.config[:pools]).to eq(default_config[:pools]) end end + + it 'keeps a copy of the original pools at startup' do + Dir.chdir(fixtures_dir) do + configuration = Vmpooler.config + expect(configuration[:pools]).to eq(configuration[:pools_at_startup]) + end + end + + it 'the copy is a separate object and not a reference' do + Dir.chdir(fixtures_dir) do + configuration = Vmpooler.config + configuration[:pools][0]['template'] = 'sam' + expect(configuration[:pools]).not_to eq(configuration[:pools_at_startup]) + end + end end context 'when config variable is set' do From 48c5d6d4450e58d7c914cd569742900793136833 Mon Sep 17 00:00:00 2001 From: Jenkins Date: Mon, 15 Nov 2021 15:57:33 +0000 Subject: [PATCH 010/207] (GEM) update vmpooler version to 1.3.0 --- lib/vmpooler/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vmpooler/version.rb b/lib/vmpooler/version.rb index 6018812..d7d8c53 100644 --- a/lib/vmpooler/version.rb +++ b/lib/vmpooler/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Vmpooler - VERSION = '1.2.0' + VERSION = '1.3.0' end From bc0a369602943a9c40180451a09ef6275abc6a3c Mon Sep 17 00:00:00 2001 From: Gene Liverman Date: Mon, 29 Nov 2021 16:09:15 -0500 Subject: [PATCH 011/207] Move vsphere provider to its own gem --- README.md | 56 +- lib/vmpooler.rb | 1 - lib/vmpooler/pool_manager.rb | 8 +- lib/vmpooler/providers/vsphere.rb | 1172 --------- scripts/create_template_deltas.rb | 98 - spec/rbvmomi_helper.rb | 922 ------- spec/spec_helper.rb | 2 - spec/unit/pool_manager_spec.rb | 60 +- spec/unit/providers/vsphere_spec.rb | 3589 --------------------------- spec/unit/providers_spec.rb | 25 +- vmpooler.gemspec | 1 - 11 files changed, 47 insertions(+), 5887 deletions(-) delete mode 100644 lib/vmpooler/providers/vsphere.rb delete mode 100755 scripts/create_template_deltas.rb delete mode 100644 spec/rbvmomi_helper.rb delete mode 100644 spec/unit/providers/vsphere_spec.rb diff --git a/README.md b/README.md index 66b9bd9..0b7116c 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,38 @@ -![vmpooler](https://raw.github.com/sschneid/vmpooler/master/lib/vmpooler/public/img/logo.gif) +![VMPooler](https://raw.github.com/sschneid/vmpooler/master/lib/vmpooler/public/img/logo.gif) -# vmpooler - -vmpooler provides configurable 'pools' of instantly-available (running) virtual machines. +# VMPooler +VMPooler provides configurable 'pools' of instantly-available (pre-provisioned) and/or on-demand (provisioned on request) virtual machines. ## Usage -At [Puppet, Inc.](http://puppet.com) we run acceptance tests on thousands of disposable VMs every day. Vmpooler manages the lifecycle of these VMs from request through deletion, with options available to pool ready instances, and provision on demand. +At [Puppet, Inc.](http://puppet.com) we run acceptance tests on thousands of disposable VMs every day. VMPooler manages the life cycle of these VMs from request through deletion, with options available to pool ready instances, and provision on demand. ## Installation ### Prerequisites -vmpooler is available as a gem - -To use the gem `gem install vmpooler` +VMPooler is available as a gem. To use the gem run `gem install vmpooler` or add it to your Gemfile and install via bundler. ### Dependencies -Vmpooler requires a [Redis](http://redis.io/) server. This is the datastore used for vmpooler's inventory and queueing services. +VMPooler requires a [Redis](http://redis.io/) server. This is the data store used for VMPooler's inventory and queuing services. ### Configuration -Configuration for vmpooler may be provided via environment variables, or a configuration file. +Configuration for VMPooler may be provided via environment variables, or a configuration file. -Note on jruby 9.2.11.x: We have found when running vmpooler on jruby 9.2.11.x we occasionally encounter a stack overflow error that causes the pool\_manager application component to fail and stop doing work. To address this issue on jruby 9.2.11.x we recommend setting the jruby option 'invokedynamic.yield=false'. To set this with jruby 9.2.11.1 you can specify the environment variable 'JRUBY\_OPTS' with the value '-Xinvokedynamic.yield=false'. +**Note on JRuby 9.2.11.x:** We have found when running VMPooler on JRuby 9.2.11.x we occasionally encounter a stack overflow error that causes the pool\_manager application component to fail and stop doing work. To address this issue on JRuby 9.2.11.x we recommend setting the JRuby option 'invokedynamic.yield=false'. To set this with JRuby 9.2.11.1 you can specify the environment variable 'JRUBY\_OPTS' with the value '-Xinvokedynamic.yield=false'. -The provided configuration defaults are reasonable for small vmpooler instances with a few pools. If you plan to run a large vmpooler instance it is important to consider configuration values appropriate for the instance of your size in order to avoid starving the provider, or redis, of connections. +The provided configuration defaults are reasonable for small VMPooler instances with a few pools. If you plan to run a large VMPooler instance it is important to consider configuration values appropriate for the instance of your size in order to avoid starving the provider, or Redis, of connections. -As of vmpooler 0.13.x redis uses a connection pool to improve efficiency and ensure thread safe usage. At Puppet, we run an instance with about 100 pools at any given time. We have to provide it with 200 redis connections to the redis connection pool, and a timeout for connections of 40 seconds, to avoid timeouts. Because metrics are generated for connection available and waited your metrics provider will need to be able to cope with this volume. Statsd is recommended to ensure metrics get delivered reliably. +As of VMPooler 0.13.x Redis uses a connection pool to improve efficiency and ensure thread safe usage. At Puppet, we run an instance with about 100 pools at any given time. We have to provide it with 200 Redis connections to the Redis connection pool, and a timeout for connections of 40 seconds, to avoid timeouts. Because metrics are generated for connection available and waited your metrics provider will need to be able to cope with this volume. Prometheus or StatsD is recommended to ensure metrics get delivered reliably. -Please see this [configuration](docs/configuration.md) document for more details about configuring vmpooler via environment variables. +Please see this [configuration](docs/configuration.md) document for more details about configuring VMPooler via environment variables. The following YAML configuration sets up two pools, `debian-7-i386` and `debian-7-x86_64`, which contain 5 running VMs each: -``` +```yaml --- :providers: :vsphere: @@ -70,43 +67,43 @@ See the provided YAML configuration example, [vmpooler.yaml.example](vmpooler.ya ### Running via Docker -A [Dockerfile](/docker/Dockerfile) is included in this repository to allow running vmpooler inside a Docker container. A configuration file can be used via volume mapping, and specifying the destination as the configuration file via environment variables, or the application can be configured with environment variables alone. The Dockerfile provides an entrypoint so you may choose whether to run API, or manager services. The default behavior will run both. To build and run: +A [Dockerfile](/docker/Dockerfile) is included in this repository to allow running VMPooler inside a Docker container. A configuration file can be used via volume mapping, and specifying the destination as the configuration file via environment variables, or the application can be configured with environment variables alone. The Dockerfile provides an entrypoint so you may choose whether to run API, or manager services. The default behavior will run both. To build and run: -``` +```bash docker build -t vmpooler . && docker run -e VMPOOLER_CONFIG -p 80:4567 -it vmpooler ``` -To run only the API and dashboard +To run only the API and dashboard: -``` +```bash docker run -p 80:4567 -it vmpooler api ``` -To run only the manager component +To run only the manager component: -``` +```bash docker run -it vmpooler manager ``` ### docker-compose -A docker-compose file is provided to support running vmpooler easily via docker-compose. This is useful for development because your local code is used to build the gem used in the docker-compose environment. +A docker-compose file is provided to support running VMPooler easily via docker-compose. This is useful for development because your local code is used to build the gem used in the docker-compose environment. -``` +```bash docker-compose -f docker/docker-compose.yml up ``` ### Running Docker inside Vagrant -A vagrantfile is included in this repository. Please see [vagrant instructions](docs/vagrant.md) for details. +A Vagrantfile is included in this repository. Please see [vagrant instructions](docs/vagrant.md) for details. ## API and Dashboard -vmpooler provides an API and web front-end (dashboard) on port `:4567`. See the provided YAML configuration example, [vmpooler.yaml.example](vmpooler.yaml.example), to specify an alternative port to listen on. +VMPooler provides an API and web front-end (dashboard) on port `:4567`. See the provided YAML configuration example, [vmpooler.yaml.example](vmpooler.yaml.example), to specify an alternative port to listen on. ### API -vmpooler provides a REST API for VM management. See the [API documentation](docs/API.md) for more information. +VMPooler provides a REST API for VM management. See the [API documentation](docs/API.md) for more information. ### Dashboard @@ -122,17 +119,16 @@ A dashboard is provided to offer real-time statistics and historical graphs. It ## Vagrant plugin -- [vagrant-vmpooler](https://github.com/briancain/vagrant-vmpooler) Use Vagrant to create and manage your vmpooler instances. +- [vagrant-vmpooler](https://github.com/briancain/vagrant-vmpooler) Use Vagrant to create and manage your VMPooler instances. ## Development and further documentation -For more information about setting up a development instance of vmpooler or other subjects, see the [docs/](docs) directory. +For more information about setting up a development instance of VMPooler or other subjects, see the [docs/](docs) directory. ## Build status [![Build Status](https://travis-ci.org/puppetlabs/vmpooler.png?branch=master)](https://travis-ci.org/puppetlabs/vmpooler) - ## License -vmpooler is distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). See the [LICENSE](LICENSE) file for more details. +VMPooler is distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). See the [LICENSE](LICENSE) file for more details. diff --git a/lib/vmpooler.rb b/lib/vmpooler.rb index 7d39f18..7bd7c22 100644 --- a/lib/vmpooler.rb +++ b/lib/vmpooler.rb @@ -7,7 +7,6 @@ module Vmpooler require 'net/ldap' require 'open-uri' require 'pickup' - require 'rbvmomi' require 'redis' require 'set' require 'sinatra/base' diff --git a/lib/vmpooler/pool_manager.rb b/lib/vmpooler/pool_manager.rb index 1c9551e..7c8c727 100644 --- a/lib/vmpooler/pool_manager.rb +++ b/lib/vmpooler/pool_manager.rb @@ -628,7 +628,7 @@ module Vmpooler end # @return [Array] - a list of used providers from the config file, defaults to the default providers - # ie. ["vsphere", "dummy"] + # ie. ["dummy"] def used_providers pools = config[:pools] || [] @used_providers ||= (pools.map { |pool| pool[:provider] || pool['provider'] }.compact + default_providers).uniq @@ -638,7 +638,7 @@ module Vmpooler # note: vsphere is the default if user does not specify although this should not be # if vsphere is to no longer be loaded by default please remove def default_providers - @default_providers ||= %w[vsphere dummy] + @default_providers ||= %w[dummy] end def get_pool_name_for_vm(vm_name, redis) @@ -1561,8 +1561,8 @@ module Vmpooler # Set default provider for all pools that do not have one defined $config[:pools].each do |pool| if pool['provider'].nil? - $logger.log('d', "[!] Setting provider for pool '#{pool['name']}' to 'vsphere' as default") - pool['provider'] = 'vsphere' + $logger.log('d', "[!] Setting provider for pool '#{pool['name']}' to 'dummy' as default") + pool['provider'] = 'dummy' end end diff --git a/lib/vmpooler/providers/vsphere.rb b/lib/vmpooler/providers/vsphere.rb deleted file mode 100644 index 1933f64..0000000 --- a/lib/vmpooler/providers/vsphere.rb +++ /dev/null @@ -1,1172 +0,0 @@ -# frozen_string_literal: true - -require 'vmpooler/providers/base' -require 'bigdecimal' -require 'bigdecimal/util' - -module Vmpooler - class PoolManager - class Provider - class VSphere < Vmpooler::PoolManager::Provider::Base - # The connection_pool method is normally used only for testing - attr_reader :connection_pool - - def initialize(config, logger, metrics, redis_connection_pool, name, options) - super(config, logger, metrics, redis_connection_pool, name, options) - - task_limit = global_config[:config].nil? || global_config[:config]['task_limit'].nil? ? 10 : global_config[:config]['task_limit'].to_i - # The default connection pool size is: - # Whatever is biggest from: - # - How many pools this provider services - # - Maximum number of cloning tasks allowed - # - Need at least 2 connections so that a pool can have inventory functions performed while cloning etc. - default_connpool_size = [provided_pools.count, task_limit, 2].max - connpool_size = provider_config['connection_pool_size'].nil? ? default_connpool_size : provider_config['connection_pool_size'].to_i - # The default connection pool timeout should be quite large - 60 seconds - connpool_timeout = provider_config['connection_pool_timeout'].nil? ? 60 : provider_config['connection_pool_timeout'].to_i - logger.log('d', "[#{name}] ConnPool - Creating a connection pool of size #{connpool_size} with timeout #{connpool_timeout}") - @connection_pool = Vmpooler::PoolManager::GenericConnectionPool.new( - metrics: metrics, - connpool_type: 'provider_connection_pool', - connpool_provider: name, - size: connpool_size, - timeout: connpool_timeout - ) do - logger.log('d', "[#{name}] Connection Pool - Creating a connection object") - # Need to wrap the vSphere connection object in another object. The generic connection pooler will preserve - # the object reference for the connection, which means it cannot "reconnect" by creating an entirely new connection - # object. Instead by wrapping it in a Hash, the Hash object reference itself never changes but the content of the - # Hash can change, and is preserved across invocations. - new_conn = connect_to_vsphere - { connection: new_conn } - end - @provider_hosts = {} - @provider_hosts_lock = Mutex.new - @redis = redis_connection_pool - end - - # name of the provider class - def name - 'vsphere' - end - - def folder_configured?(folder_title, base_folder, configured_folders, whitelist) - return true if whitelist&.include?(folder_title) - return false unless configured_folders.keys.include?(folder_title) - return false unless configured_folders[folder_title] == base_folder - - true - end - - def destroy_vm_and_log(vm_name, vm_object, pool, data_ttl) - try = 0 if try.nil? - max_tries = 3 - @redis.with_metrics do |redis| - redis.multi - redis.srem("vmpooler__completed__#{pool}", vm_name) - redis.hdel("vmpooler__active__#{pool}", vm_name) - redis.hset("vmpooler__vm__#{vm_name}", 'destroy', Time.now) - - # Auto-expire metadata key - redis.expire("vmpooler__vm__#{vm_name}", (data_ttl * 60 * 60)) - redis.exec - end - - start = Time.now - - if vm_object.is_a? RbVmomi::VIM::Folder - logger.log('s', "[!] [#{pool}] '#{vm_name}' is a folder, bailing on destroying") - raise('Expected VM, but received a folder object') - end - vm_object.PowerOffVM_Task.wait_for_completion if vm_object.runtime&.powerState && vm_object.runtime.powerState == 'poweredOn' - vm_object.Destroy_Task.wait_for_completion - - finish = format('%