smart_proxy_dns_powerdns 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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')