Project

General

Profile

« Previous | Next » 

Revision a6f4f5f7

Added by Ohad Levy almost 13 years ago

  • ID a6f4f5f7e750c345d4e6d83b65311fe74636d6a4

fixes #1120 - Replaced DHCP functionaitlity by the new net dhcp record classes

View differences:

app/models/host.rb
has_many :host_parameters, :dependent => :destroy, :foreign_key => :reference_id
accepts_nested_attributes_for :host_parameters, :reject_if => lambda { |a| a[:value].blank? }, :allow_destroy => true
belongs_to :owner, :polymorphic => true
belongs_to :sp_subnet
include Hostext::Search
include HostCommon
app/models/orchestration.rb
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
attr_reader :queue, :old
attr_reader :queue, :old, :record_conflicts
# stores actions to be performed on our proxies based on priority
before_validation :set_queue
before_validation :setup_clone
......
begin
task.status = execute({:action => task.action}) ? "completed" : "failed"
rescue Net::Conflict => e
task.status = "conflict"
@record_conflicts << e
failure e.message
rescue => e
task.status = "failed"
failed "failed #{e}"
failure "failed #{e}"
end
end
# if we have no failures - we are done
return true if q.failed.empty? and q.pending.empty? and errors.empty?
return true if q.failed.empty? and q.pending.empty? and q.conflict.empty? and errors.empty?
logger.debug "Rolling back due to a problem: #{q.failed}"
logger.debug "Rolling back due to a problem: #{q.failed + q.conflict}"
# handle errors
# we try to undo all completed operations and trigger a DB rollback
(q.completed + q.running).sort.reverse_each do |task|
......
execute({:action => task.action, :rollback => true})
rescue => e
# if the operation failed, we can just report upon it
failed "Failed to perform rollback on #{task.name} - #{e}"
failure "Failed to perform rollback on #{task.name} - #{e}"
end
end
......
if obj.respond_to?(met)
return obj.send(met)
else
failed "invalid method #{met}"
failure "invalid method #{met}"
raise "invalid method #{met}"
end
end
def set_queue
@queue = Orchestration::Queue.new
@record_conflicts = []
end
# we keep the before update host object in order to compare changes
app/models/orchestration/dhcp.rb
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
attr_reader :dhcp
after_validation :initialize_dhcp, :queue_dhcp
before_destroy :initialize_dhcp, :queue_dhcp_destroy
validate :ip_belongs_to_subnet?
after_validation :queue_dhcp
before_destroy :queue_dhcp_destroy
validate :ip_belongs_to_subnet?, :valid_jumpstart_model
end
end
......
!subnet.nil? and !subnet.dhcp.nil? and !subnet.dhcp.url.empty?
end
protected
def sp_dhcp?
!sp_subnet.nil? and !sp_subnet.dhcp.nil? and !sp_subnet.dhcp.url.empty?
end
def initialize_dhcp sub = nil
def dhcp_record
return unless dhcp?
# there are usage cases where our object is saved across subnets
# i.e. management port is not on the same subnet
sub ||= subnet
@dhcp = ProxyAPI::DHCP.new(:url => sub.dhcp.url)
rescue => e
failure "Failed to initialize the DHCP proxy: #{e}"
end
# Retrieves the DHCP entry for this host via a lookup on the MAC
# Returns: Hash Example {
# "mac" :"22:33:44:55:66:11"
# "nextServer":"192.168.122.1"
# "title" :"some.host.name"
# "filename" :"pxelinux.0"
# "ip" :"192.168.122.4"}
def getDHCP
logger.info "Query a DHCP reservation for #{name}/#{ip}"
dhcp.record subnet.network, mac
rescue => e
failure "Failed to read the DHCP record: #{proxy_error e}"
@dhcp_record ||= Net::DhcpRecord.new dhcp_attrs
end
# Deletes the DHCP entry for this host
def delDHCP
logger.info "{#{User.current.login}}Delete the DHCP reservation for #{name}/#{ip}"
dhcp.delete subnet.network, mac
rescue => e
failure "Failed to delete the DHCP record: #{proxy_error e}"
def sp_dhcp_record
return unless sp_dhcp?
@sp_dhcp_record ||= Net::DhcpRecord.new sp_dhcp_attrs
end
def delSPDHCP
return true unless sp_valid?
initialize_dhcp(sp_subnet) unless subnet == sp_subnet
logger.info "{#{User.current.login}}Delete a DHCP reservation for #{sp_name}/#{sp_ip}"
dhcp.delete subnet.network, sp_mac
rescue => e
failure "Failed to delete the Service Processor DHCP record: #{proxy_error e}"
protected
def set_dhcp
dhcp_record.create
end
# Updates the DHCP scope to add a reservation for this host
# +returns+ : Boolean true on success
def setDHCP
logger.info "{#{User.current.login}}Add a DHCP reservation for #{name}/#{ip}"
dhcp_attr = {:name => name, :filename => operatingsystem.boot_filename(self),
:ip => ip, :mac => mac, :hostname => name}
def set_sp_dhcp
sp_dhcp_record.create
end
next_server = boot_server
dhcp_attr.merge!(:nextserver => next_server) if next_server
def del_dhcp
dhcp_record.destroy
end
if jumpstart?
raise "Host's operating system has an unknown vendor class" unless (vendor = model.vendor_class and !vendor.empty?)
def del_sp_dhcp
sp_dhcp_record.destroy
end
private
# returns a hash of dhcp record settings
def dhcp_attrs
return unless dhcp?
dhcp_attr = { :name => name, :filename => operatingsystem.boot_filename(self),
:ip => ip, :mac => mac, :hostname => name, :proxy => proxy_for_host,
:network => subnet.network, :nextServer => boot_server }
jumpstart_arguments = os.jumpstart_params self, vendor
if jumpstart?
jumpstart_arguments = os.jumpstart_params self, model.vendor_class
dhcp_attr.merge! jumpstart_arguments unless jumpstart_arguments.empty?
end
dhcp.set subnet.network, dhcp_attr
rescue => e
failure "Failed to set the DHCP record: #{proxy_error e}"
dhcp_attr
end
def setSPDHCP
return true unless sp_valid?
initialize_dhcp(sp_subnet) unless subnet == sp_subnet
logger.info "{#{User.current.login}}Add a DHCP reservation for #{sp_name}/#{sp_ip}"
dhcp.set sp_subnet.network, sp_mac, :name => sp_name, :ip => sp_ip
rescue => e
failure "Failed to set the Service Processor DHCP record: #{proxy_error e}"
# returns a hash of service processor / ilo dhcp record settings
def sp_dhcp_attrs
return unless sp_dhcp?
{ :name => sp_name, :ip => sp_ip, :mac => sp_mac, :proxy => proxy_for_sp, :network => sp_subnet.network }
end
private
# where are we booting from
def boot_server
# if we don't manage tftp at all, we dont create a next-server entry.
return nil if tftp.nil?
begin
bs = tftp.bootServer
rescue RestClient::ResourceNotFound
nil
end
bs = tftp.bootServer
if bs.blank?
# trying to guess out tftp next server based on the smart proxy hostname
bs = URI.parse(subnet.tftp.url).host if subnet and subnet.tftp and subnet.tftp.url
bs = URI.parse(subnet.tftp.url).host if respond_to?(:tftp?) and tftp?
end
return bs unless bs.blank?
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."
......
end
def queue_dhcp_create
queue.create(:name => "DHCP Settings for #{self}", :priority => 10,
:action => [self, :setDHCP])
queue.create(:name => "DHCP Settings for #{sp_name}", :priority => 15,
:action => [self, :setSPDHCP]) if sp_valid?
queue.create(:name => "DHCP Settings for #{self}", :priority => 10,
:action => [self, :set_dhcp])
queue.create(:name => "DHCP Settings for #{sp_name}", :priority => 15,
:action => [self, :set_sp_dhcp]) if sp_dhcp?
end
def queue_dhcp_update
update = false
# IP Address / name changed
if (old.ip != ip) or (old.name != name) or (old.mac != mac)
update = true
if old.dhcp?
old.initialize_dhcp
queue.create(:name => "DHCP Settings for #{old}", :priority => 5,
:action => [old, :delDHCP])
end
end
if old.sp_valid? and ((old.sp_name != sp_name) or (old.sp_mac != sp_mac) or (old.sp_ip != sp_ip))
update = true
if old.sp_subnet and old.sp_subnet.dhcp and old.sp_subnet.dhcp.url
queue.create(:name => "DHCP Settings for #{old.sp_name}", :priority => 5,
:action => [old, :delSPDHCP])
end
end
queue.create(:name => "DHCP Settings for #{old}", :priority => 5,
:action => [old, :del_dhcp]) if old.dhcp? and dhcp_update_required?
queue.create(:name => "DHCP Settings for #{old.sp_name}", :priority => 5,
:action => [old, :del_sp_dhcp]) if old.sp_dhcp? and sp_dhcp_update_required?
queue_dhcp_create if dhcp_update_required?
end
# do we need to update our dhcp reservations
def dhcp_update_required?
# IP Address / name changed
return true if (old.ip != ip) or (old.name != name) or (old.mac != mac)
# Handle jumpstart
if jumpstart?
if !old.build? or (old.medium != medium or old.arch != arch) or
(os and old.os and (old.os.name != os.name or old.os != os))
update = true
old.initialize_dhcp if old.dhcp.nil? and old.dhcp?
queue.create(:name => "DHCP Settings for #{old}", :priority => 5, :action => [old, :delDHCP])
(os and old.os and (old.os.name != os.name or old.os != os))
return true
end
end
queue_dhcp_create if update
false
end
def sp_dhcp_update_required?
return true if old.sp_valid? and ((old.sp_name != sp_name) or (old.sp_mac != sp_mac) or (old.sp_ip != sp_ip))
false
end
def queue_dhcp_destroy
return unless dhcp? and errors.empty?
queue.create(:name => "DHCP Settings for #{self}", :priority => 5,
:action => [self, :delDHCP])
queue.create(:name => "DHCP Settings for #{sp_name}", :priority => 5,
:action => [self, :delSPDHCP]) if sp_valid?
queue.create(:name => "DHCP Settings for #{self}", :priority => 5,
:action => [self, :del_dhcp])
queue.create(:name => "DHCP Settings for #{sp_name}", :priority => 5,
:action => [self, :del_sp_dhcp]) if sp_valid?
true
end
......
errors.add :ip, "Does not match selected Subnet"
return false
end
rescue => e
rescue
# probably an invalid ip / subnet were entered
# we let other validations handle that
end
def valid_jumpstart_model
return unless jumpstart?
errors.add :model, "Has an unknown vendor class" if model.vendor_class.empty?
false
end
def proxy_for_host
ProxyAPI::DHCP.new(:url => subnet.dhcp.url) if dhcp?
end
def proxy_for_sp
ProxyAPI::DHCP.new(:url => sp_subnet.dhcp.url) if sp_dhcp?
end
end
end
lib/net.rb
module Net
autoload :DhcpRecord, "net/dhcp_record.rb"
class Record
include Net::Validations
attr_accessor :hostname, :proxy, :logger
def initialize opts = {}
# set all attributes
opts.each do |k,v|
eval("self.#{k}= v") if self.respond_to?("#{k}=")
end if opts
self.logger ||= Rails.logger
raise "Must define a hostname" if hostname.blank?
raise "Must define a proxy" if proxy.nil?
end
def inspect
to_s
end
# Do we have conflicting entries?
def conflicting?
!conflicts.empty?
end
# clears internal cache
def reload!
@conflicts = nil
end
# Compares two records by their attributes
def == other
return false unless other.respond_to? :attrs
self.attrs == other.attrs
end
end
class Error < RuntimeError; end
class Conflict < Exception
attr_accessor :type, :expected, :actual, :message
end
end
lib/net/dhcp_record.rb
module Net
class DhcpRecord < Record
attr_accessor :ip, :mac, :network, :nextServer, :filename
def initialize opts = { }
super(opts)
self.mac = validate_mac self.mac
self.network = validate_network self.network
self.ip = validate_ip self.ip
end
def to_s
"#{hostname}-#{mac}/#{ip}"
end
# Deletes the DHCP entry
def destroy
logger.debug "Delete DHCP reservation for #{to_s}"
# it is save to call destroy even if the entry does not exists, so we don't bother with validating anything here.
proxy.delete network, mac
end
# Create a DHCP entry
def create
logger.debug "Create DHCP reservation for #{to_s}"
begin
proxy.set network, attrs
rescue RestClient::Conflict
logger.warn "Conflicting DHCP reservation for #{to_s} detected"
e = Net::Conflict.new
e.type = "dhcp"
e.expected = to_s
e.actual = conflicts
e.message = "DHCP conflict detected - expected #{to_s}, found #{conflicts.map(&:to_s).join(', ')}"
raise e
end
end
# Returns an array of record objects which are conflicting with our own
def conflicts
@conflicts ||= [proxy.record(network, mac), proxy.record(network, ip)].delete_if { |c| c == self }.compact
end
# Verifies that are record already exists on the dhcp server
def valid?
logger.debug "Fetching DHCP reservation for #{to_s}"
self == proxy.record(network, mac)
end
def attrs
{ :hostname => hostname, :mac => mac, :ip => ip, :network => network,
:nextServer => nextServer, :filename => filename
}.delete_if { |k, v| v.nil? }
end
end
end
lib/net/validations.rb
module Net
module Validations
class Error < RuntimeError; end
# validates the ip address
def validate_ip ip
raise Error, "Invalid IP Address #{ip}" unless (ip =~ /(\d{1,3}\.){3}\d{1,3}/)
ip
end
# validates the mac
def validate_mac mac
raise Error, "Invalid MAC #{mac}" unless (mac =~ /([a-f0-9]{1,2}:){5}[a-f0-9]{1,2}/i)
mac
end
def validate_network network
begin
validate_ip(network)
rescue Error
raise Error, "Invalid Network #{network}"
end
network
end
end
end
lib/orchestration/queue.rb
class Queue
attr_reader :items
STATUS = %w[ pending running failed completed rollbacked ]
STATUS = %w[ pending running failed completed rollbacked conflict ]
def initialize
@items = []
lib/proxy_api.rb
require "rest_client"
require "json"
require "uri"
require 'net'
module ProxyAPI
......
# [+mac+] : String in coloned sextuplet format
# Returns : Hash or false
def record subnet, mac
parse get("#{subnet}/#{mac}")
response = parse(get("#{subnet}/#{mac}"))
Net::DhcpRecord.new response.merge(:network => subnet, :proxy => self)
rescue RestClient::ResourceNotFound
false
end
......
return response["serverName"]
end
false
rescue RestClient::ResourceNotFound
nil
end
# Create a default pxe menu
lib/tasks/test_lib_dir.rake
namespace :test do
desc "Test lib source"
Rake::TestTask.new(:lib) do |t|
t.libs << "test"
t.pattern = 'test/lib/**/*_test.rb'
t.verbose = true
end
end
test/functional/hosts_controller_test.rb
private
def initialize_host
User.current = users(:admin)
disable_orchestration
@host = Host.create :name => "myfullhost",
:mac => "aabbecddeeff",
:ip => "2.3.4.99",
test/lib/net/dhcp_test.rb
require 'test_helper'
require 'net'
class DhcpTest < ActiveSupport::TestCase
test "dhcp record should not be created without a mac" do
assert_raise Net::Validations::Error do
Net::DhcpRecord.new :name => "test", "proxy" => smart_proxies(:one)
end
end
test "dhcp record should not be created without a network" do
assert_raise Net::Validations::Error do
Net::DhcpRecord.new :name => "test", :mac => "aa:bb:cc:dd:ee:ff", "proxy" => smart_proxies(:one)
end
end
test "dhcp record should not be created without an ip" do
assert_raise Net::Validations::Error do
Net::DhcpRecord.new :name => "test", :mac => "aa:bb:cc:dd:ee:ff", :network => "127.0.0.0", "proxy" => smart_proxies(:one)
end
end
test "record should have dhcp attributes" do
record = Net::DhcpRecord.new(:name => "test", :mac => "aa:bb:cc:dd:ee:ff",
:network => "127.0.0.0", :ip => "127.0.0.1", "proxy" => smart_proxies(:one))
assert_equal({:name => "test", :mac => "aa:bb:cc:dd:ee:ff",:network => "127.0.0.0", :ip => "127.0.0.1"}, record.send(:attrs))
end
test "record should be equal if their attrs are the same" do
record1 = Net::DhcpRecord.new(:name => "test", :mac => "aa:bb:cc:dd:ee:ff",
:network => "127.0.0.0", :ip => "127.0.0.1", "proxy" => smart_proxies(:one))
record2 = Net::DhcpRecord.new(:name => "test", :mac => "aa:bb:cc:dd:ee:ff",
:network => "127.0.0.0", :ip => "127.0.0.1", "proxy" => smart_proxies(:one))
assert_equal record1, record2
end
end
test/lib/net/net_test.rb
require 'test_helper'
require 'net'
class NetTest < ActiveSupport::TestCase
test "Net record should auto assign attributes" do
record = Net::Record.new :name => "test", "proxy" => smart_proxies(:one)
assert_equal "test", record.name
end
test "should have a logger" do
record = Net::Record.new :name => "test", "proxy" => smart_proxies(:one)
assert_not_nil record.logger
end
test "should default logger to rails logger" do
record = Net::Record.new :name => "test", "proxy" => smart_proxies(:one)
assert_equal logger, record.logger
end
end
test/lib/net/validations_test.rb
require 'test_helper'
require 'net'
class ValidationsTest < ActiveSupport::TestCase
include Net::Validations
test "mac address should be valid" do
assert_nothing_raised Net::Validations::Error do
validate_mac "aa:bb:cc:dd:ee:ff"
end
end
test "mac should be invalid" do
assert_raise Net::Validations::Error do
validate_mac "abc123asdas"
end
end
end
test/test_helper.rb
def unattended?
SETTINGS[:unattended].nil? or SETTINGS[:unattended]
end
def self.disable_orchestration
#This disables the DNS/DHCP orchestration
Host.any_instance.stubs(:boot_server).returns("boot_server")
end
def disable_orchestration
ActiveSupport::TestCase.disable_orchestration
end
end
test/unit/domain_test.rb
end
test "should not destroy if it contains hosts" do
disable_orchestration
host = create_a_host
host.valid?
puts host.errors.full_messages unless host.errors.empty?
assert host.save
domain = host.domain
test/unit/host_test.rb
class HostTest < ActiveSupport::TestCase
setup do
disable_orchestration
User.current = User.find_by_login "admin"
end
......
:operatingsystem => operatingsystems(:redhat), :subnet => subnets(:one),
:architecture => architectures(:x86_64), :environment => environments(:production), :disk => "aaa"
assert host.save
assert_equal domain, host.domain
end
test/unit/orchestration/dhcp_test.rb
require 'test_helper'
class DhcpOrchestrationTest < ActiveSupport::TestCase
def setup
disable_orchestration
end
def test_host_should_have_dhcp
if unattended?
h = hosts(:one)
assert h.valid?
assert h.dhcp != nil
assert h.dhcp?
assert_instance_of Net::DhcpRecord, h.dhcp_record
end
end
......
if unattended?
h = hosts(:minimal)
assert h.valid?
assert_equal h.dhcp, nil
assert_equal h.dhcp?, false
assert_equal false, h.dhcp?
end
end

Also available in: Unified diff