smart_proxy_dhcp_kea 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,52 @@
1
+ # SmartProxyDhcpKea
2
+
3
+ This Foreman smart proxy plugin allow to interface with Kea DHCP provider. Postgres and Mysql database are supported, anyway Mysql has not been tested yet.
4
+
5
+ # Installation
6
+
7
+ Smart proxy plugin are ruby gems. They can be easly instaled with `gem`.
8
+
9
+ ## Prerequisites
10
+
11
+ The smart proxy need to interact both with postgresql and mysql. To do that it uses `mysql2` and `pg` gems, which are automatically installed, but some dependencies are needed. So if gem installation goes wrong, try to install them with `yum`:
12
+
13
+ yum install ruby-devel
14
+ yum install potgresql-devel
15
+ yum install mysql-devel
16
+
17
+ ## Manual installation
18
+
19
+ It's possible to install manually this smart proxy plugin using `gem`
20
+
21
+ gem install 'smart_proxy_dhcp_kea'
22
+
23
+ The gem with all its dependencies will be installed. Next you need to specify the gem into the `~foreman-proxy/bundler.d/Gemfile.local.rb` file. Create the file if it doesn't exist.
24
+
25
+ echo "gem 'smat_proxy_dhcp_kea'" > /usr/share/foreman-proxy/bundler.d/Gemfile.local.rb
26
+
27
+ Next restart the foreman-proxy service and refresh features form the control panel.
28
+
29
+ systemctl restart foreman-proxy
30
+
31
+ The DHCP feature should be now active.
32
+
33
+ # Configuration
34
+
35
+ To enable this DHCP provider, edit `/etc/foreman-proxy/settings.d/dhcp.yml` and set:
36
+
37
+ :use_provider: dhcp_kea
38
+
39
+ Then you need to create /etc/foreman-proxy/settings.d/dhcp\_kea.yml file and specify the following parameters.
40
+
41
+ :db: [postgres, mysql]
42
+ :dbname: [database_name]
43
+ :username: [username]
44
+ :password: [password]
45
+ :host: [host_address]
46
+ :port: [db_port]
47
+
48
+
49
+ This smart proxy support both postgres and mysql database for kea. You need to specify in the configuration file which one are you going to use. Mysql is the default databse. Also, if postgres is choosen, a table called subnets, in which subnets are stored into keasubnets database is required. The subnet schema is indeed present by default in MySQL database.
50
+
51
+ # Contributing
52
+ Pierfrancesco Cifra
@@ -0,0 +1,4 @@
1
+ group :dhcp_kea do
2
+ gem 'rsec', '< 1'
3
+ end
4
+ gem 'smart_proxy_dhcp_kea'
@@ -0,0 +1,24 @@
1
+ ---
2
+ #
3
+ # Configuration file for ISC dhcp provider
4
+ #
5
+
6
+ #:config: /etc/dhcp/dhcpd.conf
7
+ #:leases: /var/lib/dhcpd/dhcpd.leases
8
+ #
9
+ # Redhat 5
10
+ #
11
+ #:config: /etc/dhcpd.conf
12
+ #
13
+ # Settings for Ubuntu
14
+ #
15
+ #:config: /etc/dhcp3/dhcpd.conf
16
+ #:leases: /var/lib/dhcp3/dhcpd.leases
17
+
18
+ # Specifies TSIG key name and secret
19
+ #:key_name: secret_key_name
20
+ #:key_secret: secret_key
21
+
22
+ #:omapi_port: 7911
23
+
24
+ # use :server setting in dhcp.yml if you are managing a dhcp server which is not localhost
@@ -0,0 +1,9 @@
1
+ module Proxy
2
+ module DHCP
3
+ module DhcpKea; end
4
+ end
5
+ end
6
+
7
+ require 'smart_proxy_dhcp_kea/version'
8
+ require 'smart_proxy_dhcp_kea/plugin_configuration'
9
+ require 'smart_proxy_dhcp_kea/dhcp_kea_plugin'
@@ -0,0 +1,4 @@
1
+ require 'dhcp_common/dhcp_common'
2
+ require 'dhcp_keapostgre/configuration_loader'
3
+ require 'dhcp_keapostgre/dhcp_keapostgre_plugin'
4
+
@@ -0,0 +1,127 @@
1
+ require 'dhcp_common/dhcp_common'
2
+ require 'dhcp_common/server'
3
+
4
+
5
+ module Proxy::DHCP::DhcpKea
6
+ class Provider < ::Proxy::DHCP::Server
7
+ attr_reader :host, :port, :dbname, :username, :password, :keapostgre_network, :subnet_service, :free_ips, :db
8
+
9
+ def initialize(host, port, dbname, username, password, keapostgre_network, subnet_service, free_ip_service, db)
10
+ @host = host
11
+ @port = port
12
+ @db = db
13
+ @dbname = dbname
14
+ @username = username
15
+ @password = password
16
+ @keapostgre_network = keapostgre_network
17
+ @subnet_service = subnet_service
18
+ @free_ips = free_ip_service
19
+ super('kea', 'nil', subnet_service, free_ips)
20
+ end
21
+
22
+ def unused_ip(subnet_address, mac_address, from_address, to_address)
23
+ require 'ipaddr'
24
+ require 'resolv'
25
+ require 'pg'
26
+ require 'mysql2'
27
+
28
+
29
+ if db == 'postgres'
30
+ con = PG.connect :dbname => 'keasubnets', :user => username, :password => password, :host => host, :port => port
31
+ subnet = con.exec_params("select encode(address, 'escape') from subnets where encode(address, 'escape') like $1;", ['%' + subnet_address + '%'])
32
+
33
+
34
+ if subnet.num_tuples.zero?
35
+ logger.error 'This subnet does not exist. Not found in databse'
36
+ return nil
37
+ end
38
+
39
+ else
40
+ con = Mysql2::Client.new(:dbname => dbname, :user => username, :password => password, :host => host, :port => port)
41
+ st = con.prepare("select subnet_prefix from dhcp4_subnet where subnet_prefix like ?;")
42
+ subnet = st.execute('%' + subnet_address + '%')
43
+
44
+ if subnet.count.zero?
45
+ logger.warning 'This subnet does not exist. Not found in databse'
46
+ return nil
47
+ end
48
+
49
+ end
50
+
51
+
52
+
53
+ mask = '/' + subnet.getvalue(0,0)[-2..-1]
54
+ subnet_address = subnet_address + mask
55
+ logger.debug 'Start searching ip address in subnet ' + subnet_address
56
+
57
+
58
+ unless from_address
59
+ addr = IPAddr.new(subnet_address.to_s)
60
+ from_address = IPAddr.new((addr.to_range().first.to_i + 1), Socket::AF_INET).to_s
61
+ end
62
+
63
+ unless to_address
64
+ addr = IPAddr.new(subnet_address.to_s)
65
+ to_address = IPAddr.new((addr.to_range().last.to_i - 1), Socket::AF_INET).to_s
66
+ end
67
+
68
+ logger.debug 'Starting search for a free ip address form ' + from_address.to_s + ' to ' + to_address.to_s
69
+ possible_ip = free_ips.find_free_ip(from_address, to_address, all_hosts(subnet_address) + all_leases(subnet_address))
70
+
71
+ logger.debug 'Possible ip found: ' + possible_ip
72
+
73
+ while possible_ip != nil
74
+ begin
75
+ Resolv.getname possible_ip
76
+ logger.warning 'Address ' + possible_ip + ' resolved. Cannot use it'
77
+ possible_ip = free_ips.find_free_ip(from_address, to_address, all_hosts(subnet_address) + all_leases(subnet_address))
78
+ rescue Resolv::ResolvError => e
79
+ logger.warning 'Address ' + possible_ip + ' not resolved. Can use it'
80
+ return possible_ip
81
+ end
82
+ end
83
+ end
84
+
85
+
86
+
87
+ def add_record(options={})
88
+ logger.debug "Hui1 '#{options}'"
89
+ record = super(options)
90
+ logger.debug "Hui '#{record}'"
91
+ keapostgre_network.add_dhcp_record options
92
+ record
93
+ rescue Exception => e
94
+ logger.error msg = "Error adding DHCP record: #{e}"
95
+ raise Proxy::DHCP::Error, msg
96
+ end
97
+
98
+ def del_record(record)
99
+ logger.debug "Hui '#{record}'"
100
+ # libvirt only supports one subnet per network
101
+ keapostgre_network.del_dhcp_record record
102
+ rescue Exception => e
103
+ logger.error msg = "Error removing DHCP record: #{e}"
104
+ raise Proxy::DHCP::Error, msg
105
+ end
106
+
107
+ def find_record(subnet_address, ip_or_mac_address)
108
+ if ip_or_mac_address =~ Resolv::IPv4::Regex
109
+ keapostgre_network.find_records_by_ip ip_or_mac_address
110
+ else
111
+ keapostgre_network.find_records_by_mac ip_or_mac_address
112
+ end
113
+ end
114
+
115
+ def find_records_by_ip(subnet_address, ip_address)
116
+ keapostgre_network.find_records_by_ip ip_address
117
+ end
118
+
119
+ def find_records_by_mac(subnet_address, mac_address)
120
+ keapostgre_network.find_records_by_mac mac_address
121
+ end
122
+
123
+ def del_record_by_mac(subnet_address, mac_address)
124
+ keapostgre_network.del_record_by_mac mac_address
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,16 @@
1
+ module Proxy::DHCP::DhcpKea
2
+ class Plugin < ::Proxy::Provider
3
+ plugin :dhcp_kea, ::Proxy::DHCP::DhcpKea::VERSION
4
+
5
+ default_settings :host => '127.0.0.1', :port => '5432', :dbname => 'keadb', :username => 'postgres', :db => 'postgres'
6
+
7
+ requires :dhcp, '>= 1.16'
8
+
9
+ validate_readable :config, :leases
10
+
11
+ load_classes ::Proxy::DHCP::DhcpKea::PluginConfiguration
12
+ load_dependency_injection_wirings ::Proxy::DHCP::DhcpKea::PluginConfiguration
13
+
14
+ start_services :free_ips
15
+ end
16
+ end
@@ -0,0 +1,132 @@
1
+ require 'pg'
2
+ require 'mysql2'
3
+
4
+ module ::Proxy::DHCP::DhcpKea
5
+ class KeaDHCPNetwork
6
+
7
+ attr_reader :host, :port, :dbname, :username, :password, :db
8
+
9
+ def initialize(host, port, dbname, username, password, free_ips_service = nil, db)
10
+ @host = host
11
+ @port = port
12
+ @db = db
13
+ @dbname = dbname
14
+ @username = username
15
+ @password = password
16
+ @free_ips = free_ips_service
17
+ end
18
+
19
+ def dhcp_leases
20
+ find_network.dhcp_leases
21
+ rescue ArgumentError
22
+ # workaround for ruby-libvirt < 0.6.1 - DHCP leases API is broken there
23
+ # (http://libvirt.org/git/?p=ruby-libvirt.git;a=commit;h=c2d4192ebf28b8030b753b715a72f0cdf725d313)
24
+ []
25
+ end
26
+
27
+ def add_dhcp_record(record)
28
+
29
+
30
+
31
+ if db.to_s == 'postgres'
32
+ con = PG.connect :dbname => 'keasubnets', :user => username, :password => password, :host => host, :port => port
33
+ query = "SELECT subnet_id from subnets WHERE encode(address,'escape') ~ $1 ORDER BY RIGHT(encode(address,'escape'),2) DESC;"
34
+ subnet_id = nil
35
+ subnet_id = con.exec_params(query, [record['network']])[0]['subnet_id']
36
+ else
37
+ con = Mysql2::Client.new(:dbname => dbname, :user => username, :password => password, :host => host, :port => port)
38
+ st = con.prepare("SELECT subnet_id from dhcp4_subnet WHERE subnet_prefix like ? ORDER BY RIGHT(subnet_prefix,2) DESC;")
39
+ subnet_id = nil
40
+ subnet_id = st.execute(record['network'].to_s + '%')[0]['subnet_id']
41
+
42
+ end
43
+
44
+ unless subnet_id
45
+ raise Proxy::DHCP::Error, "Unable to find subnet_id for ip: #{record.ip}"
46
+ end
47
+ con.close if con
48
+
49
+ if db == 'postgres'
50
+ con = PG.connect :dbname => dbname, :user => username, :password => password, :host => host, :port => port
51
+ query = "INSERT INTO hosts(dhcp_identifier,dhcp_identifier_type,dhcp4_subnet_id,ipv4_address,hostname,dhcp4_client_classes,dhcp4_next_server,dhcp4_boot_file_name) VALUES(DECODE($1,'hex'),0,$2,(SELECT ($3::inet - '0.0.0.0'::inet) as ip_integer),$4,'foreman',(SELECT ($5::inet - '0.0.0.0'::inet) as ip_integer),$6)"
52
+ con.exec_params(query, [record['mac'].gsub(":",""), subnet_id, record['ip'], record['name'], record['nextServer'],record['filename']])
53
+ else
54
+ con = Mysql2::Client.new(:dbname => dbname, :user => username, :password => password, :host => host, :port => port)
55
+ st = con.prepare("INSERT INTO hosts(dhcp_identifier,dhcp_identifier_type,dhcp4_subnet_id,ipv4_address,hostname,dhcp4_client_classes,dhcp4_next_server,dhcp4_boot_file_name) VALUES(?,0,?,INET_ATON(?) as ip_integer),?,'foreman',INET_ATON(?) as ip_integer),?)")
56
+ st.execute(record['mac'].gsub(":",""), subnet_id, record['ip'], record['name'], record['nextServer'],record['filename'])
57
+
58
+ end
59
+
60
+ con.close if con
61
+
62
+ end
63
+
64
+
65
+ def del_dhcp_record(record)
66
+
67
+ if db == 'postgres'
68
+ con = PG.connect :dbname => dbname, :user => username, :password => password, :host => host, :port => port
69
+ con.exec_params("DELETE FROM hosts WHERE encode(dhcp_identifier,'hex') = $1", [record.mac.gsub(":","")])
70
+ else
71
+ con = Mysql2::Client.new(:dbname => dbname, :user => username, :password => password, :host => host, :port => port)
72
+ st = con.prepare("DELETE FROM hosts WHERE dhcp_identifier = ?")
73
+ st.execute(record.mac.gsub(":",""))
74
+ end
75
+
76
+ con.close if con
77
+ end
78
+
79
+
80
+ def del_record_by_mac(mac)
81
+
82
+ if db == 'postgres'
83
+ con = PG.connect :dbname => dbname, :user => username, :password => password, :host => host, :port => port
84
+ con.exec_params("DELETE FROM hosts WHERE encode(dhcp_identifier,'hex') = $1", [mac.gsub(":","")])
85
+ else
86
+ con = Mysql2::Client.new(:dbname => dbname, :user => username, :password => password, :host => host, :port => port)
87
+ st = con.prepare("DELETE FROM hosts WHERE dhcp_identifier = ?")
88
+ st.execute(mac.gsub(":",""))
89
+ end
90
+
91
+ con.close if con
92
+
93
+ end
94
+
95
+
96
+ def find_records_by_mac(mac)
97
+
98
+ if db == 'postgres'
99
+ con = PG.connect :dbname => dbname, :user => username, :password => password, :host => host, :port => port
100
+ host_info = con.exec_params("SELECT encode(dhcp_identifier,'hex') as mac,(SELECT('0.0.0.0'::inet + ipv4_address)) as ip,hostname FROM hosts WHERE encode(dhcp_identifier,'hex') = $1", [mac.gsub(":","")] )
101
+ else
102
+ con = Mysql2::Client.new(:dbname => dbname, :user => username, :password => password, :host => host, :port => port)
103
+ st = con.prepare("SELECT dhcp_identifier as mac,(INET_NTOA(ipv4_address)) as ip,hostname FROM hosts WHERE dhcp_identifier = ?")
104
+ host_info = st.execute(mac.gsub(":",""))
105
+ end
106
+
107
+ con.close if con
108
+ Proxy::DHCP::Record.new(host_info[0]['hostname'], host_info[0]['ip'], host_info[0]['mac'].scan(/.{1,2}/).join(':'))
109
+ end
110
+
111
+ def find_records_by_ip(ip)
112
+
113
+ if db == 'postgres'
114
+ con = PG.connect :dbname => dbname, :user => username, :password => password, :host => host, :port => port
115
+ host_info = con.exec_params("SELECT encode(dhcp_identifier,'hex') as mac,(SELECT('0.0.0.0'::inet + ipv4_address)) as ip,hostname FROM hosts WHERE ipv4_address = (SELECT ($1::inet - '0.0.0.0'::inet) as ip_integer)", [ip] )
116
+ else
117
+ con = Mysql2::Client.new(:dbname => dbname, :user => username, :password => password, :host => host, :port => port)
118
+ st = con.prepare("SELECT dhcp_identifier as mac,INET_NTOA(ipv4_address) as ip,hostname FROM hosts WHERE ipv4_address = INET_ATON(?)")
119
+ host_info = st.execute(ip)
120
+ end
121
+
122
+ con.close if con
123
+ Proxy::DHCP::Record.new(host_info[0]['hostname'], host_info[0]['ip'], host_info[0]['mac'].scan(/.{1,2}/).join(':'))
124
+ rescue Exception => e
125
+ return ''
126
+ raise e
127
+ end
128
+
129
+ end
130
+
131
+ end
132
+
@@ -0,0 +1,58 @@
1
+ module Proxy::DHCP::DhcpKea
2
+ class PluginConfiguration
3
+
4
+ include Proxy::Log
5
+
6
+
7
+
8
+ def load_dependency_injection_wirings(container, settings)
9
+
10
+
11
+ logger.debug 'Plugin initializer, database used is ' + settings[:db].to_s
12
+
13
+ container.dependency :memory_store, ::Proxy::MemoryStore
14
+
15
+ container.dependency :subnet_service, (lambda do
16
+ ::Proxy::DHCP::SubnetService.new(container.get_dependency(:memory_store),
17
+ container.get_dependency(:memory_store), container.get_dependency(:memory_store),
18
+ container.get_dependency(:memory_store), container.get_dependency(:memory_store))
19
+ end)
20
+
21
+
22
+ container.dependency :subnet_service_initializer, (lambda do
23
+ ::Proxy::DHCP::DhcpKea::SubnetServiceInitializer.new(settings[:config], settings[:leases],
24
+ container.get_dependency(:parser), container.get_dependency(:subnet_service))
25
+ end)
26
+
27
+
28
+ container.dependency :keapostgre_network, (lambda do
29
+ ::Proxy::DHCP::DhcpKea::KeaDHCPNetwork.new(settings[:host], settings[:port], settings[:dbname], settings[:username], settings[:password], settings[:db])
30
+ end)
31
+
32
+
33
+ container.dependency :initialized_subnet_service, (lambda do
34
+ ::Proxy::DHCP::DhcpKea::SubnetServiceInitializer.new(settings[:host], settings[:port], settings[:dbname], settings[:username], settings[:password], settings[:db]).initialized_subnet_service(container.get_dependency(:subnet_service))
35
+ end)
36
+
37
+
38
+ container.singleton_dependency :free_ips, lambda {::Proxy::DHCP::FreeIps.new }
39
+
40
+ container.dependency :dhcp_provider, (lambda do
41
+ ::Proxy::DHCP::DhcpKea::Provider.new(settings[:host], settings[:port], settings[:dbname], settings[:username], settings[:password], container.get_dependency(:keapostgre_network),
42
+ container.get_dependency(:initialized_subnet_service),
43
+ container.get_dependency(:free_ips), settings[:db])
44
+ end)
45
+
46
+
47
+ end
48
+
49
+ def load_classes
50
+ require 'smart_proxy_dhcp_kea/keapostgre_dhcp_network'
51
+ require 'dhcp_common/subnet_service'
52
+ require 'dhcp_common/free_ips'
53
+ require 'dhcp_common/server'
54
+ require 'smart_proxy_dhcp_kea/subnet_service_initializer'
55
+ require 'smart_proxy_dhcp_kea/dhcp_kea_main'
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,100 @@
1
+ require 'rexml/document'
2
+ require 'ipaddr'
3
+ require 'pg'
4
+ require 'mysql2'
5
+
6
+
7
+ module Proxy::DHCP::DhcpKea
8
+ class SubnetServiceInitializer
9
+ include Proxy::Log
10
+
11
+ attr_reader :host, :port, :dbname, :username, :password, :db
12
+
13
+ def initialize(host, port, dbname, username, password, db, free_ips_service = nil)
14
+ @host = host
15
+ @port = port
16
+ @db = db
17
+ @dbname = dbname
18
+ @username = username
19
+ @password = password
20
+ @free_ips = free_ips_service
21
+ end
22
+
23
+ def initialized_subnet_service(subnet_service)
24
+ subnet_service.add_subnets(*parse_config_for_subnets)
25
+ logger.debug msg = "Initializing subnet"
26
+ # load_subnet_data(subnet_service)
27
+ subnet_service
28
+ end
29
+
30
+ def parse_config_for_subnets
31
+
32
+ ret_val = []
33
+
34
+ if db == 'postgres'
35
+ con = PG.connect :dbname => 'keasubnets', :user => username, :password => password, :host => host, :port => port
36
+ rs = con.exec "SELECT encode(address,'escape') from subnets"
37
+ else
38
+ con = Mysql2::Client.new(:dbname => dbname, :user => username, :password => password, :host => host, :port => port)
39
+ rs = con.query ("SELECT subnet_prefix from dhcp4_subnet")
40
+ end
41
+
42
+ require 'ipaddress'
43
+ rs.each do |subnet|
44
+ ip = IPAddress::IPv4.new subnet['encode']
45
+ ret_val << Proxy::DHCP::Subnet.new(ip.address, ip.netmask)
46
+ logger.debug ip.to_s
47
+ end
48
+ logger.debug ret_val.to_s
49
+ ret_val
50
+ rescue Exception => e
51
+ logger.error msg = "Unable to parse postgres subnets: #{e}"
52
+ raise Proxy::DHCP::Error, msg
53
+ end
54
+
55
+ # Expects subnet_service to have subnet data
56
+ def parse_config_for_dhcp_reservations(subnet_service)
57
+ to_ret = []
58
+ doc = REXML::Document.new xml = libvirt_network.dump_xml
59
+ REXML::XPath.each(doc, "//network/ip[not(@family) or @family='ipv4']/dhcp/host") do |e|
60
+ subnet = subnet_service.find_subnet(e.attributes['ip'])
61
+ to_ret << Proxy::DHCP::Reservation.new(
62
+ e.attributes["name"],
63
+ e.attributes["ip"],
64
+ e.attributes["mac"],
65
+ subnet,
66
+ :hostname => e.attributes["name"])
67
+ end
68
+ to_ret
69
+ rescue Exception => e
70
+ logger.error msg = "Unable to parse reservations XML: #{e}"
71
+ logger.debug xml if defined?(xml)
72
+ raise Proxy::DHCP::Error, msg
73
+ end
74
+
75
+ def load_subnet_data(subnet_service)
76
+ reservations = parse_config_for_dhcp_reservations(subnet_service)
77
+ reservations.each { |record| subnet_service.add_host(record.subnet_address, record) }
78
+ leases = load_leases(subnet_service)
79
+ leases.each { |lease| subnet_service.add_lease(lease.subnet_address, lease) }
80
+ end
81
+
82
+ # Expects subnet_service to have subnet data
83
+ def load_leases(subnet_service)
84
+ leases = libvirt_network.dhcp_leases
85
+ leases.map do |element|
86
+ subnet = subnet_service.find_subnet(element['ipaddr'])
87
+ Proxy::DHCP::Lease.new(
88
+ nil,
89
+ element['ipaddr'],
90
+ element['mac'],
91
+ subnet,
92
+ Time.now.utc,
93
+ Time.at(element['expirytime'] || 0).utc,
94
+ 'active'
95
+ )
96
+ end
97
+ end
98
+ end
99
+ end
100
+