|
module Orchestration::DHCP
|
|
extend ActiveSupport::Concern
|
|
include Orchestration::Common
|
|
|
|
included do
|
|
after_validation :dhcp_conflict_detected?, :unless => :skip_orchestration?
|
|
after_validation :queue_dhcp
|
|
before_destroy :queue_dhcp_destroy
|
|
register_rebuild(:rebuild_dhcp, N_('DHCP'))
|
|
end
|
|
|
|
def dhcp?
|
|
# host.managed? and managed? should always come first so that orchestration doesn't
|
|
# even get tested for such objects
|
|
#
|
|
# The subnet boot mode is ignored as DHCP can be required for PXE or image provisioning
|
|
# steps, while boot mode can be used in templates later.
|
|
(host.nil? || host.managed?) && managed? && hostname.present? && ip_available? && mac_available? &&
|
|
!subnet.nil? && subnet.dhcp? && SETTINGS[:unattended] && (!provision? || operatingsystem.present?)
|
|
end
|
|
|
|
def dhcp_records
|
|
return [] unless dhcp?
|
|
@dhcp_records ||= mac_addresses_for_provisioning.map do |record_mac|
|
|
build_dhcp_record(record_mac)
|
|
end
|
|
end
|
|
|
|
def reset_dhcp_record_cache
|
|
@dhcp_records = nil
|
|
end
|
|
|
|
def rebuild_dhcp
|
|
unless dhcp?
|
|
logger.info "DHCP not supported for #{name}, #{ip}, skipping orchestration rebuild"
|
|
return true
|
|
end
|
|
|
|
del_dhcp_safe
|
|
begin
|
|
set_dhcp
|
|
rescue => e
|
|
Foreman::Logging.exception "Failed to rebuild DHCP record for #{name}, #{ip}", e, :level => :error
|
|
false
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def del_dhcp_safe
|
|
del_dhcp
|
|
rescue => e
|
|
Foreman::Logging.exception "Proxy failed to delete DHCP record for #{name}, #{ip}", e, :level => :error
|
|
end
|
|
|
|
def set_dhcp
|
|
dhcp_records.all? { |record| record.create }
|
|
end
|
|
|
|
def set_dhcp_conflicts
|
|
dhcp_records.all? do |record|
|
|
record.conflicts.each { |conflict| conflict.create }
|
|
end
|
|
end
|
|
|
|
def del_dhcp
|
|
dhcp_records.all? { |record| record.destroy }
|
|
end
|
|
|
|
def del_dhcp_conflicts
|
|
dhcp_records.all? do |record|
|
|
record.conflicts.all? { |conflict| conflict.destroy }
|
|
end
|
|
end
|
|
|
|
# where are we booting from
|
|
def boot_server
|
|
# if we don't manage tftp for IPv4 at all, we dont create a next-server entry.
|
|
return unless tftp?
|
|
|
|
# first try to ask our IPv4 TFTP server for its boot server
|
|
bs = tftp.bootServer
|
|
# if that failed, trying to guess out tftp next server based on the smart proxy hostname
|
|
bs ||= URI.parse(subnet.tftp.url).host
|
|
# now convert it into an ip address (see http://theforeman.org/issues/show/1381)
|
|
ip = NicIpResolver.new(:nic => self).to_ip_address(bs) if bs.present?
|
|
return ip unless ip.nil?
|
|
|
|
failure _("Unable to determine the host's boot server. The DHCP smart proxy failed to provide this information and this subnet is not provided with TFTP services.")
|
|
rescue => e
|
|
failure _("failed to detect boot server: %s") % e, e
|
|
end
|
|
|
|
private
|
|
|
|
def build_dhcp_record(record_mac)
|
|
raise ::Foreman::Exception.new(N_("DHCP not supported for this NIC")) unless dhcp?
|
|
record_attrs = dhcp_attrs(record_mac)
|
|
record_type = (provision? && jumpstart?) ? Net::DHCP::SparcRecord : Net::DHCP::Record
|
|
handle_validation_errors do
|
|
record_type.new(record_attrs)
|
|
end
|
|
end
|
|
|
|
# returns a hash of dhcp record settings
|
|
def dhcp_attrs(record_mac)
|
|
raise ::Foreman::Exception.new(N_("DHCP not supported for this NIC")) unless dhcp?
|
|
|
|
dhcp_attr = {
|
|
:name => dhcp_record_name(record_mac),
|
|
:hostname => hostname,
|
|
:ip => ip,
|
|
:mac => record_mac,
|
|
:proxy => subnet.dhcp_proxy,
|
|
:network => subnet.network,
|
|
:related_macs => mac_addresses_for_provisioning - [record_mac]
|
|
}
|
|
|
|
if provision?
|
|
dhcp_attr[:nextServer] = boot_server
|
|
filename = operatingsystem.boot_filename(self.host)
|
|
dhcp_attr[:filename] = filename if filename.present?
|
|
if jumpstart?
|
|
jumpstart_arguments = os.jumpstart_params self.host, model.vendor_class
|
|
dhcp_attr.merge! jumpstart_arguments unless jumpstart_arguments.empty?
|
|
end
|
|
end
|
|
|
|
dhcp_attr
|
|
end
|
|
|
|
def dhcp_record_name(record_mac)
|
|
return name if mac_addresses_for_provisioning.size <= 1
|
|
"#{name}-#{'%02d' % (mac_addresses_for_provisioning.index(record_mac) + 1)}"
|
|
end
|
|
|
|
def queue_dhcp
|
|
return log_orchestration_errors unless (dhcp? || (old&.dhcp?)) && orchestration_errors?
|
|
queue_remove_dhcp_conflicts
|
|
new_record? ? queue_dhcp_create : queue_dhcp_update
|
|
end
|
|
|
|
def queue_dhcp_create
|
|
logger.debug "Scheduling new DHCP reservations for #{self}"
|
|
queue.create(id: "dhcp_create_#{self.mac}", name: _("Create DHCP Settings for %s") % self, priority: 10, action: [self, :set_dhcp]) if dhcp?
|
|
end
|
|
|
|
def queue_dhcp_update
|
|
return unless dhcp_update_required?
|
|
logger.debug("Detected a changed required for DHCP record")
|
|
queue.create(id: "dhcp_remove_#{old.mac}", name: _("Remove DHCP Settings for %s") % old, priority: 5, action: [old, :del_dhcp]) if old.dhcp?
|
|
queue.create(id: "dhcp_create_#{self.mac}", name: _("Create DHCP Settings for %s") % self, priority: 9, action: [self, :set_dhcp]) if dhcp?
|
|
end
|
|
|
|
# do we need to update our dhcp reservations
|
|
def dhcp_update_required?
|
|
# IP Address / name changed, or 'rebuild' action is triggered and DHCP record on the smart proxy is not present/identical.
|
|
return true if ((old.ip != ip) || (old.hostname != hostname) || provision_mac_addresses_changed? || (old.subnet != subnet) || (operatingsystem.boot_filename(old.host) != operatingsystem.boot_filename(self.host)) ||
|
|
(!old.build? && build? && !all_dhcp_records_valid?))
|
|
# Handle jumpstart
|
|
# TODO, abstract this way once interfaces are fully used
|
|
if self.is_a?(Host::Base) && jumpstart?
|
|
if !old.build? || (old.medium != medium || old.arch != arch) ||
|
|
(os && old.os && (old.os.name != os.name || old.os != os))
|
|
return true
|
|
end
|
|
end
|
|
false
|
|
end
|
|
|
|
def all_dhcp_records_valid?
|
|
dhcp_records.all? { |record| record.valid? }
|
|
end
|
|
|
|
def queue_dhcp_destroy
|
|
return unless dhcp? && errors.empty?
|
|
queue.create(id: "dhcp_remove_#{self.mac}", name: _("Remove DHCP Settings for %s") % self, priority: 5, action: [self, :del_dhcp])
|
|
true
|
|
end
|
|
|
|
def queue_remove_dhcp_conflicts
|
|
return if !dhcp? || !overwrite?
|
|
|
|
logger.debug "Scheduling DHCP conflicts removal"
|
|
queue.create(id: "dhcp_conflicts_remove_#{self.mac}", name: _("DHCP conflicts removal for %s") % self, priority: 5, action: [self, :del_dhcp_conflicts])
|
|
end
|
|
|
|
def dhcp_conflict_detected?
|
|
# we can't do any dhcp based validations when our MAC address is defined afterwards (e.g. in vm creation)
|
|
return false if mac.blank? || hostname.blank?
|
|
return false unless dhcp?
|
|
|
|
if dhcp_records.any? && dhcp_records.any? { |record| record.conflicting? } && !overwrite?
|
|
failure(_("DHCP records %s already exists") % dhcp_records.map {|record| record.conflicts}.flatten.to_sentence, nil, :conflict) # compact?
|
|
return true
|
|
end
|
|
false
|
|
end
|
|
|
|
def provision_mac_addresses_changed?
|
|
old.mac_addresses_for_provisioning != mac_addresses_for_provisioning
|
|
end
|
|
end
|