Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update change_date field of records for MySQL and PostgreSQL #34

Merged
merged 3 commits into from
Jan 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ Or a more complex example:

Note that PowerDNS 4.x now uses `pdnsutil` rather than `pdnssec`.

### SOA autoserial with MySQL and PostgreSQL

PowerDNS (>= 3.3) provides a feature called `autoserial` that takes care of managing the serial of `SOA` records.

There are many options available regarding how PowerDNS generates the serial and details can be found looking for the `SOA-EDIT` option in PowerDNS.

One option is to let the PowerDNS backend determine the `SOA` serial using the biggest `change_date` of the records associated with the DNS domain.
`smart_proxy_dns_powerdns` uses this approach and updates the `change_date` field of changed records, setting them to the current timestamp of the database server, represented as **the number of seconds since EPOCH**.

* when a new record is created, its `change_date` is set accordingly
* when a record is deleted, the `change_date` of the `SOA` record for the domain is updated

## Contributing

Fork and send a Pull Request. Thanks!
Expand Down
13 changes: 11 additions & 2 deletions lib/smart_proxy_dns_powerdns/backend/mysql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,24 @@ def create_record domain_id, name, type, content
name = connection.escape(name)
content = connection.escape(content)
type = connection.escape(type)
connection.query("INSERT INTO records (domain_id, name, ttl, content, type) VALUES (#{domain_id}, '#{name}', #{ttl.to_i}, '#{content}', '#{type}')")
connection.query("INSERT INTO records (domain_id, name, ttl, content, type, change_date) VALUES (#{domain_id}, '#{name}', #{ttl.to_i}, '#{content}', '#{type}', UNIX_TIMESTAMP())")
connection.affected_rows == 1
end

def delete_record domain_id, name, type
name = connection.escape(name)
type = connection.escape(type)
connection.query("DELETE FROM records WHERE domain_id=#{domain_id} AND name='#{name}' AND type='#{type}'")
connection.affected_rows >= 1
return false if connection.affected_rows == 0

connection.query("UPDATE records SET change_date=UNIX_TIMESTAMP() WHERE domain_id=#{domain_id} AND type='SOA'")
affected_rows = connection.affected_rows
if affected_rows > 1
logger.warning("Updated multiple SOA records (host=#{name}, domain_id=#{domain_id}). Check your zone records for duplicate SOA entries.")
elsif affected_rows == 0
logger.info("No SOA record updated (host=#{name}, domain_id=#{domain_id}). This can be caused by either a missing SOA record for the zone or consecutive updates of the same zone during the same second.")
end
true
end
end
end
13 changes: 11 additions & 2 deletions lib/smart_proxy_dns_powerdns/backend/postgresql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,22 @@ def get_zone name
end

def create_record domain_id, name, type, content
result = connection.exec_params("INSERT INTO records (domain_id, name, ttl, content, type) VALUES ($1::int, $2, $3::int, $4, $5)", [domain_id, name, ttl, content, type])
result = connection.exec_params("INSERT INTO records (domain_id, name, ttl, content, type, change_date) VALUES ($1::int, $2, $3::int, $4, $5, extract(epoch from now()))", [domain_id, name, ttl, content, type])
result.cmdtuples == 1
end

def delete_record domain_id, name, type
result = connection.exec_params("DELETE FROM records WHERE domain_id=$1::int AND name=$2 AND type=$3", [domain_id, name, type])
result.cmdtuples >= 1
return false if result.cmdtuples == 0

result = connection.exec_params("UPDATE records SET change_date=extract(epoch from now()) WHERE domain_id=$1::int AND type='SOA'", [domain_id])
affected_rows = result.cmdtuples
if affected_rows > 1
logger.warning("Updated multiple SOA records (host=#{name}, domain_id=#{domain_id}). Check your zone records for duplicate SOA entries.")
elsif affected_rows == 0
logger.info("No SOA record updated (host=#{name}, domain_id=#{domain_id}). This can be caused by either a missing SOA record for the zone or consecutive updates of the same zone during the same second.")
end
true
end
end
end
69 changes: 63 additions & 6 deletions test/unit/dns_powerdns_record_mysql_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,76 @@ def test_create_record
@connection.expects(:escape).with('test.example.com').returns('test.example.com')
@connection.expects(:escape).with('A').returns('A')
@connection.expects(:escape).with('10.1.1.1').returns('10.1.1.1')
@connection.expects(:query).with("INSERT INTO records (domain_id, name, ttl, content, type) VALUES (1, 'test.example.com', 86400, '10.1.1.1', 'A')")
@connection.expects(:query).with("INSERT INTO records (domain_id, name, ttl, content, type, change_date) VALUES (1, 'test.example.com', 86400, '10.1.1.1', 'A', UNIX_TIMESTAMP())")
@connection.expects(:affected_rows).returns(1)

assert @provider.create_record(1, 'test.example.com', 'A', '10.1.1.1')
end

def test_delete_record
@connection.expects(:escape).with('test.example.com').returns('test.example.com')
@connection.expects(:escape).with('A').returns('A')
@connection.expects(:query).with("DELETE FROM records WHERE domain_id=1 AND name='test.example.com' AND type='A'")
@connection.expects(:affected_rows).returns(1)
mock_escapes(fqdn, 'A')
@connection.expects(:query).with(query_delete)
@connection.expects(:query).with(query_update_soa)
@connection.expects(:affected_rows).twice.returns(1)
assert @provider.delete_record(domain_id, fqdn, 'A')
end

def test_delete_no_record
mock_escapes(fqdn, 'A')
@connection.expects(:query).with(query_delete)
@connection.expects(:affected_rows).returns(0)

assert_false @provider.delete_record(domain_id, fqdn, 'A')
end

def test_delete_record_no_soa
mock_escapes(fqdn, 'A')
@connection.expects(:query).with(query_delete)
@connection.expects(:query).with(query_update_soa)
@connection.expects(:affected_rows).twice.returns(1, 0)
logger = mock()
logger.expects(:info)
@provider.stubs(:logger).returns(logger)

assert @provider.delete_record(domain_id, fqdn, 'A')
end

def test_delete_record_multiple_soa
mock_escapes(fqdn, 'A')
@connection.expects(:query).with(query_delete)
@connection.expects(:query).with(query_update_soa)
@connection.expects(:affected_rows).twice.returns(1, 2)
logger = mock()
logger.expects(:warning)
@provider.stubs(:logger).returns(logger)

assert @provider.delete_record(domain_id, fqdn, 'A')
end

private

def mock_escapes(*elts)
elts.each { |e| @connection.expects(:escape).with(e).returns(e) }
end

def domain
'example.com'
end

def fqdn
"test.#{domain}"
end

def domain_id
1
end

def query_delete(type='A')
"DELETE FROM records WHERE domain_id=#{domain_id} AND name='#{fqdn}' AND type='#{type}'"
end

assert @provider.delete_record(1, 'test.example.com', 'A')
def query_update_soa
"UPDATE records SET change_date=UNIX_TIMESTAMP() WHERE domain_id=#{domain_id} AND type='SOA'"
end

end
84 changes: 71 additions & 13 deletions test/unit/dns_powerdns_record_postgresql_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,33 +35,91 @@ def test_get_zone_without_existing_zone

def test_create_record
@connection.expects(:exec_params).
with("INSERT INTO records (domain_id, name, ttl, content, type) VALUES ($1::int, $2, $3::int, $4, $5)", [1, 'test.example.com', 86400, '10.1.1.1', 'A']).
with("INSERT INTO records (domain_id, name, ttl, content, type, change_date) VALUES ($1::int, $2, $3::int, $4, $5, extract(epoch from now()))", [1, 'test.example.com', 86400, '10.1.1.1', 'A']).
returns(mock(:cmdtuples => 1))

assert_true @provider.create_record(1, 'test.example.com', 'A', '10.1.1.1')
end

def test_delete_record_no_records
@connection.expects(:exec_params).
with("DELETE FROM records WHERE domain_id=$1::int AND name=$2 AND type=$3", [1, 'test.example.com', 'A']).
returns(mock(:cmdtuples => 0))

assert_false @provider.delete_record(1, 'test.example.com', 'A')
mock_delete_tuples(0)
assert_false run_delete_record
end

def test_delete_record_single_record
@connection.expects(:exec_params).
with("DELETE FROM records WHERE domain_id=$1::int AND name=$2 AND type=$3", [1, 'test.example.com', 'A']).
returns(mock(:cmdtuples => 1))
mock_delete_tuples(1)
mock_update_soa_tuples(1)

assert_true @provider.delete_record(1, 'test.example.com', 'A')
assert_true run_delete_record
end

def test_delete_record_multiple_records
mock_delete_tuples(2)
mock_update_soa_tuples(1)

assert_true run_delete_record
end

def test_delete_record_no_soa
mock_delete_tuples(1)
mock_update_soa_tuples(0)
logger = mock()
logger.expects(:info)
@provider.stubs(:logger).returns(logger)

assert_true run_delete_record
end

def test_delete_record_multiple_soa
mock_delete_tuples(1)
mock_update_soa_tuples(2)
logger = mock()
logger.expects(:warning)
@provider.stubs(:logger).returns(logger)

assert_true run_delete_record
end

private

def mock_delete_tuples(cmdtuples)
@connection.expects(:exec_params).
with("DELETE FROM records WHERE domain_id=$1::int AND name=$2 AND type=$3", [1, 'test.example.com', 'A']).
returns(mock(:cmdtuples => 2))
with(query_delete, [domain_id, fqdn, record_type]).
returns(mock(:cmdtuples => cmdtuples))
end

assert_true @provider.delete_record(1, 'test.example.com', 'A')
def mock_update_soa_tuples(cmdtuples)
@connection.expects(:exec_params).
with(query_update_soa, [domain_id]).
returns(mock(:cmdtuples => cmdtuples))
end

def run_delete_record
@provider.delete_record(domain_id, fqdn, record_type)
end

def domain
'example.com'
end

def fqdn
"test.#{domain}"
end

def domain_id
1
end

def record_type
'A'
end

def query_delete
"DELETE FROM records WHERE domain_id=$1::int AND name=$2 AND type=$3"
end

def query_update_soa
"UPDATE records SET change_date=extract(epoch from now()) WHERE domain_id=$1::int AND type='SOA'"
end

end