Project

General

Profile

« Previous | Next » 

Revision 8838eb42

Added by Ohad Levy over 11 years ago

  • ID 8838eb42a2a292f50126966d3fa768edad3a237a

fixes #1814 - converts sp_* attributes into a BMC interface class

this patch also includes the following

  • added a new interfaces table, and STI objects to represent a NIC, BMC, Managed
    and a bootable interface.
  • refactored DHCP/DNS orchestation code, so they can work on the
    interface objects and on the primary interface information that still
    reside in the hosts table.
  • added basic UI for interface selection

this refactor also helps to simply refactoring the host object into
multiple objects.

this patch also fixes
fixes #1325 - BMC NIC should also create DNS entry
fixes #1813 - allow to support multiple NIC per host

View differences:

app/controllers/hosts_controller.rb
@report_summary = Report.summarise(@range.days.ago, @host)
}
format.yaml { render :text => params["rundeck"].nil? ? @host.info.to_yaml : @host.rundeck.to_yaml }
format.json { render :json => @host.to_json({:methods => [:host_parameters]}) }
format.json { render :json => @host.to_json({:methods => [:host_parameters], :include => :interfaces }) }
end
end
......
if @domain.subnets.any?
page['#subnet_select'].html(render(:partial => 'common/domain', :locals => {:item => @host}))
page['#host_subnet_id'].val(@subnet.id).change if @subnet
page['#sp_subnet'].html(render(:partial => 'hosts/sp_subnet', :locals => {:item => @host}))
end
end
end
app/controllers/subnets_controller.rb
# query our subnet dhcp proxy for an unused IP
def freeip
not_found and return unless (s=params[:subnet_id].to_i) > 0
not_found and return unless (subnet = Subnet.find(s))
if (ip = subnet.unused_ip(params[:host_mac]))
respond_to do |format|
format.html do
render :update do |page|
page['#host_ip'].val(ip)
page['#host_ip'].show('highlight', 5000)
@organization = params[:organization_id].blank? ? nil : Organization.find(params[:organization_id])
@location = params[:location_id].blank? ? nil : Location.find(params[:location_id])
Taxonomy.as_taxonomy @organization, @location do
not_found and return unless (subnet = Subnet.find(s))
if (ip = subnet.unused_ip(params[:host_mac]))
respond_to do |format|
format.html do
render :update do |page|
page['#host_ip'].val(ip)
page['#host_ip'].show('highlight', 5000)
end
end
format.json { render :json => {:ip => ip} }
end
format.json { render :json => {:ip => ip} }
else
# we don't want any failures if we failed to query our proxy
head :status => 200
end
else
# we don't want any failures if we failed to query our proxy
head :status => 200
end
rescue => e
logger.warn "Failed to query #{subnet} for free ip: #{e}"
app/helpers/hosts_and_hostgroups_helper.rb
classes.select { |pc| klasses.include?(pc.id) }
end
def ifs_bmc_opts obj
case obj.read_attribute(:type)
when "Nic::BMC"
{}
else
{ :disabled => true, :value => nil }
end
end
end
app/helpers/hosts_helper.rb
)
)
end
def conflict_objects errors
errors.keys.map(&:to_s).grep(/conflict$/).map(&:to_sym)
end
def has_conflicts? errors
conflict_objects(errors).each do |c|
return true if errors[c.to_sym].any?
end
false
end
end
app/models/domain.rb
belongs_to :dns, :class_name => "SmartProxy"
has_many :domain_parameters, :dependent => :destroy, :foreign_key => :reference_id
has_and_belongs_to_many :users, :join_table => "user_domains"
has_many :interfaces, :class_name => 'Nic::Base'
accepts_nested_attributes_for :domain_parameters, :reject_if => lambda { |a| a[:value].blank? }, :allow_destroy => true
validates_uniqueness_of :name
validates_uniqueness_of :fullname, :allow_blank => true, :allow_nil => true
app/models/host.rb
has_many :reports, :dependent => :destroy
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
has_many :interfaces, :dependent => :destroy, :inverse_of => :host, :class_name => 'Nic::Base'
accepts_nested_attributes_for :interfaces, :reject_if => lambda { |a| a[:mac].blank? }, :allow_destroy => true
belongs_to :owner, :polymorphic => true
belongs_to :sp_subnet, :class_name => "Subnet"
belongs_to :compute_resource
belongs_to :image
......
# handles all orchestration of smart proxies.
include Foreman::Renderer
include Orchestration
include Orchestration::DHCP
include Orchestration::DNS
include Orchestration::Compute
include Orchestration::TFTP
include Orchestration::Puppetca
include Orchestration::SSHProvision
include HostTemplateHelpers
validates_uniqueness_of :ip, :if => Proc.new {|host| host.require_ip_validation?}
validates_uniqueness_of :mac, :unless => Proc.new { |host| host.compute? or !host.managed }
validates_uniqueness_of :sp_mac, :allow_nil => true, :allow_blank => true
validates_uniqueness_of :sp_name, :sp_ip, :allow_blank => true, :allow_nil => true
validates_presence_of :architecture_id, :operatingsystem_id, :if => Proc.new {|host| host.managed}
validates_presence_of :domain_id, :if => Proc.new {|host| host.managed}
validates_presence_of :mac, :unless => Proc.new { |host| host.compute? or !host.managed }
......
validates_format_of :ip, :with => Net::Validations::IP_REGEXP, :if => Proc.new { |host| host.require_ip_validation? }
validates_presence_of :ptable_id, :message => "cant be blank unless a custom partition has been defined",
:if => Proc.new { |host| host.managed and host.disk.empty? and not defined?(Rake) and capabilities.include?(:build) }
validates_format_of :sp_mac, :with => Net::Validations::MAC_REGEXP, :allow_nil => true, :allow_blank => true
validates_format_of :sp_ip, :with => Net::Validations::IP_REGEXP, :allow_nil => true, :allow_blank => true
validates_format_of :serial, :with => /[01],\d{3,}n\d/, :message => "should follow this format: 0,9600n8", :allow_blank => true, :allow_nil => true
validates_presence_of :puppet_proxy_id, :if => Proc.new {|h| h.managed? } if SETTINGS[:unattended]
......
false
end
def sp_valid?
!sp_name.empty? and !sp_ip.empty? and !sp_mac.empty?
end
def jumpstart?
operatingsystem.family == "Solaris" and architecture.name =~/Sparc/i rescue false
end
......
new.puppetclasses = puppetclasses
# Clone any parameters as well
host_parameters.each{|param| new.host_parameters << HostParameter.new(:name => param.name, :value => param.value, :nested => true)}
interfaces.each {|int| new.interfaces << int.clone }
# clear up the system specific attributes
[:name, :mac, :ip, :uuid, :certname, :last_report, :sp_mac, :sp_ip, :sp_name, :puppet_status, ].each do |attr|
[:name, :mac, :ip, :uuid, :certname, :last_report].each do |attr|
new.send "#{attr}=", nil
end
new.puppet_status = 0
new
end
def sp_ip
bmc_nic.try(:ip)
end
def sp_mac
bmc_nic.try(:mac)
end
def sp_subnet_id
bmc_nic.try(:subnet_id)
end
def sp_subnet
bmc_nic.try(:subnet)
end
def sp_name
bmc_nic.try(:name)
end
def host_status
if build
"Pending Installation"
......
Classification.new(:host => self).enc
end
# align common mac and ip address input
def normalize_addresses
# a helper for variable scoping
helper = []
[self.mac,self.sp_mac].each do |m|
unless m.empty?
m.downcase!
if m=~/[a-f0-9]{12}/
m = m.gsub(/(..)/){|mh| mh + ":"}[/.{17}/]
elsif mac=~/([a-f0-9]{1,2}:){5}[a-f0-9]{1,2}/
m = m.split(":").map{|nibble| "%02x" % ("0x" + nibble)}.join(":")
end
end
helper << m
end
self.mac, self.sp_mac = helper
helper = []
[self.ip,self.sp_ip].each do |i|
unless i.empty?
i = i.split(".").map{|nibble| nibble.to_i}.join(".") if i=~/(\d{1,3}\.){3}\d{1,3}/
end
helper << i
end
self.ip, self.sp_ip = helper
def bmc_nic
interfaces.bmc.first
end
# ensure that host name is fqdn
......
self.certname = Foreman.uuid if read_attribute(:certname).blank? or new_record?
end
def normalize_addresses
self.mac = Net::Validations.normalize_mac(mac)
self.ip = Net::Validations.normalize_ip(ip)
end
def force_lookup_value_matcher
lookup_values.each { |v| v.match = "fqdn=#{fqdn}" }
end
end
app/models/nic.rb
# Represents a Host's network interface
# This class is the both parent
module Nic
class Base < ActiveRecord::Base
include Authorization
include Foreman::STI
set_table_name :nics
attr_accessible :host_id, :host,
:mac,
:_destroy # used for nested_attributes
before_validation :normalize_mac
validates_uniqueness_of :mac
validates_presence_of :mac
validates_format_of :mac, :with => Net::Validations::MAC_REGEXP
validate :uniq_with_hosts
validates_presence_of :host
scope :bootable, where(:type => "Nic::Bootable")
scope :bmc, where(:type => "Nic::BMC")
scope :interfaces, where(:type => "Nic::Interface")
scope :managed, where(:type => "Nic::Managed")
belongs_to :host, :inverse_of => :interfaces
# keep extra attributes needed for sub classes.
serialize :attrs, Hash
protected
def uniq_fields_with_hosts
[:mac]
end
# make sure we don't have a conflicting interface with an host record
def uniq_with_hosts
failed = false
uniq_fields_with_hosts.each do |attr|
value = self.send(attr)
unless value.blank?
if host.send(attr) == value
errors.add attr, "Can't use the same value as the primary interface"
failed = true
elsif Host.where(attr => value).limit(1).pluck(attr).any?
errors.add attr, "already in use"
failed = true
end
end
end
!failed
end
def normalize_mac
self.mac = Net::Validations.normalize_mac(mac)
end
end
end
app/models/nic/bmc.rb
module Nic
class BMC < Managed
ATTRIBUTES = [:username, :password, :provider]
attr_accessible *ATTRIBUTES
PROVIDERS = %w(IPMI)
validates_inclusion_of :provider, :in => PROVIDERS
ATTRIBUTES.each do |method|
define_method method do
self.attrs ||= { }
self.attrs[method]
end
define_method "#{method}=" do |value|
self.attrs ||= { }
self.attrs[method] = value
end
end
end
end
app/models/nic/bootable.rb
module Nic
class Bootable < Managed
delegate :tftp?, :tftp, :to => :subnet
delegate :jumpstart?, :build?, :to => :host
# ensure that we can only have one bootable interface
validates_uniqueness_of :type, :scope => :host_id, :message => "Only one bootable interface is allowed"
def dhcp_record
return unless dhcp? or @dhcp_record
@dhcp_record ||= host.jumpstart? ? Net::DHCP::SparcRecord.new(dhcp_attrs) : Net::DHCP::Record.new(dhcp_attrs)
end
protected
def dhcp_attrs
attrs = super.merge({
:filename => host.operatingsystem.boot_filename(host),
:nextServer => boot_server
})
# Are we booting SPARC solaris?
if host.jumpstart?
jumpstart_arguments = host.os.jumpstart_params host, host.model.vendor_class
attrs.merge! jumpstart_arguments unless jumpstart_arguments.empty?
end
attrs
end
end
end
app/models/nic/interface.rb
module Nic
class Interface < Base
attr_accessible :ip
validates_uniqueness_of :ip
validates_presence_of :ip
validates_format_of :ip, :with => Net::Validations::IP_REGEXP
validate :normalize_ip
protected
def uniq_fields_with_hosts
[:mac, :ip]
end
def normalize_ip
self.ip = Net::Validations.normalize_ip(ip)
end
end
end
app/models/nic/managed.rb
module Nic
class Managed < Interface
include Orchestration
include Orchestration::DHCP
include Orchestration::DNS
attr_accessible :name, :subnet_id, :subnet, :domain_id, :domain
validates_uniqueness_of :name, :scope => :domain_id
validates_presence_of :subnet_id, :domain_id
belongs_to :subnet
belongs_to :domain
delegate :vlanid, :network, :to => :subnet
# Interface normally are not executed by them self, so we use the host queue and related methods.
# this ensures our orchestration works on both a host and a managed interface
delegate :progress_report_id, :require_ip_validation?, :overwrite?, :capabilities, :managed?, :to => :host
# this ensures we can create an interface even when there is no host queue
# e.g. outside to Host nested attributes
def queue_with_host
if host
logger.debug 'Using host queue'
host.queue
else
logger.debug 'Using nic queue'
queue_without_host
end
end
alias_method_chain :queue, :host
# returns a DHCP reservation object
def dhcp_record
return unless dhcp? or @dhcp_record
@dhcp_record ||= Net::DHCP::Record.new(dhcp_attrs)
end
protected
def uniq_fields_with_hosts
[:mac, :ip, :name]
end
# returns a hash of dhcp record attributes
def dhcp_attrs
raise "DHCP not supported for this NIC" unless dhcp?
{
:hostname => name,
:ip => ip,
:mac => mac,
:proxy => subnet.dhcp_proxy,
:network => network
}
end
end
end
app/models/orchestration.rb
require_dependency "proxy_api"
require "proxy_api"
require 'orchestration/queue'
module Orchestration
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
attr_reader :queue, :post_queue, :old, :record_conflicts
# stores actions to be performed on our proxies based on priority
before_validation :set_queue
before_validation :setup_clone
# extend our Host model to know how to handle subsystems
include Orchestration::DNS
include Orchestration::DHCP
include Orchestration::TFTP
include Orchestration::Puppetca
include Orchestration::Compute
include Orchestration::SSHProvision
attr_reader :old
# save handles both creation and update of hosts
before_save :on_save
......
# after validation callbacks status, as rails by default does
# not care about their return status.
def valid?(context = nil)
setup_clone
super
orchestration_errors?
end
# we override the destroy method, in order to ensure our queue exists before other callbacks
# and to process the queue only if we found no errors
def destroy
set_queue
super
def queue
@queue ||= Orchestration::Queue.new
end
def post_queue
@post_queue ||= Orchestration::Queue.new
end
def record_conflicts
@record_conflicts ||= []
end
private
......
# in order not to keep any left overs in our proxies.
def process queue_name
return true if Rails.env == "test"
# queue is empty - nothing to do.
q = send(queue_name)
return if q.empty?
......
rescue Net::Conflict => e
task.status = "conflict"
@record_conflicts << e
record_conflicts << e
failure e.message, nil, :conflict
#TODO: This is not a real error, but at the moment the proxy / foreman lacks better handling
# of the error instead of explode.
......
end
end
def set_queue
@queue = Orchestration::Queue.new
@post_queue = Orchestration::Queue.new
@record_conflicts = []
end
# we keep the before update host object in order to compare changes
def setup_clone
return if new_record?
......
end
def update_cache
Rails.cache.write(progress_report_id, (queue.all + post_queue.all).to_json, :expires_in => 5.minutes)
Rails.cache.write(progress_report_id, (queue.all + post_queue.all).to_json, :expires_in => 5.minutes)
end
end
app/models/orchestration/dhcp.rb
base.class_eval do
after_validation :queue_dhcp
before_destroy :queue_dhcp_destroy
validate :ip_belongs_to_subnet?, :valid_jumpstart_model
validate :ip_belongs_to_subnet?
end
end
module InstanceMethods
def dhcp?
!subnet.nil? and subnet.dhcp? and managed? and capabilities.include?(:build)
end
def sp_dhcp?
sp_valid? and !sp_subnet.nil? and sp_subnet.dhcp?
name.present? and ip.present? and !subnet.nil? and subnet.dhcp? and managed? and capabilities.include?(:build)
end
def dhcp_record
......
@dhcp_record ||= jumpstart? ? Net::DHCP::SparcRecord.new(dhcp_attrs) : Net::DHCP::Record.new(dhcp_attrs)
end
def sp_dhcp_record
return unless sp_dhcp? or @sp_dhcp_record
@sp_dhcp_record ||= Net::DHCP::Record.new sp_dhcp_attrs
end
protected
def set_dhcp
......
dhcp_record.conflicts.each{|conflict| conflict.create}
end
def set_sp_dhcp
sp_dhcp_record.create
end
def set_sp_dhcp_conflicts
sp_dhcp_record.conflicts.each{|conflict| conflict.create}
end
def del_dhcp
dhcp_record.destroy
end
......
dhcp_record.conflicts.each{|conflict| conflict.destroy}
end
def del_sp_dhcp
sp_dhcp_record.destroy
end
def del_sp_dhcp_conflicts
sp_dhcp_record.conflicts.each{|conflict| conflict.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 }
if jumpstart?
jumpstart_arguments = os.jumpstart_params self, model.vendor_class
dhcp_attr.merge! jumpstart_arguments unless jumpstart_arguments.empty?
end
dhcp_attr
end
# returns a hash of service processor / ilo dhcp record settings
def sp_dhcp_attrs
return unless sp_dhcp?
{ :hostname => sp_name, :name => sp_name, :ip => sp_ip, :mac => sp_mac, :proxy => proxy_for_sp, :network => sp_subnet.network }
end
# where are we booting from
def boot_server
# if we don't manage tftp at all, we dont create a next-server entry.
......
failure "failed to detect boot server: #{e}"
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 => subnet.dhcp_proxy,
:network => subnet.network, :nextServer => boot_server }
if jumpstart?
jumpstart_arguments = os.jumpstart_params self, model.vendor_class
dhcp_attr.merge! jumpstart_arguments unless jumpstart_arguments.empty?
end
dhcp_attr
end
def queue_dhcp
return unless (dhcp? or (old and old.dhcp?) or sp_dhcp? or (old and old.sp_dhcp?)) and orchestration_errors?
return unless (dhcp? or (old and old.dhcp?)) and orchestration_errors?
queue_remove_dhcp_conflicts if dhcp_conflict_detected?
new_record? ? queue_dhcp_create : queue_dhcp_update
end
def queue_dhcp_create
logger.debug "Scheduling new DHCP reservations"
logger.debug "Scheduling new DHCP reservations for #{self}"
queue.create(:name => "Create DHCP Settings for #{self}", :priority => 10,
:action => [self, :set_dhcp]) if dhcp?
queue.create(:name => "Create DHCP Settings for #{sp_name}", :priority => 15,
:action => [self, :set_sp_dhcp]) if sp_dhcp?
end
def queue_dhcp_update
......
queue.create(:name => "Create DHCP Settings for #{self}", :priority => 9,
:action => [self, :set_dhcp]) if dhcp?
end
if sp_dhcp_update_required?
logger.debug("Detected a changed required for BMC DHCP record")
queue.create(:name => "Remove DHCP Settings for #{old.sp_name}", :priority => 5,
:action => [old, :del_sp_dhcp]) if old.sp_dhcp?
queue.create(:name => "Create DHCP Settings for #{sp_name}", :priority => 14,
:action => [self, :set_sp_dhcp]) if sp_dhcp?
end
end
# do we need to update our dhcp reservations
......
# IP Address / name changed
return true if ((old.ip != ip) or (old.name != name) or (old.mac != mac) or (old.subnet != subnet))
# Handle jumpstart
if jumpstart?
#TODO, abstract this way once interfaces are fully used
if self.is_a?(Host) and 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))
return true
......
false
end
def sp_dhcp_update_required?
return true if ((old.sp_name != sp_name) or (old.sp_mac != sp_mac) or (old.sp_ip != sp_ip) or (old.sp_subnet != sp_subnet))
false
end
def queue_dhcp_destroy
return unless dhcp? and errors.empty?
queue.create(:name => "Remove DHCP Settings for #{self}", :priority => 5,
:action => [self, :del_dhcp])
queue.create(:name => "Remove DHCP Settings for #{sp_name}", :priority => 5,
:action => [self, :del_sp_dhcp]) if sp_valid?
true
end
......
logger.debug "Scheduling DHCP conflicts removal"
queue.create(:name => "DHCP conflicts removal for #{self}", :priority => 5,
:action => [self, :del_dhcp_conflicts]) if dhcp_record and dhcp_record.conflicting?
queue.create(:name => "DHCP conflicts removal for #{sp_name}", :priority => 5,
:action => [self, :del_sp_dhcp_conflicts]) if sp_valid? and sp_dhcp and sp_dhcp_record.conflicting?
end
def ip_belongs_to_subnet?
......
# we let other validations handle that
end
def valid_jumpstart_model
return unless jumpstart?
errors.add :model_id, "is required for Solaris SPARC deployment" if model.blank?
errors.add :model_id, "Has an unknown vendor class" if model and model.vendor_class.empty?
false
end
def proxy_for_host
subnet.dhcp_proxy
end
def proxy_for_sp
sp_subnet.dhcp_proxy
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? or name.blank?
......
return false unless dhcp?
status = true
status = failure("DHCP record #{dhcp_record.conflicts[0]} already exists", nil, :conflict) if dhcp_record and dhcp_record.conflicting?
status &= failure("DHCP record #{sp_dhcp_record.conflicts[0]} already exists", nil, :conflict) if sp_dhcp? and sp_dhcp_record and sp_dhcp_record.conflicting?
status = failure("DHCP records #{dhcp_record.conflicts.to_sentence} already exists", nil, :conflict) if dhcp_record and dhcp_record.conflicting?
overwrite? ? errors.are_all_conflicts? : status
end
app/models/orchestration/dns.rb
module InstanceMethods
def dns?
!domain.nil? and !domain.proxy.nil? and managed?
name.present? and ip.present? and !domain.nil? and !domain.proxy.nil? and managed?
end
def reverse_dns?
!subnet.nil? and !subnet.dns_proxy.nil? and managed? and capabilities.include?(:build)
name.present? and ip.present? and !subnet.nil? and subnet.dns? and managed?
end
def dns_a_record
......
:action => [self, :del_conflicting_dns_a_record]) if dns? and dns_a_record and dns_a_record.conflicting?
queue.create(:name => "Remove conflicting Reverse DNS record for #{self}", :priority => 0,
:action => [self, :del_conflicting_dns_ptr_record]) if reverse_dns? and dns_ptr_record and dns_ptr_record.conflicting?
end
def dns_conflict_detected?
......
return false if overwrite?
status = true
status = failure("DNS A Record #{dns_a_record.conflicts[0]} already exists", nil, :conflict) if dns? and dns_a_record and dns_a_record.conflicting?
status &= failure("DNS PTR Record #{dns_ptr_record.conflicts[0]} already exists", nil, :conflict) if reverse_dns? and dns_ptr_record and dns_ptr_record.conflicting?
status = failure("DNS A Records #{dns_a_record.conflicts.to_sentence} already exists", nil, :conflict) if dns? and dns_a_record and dns_a_record.conflicting?
status &= failure("DNS PTR Records #{dns_ptr_record.conflicts.to_sentence} already exists", nil, :conflict) if reverse_dns? and dns_ptr_record and dns_ptr_record.conflicting?
status
end
app/models/subnet.rb
include Authorization
include Taxonomix
has_many :hosts
# sps = Service processors / ilom boards etc
has_many :sps, :class_name => "Host", :foreign_key => 'sp_subnet_id'
belongs_to :dhcp, :class_name => "SmartProxy"
belongs_to :tftp, :class_name => "SmartProxy"
belongs_to :dns, :class_name => "SmartProxy"
has_many :subnet_domains, :dependent => :destroy
has_many :domains, :through => :subnet_domains
has_many :interfaces, :class_name => 'Nic::Base'
validates_presence_of :network, :mask, :name
validates_associated :subnet_domains
validates_uniqueness_of :network
......
end
}
before_destroy EnsureNotUsedBy.new(:hosts, :sps)
before_destroy EnsureNotUsedBy.new(:hosts, :interfaces )
scoped_search :on => [:name, :network, :mask, :gateway, :dns_primary, :dns_secondary, :vlanid], :complete_value => true
scoped_search :in => :domains, :on => :name, :rename => :domain, :complete_value => true
......
end.compact
end
def as_json options = {}
super({:methods => [:cidr, :to_label]}.merge(options))
end
private
def validate_ranges
app/views/api/v1/subnets/show.json.rabl
attributes :id, :name, :network, :mask, :priority, :vlanid,
:gateway, :dns_primary, :dns_secondary, :from, :to, :domain_ids,
:dns_id, :dhcp_id, :tftp_id
:dns_id, :dhcp_id, :tftp_id, :cidr
child :dhcp => :dhcp do
attributes :id, :name, :url
app/views/compute_resources_vms/form/libvirt/_network.html.erb
<div class="fields">
<% if (networks = compute_resource.networks).any? -%>,
<% if (networks = compute_resource.networks).any? -%>
<%= selectable_f f, :bridge, networks.map(&:name), { }, :class => "span2", :label => "Network",
:help_inline => remove_child_link("X", f, { :method => :'_delete', :title => 'remove network interface', :class => 'label label-important' }) %>
<% else -%>
app/views/hosts/_conflicts.html.erb
<p> Please review them carefully, if you are certain that they should be removed, please click on overwrite.</p>
<div class="alert alert-message alert-block base">
<% @host.errors[:conflict].each do |e| -%>
<li><%= e %></li>
<% conflict_objects(@host.errors).each do |obj| -%>
<% @host.errors[obj].each do |e| -%>
<li><%= e %></li>
<% end -%>
<% end -%>
</div>
app/views/hosts/_form.html.erb
<%= javascript 'host_edit', 'compute_resource', 'lookup_keys'%>
<%= render "conflicts" if @host.errors[:conflict].any? %>
<%= render "conflicts" if has_conflicts?(@host.errors) %>
<%= render "progress" %>
<%= form_for @host, :html => {:'data-submit' => 'progress_bar'} do |f| %>
<%= base_errors_for @host %>
app/views/hosts/_interfaces.html.erb
<div class="fields">
<%= field_set_tag "Interface #{remove_child_link('x', f, { :rel => 'twipsy', "data-title" => 'remove network interface', :'data-placement' => 'left',
:class => 'fr badge badge-important'})}".html_safe, :id => "interface" do -%>
<%= selectable_f f, :type, [["Interface", Nic::Managed],["BMC", Nic::BMC]], {}, :class => 'interface_type', :disabled => !f.object.new_record? %>
<%= text_f f, :mac, :label => "MAC" %>
<%= text_f f, :name, :help_inline => "DNS name" %>
<%= select_f f, :domain_id, accessible_domains, :id, :to_label,
{ :include_blank => accessible_domains.any? ? true : "No domains"},
{ :disabled => accessible_domains.empty? ? true : false,
:help_inline => image_tag("spinner.gif", :class => "indicator hide"),
:class => 'interface_domain', :'data-url' => domain_selected_hosts_path } %>
<%= select_f f, :subnet_id, domain_subnets, :id, :title,
{ :include_blank => domain_subnets.any? ? true : "No subnets"},
{ :disabled => domain_subnets.empty? ? true : false,
:help_inline => image_tag("spinner.gif", :class => "indicator hide"),
:class => 'interface_subnet', :'data-url' => freeip_subnets_path } %>
<%= text_f f, :ip, :label => "IP" %>
<%# hack to get BMC attributes show up without AJAX -%>
<%= content_tag :span, :id => 'bmc_fields', :class => f.object.is_a?(Nic::BMC) ? '' : 'hide' do %>
<%= text_f f, :username, ifs_bmc_opts(f.object) %>
<%= password_f f, :password, ifs_bmc_opts(f.object) %>
<%# TODO: current rails version does not allow to pass a selected value where there is no method (e.g. providers below) rescue here is a hack %>
<%= selectable_f f, :provider, Nic::BMC::PROVIDERS, {:selected => nil}, ifs_bmc_opts(f.object) rescue f.hidden_field :provider, :value => 'IPMI' %>
<% end %>
<% end %>
</div>
app/views/hosts/_sp_subnet.html.erb
<%= fields_for item do |f| -%>
<%= select_f f, :sp_subnet_id, domain_subnets, :id, :title,
{ :include_blank => domain_subnets.any? ? true : "No subnets"},
{ :disabled => domain_subnets.empty? ? true : false, :label => "BMC Subnet" }
%>
<% end -%>
app/views/hosts/_unattended.html.erb
</div>
<div class="tab-pane" id="network">
<div id="mac_address" <%= display? @host.compute_resource_id %> >
<%= text_f f, :mac, :label => "MAC", :help_inline => "MAC address for this host", :autocomplete => 'off' %>
</div>
<%= field_set_tag "Primary Interface", :id => "primary_interface" do -%>
<div id="mac_address" <%= display? @host.compute_resource_id %> >
<%= text_f f, :mac, :label => "MAC", :help_inline => "MAC address for this host", :autocomplete => 'off' %>
</div>
<%= select_f f, :domain_id, accessible_domains, :id, :to_label, {:include_blank => true},
{:onchange => 'domain_selected(this);', :'data-url' => domain_selected_hosts_path} %>
<% if @host.capabilities.include?(:build) %>
<%= select_f f, :domain_id, accessible_domains, :id, :to_label, {:include_blank => true},
{:onchange => 'domain_selected(this);', :'data-url' => domain_selected_hosts_path} %>
<% if @host.capabilities.include?(:build) %>
<div id='manage_network'>
<span id="subnet_select">
<%= render 'common/domain', :item => @host %>
</span>
<%= text_f f, :ip, :help_inline => "IP Address for this host, if DHCP Smart proxy is enabled, this should be auto suggested to you", :label => "IP" , :autocomplete => 'off'%>
<div id="bmc" <%= display? @host.compute_resource_id %> >
<%= text_f f, :sp_name, :help_inline => "BMC interface DNS name", :label => "BMC Name" , :autocomplete => 'off'%>
<%= text_f f, :sp_ip, :label => "BMC IP" , :autocomplete => 'off'%>
<%= text_f f, :sp_mac, :label => "BMC MAC", :autocomplete => 'off' %>
<span id="sp_subnet">
<%= render 'sp_subnet', :item => @host %>
</span>
</div>
<%# the following field is required to see child validations %>
<%= f.hidden_field :updated_at, :value => Time.now.to_i %>
<%= f.fields_for :interfaces do |interfaces| %>
<%= render 'interfaces', :f => interfaces %>
<% end %>
<%= new_child_fields_template(f, :interfaces, {:partial => "interfaces"})%>
<%= add_child_link "+ Add Interface", :interfaces, { :class => "info", :title => 'add new network interface' } %>
</div>
<% end %>
<% end %>
</div>
db/migrate/20120110113051_create_subnet_domain.rb
end
def self.down
add_column :subnets, :domain, :references
add_column :subnets, :domain_id, :integer
drop_table :subnet_domains
end
end
db/migrate/20120311081257_create_nics.rb
class CreateNics < ActiveRecord::Migration
def self.up
create_table :nics do |t|
t.string :mac
t.string :ip
t.string :type
t.string :name
t.references :host
t.references :subnet
t.references :domain
t.text :attrs
t.timestamps
end
add_index :nics, [:type], :name => 'index_by_type'
add_index :nics, [:host_id], :name => 'index_by_host'
add_index :nics, [:type, :id], :name => 'index_by_type_and_id'
Host.where(["sp_mac <> ? and sp_ip <> ?", "", ""]).each do |host|
begin
sp_ip = host.read_attribute(:sp_ip)
sp_mac = host.read_attribute(:sp_mac)
Nic::BMC.create! :host_id => host.id, :mac => sp_mac, :ip => sp_ip, :subnet_id => host.read_attribute(:sp_subnet_id),
:name => host.read_attribute(:sp_name), :priority => 1
say "created BMC interface for #{host}"
rescue => e
say "failed to import nics for #{host} : #{e}"
end
end
remove_columns :hosts, :sp_mac, :sp_ip, :sp_name, :sp_subnet_id
# TODO: fix this stuff in search
end
def self.down
add_column :hosts, :sp_mac, :string, :limit => 17, :default => ""
add_column :hosts, :sp_ip, :string, :limit => 15, :default => ""
add_column :hosts, :sp_name, :string, :default => ""
add_column :hosts, :sp_subnet_id, :integer
Nic::BMC.all.each do |bmc|
if bmc.host_id
bmc.host.update_attributes(:sp_mac => bmc.mac, :sp_ip => bmc.ip, :sp_name => bmc.name, :sp_subnet_id => bmc.subnet_id)
end
end
drop_table :nics
end
end
lib/foreman/controller/host_details.rb
end
def domain_selected
assign_parameter "domain", "common/"
respond_to do |format|
format.html {assign_parameter "domain", "common/"}
format.json do
@organization = params[:organization_id].blank? ? nil : Organization.find(params[:organization_id])
@location = params[:location_id].blank? ? nil : Location.find(params[:location_id])
Taxonomy.as_taxonomy @organization, @location do
if (domain = Domain.find(params[:domain_id]))
render :json => domain.subnets
else
not_found
end
end
end
end
end
def use_image_selected
lib/foreman/sti.rb
module Foreman
module STI
def self.included(base)
base.class_eval do
class << self
# ensures that the correct STI object is created when :type is passed.
def new_with_cast(*attributes, &block)
if (h = attributes.first).is_a?(Hash) && (type = h.delete(:type)) && type.length > 0
if (klass = type.constantize) != self
raise "Invalid type #{type}" unless klass <= self
return klass.new(*attributes, &block)
end
end
new_without_cast(*attributes, &block)
end
alias_method_chain :new, :cast
end
end
end
end
end
lib/net/validations.rb
network
end
# ensures that the ip address does not contain any leading spaces or invalid strings
def self.normalize_ip ip
return unless ip.present?
ip.split(".").map(&:to_i).join(".")
end
def self.normalize_mac mac
return unless mac.present?
m = mac.downcase
case m
when /[a-f0-9]{12}/
m.gsub(/(..)/) { |mh| mh + ":" }[/.{17}/]
when /([a-f0-9]{1,2}:){5}[a-f0-9]{1,2}/
m.split(":").map { |nibble| "%02x" % ("0x" + nibble) }.join(":")
end
end
end
end
public/javascripts/host_edit.js
var label = $(item).children(":selected").text();
if(compute=='') { //Bare Metal
$('#mac_address').show();
$('#bmc').show();
$("#model_name").show();
$('#compute_resource').empty();
$('#vm_details').empty();
......
else
{
$('#mac_address').hide();
$('#bmc').hide();
$("#model_name").hide();
$("#compute_resource_tab").show();
$('#vm_details').empty();
......
$('#image_selection').appendTo($('#image_provisioning'));
$('#params-tab').on('shown', function(){mark_params_override()});
}
$(document).on('change', '.interface_domain', function () {
interface_domain_selected(this);
});
$(document).on('change', '.interface_subnet', function () {
interface_subnet_selected(this);
});
$(document).on('change', '.interface_type', function () {
interface_type_selected(this);
});
function interface_domain_selected(element) {
var domain_id = element.value;
var subnet_options = $(element).parentsUntil('.fields').parent().find('[id$=_subnet_id]').empty();
var indicator = $(element).parent().find('.indicator')
subnet_options.attr('disabled', true);
if (domain_id == '') {
subnet_options.append($("<option />").val(null).text('No subnets'));
return false;
}
indicator.removeClass('hide');
var url = $(element).attr('data-url');
var org = $('#host_organization_id :selected').val();
var loc = $('#host_location_id :selected').val();
$.ajax({
data:{domain_id: domain_id, organization_id:org, location_id: loc},
type:'post',
url:url,
dataType:'json',
success:function (result) {
if (result.length > 1)
subnet_options.append($("<option />").val(null).text('Please select'));
$.each(result, function () {
subnet_options.append($("<option />").val(this.subnet.id).text(this.subnet.name + ' (' + this.subnet.to_label + ')'));
});
if (subnet_options.find('option').length > 0) {
subnet_options.attr('disabled', false);
subnet_options.change();
}
else {
subnet_options.append($("<option />").text('No subnets'));
subnet_options.attr('disabled', true);
}
indicator.addClass('hide');
}
});
}
function interface_subnet_selected(element) {
var subnet_id = $(element).val();
if (subnet_id == '') return;
var indicator = $(element).parent().find('.indicator')
var interface_ip = $(element).parentsUntil('.fields').parent().find('input[id$=_ip]')
interface_ip.attr('disabled', true);
indicator.removeClass('hide');
// We do not query the proxy if the ip field is filled in and contains an
// IP that is in the selected subnet
var drop_text = $(element).children(":selected").text();
// extracts network / cidr / ip
if (drop_text.length != 0 && drop_text.search(/^.+ \([0-9\.\/]+\)/) != -1) {
var details = drop_text.replace(/^[^(]+\(/, "").replace(")", "").split("/");
var network = details[0];
var cidr = details[1];
if (subnet_contains(network, cidr, interface_ip.val())) {
interface_ip.attr('disabled', false);
indicator.addClass('hide');
return;
}
}
var interface_mac = $(element).parentsUntil('.fields').parent().find('input[id$=_mac]')
var url = $(element).attr('data-url');
var org = $('#host_organization_id :selected').val();
var loc = $('#host_location_id :selected').val();
var data = {subnet_id: subnet_id, host_mac: interface_mac.val(), organization_id:org, location_id:loc }
$.ajax({
data: data,
type:'post',
url: url,
dataType:'json',
success:function (result) {
interface_ip.val(result['ip']);
},
complete:function () {
indicator.addClass('hide');
interface_ip.attr('disabled', false);
}
});
}
function interface_type_selected(element) {
var type = $(element).find('option:selected').text();
var bmc_fields = $(element).parentsUntil('.fields').parent().find('#bmc_fields')
if (type == 'BMC') {
bmc_fields.find("input:disabled").prop('disabled',false);
bmc_fields.removeClass("hide");
} else {
bmc_fields.find("input").prop('disabled',true);
bmc_fields.addClass("hide");
}
}
public/javascripts/lookup_keys.js
}
$(item).closest("form").trigger({type: 'nested:fieldAdded', field: field});
$('a[rel="popover"]').popover();
$('a[rel="twipsy"]').tooltip();
return new_id;
}
test/fixtures/hosts.yml
ptable: ubuntu
medium: ubuntu
puppet_proxy: puppetmaster
domain: useless
compute_resource: one
sol10host:
......
operatingsystem: redhat
ptable: one
subnet: one
sp_subnet: one
sp_name: dhcp-bmc.mydomain.net
sp_ip: 2.3.4.50
sp_mac: da:bb:cc:dd:ee:aa
domain: mydomain
puppet_proxy: puppetmaster
managed: true
test/fixtures/nics.yml
nic:
mac: aabbCCddedee
test/fixtures/smart_proxies.yml
one:
name: DHCP Proxy
url: https://somewhere.net:8443
features: dhcp
two:
name: TFTP Proxy
......
three:
name: DNS Proxy
url: http://else.where:4567
features: dns
puppetmaster:
name: Puppetmaster Proxy
test/functional/api/v1/subnets_controller_test.rb
assert_response :unprocessable_entity
end
def test_destroy_json
subnet = Subnet.first
subnet.hosts.clear
subnet.interfaces.clear
as_admin { delete :destroy, {:id => subnet.id} }
ActiveSupport::JSON.decode(@response.body)
assert_response :ok
assert !Subnet.exists?(:id => subnet.id)
end
end
test/functional/subnets_controller_test.rb
def test_destroy
subnet = Subnet.first
subnet.hosts.clear
subnet.sps.clear
subnet.interfaces.clear
delete :destroy, {:id => subnet}, set_session_user
assert_redirected_to subnets_url
assert !Subnet.exists?(subnet.id)
......
def test_destroy_json
subnet = Subnet.first
subnet.hosts.clear
subnet.sps.clear
subnet.interfaces.clear
delete :destroy, {:format => "json", :id => subnet}, set_session_user
subnet = ActiveSupport::JSON.decode(@response.body)
assert_response :ok
test/test_helper.rb
Net::DHCP::SparcRecord.any_instance.stubs(:create).returns(true)
Net::DHCP::Record.any_instance.stubs(:conflicting?).returns(false)
ProxyAPI::Puppet.any_instance.stubs(:environments).returns(["production"])
ProxyAPI::DHCP.any_instance.stubs(:unused_ip).returns('127.0.0.1')
end
def disable_orchestration
test/unit/domain_test.rb
test "user with destroy permissions should be able to destroy" do
setup_user "destroy"
record = domains(:useless)
record.interfaces.clear
record.hosts.clear
assert record.destroy
assert record.frozen?
end
test/unit/host_test.rb
assert host.save!
end
test "should have only one bootable interface" do
h = hosts(:redhat)
assert_equal 0, h.interfaces.count
bootable = Nic::Bootable.create! :host => h, :name => "dummy-bootable", :ip => "2.3.4.102", :mac => "aa:bb:cd:cd:ee:ff",
:subnet => h.subnet, :type => 'Nic::Bootable', :domain => h.domain
assert_equal 1, h.interfaces.count
h.interfaces_attributes = [{:name => "dummy-bootable2", :ip => "2.3.4.103", :mac => "aa:bb:cd:cd:ee:ff",
:subnet_id => h.subnet_id, :type => 'Nic::Bootable', :domain_id => h.domain_id }]
assert !h.valid?
assert_equal "Only one bootable interface is allowed", h.errors['interfaces.type'][0]
assert_equal 1, h.interfaces.count
end
# Token tests
test "built should clean tokens" do
Setting[:token_duration] = 30
h = hosts(:one)
......
h = hosts(:one)
assert_equal h.token, nil
end
end
test/unit/nic_test.rb
require 'test_helper'
class NicTest < ActiveSupport::TestCase
def setup
disable_orchestration
User.current = User.admin
end
def teardown
User.current = nil
end
test "should create simple interface" do
i = ''
assert_nothing_raised { i = Nic::Base.create! :mac => "cabbccddeeff", :host => hosts(:one) }
assert_equal "Nic::Base", i.class.to_s
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff