Skip to content

Commit

Permalink
Fixes #34205 - External IPAM Integration
Browse files Browse the repository at this point in the history
  • Loading branch information
Christopher Smith committed Jan 3, 2022
1 parent d60623c commit de613b4
Show file tree
Hide file tree
Showing 23 changed files with 1,498 additions and 0 deletions.
1 change: 1 addition & 0 deletions Contributors
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Ashley Penney <[email protected]>
Baptiste Agasse <[email protected]>
Brandon Weeks <[email protected]>
Christian Arnold <[email protected]>
Christopher Smith <[email protected]>
Corey Osman <[email protected]>
Daniel Baeurer <[email protected]>
Daniel Helgenberger <[email protected]>
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Currently Supported modules:
* HTTPBoot - endpoint exposing a (TFTP) directory via HTTP(s) for UEFI HTTP booting
* Logs - log buffer of proxy logs for easier troubleshooting
* Templates - unattended Foreman endpoint proxy
* External IPAM - Integration with External IPAM providers

# Installation
Read the [Smart Proxy Installation section](https://theforeman.org/manuals/latest/index.html#4.3.1SmartProxyInstallation) of the manual.
Expand Down
8 changes: 8 additions & 0 deletions config/settings.d/externalipam.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
:enabled: false

# Supported providers:
# 1. phpIPAM: externalipam_phpipam
# 2. Netbox: externalipam_netbox

:use_provider: externalipam_netbox
3 changes: 3 additions & 0 deletions config/settings.d/externalipam_netbox.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
:url: 'http://my-netbox-url.com'
:token: 'netbox_token'
4 changes: 4 additions & 0 deletions config/settings.d/externalipam_phpipam.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
:url: 'http://my-phpipam-url.com'
:user: 'ipam_user'
:password: 'ipam_password'
1 change: 1 addition & 0 deletions lib/smart_proxy_main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ module Proxy
require 'dhcp_isc/dhcp_isc'
require 'dhcp_native_ms/dhcp_native_ms'
require 'dhcp_libvirt/dhcp_libvirt'
require 'externalipam/externalipam'
require 'puppetca/puppetca'
require 'puppetca_http_api/puppetca_http_api'
require 'puppetca_puppet_cert/puppetca_puppet_cert'
Expand Down
54 changes: 54 additions & 0 deletions modules/externalipam/api_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require 'yaml'
require 'json'
require 'net/http'
require 'uri'
require 'externalipam/ipam_helper'

module Proxy::Ipam
# Class to handle authentication and HTTP transactions with External IPAM providers
class ApiResource
include ::Proxy::Log
include Proxy::Ipam::IpamHelper

def initialize(params = {})
@api_base = params[:api_base]
@token = params[:token]
@auth_header = params[:auth_header] || 'Authorization'
end

def get(path)
uri = URI(@api_base + path)
request = Net::HTTP::Get.new(uri)
request[@auth_header] = @token
request['Accept'] = 'application/json'

Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
end

def delete(path)
uri = URI(@api_base + path)
request = Net::HTTP::Delete.new(uri)
request[@auth_header] = @token
request['Accept'] = 'application/json'

Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
end

def post(path, body = nil)
uri = URI(@api_base + path)
request = Net::HTTP::Post.new(uri)
request.body = body
request[@auth_header] = @token
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'

Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
end
end
end
8 changes: 8 additions & 0 deletions modules/externalipam/configuration_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module ::Proxy::Ipam
class ConfigurationLoader
def load_classes
require 'externalipam/dependency_injection'
require 'externalipam/ipam_api'
end
end
end
8 changes: 8 additions & 0 deletions modules/externalipam/dependency_injection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Proxy::Ipam
module DependencyInjection
include Proxy::DependencyInjection::Accessors
def container_instance
@container_instance ||= ::Proxy::Plugins.instance.find { |p| p[:name] == :externalipam }[:di_container]
end
end
end
2 changes: 2 additions & 0 deletions modules/externalipam/externalipam.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'externalipam/externalipam_plugin'
require 'externalipam/configuration_loader'
11 changes: 11 additions & 0 deletions modules/externalipam/externalipam_plugin.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'externalipam/phpipam/phpipam_plugin'
require 'externalipam/netbox/netbox_plugin'

module Proxy::Ipam
class Plugin < ::Proxy::Plugin
plugin :externalipam, ::Proxy::VERSION
uses_provider
default_settings use_provider: 'externalipam_phpipam'
rackup_path File.expand_path('http_config.ru', __dir__)
end
end
5 changes: 5 additions & 0 deletions modules/externalipam/http_config.ru
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'externalipam/ipam_api'

map '/ipam' do
run Proxy::Ipam::Api
end
133 changes: 133 additions & 0 deletions modules/externalipam/ip_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
require 'yaml'
require 'json'
require 'monitor'
require 'concurrent'
require 'time'
require 'externalipam/ipam_helper'
require 'singleton'

module Proxy::Ipam
# Class for managing temp in-memory cache to prevent same IP's being suggested in race conditions
class IpCache
include Singleton
include Proxy::Log
include Proxy::Ipam::IpamHelper

DEFAULT_CLEANUP_INTERVAL = 60

def initialize
@m = Monitor.new
init_cache
start_cleanup_task
end

def provider_name(provider)
@provider = provider
end

def set_group(group, value)
@ip_cache[group.to_sym] = value
end

def get_group(group)
@ip_cache[group.to_sym]
end

def get_cidr(group, cidr)
@ip_cache[group.to_sym][cidr.to_sym]
end

def get_ip(group_name, cidr, mac)
@ip_cache[group_name.to_sym][cidr.to_sym][mac.to_sym][:ip]
end

def cleanup_interval
DEFAULT_CLEANUP_INTERVAL
end

def ip_exists(ip, cidr, group_name)
cidr_key = @ip_cache[group_name.to_sym][cidr.to_sym]&.to_s
cidr_key.include?(ip.to_s)
end

def add(ip, mac, cidr, group_name)
logger.debug("Adding IP '#{ip}' to cache for subnet '#{cidr}' in group '#{group_name}' for IPAM provider #{@provider}")
@m.synchronize do
mac_addr = mac.nil? || mac.empty? ? SecureRandom.uuid : mac
group_hash = @ip_cache[group_name.to_sym]

group_hash.each do |key, values|
if values.key? mac_addr.to_sym
@ip_cache[group_name.to_sym][key].delete(mac_addr.to_sym)
end
@ip_cache[group_name.to_sym].delete(key) if @ip_cache[group_name.to_sym][key].nil? || @ip_cache[group_name.to_sym][key].empty?
end

if group_hash.key?(cidr.to_sym)
@ip_cache[group_name.to_sym][cidr.to_sym][mac_addr.to_sym] = {ip: ip.to_s, timestamp: Time.now.to_s}
else
@ip_cache = @ip_cache.merge({group_name.to_sym => {cidr.to_sym => {mac_addr.to_sym => {ip: ip.to_s, timestamp: Time.now.to_s}}}})
end
end
end

private

def start_cleanup_task
logger.info("Starting ip cache maintenance for IPAM provider #{@provider}, used by /next_ip.")
@timer_task = Concurrent::TimerTask.new(execution_interval: DEFAULT_CLEANUP_INTERVAL) { init_cache }
@timer_task.execute
end

# @ip_cache structure
#
# Groups of subnets are cached under the External IPAM Group name. For example,
# "IPAM Group Name" would be the section name in phpIPAM. All IP's cached for subnets
# that do not have an External IPAM group specified, they are cached under the "" key. IP's
# are cached using one of two possible keys:
# 1). Mac Address
# 2). UUID (Used when Mac Address not specified)
#
# {
# "": {
# "100.55.55.0/24":{
# "00:0a:95:9d:68:10": {"ip": "100.55.55.1", "timestamp": "2019-09-17 12:03:43 -D400"},
# "906d8bdc-dcc0-4b59-92cb-665935e21662": {"ip": "100.55.55.2", "timestamp": "2019-09-17 11:43:22 -D400"}
# },
# },
# "IPAM Group Name": {
# "123.11.33.0/24":{
# "00:0a:95:9d:68:33": {"ip": "123.11.33.1", "timestamp": "2019-09-17 12:04:43 -0400"},
# "00:0a:95:9d:68:34": {"ip": "123.11.33.2", "timestamp": "2019-09-17 12:05:48 -0400"},
# "00:0a:95:9d:68:35": {"ip": "123.11.33.3", "timestamp:: "2019-09-17 12:06:50 -0400"}
# }
# },
# "Another IPAM Group": {
# "185.45.39.0/24":{
# "00:0a:95:9d:68:55": {"ip": "185.45.39.1", "timestamp": "2019-09-17 12:04:43 -0400"},
# "00:0a:95:9d:68:56": {"ip": "185.45.39.2", "timestamp": "2019-09-17 12:05:48 -0400"}
# }
# }
# }
def init_cache
@m.synchronize do
if @ip_cache && !@ip_cache.empty?
logger.debug("Processing ip cache for IPAM provider #{@provider}")
@ip_cache.each do |group, subnets|
subnets.each do |cidr, macs|
macs.each do |mac, ip|
if Time.now - Time.parse(ip[:timestamp]) > DEFAULT_CLEANUP_INTERVAL
@ip_cache[group][cidr].delete(mac)
end
end
@ip_cache[group].delete(cidr) if @ip_cache[group][cidr].nil? || @ip_cache[group][cidr].empty?
end
end
else
logger.debug("Clearing ip cache for IPAM provider #{@provider}")
@ip_cache = {'': {}}
end
end
end
end
end
Loading

0 comments on commit de613b4

Please sign in to comment.