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

Support PostgreSQL #8

Merged
merged 3 commits into from Jan 20, 2016
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion Gemfile
@@ -1,10 +1,14 @@
source 'https://rubygems.org'
gemspec

if RUBY_VERSION.start_with? '1.8'
gem 'pg', '< 0.18.0'
end

group :development do
gem 'smart_proxy', :github => 'theforeman/smart-proxy', :branch => 'develop'
end

group :test do
gem 'test-unit'
gem 'test-unit' unless RUBY_VERSION.start_with? '1.8'
end
20 changes: 18 additions & 2 deletions README.md
Expand Up @@ -7,25 +7,41 @@ This plugin adds a new DNS provider for managing records in PowerDNS.
See [How_to_Install_a_Smart-Proxy_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Smart-Proxy_Plugin)
for how to install Smart Proxy plugins

This plugin is compatible with Smart Proxy 1.10 or higher.
This plugin is compatible with Smart Proxy 1.11 or higher.

When installing using "gem", make sure to install the bundle file:

echo "gem 'smart_proxy_dns_powerdns'" > /usr/share/foreman-proxy/bundler.d/dns_powerdns.rb

## Upgrading

Per version 0.2.0 the backend is a required parameter.

## Configuration

To enable this DNS provider, edit `/etc/foreman-proxy/settings.d/dns.yml` and set:

:use_provider: dns_powerdns

Configuration options for this plugin are in `/etc/foreman-proxy/settings.d/dns_powerdns.yml` and include:
Configuration options for this plugin are in `/etc/foreman-proxy/settings.d/dns_powerdns.yml`.

### MySQL

To use MySQL, set the following parameters:

:powerdns_backend: 'mysql'
:powerdns_mysql_hostname: 'localhost'
:powerdns_mysql_username: 'powerdns'
:powerdns_mysql_password: ''
:powerdns_mysql_database: 'powerdns'

### PostgreSQL

To use PostgreSQL, set the following parameters:

:powerdns_backend: 'postgresql'
:powerdns_postgresql_connection: 'host=localhost user=powerdns password=mypassword dbname=powerdns'

### DNSSEC

In case you've enabled DNSSEC (as you should), a rectify-zone is required after every zone change. The pdnssec command is configurable:
Expand Down
24 changes: 24 additions & 0 deletions lib/smart_proxy_dns_powerdns/backend/dummy.rb
@@ -0,0 +1,24 @@
module Proxy::Dns::Powerdns::Backend
class Dummy < ::Proxy::Dns::Powerdns::Record

def initialize(a_server = nil, a_ttl = nil)
super(a_server, a_ttl)
end

def get_zone name
{
'id' => 1,
'name' => name.partition('.')[2]
}
end

def create_record domain_id, name, ttl, content, type
false
end

def delete_record domain_id, name, type
false
end
end
end

49 changes: 49 additions & 0 deletions lib/smart_proxy_dns_powerdns/backend/mysql.rb
@@ -0,0 +1,49 @@
require 'mysql2'

module Proxy::Dns::Powerdns::Backend
class Mysql < ::Proxy::Dns::Powerdns::Record

attr_reader :hostname, :username, :password, :database

def initialize(a_server = nil, a_ttl = nil)
@hostname = Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_hostname || 'localhost'
@username = Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_username
@password = Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_password
@database = Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_database

super(a_server, a_ttl)
end

def connection
@connection ||= Mysql2::Client.new(:host => hostname, :username => username, :password => password, :database => database)
end

def get_zone name
domain = nil

name = connection.escape(name)
connection.query("SELECT LENGTH(name) domain_length, id, name FROM domains WHERE '#{name}' LIKE CONCAT('%%.', name) ORDER BY domain_length DESC LIMIT 1").each do |row|
domain = row
end

raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS." unless domain

domain
end

def create_record domain_id, name, ttl, content, type
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}')")
true
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}'")
true
end
end
end
42 changes: 42 additions & 0 deletions lib/smart_proxy_dns_powerdns/backend/postgresql.rb
@@ -0,0 +1,42 @@
require 'pg'

module Proxy::Dns::Powerdns::Backend
class Postgresql < ::Proxy::Dns::Powerdns::Record

attr_reader :connection_str

def initialize(a_server = nil, a_ttl = nil)
@connection_str = Proxy::Dns::Powerdns::Plugin.settings.powerdns_postgresql_connection

super(a_server, a_ttl)
end

def connection
@connection ||= PG.connect(connection_str)
end

def get_zone name
domain = nil

connection.exec_params("SELECT LENGTH(name) domain_length, id, name FROM domains WHERE $1 LIKE CONCAT('%%.', name) ORDER BY domain_length DESC LIMIT 1", [name]) do |result|
result.each do |row|
domain = row
end
end

raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS." unless domain

domain
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.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
end
end
end
15 changes: 15 additions & 0 deletions lib/smart_proxy_dns_powerdns/dependencies.rb
@@ -0,0 +1,15 @@
require 'dns_common/dependency_injection/dependencies'

class Proxy::Dns::DependencyInjection::Dependencies
case Proxy::Dns::Powerdns::Plugin.settings.powerdns_backend
when 'mysql'
require 'smart_proxy_dns_powerdns/backend/mysql'
dependency :dns_provider, Proxy::Dns::Powerdns::Backend::Mysql
when 'postgresql'
require 'smart_proxy_dns_powerdns/backend/postgresql'
dependency :dns_provider, Proxy::Dns::Powerdns::Backend::Postgresql
when 'dummy'
require 'smart_proxy_dns_powerdns/backend/dummy'
dependency :dns_provider, Proxy::Dns::Powerdns::Backend::Dummy
end
end
@@ -0,0 +1,32 @@
require 'smart_proxy_dns_powerdns/dns_powerdns_plugin'

module Proxy::Dns::Powerdns
class ConfigurationValidator
def validate_settings!(settings)
validate_choice(settings, :powerdns_backend, ['mysql', 'postgresql', 'dummy'])

case settings.powerdns_backend
when 'mysql'
validate_presence(settings, [:powerdns_mysql_username, :powerdns_mysql_password, :powerdns_mysql_database])
when 'postgresql'
validate_presence(settings, [:powerdns_postgresql_connection])
end
end

def validate_choice(settings, setting, choices)
value = settings.send(setting)
unless choices.include?(value)
raise ::Proxy::Error::ConfigurationError, "Parameter '#{setting}' is expected to be one of #{choices.join(",")}"
end
true
end

def validate_presence(settings, names)
names.each do |name|
value = settings.send(name)
raise ::Proxy::Error::ConfigurationError, "Parameter '#{name}' is expected to have a non-empty value" if value.nil? || value.to_s.empty?
end
true
end
end
end
123 changes: 38 additions & 85 deletions lib/smart_proxy_dns_powerdns/dns_powerdns_main.rb
@@ -1,120 +1,73 @@
require 'dns/dns'
require 'dns_common/dns_common'
require 'ipaddr'
require 'mysql2'

module Proxy::Dns::Powerdns
class Record < ::Proxy::Dns::Record
include Proxy::Log
include Proxy::Util

attr_reader :mysql_connection, :powerdns_pdnssec
attr_reader :pdnssec

def self.record(attrs = {})
new(attrs.merge(
:powerdns_mysql_hostname => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_hostname,
:powerdns_mysql_username => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_username,
:powerdns_mysql_password => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_password,
:powerdns_mysql_database => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_database,
:powerdns_pdnssec => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_pdnssec
))
def initialize(a_server = nil, a_ttl = nil)
@pdnssec = Proxy::Dns::Powerdns::Plugin.settings.powerdns_pdnssec
super(a_server, a_ttl || Proxy::Dns::Plugin.settings.dns_ttl)
end

def initialize options = {}
raise "dns_powerdns provider needs 'powerdns_mysql_hostname' option" unless options[:powerdns_mysql_hostname]
raise "dns_powerdns provider needs 'powerdns_mysql_username' option" unless options[:powerdns_mysql_username]
raise "dns_powerdns provider needs 'powerdns_mysql_password' option" unless options[:powerdns_mysql_password]
raise "dns_powerdns provider needs 'powerdns_mysql_database' option" unless options[:powerdns_mysql_database]
@mysql_connection = Mysql2::Client.new(
:host => options[:powerdns_mysql_hostname],
:username => options[:powerdns_mysql_username],
:password => options[:powerdns_mysql_password],
:database => options[:powerdns_mysql_database]
)

@powerdns_pdnssec = options[:powerdns_pdnssec] || false

# Normalize the somewhat weird PTR API spec to name / content
case options[:type]
when "PTR"
if options[:value] =~ /\.(in-addr|ip6)\.arpa$/
@name = options[:value]
else
@name = IPAddr.new(options[:value]).reverse
end
@content = options[:fqdn]
else
@name = options[:fqdn]
@content = options[:value]
def create_a_record(fqdn, ip)
if found = dns_find(fqdn)
raise Proxy::Dns::Collision, "#{fqdn} is already in use by #{ip}" unless found == ip
end

super(options)
do_create(fqdn, ip, "A")
end

def create
domain_row = domain
raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS." unless domain_row

if ip = dns_find(domain_row['id'], @name)
raise Proxy::Dns::Collision, "#{@name} is already in use by #{ip}"
def create_ptr_record(fqdn, ip)
if found = dns_find(ip)
raise Proxy::Dns::Collision, "#{ip} is already in use by #{found}" unless found == fqdn
end

create_record(domain_row['id'], @name, @ttl, @content, @type)

rectify_zone(domain_row['name'])
name = IPAddr.new(ip).reverse
do_create(name, fqdn, "PTR")
end

def remove
domain_row = domain
raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS." unless domain_row

delete_record(domain_row['id'], @name, @type)

rectify_zone(domain_row['name'])
def do_create(name, value, type)
zone = get_zone(name)
create_record(zone['id'], name, type, value) and rectify_zone(zone['name'])
end

private
def domain
domain = nil
def remove_a_record(fqdn)
do_remove(fqdn, "A")
end

name = mysql_connection.escape(@name)
mysql_connection.query("SELECT LENGTH(name) domain_length, id, name FROM domains WHERE '#{name}' LIKE CONCAT('%%.', name) ORDER BY domain_length DESC LIMIT 1").each do |row|
domain = row
end
def remove_ptr_record(ip)
name = ip # Note ip is already in-addr.arpa
do_remove(name, "PTR")
end

domain
def do_remove(name, type)
zone = get_zone(name)
delete_record(zone['id'], name, type) and rectify_zone(zone['name'])
end

private
def dns_find domain_id, key
value = nil
key = mysql_connection.escape(key)
mysql_connection.query("SELECT content FROM records WHERE domain_id=#{domain_id} AND name = '#{key}' LIMIT 1").each do |row|
value = row["content"]
end
value || false
def get_zone(fqdn)
# TODO: backend specific
raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS."
end

private
def create_record domain_id, name, ttl, content, type
name = mysql_connection.escape(name)
content = mysql_connection.escape(content)
type = mysql_connection.escape(type)
mysql_connection.query("INSERT INTO records (domain_id, name, ttl, content, type) VALUES (#{domain_id}, '#{name}', #{ttl.to_i}, '#{content}', '#{type}')")
true
def create_record(domain_id, name, type, content)
# TODO: backend specific
false
end

private
def delete_record domain_id, name, type
name = mysql_connection.escape(name)
type = mysql_connection.escape(type)
mysql_connection.query("DELETE FROM records WHERE domain_id=#{domain_id} AND name='#{name}' AND type='#{type}'")
true
def delete_record(domain_id, name, type)
# TODO: backend specific
false
end

private
def rectify_zone domain
if @powerdns_pdnssec
%x(#{@powerdns_pdnssec} rectify-zone "#{domain}")
if @pdnssec
%x(#{@pdnssec} rectify-zone "#{domain}")

$?.exitstatus == 0
else
Expand Down