Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #27067 - IPAM Integration with phpIPAM #6847

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/helpers/subnets_helper.rb
Expand Up @@ -33,4 +33,8 @@ def subnet_ipam_modes(type)
return [] unless Subnet::SUBNET_TYPES.key?(type.to_sym)
type.safe_constantize.supported_ipam_modes_with_translations
end

def external_ipam?(subnet)
subnet&.ipam&.downcase == "external ipam"
grizzthedj marked this conversation as resolved.
Show resolved Hide resolved
end
end
86 changes: 86 additions & 0 deletions app/models/concerns/orchestration/external_ipam.rb
@@ -0,0 +1,86 @@
module Orchestration::ExternalIPAM
extend ActiveSupport::Concern
include Orchestration::Common
include SubnetsHelper

included do
after_validation :queue_external_ipam
before_destroy :queue_external_ipam_destroy
end

def generate_external_ipam_task_id(action, interface = self)
id = [interface.mac, interface.ip, interface.identifier, interface.id].find {|x| x&.present?}
"external_ipam_#{action}_#{id}"
end

protected

def set_external_ip
if ip_is_available?
response = subnet.external_ipam_proxy.add_ip_to_subnet(ip, subnet)
success?(response, 'Address created')
else
self.errors.add :ip, _('This IP address has already been reserved in External IPAM')
self.errors.add :interfaces, _('Some interfaces are invalid')
grizzthedj marked this conversation as resolved.
Show resolved Hide resolved
end
end

# Empty method for rollbacks. We don't want to delete IP's from IPAM when there
# is a conflict(i.e. IP address already taken)
def del_external_ip
end

def remove_external_ip
response = subnet.external_ipam_proxy.delete_ip_from_subnet(ip, subnet)
success?(response, 'Address deleted')
end

def requires_update?
return false if new_record?
old.ip != self.ip
end
grizzthedj marked this conversation as resolved.
Show resolved Hide resolved

def requires_delete?
old = Nic::Base.find(id)
!old.ip.nil? && !old.subnet_id.nil?
end

def ip_is_available?
response = subnet.external_ipam_proxy.ip_exists(ip, subnet)
success?(response, 'No addresses found')
end

def success?(response, message)
(response['message'] && response['message'] == message) ? true : false
end

private

def queue_external_ipam
new_record? ? queue_external_ipam_create : queue_external_ipam_update
end

def queue_external_ipam_create
return unless external_ipam?(subnet) && errors.empty?
logger.debug "Scheduling new IP reservation in external IPAM for #{self}"
queue.create(id: generate_external_ipam_task_id("create"), name: _("Creating IP in External IPAM for %s") % self, priority: 10, action: [self, :set_external_ip]) if external_ipam?(subnet)
true
end

def queue_external_ipam_destroy
return unless external_ipam?(subnet) && errors.empty?
logger.debug "Removing IP reservation in external IPAM for #{self}"
queue.create(id: generate_external_ipam_task_id("remove"), name: _("Removing IP in External IPAM for %s") % self, priority: 5, action: [self, :remove_external_ip]) if external_ipam?(subnet) && requires_delete?
true
end

def queue_external_ipam_update
return unless external_ipam?(subnet) && errors.empty?
if requires_update?
logger.debug "Updating IP reservation in external IPAM for #{self}"
queue.create(id: generate_external_ipam_task_id("remove"), name: _("Removing IP in External IPAM for %s") % self, priority: 5, action: [old, :remove_external_ip]) if external_ipam?(subnet) && requires_delete?
queue.create(id: generate_external_ipam_task_id("create"), name: _("Creating IP in External IPAM for %s") % self, priority: 10, action: [self, :set_external_ip]) if external_ipam?(subnet)
end
true
end
end
1 change: 1 addition & 0 deletions app/models/nic/managed.rb
Expand Up @@ -4,6 +4,7 @@ class Managed < Interface
include Orchestration::DHCP
include Orchestration::DNS
include Orchestration::TFTP
include Orchestration::ExternalIPAM
include DnsInterface
include InterfaceCloning

Expand Down
36 changes: 35 additions & 1 deletion app/models/subnet.rb
Expand Up @@ -58,6 +58,12 @@ def model_name
:api_description => N_('HTTPBoot Proxy ID to use within this subnet'),
:description => N_('HTTPBoot Proxy to use within this subnet')

belongs_to_proxy :external_ipam,
:feature => N_('external_ipam'),
:label => N_('IP Address Management with various external IPAM providers'),
:api_description => N_('External IPAM Proxy ID to use within this subnet'),
:description => N_('External IPAM Proxy to use within this subnet')

belongs_to_proxy :dns,
:feature => N_('DNS'),
:label => N_('Reverse DNS Proxy'),
Expand Down Expand Up @@ -86,6 +92,7 @@ def model_name
validates :mtu, numericality: { :only_integer => true, :greater_than_or_equal_to => 68}, :presence => true

before_validation :normalize_addresses
after_validation :validate_against_external_ipam
validate :ensure_ip_addrs_valid

validate :validate_ranges
Expand Down Expand Up @@ -197,12 +204,20 @@ def template_proxy(attrs = {})
@template_proxy ||= ProxyAPI::Template.new({:url => template.url}.merge(attrs)) if template?
end

def external_ipam?
supports_ipam_mode?(:external_ipam) && external_ipam && external_ipam.url.present?
end

def external_ipam_proxy(attrs = {})
@external_ipam_proxy ||= ProxyAPI::ExternalIpam.new({:url => external_ipam.url}.merge(attrs)) if external_ipam?
end

def ipam?
self.ipam != IPAM::MODES[:none]
end

def ipam_needs_range?
ipam? && self.ipam != IPAM::MODES[:eui64]
ipam? && self.ipam != IPAM::MODES[:eui64] && self.ipam != IPAM::MODES[:external_ipam]
end

def dhcp_boot_mode?
Expand All @@ -218,6 +233,25 @@ def unused_ip(mac = nil, excluded_ips = [])
IPAM.new(self.ipam, opts)
end

def validate_against_external_ipam
return unless self.errors.full_messages.empty?

if self.external_ipam?
external_ipam_proxy = SmartProxy.with_features('external_ipam').first

if external_ipam_proxy.nil?
self.errors.add :ipam, _('There must be at least one Smart Proxy present with External IPAM plugin installed and configured')
elsif !subnet_exists_in_external_ipam
self.errors.add :network, _('Subnet not found in the configured External IPAM instance')
end
end
end
grizzthedj marked this conversation as resolved.
Show resolved Hide resolved

def subnet_exists_in_external_ipam
subnet = self.external_ipam_proxy.get_subnet(self) if self.external_ipam_proxy
!(!subnet.kind_of?(Array) && subnet['message'] && subnet['message'] == 'No subnets found')
end

def known_ips
self.interfaces.reload
ips = self.interfaces.map(&ip_sym) + self.hosts.includes(:interfaces).map(&ip_sym)
Expand Down
2 changes: 1 addition & 1 deletion app/models/subnet/ipv4.rb
Expand Up @@ -26,7 +26,7 @@ def validate_ip(ip)
end

def self.supported_ipam_modes
[:dhcp, :db, :random_db, :none]
[:dhcp, :db, :random_db, :external_ipam, :none]
end

def self.show_mask?
Expand Down
4 changes: 3 additions & 1 deletion app/services/ipam.rb
@@ -1,5 +1,5 @@
module IPAM
MODES = {:dhcp => N_('DHCP'), :db => N_('Internal DB'), :random_db => N_('Random DB'), :eui64 => N_('EUI-64'), :none => N_('None')}
MODES = {:dhcp => N_('DHCP'), :db => N_('Internal DB'), :random_db => N_('Random DB'), :eui64 => N_('EUI-64'), :external_ipam => N_('External IPAM'), :none => N_('None')}

def self.new(type, *args)
case type
Expand All @@ -13,6 +13,8 @@ def self.new(type, *args)
IPAM::RandomDb.new(*args)
when IPAM::MODES[:eui64]
IPAM::Eui64.new(*args)
when IPAM::MODES[:external_ipam]
IPAM::ExternalIpam.new(*args)
else
raise ::Foreman::Exception.new(N_("Unknown IPAM type - can't continue"))
end
Expand Down
32 changes: 32 additions & 0 deletions app/services/ipam/external_ipam.rb
@@ -0,0 +1,32 @@
module IPAM
class ExternalIpam < Base
delegate :external_ipam_proxy, :to => :subnet

def suggest_ip
if self.mac.nil?
errors.add(:mac, "You must specify a MAC address before selecting the External IPAM subnet")
return nil
end

logger.debug "Obtaining next available IP from IPAM for subnet #{@subnet.network_address}"
response = external_ipam_proxy.next_ip(@subnet, self.mac)

if response.key?('error')
errors.add(:subnet, response['error'])
nil
else
next_ip = response["data"]
logger.debug("IPAM returned #{next_ip} as the next available IP in subnet #{@subnet.network_address}")
next_ip
end
rescue => e
logger.warn "Failed to fetch the next available IP address from IPAM: #{e}"
errors.add(:subnet, _('Failed to fetch the next available IP address from IPAM %{message}') % {:message => e})
nil
end

def suggest_new?
false
end
end
end
3 changes: 2 additions & 1 deletion app/views/subnets/_fields.html.erb
Expand Up @@ -8,13 +8,14 @@
<%= text_f f, :gateway, :label => _("Gateway Address"), :help_inline => _("Optional: Gateway for this subnet") %>
<%= text_f f, :dns_primary, :label => _("Primary DNS Server"), :help_inline => _("Optional: Primary DNS for this subnet") %>
<%= text_f f, :dns_secondary, :label => _("Secondary DNS Server"), :help_inline => _("Optional: Secondary DNS for this subnet") %>
<%= selectable_f f, :ipam, subnet_ipam_modes(f.object.type), {}, :data => {'disable-auto-suggest-on' => [IPAM::MODES[:none], IPAM::MODES[:eui64]]},
<%= selectable_f f, :ipam, subnet_ipam_modes(f.object.type), {}, :data => {'disable-auto-suggest-on' => [IPAM::MODES[:none], IPAM::MODES[:eui64], IPAM::MODES[:external_ipam]]},
:label => _('IPAM'),
:label_help => _("You can select one of the IPAM modes supported by the selected IP protocol:<br/>" +
"<ul><li><strong>DHCP</strong> - will manage the IP on DHCP through assigned DHCP proxy, auto-suggested IPs come from DHCP <em>(IPv4)</em></li>" +
"<li><strong>Internal DB</strong> - use internal DB to auto-suggest free IP based on other interfaces on same subnet respecting range if specified, useful mainly with static boot mode <em>(IPv4, IPv6)</em>, preserves natural ordering</li>" +
"<li><strong>Random DB</strong> - same as Internal DB but randomizes results to prevent race conditions <em>(IPv4)</em></li>" +
"<li><strong>EUI-64</strong> - will assign the IPv6 address based on the MAC address of the interface <em>(IPv6)</em></li>" +
"<li><strong>External IPAM</strong> - will auto-suggest the next available address via an external IPAM Smart-proxy plugin (IPv4)</li>" +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to limit external IPAM to IPv4?

"<li><strong>None</strong> - leave IP management solely on user, no auto-suggestion <em>(IPv4, IPv6)</em></li></ul>").html_safe,
:label_help_options => { :title => _("IP Address Management"), :'data-placement' => 'top' }%>
<div id='ipam_options' class ='<%= f.object.ipam_needs_range? ? "" : "hide" %>'>
Expand Down
9 changes: 9 additions & 0 deletions db/migrate/20190617165331_add_external_ipam_id_to_subnets.rb
@@ -0,0 +1,9 @@
class AddExternalIpamIdToSubnets < ActiveRecord::Migration[5.2]
def up
add_column :subnets, :external_ipam_id, :integer
end

def down
remove_column :subnets, :external_ipam_id
end
end