smart_proxy_dns_powerdns 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d7c3fb0d2793d1ed7570954bb2c235db1e700895
4
- data.tar.gz: 6359e73f07094d0aebcdf1a6f90b862b969ae944
3
+ metadata.gz: c0bc4acefdd0c176ea6f1d9fcbb4037f36da5606
4
+ data.tar.gz: 4386be7b8b5d77463f91fd005a35735893fa64f4
5
5
  SHA512:
6
- metadata.gz: beef8fc52fe5364a152d7de8dde2a5171898b0bf61e12b23cc16ad8e9f551b3ece9ae040b87b4e3612fc36c076ec17d87a565242668f2eeb02a15ec705ffbfae
7
- data.tar.gz: 4fdc289bb30b53b9ffa2dfc7c2b8b93a561b4461757f113a7697cce92e451edf9d17c5683f03477008cd4808f5c3e3ec74bab3933d95aea5a2a98ce34ba5d9fb
6
+ metadata.gz: 2a4143021040bf61c3de7509fe63db0c3951cc3bc3a3f42ab8ce3f9e95777228282063801cae652de32b7e8d51e5fe9f18a61b66e5061021fc0934cf05d692b3
7
+ data.tar.gz: 6ef93f4db7399b45c31937ba71a5a5fbb208880a5ec503c4a3c82ff7255167850f78ce9a6f7951dc4beb6dc7e612105ac5531116da6be32c6a33140f26e2d237
data/README.md CHANGED
@@ -7,7 +7,15 @@ This plugin adds a new DNS provider for managing records in PowerDNS.
7
7
  See [How_to_Install_a_Smart-Proxy_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Smart-Proxy_Plugin)
8
8
  for how to install Smart Proxy plugins
9
9
 
10
- This plugin is compatible with Smart Proxy 1.10 or higher.
10
+ This plugin is compatible with Smart Proxy 1.11 or higher.
11
+
12
+ When installing using "gem", make sure to install the bundle file:
13
+
14
+ echo "gem 'smart_proxy_dns_powerdns'" > /usr/share/foreman-proxy/bundler.d/dns_powerdns.rb
15
+
16
+ ## Upgrading
17
+
18
+ Per version 0.2.0 the backend is a required parameter.
11
19
 
12
20
  ## Configuration
13
21
 
@@ -15,13 +23,25 @@ To enable this DNS provider, edit `/etc/foreman-proxy/settings.d/dns.yml` and se
15
23
 
16
24
  :use_provider: dns_powerdns
17
25
 
18
- Configuration options for this plugin are in `/etc/foreman-proxy/settings.d/dns_powerdns.yml` and include:
26
+ Configuration options for this plugin are in `/etc/foreman-proxy/settings.d/dns_powerdns.yml`.
27
+
28
+ ### MySQL
19
29
 
30
+ To use MySQL, set the following parameters:
31
+
32
+ :powerdns_backend: 'mysql'
20
33
  :powerdns_mysql_hostname: 'localhost'
21
34
  :powerdns_mysql_username: 'powerdns'
22
35
  :powerdns_mysql_password: ''
23
36
  :powerdns_mysql_database: 'powerdns'
24
37
 
38
+ ### PostgreSQL
39
+
40
+ To use PostgreSQL, set the following parameters:
41
+
42
+ :powerdns_backend: 'postgresql'
43
+ :powerdns_postgresql_connection: 'host=localhost user=powerdns password=mypassword dbname=powerdns'
44
+
25
45
  ### DNSSEC
26
46
 
27
47
  In case you've enabled DNSSEC (as you should), a rectify-zone is required after every zone change. The pdnssec command is configurable:
@@ -38,22 +58,20 @@ Fork and send a Pull Request. Thanks!
38
58
 
39
59
  ### Running the integration tests
40
60
 
41
- Since I'm mostly a python developer, I've written the integration tests in python.
42
-
43
- First you need to run the smart proxy on `http://localhost:8000` and a powerdns instance on `127.0.0.1:5300` or change it in the fixtures.
61
+ First you need to run the smart proxy on `http://localhost:8000` and a powerdns instance on `127.0.0.1:5300`.
44
62
 
45
63
  It is assumed the powerdns instance has both the `example.com` and `in-addr.arpa` domains configured. If not, create them:
46
64
 
47
65
  INSERT INTO domains (name, type) VALUES ('example.com', 'master'), ('in-addr.arpa', 'master');
48
66
  INSERT INTO records (domain_id, name, type, content) SELECT id domain_id, name, 'SOA', 'ns1.example.com hostmaster.example.com. 0 3600 1800 1209600 3600' FROM domains WHERE NOT EXISTS (SELECT 1 FROM records WHERE records.domain_id=domains.id AND records.name=domains.name AND type='SOA');
49
67
 
50
- Then you need to install the required dependencies (dnspython, pytest and requests). Then run the tests:
68
+ Then run the tests:
51
69
 
52
- py.test test/integration_tests.py
70
+ bundle exec rake test:integration
53
71
 
54
72
  ## Copyright
55
73
 
56
- Copyright (c) 2015 Ewoud Kohl van Wijngaarden
74
+ Copyright (c) 2015 - 2016 Ewoud Kohl van Wijngaarden
57
75
 
58
76
  This program is free software: you can redistribute it and/or modify
59
77
  it under the terms of the GNU General Public License as published by
@@ -0,0 +1,24 @@
1
+ module Proxy::Dns::Powerdns::Backend
2
+ class Dummy < ::Proxy::Dns::Powerdns::Record
3
+
4
+ def initialize(a_server = nil, a_ttl = nil)
5
+ super(a_server, a_ttl)
6
+ end
7
+
8
+ def get_zone name
9
+ {
10
+ 'id' => 1,
11
+ 'name' => name.partition('.')[2]
12
+ }
13
+ end
14
+
15
+ def create_record domain_id, name, ttl, content, type
16
+ false
17
+ end
18
+
19
+ def delete_record domain_id, name, type
20
+ false
21
+ end
22
+ end
23
+ end
24
+
@@ -0,0 +1,49 @@
1
+ require 'mysql2'
2
+
3
+ module Proxy::Dns::Powerdns::Backend
4
+ class Mysql < ::Proxy::Dns::Powerdns::Record
5
+
6
+ attr_reader :hostname, :username, :password, :database
7
+
8
+ def initialize(a_server = nil, a_ttl = nil)
9
+ @hostname = Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_hostname || 'localhost'
10
+ @username = Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_username
11
+ @password = Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_password
12
+ @database = Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_database
13
+
14
+ super(a_server, a_ttl)
15
+ end
16
+
17
+ def connection
18
+ @connection ||= Mysql2::Client.new(:host => hostname, :username => username, :password => password, :database => database)
19
+ end
20
+
21
+ def get_zone name
22
+ domain = nil
23
+
24
+ name = connection.escape(name)
25
+ 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|
26
+ domain = row
27
+ end
28
+
29
+ raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS." unless domain
30
+
31
+ domain
32
+ end
33
+
34
+ def create_record domain_id, name, type, content
35
+ name = connection.escape(name)
36
+ content = connection.escape(content)
37
+ type = connection.escape(type)
38
+ connection.query("INSERT INTO records (domain_id, name, ttl, content, type) VALUES (#{domain_id}, '#{name}', #{ttl.to_i}, '#{content}', '#{type}')")
39
+ connection.affected_rows == 1
40
+ end
41
+
42
+ def delete_record domain_id, name, type
43
+ name = connection.escape(name)
44
+ type = connection.escape(type)
45
+ connection.query("DELETE FROM records WHERE domain_id=#{domain_id} AND name='#{name}' AND type='#{type}'")
46
+ connection.affected_rows == 1
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,42 @@
1
+ require 'pg'
2
+
3
+ module Proxy::Dns::Powerdns::Backend
4
+ class Postgresql < ::Proxy::Dns::Powerdns::Record
5
+
6
+ attr_reader :connection_str
7
+
8
+ def initialize(a_server = nil, a_ttl = nil)
9
+ @connection_str = Proxy::Dns::Powerdns::Plugin.settings.powerdns_postgresql_connection
10
+
11
+ super(a_server, a_ttl)
12
+ end
13
+
14
+ def connection
15
+ @connection ||= PG.connect(connection_str)
16
+ end
17
+
18
+ def get_zone name
19
+ domain = nil
20
+
21
+ 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|
22
+ result.each do |row|
23
+ domain = row
24
+ end
25
+ end
26
+
27
+ raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS." unless domain
28
+
29
+ domain
30
+ end
31
+
32
+ def create_record domain_id, name, type, content
33
+ 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])
34
+ result.cmdtuples == 1
35
+ end
36
+
37
+ def delete_record domain_id, name, type
38
+ result = connection.exec_params("DELETE FROM records WHERE domain_id=$1::int AND name=$2 AND type=$3", [domain_id, name, type])
39
+ result.cmdtuples == 1
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ require 'dns_common/dependency_injection/dependencies'
2
+
3
+ class Proxy::Dns::DependencyInjection::Dependencies
4
+ case Proxy::Dns::Powerdns::Plugin.settings.powerdns_backend
5
+ when 'mysql'
6
+ require 'smart_proxy_dns_powerdns/backend/mysql'
7
+ dependency :dns_provider, Proxy::Dns::Powerdns::Backend::Mysql
8
+ when 'postgresql'
9
+ require 'smart_proxy_dns_powerdns/backend/postgresql'
10
+ dependency :dns_provider, Proxy::Dns::Powerdns::Backend::Postgresql
11
+ when 'dummy'
12
+ require 'smart_proxy_dns_powerdns/backend/dummy'
13
+ dependency :dns_provider, Proxy::Dns::Powerdns::Backend::Dummy
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ require 'smart_proxy_dns_powerdns/dns_powerdns_plugin'
2
+
3
+ module Proxy::Dns::Powerdns
4
+ class ConfigurationValidator
5
+ def validate_settings!(settings)
6
+ validate_choice(settings, :powerdns_backend, ['mysql', 'postgresql', 'dummy'])
7
+
8
+ case settings.powerdns_backend
9
+ when 'mysql'
10
+ validate_presence(settings, [:powerdns_mysql_username, :powerdns_mysql_password, :powerdns_mysql_database])
11
+ when 'postgresql'
12
+ validate_presence(settings, [:powerdns_postgresql_connection])
13
+ end
14
+ end
15
+
16
+ def validate_choice(settings, setting, choices)
17
+ value = settings.send(setting)
18
+ unless choices.include?(value)
19
+ raise ::Proxy::Error::ConfigurationError, "Parameter '#{setting}' is expected to be one of #{choices.join(",")}"
20
+ end
21
+ true
22
+ end
23
+
24
+ def validate_presence(settings, names)
25
+ names.each do |name|
26
+ value = settings.send(name)
27
+ raise ::Proxy::Error::ConfigurationError, "Parameter '#{name}' is expected to have a non-empty value" if value.nil? || value.to_s.empty?
28
+ end
29
+ true
30
+ end
31
+ end
32
+ end
@@ -1,120 +1,73 @@
1
1
  require 'dns/dns'
2
+ require 'dns_common/dns_common'
2
3
  require 'ipaddr'
3
- require 'mysql2'
4
4
 
5
5
  module Proxy::Dns::Powerdns
6
6
  class Record < ::Proxy::Dns::Record
7
7
  include Proxy::Log
8
8
  include Proxy::Util
9
9
 
10
- attr_reader :mysql_connection, :powerdns_pdnssec
10
+ attr_reader :pdnssec
11
11
 
12
- def self.record(attrs = {})
13
- new(attrs.merge(
14
- :powerdns_mysql_hostname => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_hostname,
15
- :powerdns_mysql_username => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_username,
16
- :powerdns_mysql_password => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_password,
17
- :powerdns_mysql_database => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_mysql_database,
18
- :powerdns_pdnssec => ::Proxy::Dns::Powerdns::Plugin.settings.powerdns_pdnssec
19
- ))
12
+ def initialize(a_server = nil, a_ttl = nil)
13
+ @pdnssec = Proxy::Dns::Powerdns::Plugin.settings.powerdns_pdnssec
14
+ super(a_server, a_ttl || Proxy::Dns::Plugin.settings.dns_ttl)
20
15
  end
21
16
 
22
- def initialize options = {}
23
- raise "dns_powerdns provider needs 'powerdns_mysql_hostname' option" unless options[:powerdns_mysql_hostname]
24
- raise "dns_powerdns provider needs 'powerdns_mysql_username' option" unless options[:powerdns_mysql_username]
25
- raise "dns_powerdns provider needs 'powerdns_mysql_password' option" unless options[:powerdns_mysql_password]
26
- raise "dns_powerdns provider needs 'powerdns_mysql_database' option" unless options[:powerdns_mysql_database]
27
- @mysql_connection = Mysql2::Client.new(
28
- :host => options[:powerdns_mysql_hostname],
29
- :username => options[:powerdns_mysql_username],
30
- :password => options[:powerdns_mysql_password],
31
- :database => options[:powerdns_mysql_database]
32
- )
33
-
34
- @powerdns_pdnssec = options[:powerdns_pdnssec] || false
35
-
36
- # Normalize the somewhat weird PTR API spec to name / content
37
- case options[:type]
38
- when "PTR"
39
- if options[:value] =~ /\.(in-addr|ip6)\.arpa$/
40
- @name = options[:value]
41
- else
42
- @name = IPAddr.new(options[:value]).reverse
43
- end
44
- @content = options[:fqdn]
45
- else
46
- @name = options[:fqdn]
47
- @content = options[:value]
17
+ def create_a_record(fqdn, ip)
18
+ if found = dns_find(fqdn)
19
+ raise Proxy::Dns::Collision, "#{fqdn} is already in use by #{ip}" unless found == ip
48
20
  end
49
21
 
50
- super(options)
22
+ do_create(fqdn, ip, "A")
51
23
  end
52
24
 
53
- def create
54
- domain_row = domain
55
- raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS." unless domain_row
56
-
57
- if ip = dns_find(domain_row['id'], @name)
58
- raise Proxy::Dns::Collision, "#{@name} is already in use by #{ip}"
25
+ def create_ptr_record(fqdn, ip)
26
+ if found = dns_find(ip)
27
+ raise Proxy::Dns::Collision, "#{ip} is already in use by #{found}" unless found == fqdn
59
28
  end
60
29
 
61
- create_record(domain_row['id'], @name, @ttl, @content, @type)
62
-
63
- rectify_zone(domain_row['name'])
30
+ name = IPAddr.new(ip).reverse
31
+ do_create(name, fqdn, "PTR")
64
32
  end
65
33
 
66
- def remove
67
- domain_row = domain
68
- raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS." unless domain_row
69
-
70
- delete_record(domain_row['id'], @name, @type)
71
-
72
- rectify_zone(domain_row['name'])
34
+ def do_create(name, value, type)
35
+ zone = get_zone(name)
36
+ create_record(zone['id'], name, type, value) and rectify_zone(zone['name'])
73
37
  end
74
38
 
75
- private
76
- def domain
77
- domain = nil
39
+ def remove_a_record(fqdn)
40
+ do_remove(fqdn, "A")
41
+ end
78
42
 
79
- name = mysql_connection.escape(@name)
80
- 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|
81
- domain = row
82
- end
43
+ def remove_ptr_record(ip)
44
+ name = ip # Note ip is already in-addr.arpa
45
+ do_remove(name, "PTR")
46
+ end
83
47
 
84
- domain
48
+ def do_remove(name, type)
49
+ zone = get_zone(name)
50
+ delete_record(zone['id'], name, type) and rectify_zone(zone['name'])
85
51
  end
86
52
 
87
- private
88
- def dns_find domain_id, key
89
- value = nil
90
- key = mysql_connection.escape(key)
91
- mysql_connection.query("SELECT content FROM records WHERE domain_id=#{domain_id} AND name = '#{key}' LIMIT 1").each do |row|
92
- value = row["content"]
93
- end
94
- value || false
53
+ def get_zone(fqdn)
54
+ # TODO: backend specific
55
+ raise Proxy::Dns::Error, "Unable to determine zone. Zone must exist in PowerDNS."
95
56
  end
96
57
 
97
- private
98
- def create_record domain_id, name, ttl, content, type
99
- name = mysql_connection.escape(name)
100
- content = mysql_connection.escape(content)
101
- type = mysql_connection.escape(type)
102
- mysql_connection.query("INSERT INTO records (domain_id, name, ttl, content, type) VALUES (#{domain_id}, '#{name}', #{ttl.to_i}, '#{content}', '#{type}')")
103
- true
58
+ def create_record(domain_id, name, type, content)
59
+ # TODO: backend specific
60
+ false
104
61
  end
105
62
 
106
- private
107
- def delete_record domain_id, name, type
108
- name = mysql_connection.escape(name)
109
- type = mysql_connection.escape(type)
110
- mysql_connection.query("DELETE FROM records WHERE domain_id=#{domain_id} AND name='#{name}' AND type='#{type}'")
111
- true
63
+ def delete_record(domain_id, name, type)
64
+ # TODO: backend specific
65
+ false
112
66
  end
113
67
 
114
- private
115
68
  def rectify_zone domain
116
- if @powerdns_pdnssec
117
- %x(#{@powerdns_pdnssec} rectify-zone "#{domain}")
69
+ if @pdnssec
70
+ %x(#{@pdnssec} rectify-zone "#{domain}")
118
71
 
119
72
  $?.exitstatus == 0
120
73
  else
@@ -2,13 +2,18 @@ require 'smart_proxy_dns_powerdns/dns_powerdns_version'
2
2
 
3
3
  module Proxy::Dns::Powerdns
4
4
  class Plugin < ::Proxy::Provider
5
- plugin :dns_powerdns, ::Proxy::Dns::Powerdns::VERSION,
6
- :factory => proc { |attrs| ::Proxy::Dns::Powerdns::Record.record(attrs) }
5
+ plugin :dns_powerdns, ::Proxy::Dns::Powerdns::VERSION
7
6
 
8
- requires :dns, '>= 1.10'
7
+ requires :dns, '>= 1.11'
8
+
9
+ validate_presence :powerdns_backend
9
10
 
10
11
  after_activation do
12
+ require 'smart_proxy_dns_powerdns/dns_powerdns_configuration_validator'
13
+ ::Proxy::Dns::Powerdns::ConfigurationValidator.new.validate_settings!(settings)
14
+
11
15
  require 'smart_proxy_dns_powerdns/dns_powerdns_main'
16
+ require 'smart_proxy_dns_powerdns/dependencies'
12
17
  end
13
18
  end
14
19
  end
@@ -1,7 +1,7 @@
1
1
  module Proxy
2
2
  module Dns
3
3
  module Powerdns
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,83 @@
1
+ require 'test_helper'
2
+
3
+ require 'ipaddr'
4
+ require 'net/http'
5
+
6
+ class DnsPowerdnsIntegrationTest < Test::Unit::TestCase
7
+
8
+ def test_forward_dns
9
+ data = {'fqdn' => fqdn, 'value' => ip, 'type' => 'A'}
10
+
11
+ uri = URI(smart_proxy_url)
12
+
13
+ Net::HTTP.start(uri.host, uri.port) do |http|
14
+ request = Net::HTTP::Post.new(smart_proxy_url + 'dns/')
15
+ request.form_data = data
16
+ response = http.request request
17
+ assert_equal(200, response.code.to_i)
18
+
19
+ addresses = resolver.getaddresses(data['fqdn'])
20
+ assert_equal([Resolv::IPv4.create(data['value'])], addresses, "#{data['fqdn']} should resolve to #{data['value']}")
21
+
22
+ request = Net::HTTP::Delete.new(smart_proxy_url + 'dns/' + data['fqdn'])
23
+ response = http.request request
24
+ assert_equal(200, response.code.to_i)
25
+
26
+ assert(purge_cache data['fqdn'])
27
+
28
+ addresses = resolver.getaddresses(data['fqdn'])
29
+ assert_equal([], addresses)
30
+ end
31
+ end
32
+
33
+ def test_reverse_dns
34
+ data = {'fqdn' => fqdn, 'value' => ip, 'type' => 'PTR'}
35
+
36
+ uri = URI(smart_proxy_url)
37
+
38
+ Net::HTTP.start(uri.host, uri.port) do |http|
39
+ request = Net::HTTP::Post.new(smart_proxy_url + 'dns/')
40
+ request.form_data = data
41
+ response = http.request request
42
+ assert_equal(200, response.code.to_i)
43
+
44
+ name = Resolv::IPv4.create(data['value']).to_name.to_s
45
+
46
+ addresses = resolver.getnames(data['value'])
47
+ assert_equal([Resolv::DNS::Name.create(data['fqdn'] + '.')], addresses, "#{data['value']} should reverse to #{data['fqdn']}")
48
+
49
+ request = Net::HTTP::Delete.new(smart_proxy_url + 'dns/' + name)
50
+ response = http.request request
51
+ assert_equal(200, response.code.to_i)
52
+
53
+ assert(purge_cache name)
54
+
55
+ addresses = resolver.getnames(data['value'])
56
+ assert_equal([], addresses)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def resolver
63
+ Resolv::DNS.new(:nameserver_port => [['127.0.0.1', 5300]])
64
+ end
65
+
66
+ def smart_proxy_url
67
+ 'http://localhost:8000/'
68
+ end
69
+
70
+ def fqdn
71
+ set = ('a' .. 'z').to_a + ('0' .. '9').to_a
72
+ 10.times.collect {|i| set[rand(set.size)] }.join + '.example.com'
73
+ end
74
+
75
+ def ip
76
+ IPAddr.new(rand(2 ** 32), Socket::AF_INET).to_s
77
+ end
78
+
79
+ def purge_cache name
80
+ %x{#{ENV['PDNS_CONTROL'] || "pdns_control"} purge "#{name}"}
81
+ $? == 0
82
+ end
83
+ end
@@ -0,0 +1,78 @@
1
+ require 'test_helper'
2
+ require 'ostruct'
3
+
4
+ require 'smart_proxy_dns_powerdns/dns_powerdns_plugin'
5
+ require 'smart_proxy_dns_powerdns/dns_powerdns_configuration_validator'
6
+
7
+ class DnsPowerdnsConfigurationValidatorTest < Test::Unit::TestCase
8
+ def setup
9
+ @config_validator = Proxy::Dns::Powerdns::ConfigurationValidator.new
10
+ end
11
+
12
+ def test_initialize_missing_backend
13
+ settings = OpenStruct.new(:dns_provider => 'powerdns', :powerdns_backend => nil)
14
+
15
+ assert_raise Proxy::Error::ConfigurationError do
16
+ @config_validator.validate_settings!(settings)
17
+ end
18
+ end
19
+
20
+ def test_initialize_invalid_backend
21
+ settings = OpenStruct.new(:dns_provider => 'powerdns', :powerdns_backend => 'invalid')
22
+
23
+ assert_raise Proxy::Error::ConfigurationError do
24
+ @config_validator.validate_settings!(settings)
25
+ end
26
+ end
27
+
28
+ def test_initialize_dummy_with_settings
29
+ settings = OpenStruct.new(:dns_provider => 'powerdns', :powerdns_backend => 'dummy')
30
+
31
+ assert_nothing_raised do
32
+ @config_validator.validate_settings!(settings)
33
+ end
34
+ end
35
+
36
+ def test_initialize_mysql_without_settings
37
+ settings = OpenStruct.new(:dns_provider => 'powerdns', :powerdns_backend => 'mysql')
38
+
39
+ assert_raise Proxy::Error::ConfigurationError do
40
+ @config_validator.validate_settings!(settings)
41
+ end
42
+ end
43
+
44
+ def test_initialize_mysql_with_settings
45
+ settings = OpenStruct.new(
46
+ :dns_provider => 'powerdns',
47
+ :powerdns_backend => 'mysql',
48
+ :powerdns_mysql_hostname => 'localhost',
49
+ :powerdns_mysql_username => 'username',
50
+ :powerdns_mysql_password => 'password',
51
+ :powerdns_mysql_database => 'powerdns'
52
+ )
53
+
54
+ assert_nothing_raised do
55
+ @config_validator.validate_settings!(settings)
56
+ end
57
+ end
58
+
59
+ def test_initialize_postgresql_without_settings
60
+ settings = OpenStruct.new(:dns_provider => 'powerdns', :powerdns_backend => 'postgresql')
61
+
62
+ assert_raise Proxy::Error::ConfigurationError do
63
+ @config_validator.validate_settings!(settings)
64
+ end
65
+ end
66
+
67
+ def test_initialize_postgresql_with_settings
68
+ settings = OpenStruct.new(
69
+ :dns_provider => 'powerdns',
70
+ :powerdns_backend => 'postgresql',
71
+ :powerdns_postgresql_connection => 'dbname=powerdns'
72
+ )
73
+
74
+ assert_nothing_raised do
75
+ @config_validator.validate_settings!(settings)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,77 @@
1
+ require 'test_helper'
2
+
3
+ require 'smart_proxy_dns_powerdns/dns_powerdns_plugin'
4
+ require 'smart_proxy_dns_powerdns/dns_powerdns_main'
5
+ require 'smart_proxy_dns_powerdns/backend/mysql'
6
+
7
+ class DnsPowerdnsBackendMysqlTest < Test::Unit::TestCase
8
+ # Test that correct initialization works
9
+ def test_initialize_dummy_with_settings
10
+ Proxy::Dns::Powerdns::Plugin.load_test_settings(
11
+ :powerdns_mysql_hostname => 'db.example.com',
12
+ :powerdns_mysql_username => 'the_user',
13
+ :powerdns_mysql_password => 'something_secure',
14
+ :powerdns_mysql_database => 'db_pdns'
15
+ )
16
+ provider = klass.new
17
+ assert_equal 'db.example.com', provider.hostname
18
+ assert_equal 'the_user', provider.username
19
+ assert_equal 'something_secure', provider.password
20
+ assert_equal 'db_pdns', provider.database
21
+ end
22
+
23
+ def test_get_zone_with_existing_zone
24
+ instance = klass.new
25
+
26
+ connection = mock()
27
+ instance.stubs(:connection).returns(connection)
28
+ connection.expects(:escape).with('test.example.com').returns('test.example.com')
29
+ connection.expects(:query).with("SELECT LENGTH(name) domain_length, id, name FROM domains WHERE 'test.example.com' LIKE CONCAT('%%.', name) ORDER BY domain_length DESC LIMIT 1").returns([{'id' => 1, 'name' => 'example.com'}])
30
+
31
+ assert_equal(instance.get_zone('test.example.com'), {'id' => 1, 'name' => 'example.com'})
32
+ end
33
+
34
+ def test_get_zone_without_existing_zone
35
+ instance = klass.new
36
+
37
+ connection = mock()
38
+ instance.stubs(:connection).returns(connection)
39
+ connection.expects(:escape).with('test.example.com').returns('test.example.com')
40
+ connection.expects(:query).with("SELECT LENGTH(name) domain_length, id, name FROM domains WHERE 'test.example.com' LIKE CONCAT('%%.', name) ORDER BY domain_length DESC LIMIT 1").returns([])
41
+
42
+ assert_raise(Proxy::Dns::Error) { instance.get_zone('test.example.com') }
43
+ end
44
+
45
+ def test_create_record
46
+ instance = klass.new
47
+
48
+ connection = mock()
49
+ instance.stubs(:connection).returns(connection)
50
+ connection.expects(:escape).with('test.example.com').returns('test.example.com')
51
+ connection.expects(:escape).with('A').returns('A')
52
+ connection.expects(:escape).with('10.1.1.1').returns('10.1.1.1')
53
+ connection.expects(:query).with("INSERT INTO records (domain_id, name, ttl, content, type) VALUES (1, 'test.example.com', 86400, '10.1.1.1', 'A')")
54
+ connection.expects(:affected_rows).returns(1)
55
+
56
+ assert instance.create_record(1, 'test.example.com', 'A', '10.1.1.1')
57
+ end
58
+
59
+ def test_delete_record
60
+ instance = klass.new
61
+
62
+ connection = mock()
63
+ instance.stubs(:connection).returns(connection)
64
+ connection.expects(:escape).with('test.example.com').returns('test.example.com')
65
+ connection.expects(:escape).with('A').returns('A')
66
+ connection.expects(:query).with("DELETE FROM records WHERE domain_id=1 AND name='test.example.com' AND type='A'")
67
+ connection.expects(:affected_rows).returns(1)
68
+
69
+ assert instance.delete_record(1, 'test.example.com', 'A')
70
+ end
71
+
72
+ private
73
+
74
+ def klass
75
+ Proxy::Dns::Powerdns::Backend::Mysql
76
+ end
77
+ end
@@ -0,0 +1,20 @@
1
+ require 'test_helper'
2
+
3
+ require 'smart_proxy_dns_powerdns/dns_powerdns_plugin'
4
+ require 'smart_proxy_dns_powerdns/dns_powerdns_main'
5
+ require 'smart_proxy_dns_powerdns/backend/postgresql'
6
+
7
+ class DnsPowerdnsBackendPostgresqlTest < Test::Unit::TestCase
8
+ # Test that correct initialization works
9
+ def test_initialize_dummy_with_settings
10
+ Proxy::Dns::Powerdns::Plugin.load_test_settings(:powerdns_postgresql_connection => 'dbname=powerdns')
11
+ provider = klass.new
12
+ assert_equal 'dbname=powerdns', provider.connection_str
13
+ end
14
+
15
+ private
16
+
17
+ def klass
18
+ Proxy::Dns::Powerdns::Backend::Postgresql
19
+ end
20
+ end
@@ -0,0 +1,115 @@
1
+ require 'test_helper'
2
+
3
+ require 'smart_proxy_dns_powerdns/dns_powerdns_plugin'
4
+ require 'smart_proxy_dns_powerdns/dns_powerdns_main'
5
+
6
+ class DnsPowerdnsRecordTest < Test::Unit::TestCase
7
+ # Test that correct initialization works
8
+ def test_initialize_dummy_with_settings
9
+ Proxy::Dns::Powerdns::Plugin.load_test_settings(:powerdns_pdnssec => 'sudo pdnssec')
10
+ provider = klass.new
11
+ assert_equal 'sudo pdnssec', provider.pdnssec
12
+ end
13
+
14
+ # Test A record creation
15
+ def test_create_a
16
+ instance = klass.new
17
+
18
+ instance.expects(:dns_find).with('test.example.com').returns(false)
19
+ instance.expects(:get_zone).with('test.example.com').returns({'id' => 1, 'name' => 'example.com'})
20
+ instance.expects(:create_record).with(1, 'test.example.com', 'A', '10.1.1.1').returns(true)
21
+ instance.expects(:rectify_zone).with('example.com').returns(true)
22
+
23
+ assert instance.create_a_record(fqdn, ip)
24
+ end
25
+
26
+ # Test A record creation fails if the record exists
27
+ def test_create_a_conflict
28
+ instance = klass.new
29
+
30
+ instance.expects(:dns_find).with('test.example.com').returns('192.168.1.1')
31
+
32
+ assert_raise(Proxy::Dns::Collision) { instance.create_a_record(fqdn, ip) }
33
+ end
34
+
35
+ # Test PTR record creation
36
+ def test_create_ptr
37
+ instance = klass.new
38
+
39
+ instance.expects(:dns_find).with('10.1.1.1').returns(false)
40
+ instance.expects(:get_zone).with('1.1.1.10.in-addr.arpa').returns({'id' => 1, 'name' => '1.1.10.in-addr.arpa'})
41
+ instance.expects(:create_record).with(1, '1.1.1.10.in-addr.arpa', 'PTR', 'test.example.com').returns(true)
42
+ instance.expects(:rectify_zone).with('1.1.10.in-addr.arpa').returns(true)
43
+
44
+ assert instance.create_ptr_record(fqdn, ip)
45
+ end
46
+
47
+ # Test PTR record creation fails if the record exists
48
+ def test_create_ptr_conflict
49
+ instance = klass.new
50
+
51
+ instance.expects(:dns_find).with('10.1.1.1').returns('test2.example.com')
52
+
53
+ assert_raise(Proxy::Dns::Collision) { instance.create_ptr_record(fqdn, ip) }
54
+ end
55
+
56
+ # Test A record removal
57
+ def test_remove_a
58
+ instance = klass.new
59
+
60
+ instance.expects(:get_zone).with('test.example.com').returns({'id' => 1, 'name' => 'example.com'})
61
+ instance.expects(:delete_record).with(1, 'test.example.com', 'A').returns(true)
62
+ instance.expects(:rectify_zone).with('example.com').returns(true)
63
+
64
+ assert instance.remove_a_record(fqdn)
65
+ end
66
+
67
+ # Test PTR record removal
68
+ def test_remove_ptr
69
+ instance = klass.new
70
+
71
+ instance.expects(:get_zone).with('1.1.1.10.in-addr.arpa').returns({'id' => 1, 'name' => '1.1.10.in-addr.arpa'})
72
+ instance.expects(:delete_record).with(1, '1.1.1.10.in-addr.arpa', 'PTR').returns(true)
73
+ instance.expects(:rectify_zone).with('1.1.10.in-addr.arpa').returns(true)
74
+
75
+ assert instance.remove_ptr_record(reverse_ip)
76
+ end
77
+
78
+ def test_do_create
79
+ instance = klass.new
80
+
81
+ instance.expects(:get_zone).with('test.example.com').returns({'id' => 1, 'name' => 'example.com'})
82
+ instance.expects(:create_record).with(1, 'test.example.com', 'A', '10.1.1.1').returns(true)
83
+ instance.expects(:rectify_zone).with('example.com').returns(true)
84
+
85
+ assert instance.do_create('test.example.com', '10.1.1.1', 'A')
86
+ end
87
+
88
+ def test_do_remove
89
+ instance = klass.new
90
+
91
+ instance.expects(:get_zone).with('test.example.com').returns({'id' => 1, 'name' => 'example.com'})
92
+ instance.expects(:delete_record).with(1, 'test.example.com', 'A').returns(true)
93
+ instance.expects(:rectify_zone).with('example.com').returns(true)
94
+
95
+ assert instance.do_remove('test.example.com', 'A')
96
+ end
97
+
98
+ private
99
+
100
+ def klass
101
+ Proxy::Dns::Powerdns::Record
102
+ end
103
+
104
+ def fqdn
105
+ 'test.example.com'
106
+ end
107
+
108
+ def ip
109
+ '10.1.1.1'
110
+ end
111
+
112
+ def reverse_ip
113
+ '1.1.1.10.in-addr.arpa'
114
+ end
115
+ end
metadata CHANGED
@@ -1,55 +1,69 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_dns_powerdns
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ewoud Kohl van Wijngaarden
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-08 00:00:00.000000000 Z
11
+ date: 2016-02-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: mocha
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: mysql2
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0'
55
69
  description: PowerDNS DNS provider plugin for Foreman's smart proxy
@@ -64,12 +78,20 @@ files:
64
78
  - bundler.d/dns_powerdns.rb
65
79
  - config/dns_powerdns.yml
66
80
  - lib/smart_proxy_dns_powerdns.rb
81
+ - lib/smart_proxy_dns_powerdns/backend/dummy.rb
82
+ - lib/smart_proxy_dns_powerdns/backend/mysql.rb
83
+ - lib/smart_proxy_dns_powerdns/backend/postgresql.rb
84
+ - lib/smart_proxy_dns_powerdns/dependencies.rb
85
+ - lib/smart_proxy_dns_powerdns/dns_powerdns_configuration_validator.rb
67
86
  - lib/smart_proxy_dns_powerdns/dns_powerdns_main.rb
68
87
  - lib/smart_proxy_dns_powerdns/dns_powerdns_plugin.rb
69
88
  - lib/smart_proxy_dns_powerdns/dns_powerdns_version.rb
70
- - test/dns_powerdns_record_test.rb
71
- - test/integration_tests.py
89
+ - test/integration/integration_test.rb
72
90
  - test/test_helper.rb
91
+ - test/unit/dns_powerdns_configuration_validator_test.rb
92
+ - test/unit/dns_powerdns_record_mysql_test.rb
93
+ - test/unit/dns_powerdns_record_postgresql_test.rb
94
+ - test/unit/dns_powerdns_record_test.rb
73
95
  homepage: https://github.com/theforeman/smart_proxy_dns_powerdns
74
96
  licenses:
75
97
  - GPLv3
@@ -80,12 +102,12 @@ require_paths:
80
102
  - lib
81
103
  required_ruby_version: !ruby/object:Gem::Requirement
82
104
  requirements:
83
- - - '>='
105
+ - - ">="
84
106
  - !ruby/object:Gem::Version
85
107
  version: '0'
86
108
  required_rubygems_version: !ruby/object:Gem::Requirement
87
109
  requirements:
88
- - - '>='
110
+ - - ">="
89
111
  - !ruby/object:Gem::Version
90
112
  version: '0'
91
113
  requirements: []
@@ -96,5 +118,8 @@ specification_version: 4
96
118
  summary: PowerDNS DNS provider plugin for Foreman's smart proxy
97
119
  test_files:
98
120
  - test/test_helper.rb
99
- - test/dns_powerdns_record_test.rb
100
- - test/integration_tests.py
121
+ - test/integration/integration_test.rb
122
+ - test/unit/dns_powerdns_record_postgresql_test.rb
123
+ - test/unit/dns_powerdns_configuration_validator_test.rb
124
+ - test/unit/dns_powerdns_record_mysql_test.rb
125
+ - test/unit/dns_powerdns_record_test.rb
@@ -1,121 +0,0 @@
1
- require 'test_helper'
2
-
3
- require 'smart_proxy_dns_powerdns/dns_powerdns_main'
4
-
5
- class DnsPowerdnsRecordTest < Test::Unit::TestCase
6
- # Test that a missing :powerdns_mysql_hostname throws an error
7
- def test_initialize_without_settings
8
- assert_raise(RuntimeError) do
9
- klass.new(settings.delete_if { |k,v| k == :powerdns_mysql_hostname })
10
- end
11
- end
12
-
13
- # Test that correct initialization works
14
- def test_initialize_with_settings
15
- assert_nothing_raised do
16
- mock_mysql
17
-
18
- klass.new(settings)
19
- end
20
- end
21
-
22
- # Test A record creation
23
- def test_create_a
24
- mock_mysql
25
-
26
- instance = klass.new(settings)
27
-
28
- instance.expects(:domain).returns({'id' => 1})
29
- instance.expects(:dns_find).with(1, 'test.example.com').returns(false)
30
- instance.expects(:create_record).with(1, 'test.example.com', 84600, '10.1.1.1', 'A').returns(true)
31
-
32
- assert instance.create
33
- end
34
-
35
- # Test A record creation fails if the record exists
36
- def test_create_a_conflict
37
- mock_mysql
38
-
39
- instance = klass.new(settings)
40
-
41
- instance.expects(:domain).returns({'id' => 1})
42
- instance.expects(:dns_find).with(1, 'test.example.com').returns('192.168.1.1')
43
-
44
- assert_raise(Proxy::Dns::Collision) { instance.create }
45
- end
46
-
47
- # Test PTR record creation
48
- def test_create_ptr
49
- mock_mysql
50
-
51
- instance = klass.new(settings.merge(:type => 'PTR'))
52
-
53
- instance.expects(:domain).returns({'id' => 1, 'name' => 'example.com'})
54
- instance.expects(:dns_find).with(1, '1.1.1.10.in-addr.arpa').returns(false)
55
- instance.expects(:create_record).with(1, '1.1.1.10.in-addr.arpa', 84600, 'test.example.com', 'PTR').returns(true)
56
- instance.expects(:rectify_zone).with('example.com').returns(true)
57
-
58
- assert instance.create
59
- end
60
-
61
- # Test PTR record creation fails if the record exists
62
- def test_create_ptr_conflict
63
- mock_mysql
64
-
65
- instance = klass.new(settings.merge(:type => 'PTR'))
66
-
67
- instance.expects(:domain).returns({'id' => 1, 'name' => '1.1.10.in-addr.arpa'})
68
- instance.expects(:dns_find).with(1, '1.1.1.10.in-addr.arpa').returns('test2.example.com')
69
-
70
- assert_raise(Proxy::Dns::Collision) { instance.create }
71
- end
72
-
73
- # Test A record removal
74
- def test_remove_a
75
- mock_mysql
76
-
77
- instance = klass.new(settings)
78
-
79
- instance.expects(:domain).returns({'id' => 1, 'name' => 'example.com'})
80
- instance.expects(:delete_record).with(1, 'test.example.com', 'A').returns(true)
81
- instance.expects(:rectify_zone).with('example.com').returns(true)
82
-
83
- assert instance.remove
84
- end
85
-
86
- # Test PTR record removal
87
- def test_remove_ptr
88
- mock_mysql
89
-
90
- instance = klass.new(settings.merge(:type => 'PTR'))
91
-
92
- instance.expects(:domain).returns({'id' => 1, 'name' => '1.1.10.in-addr.arpa'})
93
- instance.expects(:delete_record).with(1, '1.1.1.10.in-addr.arpa', 'PTR').returns(true)
94
- instance.expects(:rectify_zone).with('1.1.10.in-addr.arpa').returns(true)
95
-
96
- assert instance.remove
97
- end
98
-
99
- def mock_mysql
100
- Mysql2::Client.expects(:new).with(:host => 'localhost', :username => 'username', :password => 'password', :database => 'powerdns').returns(false)
101
- end
102
-
103
- private
104
-
105
- def klass
106
- Proxy::Dns::Powerdns::Record
107
- end
108
-
109
- def settings
110
- {
111
- :powerdns_mysql_hostname => 'localhost',
112
- :powerdns_mysql_username => 'username',
113
- :powerdns_mysql_password => 'password',
114
- :powerdns_mysql_database => 'powerdns',
115
- :fqdn => 'test.example.com',
116
- :value => '10.1.1.1',
117
- :type => 'A',
118
- :ttl => 84600,
119
- }
120
- end
121
- end
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env python
2
- import dns.resolver
3
- import dns.reversename
4
- import pytest
5
- import random
6
- import requests
7
- import socket
8
- import string
9
- import struct
10
- import subprocess
11
-
12
-
13
- @pytest.fixture
14
- def resolver():
15
- """
16
- Return a resolver object against the configured powerdns server
17
- """
18
- resolver = dns.resolver.Resolver(configure=False)
19
- resolver.nameservers = ['127.0.0.1']
20
- resolver.port = 5300
21
-
22
- return resolver
23
-
24
-
25
- @pytest.fixture
26
- def smart_proxy_url():
27
- return 'http://localhost:8000/'
28
-
29
-
30
- @pytest.fixture
31
- def fqdn():
32
- return ''.join(random.sample(string.lowercase, 10)) + '.' + 'example.com'
33
-
34
-
35
- @pytest.fixture
36
- def ip():
37
- return socket.inet_ntoa(struct.pack("!I", random.randint(1, 2 ** 32)))
38
-
39
-
40
- def purge_cache(name):
41
- subprocess.check_output(['sudo', 'pdns_control', 'purge', name])
42
-
43
-
44
- def test_forward_dns(resolver, smart_proxy_url, fqdn, ip):
45
- response = requests.post(smart_proxy_url + 'dns/',
46
- data={'fqdn': fqdn, 'value': ip, 'type': 'A'})
47
- response.raise_for_status()
48
-
49
- answer = resolver.query(fqdn, 'A')
50
- assert len(answer.rrset.items) == 1
51
- assert answer.rrset.items[0].address == ip
52
-
53
- response = requests.delete(smart_proxy_url + 'dns/' + fqdn)
54
- response.raise_for_status()
55
-
56
- purge_cache(fqdn)
57
-
58
- with pytest.raises(dns.resolver.NXDOMAIN):
59
- resolver.query(fqdn, 'A')
60
-
61
-
62
- def test_reverse_dns(resolver, smart_proxy_url, fqdn, ip):
63
- response = requests.post(smart_proxy_url + 'dns/',
64
- data={'fqdn': fqdn, 'value': ip, 'type': 'PTR'})
65
- response.raise_for_status()
66
-
67
- name = dns.reversename.from_address(ip)
68
-
69
- answer = resolver.query(name, 'PTR')
70
- assert len(answer.rrset.items) == 1
71
- assert answer.rrset.items[0].target.to_text() == fqdn + '.'
72
-
73
- response = requests.delete(smart_proxy_url + 'dns/' + name.to_text().rstrip('.'))
74
- response.raise_for_status()
75
-
76
- purge_cache(name.to_text())
77
-
78
- with pytest.raises(dns.resolver.NXDOMAIN):
79
- resolver.query(name, 'PTR')