diff --git a/app/models/domain.rb b/app/models/domain.rb index ca828fb..2fdf4c6 100644 --- a/app/models/domain.rb +++ b/app/models/domain.rb @@ -1,28 +1,33 @@ class Domain < ActiveRecord::Base self.inheritance_column = :nx def self.domain_types [ 'NATIVE', 'MASTER', 'SLAVE', ] end has_many :records has_one :soa, class_name: SOA validates :name, uniqueness: true, presence: true validates :type, presence: true, inclusion: { in: domain_types } after_create :generate_soa + attr_writer :serial_strategy + def serial_strategy + @serial_strategy ||= WebDNS.settings[:serial_strategy] + end + private # Hooks def generate_soa soa_record = SOA.new(domain: self) soa_record.save end end diff --git a/app/models/soa.rb b/app/models/soa.rb index 8cf823d..bac20ac 100644 --- a/app/models/soa.rb +++ b/app/models/soa.rb @@ -1,73 +1,77 @@ class SOA < Record validates :domain_id, uniqueness: true validates_numericality_of :serial, :refresh, :retry, :expire validates_presence_of :contact SOA_DEFAULTS = WebDNS.settings[:soa_defaults] SOA_FIELDS = SOA_DEFAULTS.keys SOA_FIELDS.each { |soa_entry| attr_accessor soa_entry } # Handle SOA Fields after_initialize :set_soa_fields # Load soa fields on reload def reload_with_soa_fields(*args) reload_without_soa_fields(*args).tap { set_soa_fields } end alias_method_chain :reload, :soa_fields before_validation :set_content before_validation :update_serial, on: :update def bump_serial! with_lock { reload - self.serial += 1 + generate_serial save! } end def serial_changed? return false if not self.content_changed? serial_index = SOA_FIELDS.index(:serial) old, new = content_change.map { |c| (c || '').split(/\s+/)[serial_index] } old != new end private + def generate_serial + self.serial = domain.serial_strategy.generate_serial(serial) + end + # Callbacks def set_soa_fields content_values = (content || '').split(/\s+/) SOA_DEFAULTS.each { |field, default_value| val = content_values.shift || default_value val = Integer(val) if default_value.is_a?(Integer) send("#{field}=", val) } end def set_content self.content = SOA_FIELDS.map { |field| send(field) }.join(' ') end def update_serial # Don't update if nothing has changed return if not self.content_changed? # Don't updade if serial is already changed return if self.serial_changed? - self.serial += 1 + generate_serial set_content end end diff --git a/config/initializers/00_settings.rb b/config/initializers/00_settings.rb index 991c443..88695d4 100644 --- a/config/initializers/00_settings.rb +++ b/config/initializers/00_settings.rb @@ -1,12 +1,13 @@ WebDNS = Base WebDNS.settings[:soa_defaults] = { primary_ns: 'ns.example.com', contact: 'domainmaster@example.com', serial: 1, refresh: 10_800, retry: 3600, expire: 604_800, nx: 3600 } +WebDNS.settings[:serial_strategy] = Strategies::Date diff --git a/lib/strategies/date.rb b/lib/strategies/date.rb new file mode 100644 index 0000000..ed4e145 --- /dev/null +++ b/lib/strategies/date.rb @@ -0,0 +1,19 @@ +module Strategies + module Date + module_function + + def generate_serial(current_serial) + # Optimization for the case that current_serial is a lot larger + # than the generated serial + new = [ + Time.now.strftime('%Y%m%d00').to_i, + current_serial + ].max + + # Increment until we find a spot + new += 1 while new <= current_serial + + new + end + end +end diff --git a/lib/strategies/incremental.rb b/lib/strategies/incremental.rb new file mode 100644 index 0000000..75a4772 --- /dev/null +++ b/lib/strategies/incremental.rb @@ -0,0 +1,9 @@ +module Strategies + module Incremental + module_function + + def generate_serial(current_serial) + current_serial + 1 + end + end +end diff --git a/test/factories/domain.rb b/test/factories/domain.rb index f216127..6ff5abf 100644 --- a/test/factories/domain.rb +++ b/test/factories/domain.rb @@ -1,7 +1,14 @@ FactoryGirl.define do sequence(:domain) { |n| "example#{n}.com" } factory :domain do name { generate(:domain) } + serial_strategy Strategies::Date + type 'NATIVE' + end + + factory :date_domain, class: Domain do + name { generate(:domain) } + serial_strategy Strategies::Date type 'NATIVE' end end diff --git a/test/models/soa_test.rb b/test/models/soa_test.rb index 3ad4cfa..a96a30f 100644 --- a/test/models/soa_test.rb +++ b/test/models/soa_test.rb @@ -1,25 +1,60 @@ require 'test_helper' class SOATest < ActiveSupport::TestCase def setup domain = create(:domain) @record = domain.soa end test 'bump_serial!' do @record.save! assert_serial_update @record do @record.bump_serial! end end test 'updating attributes bumps serial' do @record.save! assert_serial_update @record do @record.contact = 'admin@example.com' @record.save! end end + + class DateSerialTests < ActiveSupport::TestCase + setup do + domain = create(:date_domain) + @record = domain.soa + end + + test 'last bump of the day' do + assert_equal Strategies::Date, @record.domain.serial_strategy + + freeze_time do + last_for_day = Time.now.strftime('%Y%m%d99').to_i + @record.serial = last_for_day + @record.save! + + assert_serial_update @record do + @record.bump_serial! + end + end + end + + test 'existing serial points to a future date' do + assert_equal Strategies::Date, @record.domain.serial_strategy + + freeze_time do + future_day = (Time.now + 1.week).strftime('%Y%m%d00').to_i + @record.serial = future_day + @record.save! + + assert_serial_update @record do + @record.bump_serial! + end + end + end + end end