diff --git a/lib/vmdb/settings.rb b/lib/vmdb/settings.rb index 435bb7dd5a3..04eecd37163 100644 --- a/lib/vmdb/settings.rb +++ b/lib/vmdb/settings.rb @@ -7,7 +7,6 @@ module Vmdb class Settings extend Vmdb::SettingsWalker::ClassMethods - include Vmdb::Logging class ConfigurationInvalid < StandardError attr_accessor :errors @@ -22,10 +21,9 @@ def initialize(errors) PASSWORD_FIELDS = Vmdb::SettingsWalker::PASSWORD_FIELDS DUMP_LOG_FILE = Rails.root.join("log/last_settings.txt").freeze - # RESET_COMMAND remove record for selected key and specific resource - # RESET_ALL_COMMAND remove all records for selected key and all resources - MAGIC_VALUES = [RESET_COMMAND = "<>".freeze, - RESET_ALL_COMMAND = "<>".freeze].freeze + # Magic value to reset a resource's setting to the parent's value + RESET_COMMAND = "<>".freeze + RESET_VALUE = HashDiffer::MissingKey cattr_accessor :last_loaded @@ -67,14 +65,14 @@ def self.valid? end def self.save!(resource, hash) - new_settings = build_without_local(resource).load!.merge!(hash).to_hash - settings_to_validate = remove_magic_values(new_settings.deep_clone) + new_settings = build_without_local(resource).load!.merge!(hash.deep_symbolize_keys).to_hash + replace_magic_values!(new_settings, resource) - valid, errors = validate(settings_to_validate) + valid, errors = validate(new_settings) raise ConfigurationInvalid.new(errors) unless valid # rubocop:disable Style/RaiseArgs - hash_for_parent = parent_settings_without_local(resource).load!.to_hash - diff = HashDiffer.diff(hash_for_parent, new_settings) + parent_settings = parent_settings_without_local(resource).load!.to_hash + diff = HashDiffer.diff(parent_settings, new_settings) encrypt_passwords!(diff) deltas = HashDiffer.diff_to_deltas(diff) apply_settings_changes(resource, deltas) @@ -189,13 +187,24 @@ def self.reset_settings_constant(settings) end private_class_method :reset_settings_constant + def self.replace_magic_values!(settings, resource) + parent_settings = nil + + walk(settings) do |key, value, path, owner| + next unless value == RESET_COMMAND + + parent_settings ||= parent_settings_without_local(resource).load!.to_hash + owner[key] = parent_settings.key_path?(path) ? parent_settings.fetch_path(path) : RESET_VALUE + end + end + private_class_method :replace_magic_values! + def self.apply_settings_changes(resource, deltas) resource.transaction do index = resource.settings_changes.index_by(&:key) deltas.each do |delta| record = index.delete(delta[:key]) - next if process_magic_value(resource, delta) if record record.update_attributes!(delta) else @@ -206,33 +215,5 @@ def self.apply_settings_changes(resource, deltas) end end private_class_method :apply_settings_changes - - def self.process_magic_value(resource, delta) - return false unless MAGIC_VALUES.include?(delta[:value]) - case delta[:value] - when RESET_COMMAND - _log.info("resetting #{delta[:key]} on #{resource.class} id:#{resource.id}") - resource.settings_changes.where("key LIKE ?", "#{delta[:key]}%").destroy_all - when RESET_ALL_COMMAND - _log.info("resetting #{delta[:key]} to default for all resorces") - SettingsChange.where("key LIKE ?", "#{delta[:key]}%").destroy_all - end - true - end - private_class_method :process_magic_value - - def self.walk_hash(hash, &block) - hash.each do |key, value| - yield key, value, hash - walk_hash(value, &block) if value&.class == Hash - end - end - private_class_method :walk_hash - - def self.remove_magic_values(hash) - walk_hash(hash) { |key, value, owner| owner.delete(key) if MAGIC_VALUES.include?(value) } - hash - end - private_class_method :remove_magic_values end end diff --git a/spec/lib/vmdb/settings_spec.rb b/spec/lib/vmdb/settings_spec.rb index c11039b51a5..ec7515558a6 100644 --- a/spec/lib/vmdb/settings_spec.rb +++ b/spec/lib/vmdb/settings_spec.rb @@ -266,82 +266,97 @@ expect(miq_server.zone.settings_changes.count).to eq 0 end - context "deleting entries" do - let(:server_value) { 1 } - let(:zone_value) { 2 } + describe described_class::RESET_COMMAND do + let(:server_value) { "server" } + let(:zone_value) { "zone" } + let(:region_value) { "region" } + let(:server_array_value) { [{:key1 => server_value}, {:key1 => server_value}] } + let(:zone_array_value) { [{:key1 => zone_value}, {:key1 => zone_value}] } - let(:reset) { ::Vmdb::Settings::RESET_COMMAND } - let(:reset_all) { ::Vmdb::Settings::RESET_ALL_COMMAND } - - let(:second_server) { FactoryGirl.create(:miq_server, :zone => miq_server.zone) } + let(:reset) { described_class::RESET_COMMAND } before do MiqRegion.seed - described_class.save!(miq_server, :api => {:token_ttl => server_value}, :session => {:timeout => server_value}) - described_class.save!(miq_server, :api => {:new_key => "new value"}) - described_class.save!(second_server, :api => {:token_ttl => server_value}, :session => {:timeout => server_value}) + described_class.save!( + MiqRegion.first, + :api => { + :token_ttl => region_value, + :authentication_timeout => region_value + }, + :session => {:timeout => 2} + ) + described_class.save!( + miq_server.zone, + :api => { + :token_ttl => zone_value + }, + :array => zone_array_value + ) + described_class.save!( + miq_server, + :api => { + :token_ttl => server_value, + :authentication_timeout => server_value, + :new_key => "new value" + }, + :session => {:timeout => 1}, + :array => server_array_value + ) + end + + it "inherits a leaf-level value from the parent" do + described_class.save!(miq_server, :api => {:token_ttl => reset}) - described_class.save!(miq_server.zone, :api => {:token_ttl => zone_value}, :session => {:timeout => zone_value}) - described_class.save!(miq_server, :array => [:element1 => 1, :element2 => 2]) - expect(SettingsChange.count).to eq 8 + expect(described_class.for_resource(miq_server).api.token_ttl).to eq zone_value end - context "magic value <>" do - it "deletes key-value for specific key for the resource if specified on leaf level" do - described_class.save!(miq_server, :api => {:token_ttl => reset}) + it "inherits a node-level value from the parent" do + described_class.save!(miq_server, :api => reset) - expect(SettingsChange.count).to eq 7 - expect(miq_server.settings_changes.find_by(:key => "/api/token_ttl")).to be nil - expect(Vmdb::Settings.for_resource(miq_server).api.new_key).to eq "new value" - expect(Vmdb::Settings.for_resource(second_server).api.token_ttl).to eq server_value - end + expect(described_class.for_resource(miq_server).api.token_ttl).to eq zone_value + expect(described_class.for_resource(miq_server).api.authentication_timeout).to eq region_value + end - it "deletes current node and all sub-nodes for the resource if specified on node level" do - described_class.save!(miq_server, :api => reset) + it "inherits an array from the parent" do + described_class.save!(miq_server, :array => reset) - expect(SettingsChange.count).to eq 6 - expect(miq_server.settings_changes.where("key LIKE ?", "/api%").count).to eq 0 - expect(Vmdb::Settings.for_resource(miq_server).api.new_key).to eq nil - expect(Vmdb::Settings.for_resource(second_server).api.token_ttl).to eq server_value - end + expect(described_class.for_resource(miq_server).to_hash[:array]).to eq zone_array_value + end - it "deletes new key-value settings not present in defaul yaml" do - described_class.save!(miq_server, :api => {:new_key => reset}) - expect(Vmdb::Settings.for_resource(miq_server).api.new_key).to eq nil - end + it "deletes a leaf-level value not present in the parent" do + described_class.save!(miq_server, :api => {:new_key => reset}) - it "deletes array" do - described_class.save!(miq_server, :array => reset) - expect(Vmdb::Settings.for_resource(miq_server).array).to be nil - end + expect(described_class.for_resource(miq_server).api.new_key).to be_nil + expect(miq_server.reload.settings_changes.where(:key => "/api/new_key")).to_not exist + end - it "passes validation" do - described_class.save!(miq_server, :session => {:timeout => reset}) - expect(Vmdb::Settings.for_resource(miq_server).session.timeout).to eq zone_value - end + it "deletes a leaf-level value not present in the parent when reset at the node level" do + described_class.save!(miq_server, :api => reset) + + expect(described_class.for_resource(miq_server).api.new_key).to be_nil + expect(described_class.for_resource(miq_server).api.token_ttl).to eq zone_value + expect(miq_server.reload.settings_changes.where(:key => "/api/new_key")).to_not exist end - context "magic value <>" do - it "deletes all key-value for specific key for all resource if specified on leaf levelher" do - described_class.save!(miq_server, :api => {:token_ttl => reset_all}) + it "deletes an array value not present in the parent" do + described_class.save!(miq_server.zone, :array => reset) - expect(SettingsChange.where("key LIKE ?", "/api/token_ttl").count).to eq 0 - expect(SettingsChange.where("key LIKE ?", "/api%").count).to eq 1 - expect(SettingsChange.where("key LIKE ?", "/session%").count).to eq 3 - end + expect(described_class.for_resource(miq_server.zone).array).to be_nil + expect(miq_server.zone.reload.settings_changes.where(:key => "/array")).to_not exist + end - it "deletes specific node and all sub-nodes for all resources if specified on node level" do - described_class.save!(miq_server, :api => reset_all) + it "at a parent level does not push down changes to children" do + described_class.save!(miq_server.zone, :api => {:token_ttl => reset}) - expect(SettingsChange.where("key LIKE ?", "/api%").count).to eq 0 - expect(SettingsChange.where("key LIKE ?", "/session%").count).to eq 3 - end + expect(described_class.for_resource(miq_server.zone).api.token_ttl).to eq region_value + expect(described_class.for_resource(miq_server).api.token_ttl).to eq server_value + end - it "passes validation" do - described_class.save!(miq_server, :session => {:timeout => reset_all}) - expect(SettingsChange.where("key LIKE ?", "/session%").count).to eq 0 - end + it "passes validation" do + described_class.save!(miq_server, :session => {:timeout => reset}) + + expect(described_class.for_resource(miq_server).session.timeout).to eq 2 end end end