Project

General

Profile

Download (15.6 KB) Statistics
| Branch: | Tag: | Revision:
b4b14336 Ohad Levy
class Host < Puppet::Rails::Host
a393106d Ohad Levy
belongs_to :architecture
816b9c22 Ohad Levy
belongs_to :media
6e50fa1d Ohad Levy
belongs_to :model
950ddeb3 Ohad Levy
belongs_to :domain
286a2207 Ohad Levy
belongs_to :operatingsystem
c6eee281 Ohad Levy
has_and_belongs_to_many :puppetclasses
6e50fa1d Ohad Levy
belongs_to :environment
belongs_to :subnet
9a55cdc2 Ohad Levy
belongs_to :ptable
086ec942 Ohad Levy
belongs_to :hostgroup
87c40d2e Ohad Levy
has_many :reports, :dependent => :destroy
086ec942 Ohad Levy
has_many :host_parameters, :dependent => :destroy
b09b4515 Ohad Levy
accepts_nested_attributes_for :host_parameters, :reject_if => lambda { |a| a[:value].blank? }, :allow_destroy => true
816b9c22 Ohad Levy
e386628a Ohad Levy
named_scope :recent, lambda { |*args| {:conditions => ["last_report > ?", (args.first || (SETTINGS[:run_interval] + 5.minutes).ago)]} }
named_scope :out_of_sync, lambda { |*args| {:conditions => ["last_report < ?", (args.first || (SETTINGS[:run_interval] + 5.minutes).ago)]} }
06160506 Ohad Levy
named_scope :with_fact, lambda { |fact,value|
unless fact.nil? or value.nil?
{ :joins => [:fact_values, :fact_names], :select => "hosts.name", :conditions =>
["fact_values.value = ? and fact_names.name = ? and fact_values.fact_name_id = fact_names.id",value, fact ] }
else
raise "invalid fact"
end
}

named_scope :with_class, lambda { |klass|
unless klass.nil?
{ :joins => :puppetclasses, :select => "hosts.name", :conditions => {:puppetclasses => {:name => klass }} }
else
9a9b3e75 Paul Kelly
raise "invalid class"
06160506 Ohad Levy
end
}

16cb7742 Ohad Levy
named_scope :with, lambda { |*arg| { :conditions =>
"(puppet_status >> #{Report::BIT_NUM*Report::METRIC.index(arg[0])} & #{Report::MAX}) > #{arg[1] || 0}"}
}
named_scope :with_error, { :conditions => "(puppet_status > 0) and
8f823b83 Ohad Levy
((puppet_status >> #{Report::BIT_NUM*Report::METRIC.index("failed")} & #{Report::MAX}) != 0) or
((puppet_status >> #{Report::BIT_NUM*Report::METRIC.index("failed_restarts")} & #{Report::MAX}) != 0) or
((puppet_status >> #{Report::BIT_NUM*Report::METRIC.index("skipped")} & #{Report::MAX}) != 0)"
16cb7742 Ohad Levy
}


named_scope :with_changes, { :conditions => "(puppet_status > 0) and
8f823b83 Ohad Levy
((puppet_status >> #{Report::BIT_NUM*Report::METRIC.index("applied")} & #{Report::MAX}) != 0) or
((puppet_status >> #{Report::BIT_NUM*Report::METRIC.index("restarted")} & #{Report::MAX}) !=0)"
16cb7742 Ohad Levy
}

named_scope :successful, {:conditions => "puppet_status = 0"}

aa5a2230 Ohad Levy
# audit the changes to this model
acts_as_audited :except => [:last_report, :puppet_status, :last_compile]

c22d6db2 Ohad Levy
# some shortcuts
alias_attribute :os, :operatingsystem
alias_attribute :arch, :architecture
dd2b33da Ohad Levy
alias_attribute :hostname, :name
d3d91384 Ohad Levy
50b094c0 Ohad Levy
validates_uniqueness_of :name
69923df9 Ohad Levy
validates_presence_of :name, :environment_id
9c3ffb6b Ohad Levy
if SETTINGS[:unattended].nil? or SETTINGS[:unattended]
69923df9 Ohad Levy
validates_uniqueness_of :ip
validates_uniqueness_of :mac
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_format_of :sp_name, :with => /.*-sp/, :allow_nil => true, :allow_blank => true
validates_presence_of :architecture_id, :domain_id, :mac, :operatingsystem_id
validates_length_of :root_pass, :minimum => 8,:too_short => 'should be 8 characters or more'
validates_format_of :mac, :with => /([a-f0-9]{1,2}:){5}[a-f0-9]{1,2}/
4b8adb21 Ohad Levy
validates_format_of :ip, :with => /(\d{1,3}\.){3}\d{1,3}/
validates_presence_of :ptable, :message => "Cant be blank unless a custom partition has been defined",
:if => Proc.new { |host| host.disk.empty? and not defined?(Rake) }
69923df9 Ohad Levy
validates_format_of :sp_mac, :with => /([a-f0-9]{1,2}:){5}[a-f0-9]{1,2}/, :allow_nil => true, :allow_blank => true
validates_format_of :sp_ip, :with => /(\d{1,3}\.){3}\d{1,3}/, :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_associated :domain, :operatingsystem, :architecture, :subnet,:media#, :user, :deployment, :model
end
d3d91384 Ohad Levy
ad36b317 Ohad Levy
before_validation :normalize_addresses, :normalize_hostname
d3d91384 Ohad Levy
# Returns the name of this host as a string
# String: the host's name
def to_label
ea4fd101 Ohad Levy
name
d3d91384 Ohad Levy
end

290c4d84 Ohad Levy
def to_s
to_label
end

ea4fd101 Ohad Levy
def shortname
59ff4da6 Ohad Levy
domain.nil? ? name : name.chomp("." + domain.name)
d3d91384 Ohad Levy
end

9d75c16d Ohad Levy
# defines how many hosts will be shown in the hostlist
def self.per_page
20
end

b4b14336 Ohad Levy
def clearReports
d3d91384 Ohad Levy
# Remove any reports that may be held against this host
0f419629 Ohad Levy
Report.delete_all("host_id = #{self.id}")
d3d91384 Ohad Levy
end

def clearFacts
0f419629 Ohad Levy
FactValue.delete_all("host_id = #{self.id}")
d3d91384 Ohad Levy
end

# Called from the host build post install process to indicate that the base build has completed
# Build is cleared and the boot link and autosign entries are removed
# A site specific build script is called at this stage that can do site specific tasks
def built
self.build = false
self.installed_at = Time.now.utc
8613dec9 Ohad Levy
# disallow any auto signing for our host.
b602deb0 Ohad Levy
GW::Puppetca.disable self.name
532f36fa Ohad Levy
GW::Tftp.remove self.mac
d3d91384 Ohad Levy
save
9c3ffb6b Ohad Levy
site_post_built = "#{SETTINGS[:modulepath]}sites/#{self.domain.name.downcase}/built.sh"
1258651a Ohad Levy
if File.executable? site_post_built
9c3ffb6b Ohad Levy
%x{#{site_post_built} #{self.name} >> #{SETTINGS[:logfile]} 2>&1 &}
1258651a Ohad Levy
end
d3d91384 Ohad Levy
end

309f6e18 Ohad Levy
# no need to store anything in the db if the entry is plain "puppet"
def puppetmaster
9c3ffb6b Ohad Levy
read_attribute(:puppetmaster) || SETTINGS[:puppet_server] || "puppet"
309f6e18 Ohad Levy
end
d3d91384 Ohad Levy
40acaa2a Ohad Levy
def puppetmaster=(pm)
9c3ffb6b Ohad Levy
write_attribute(:puppetmaster, pm == (SETTINGS[:puppet_server] || "puppet") ? nil : pm)
40acaa2a Ohad Levy
end

b0d3f4ee Ohad Levy
#retuns fqdn of host puppetmaster
def pm_fqdn
puppetmaster == "puppet" ? "puppet.#{domain.name}" : "#{puppetmaster}"
end

309f6e18 Ohad Levy
# no need to store anything in the db if the password is our default
def root_pass
9c3ffb6b Ohad Levy
read_attribute(:root_pass) || SETTINGS[:root_pass] || "!*!*!*!*!"
309f6e18 Ohad Levy
end
50b094c0 Ohad Levy
a26b8b66 Ohad Levy
# make sure we store an encrypted copy of the password in the database
# this password can be use as is in a unix system
def root_pass=(pass)
8485883a Ohad Levy
p = pass =~ /^\$1\$foreman\$.*/ ? pass : pass.crypt("$1$foreman$")
ad36b317 Ohad Levy
write_attribute(:root_pass, p)
b4b14336 Ohad Levy
end

286a2207 Ohad Levy
# returns the host correct disk layout, custom or common
def diskLayout
9a55cdc2 Ohad Levy
disk.empty? ? ptable.layout : disk
286a2207 Ohad Levy
end

87c40d2e Ohad Levy
# reports methods

def error_count
ff1cc6b1 Ohad Levy
%w[failed failed_restarts skipped].sum {|f| status f}
87c40d2e Ohad Levy
end

ff1cc6b1 Ohad Levy
def status(type = nil)
raise "invalid type #{type}" if type and not Report::METRIC.include?(type)
h = {}
(type || Report::METRIC).each do |m|
h[m] = (read_attribute(:puppet_status) || 0) >> (Report::BIT_NUM*Report::METRIC.index(m)) & Report::MAX
end
return type.nil? ? h : h[type]
87c40d2e Ohad Levy
end

ff1cc6b1 Ohad Levy
# generate dynamically methods for all metrics
# e.g. Report.last.applied
Report::METRIC.each do |method|
define_method method do
status method
end
87c40d2e Ohad Levy
end

def no_report
e386628a Ohad Levy
last_report.nil? or last_report < Time.now - (SETTINGS[:run_interval] + 3.minutes)
87c40d2e Ohad Levy
end

1ba05a93 Ohad Levy
# returns the list of puppetclasses a host is in.
c6eee281 Ohad Levy
def puppetclasses_names
b09b4515 Ohad Levy
return all_puppetclasses.collect {|c| c.name}
c6eee281 Ohad Levy
end

b09b4515 Ohad Levy
def all_puppetclasses
return hostgroup.nil? ? puppetclasses : (hostgroup.puppetclasses + puppetclasses).uniq
end
fce2cbc0 Ohad Levy
e22af92d Ohad Levy
# provide information about each node, mainly used for puppet external nodes
# TODO: remove hard coded default parameters into some selectable values in the database.
c6eee281 Ohad Levy
def info
fce2cbc0 Ohad Levy
# Static parameters
c6eee281 Ohad Levy
param = {}
c7643fc9 Ohad Levy
# maybe these should be moved to the common parameters, leaving them in for now
ea4fd101 Ohad Levy
param["puppetmaster"] = puppetmaster
b09b4515 Ohad Levy
param["domainname"] = domain.fullname unless domain.nil? or domain.fullname.nil?
fce2cbc0 Ohad Levy
param.update self.params
16d9312d Ohad Levy
return Hash['classes' => self.puppetclasses_names, 'parameters' => param, 'environment' => environment.to_s]
c6eee281 Ohad Levy
end

0ce8fa05 Ohad Levy
def params
parameters = {}
c7643fc9 Ohad Levy
# read common parameters
CommonParameter.find_each {|p| parameters.update Hash[p.name => p.value] }
# read domain parameters
b09b4515 Ohad Levy
domain.domain_parameters.each {|p| parameters.update Hash[p.name => p.value] } unless domain.nil?
c7643fc9 Ohad Levy
# read group parameters only if a host belongs to a group
8fea4dbf Ohad Levy
hostgroup.group_parameters.each {|p| parameters.update Hash[p.name => p.value] } unless hostgroup.nil?
086ec942 Ohad Levy
# and now read host parameters, override if required
host_parameters.each {|p| parameters.update Hash[p.name => p.value] }
0ce8fa05 Ohad Levy
return parameters
end

363141af Ohad Levy
def self.importHostAndFacts yaml
e22af92d Ohad Levy
facts = YAML::load yaml
363141af Ohad Levy
raise "invalid Fact" unless facts.is_a?(Puppet::Node::Facts)

h=Host.find_or_create_by_name facts.name
return h.importFacts(facts)
end

# import host facts, required when running without storeconfigs.
# expect a Puppet::Node::Facts
def importFacts facts
raise "invalid Fact" unless facts.is_a?(Puppet::Node::Facts)

# we are not importing facts for hosts in build state (e.g. waiting for a re-installation)
raise "Host is pending for Build" if build
a7db5993 Ohad Levy
time = facts.values[:_timestamp]
time = time.to_time if time.is_a?(String)
if last_compile.nil? or (last_compile + 1.minute < time)
self.last_compile = time
363141af Ohad Levy
begin
4b8adb21 Ohad Levy
# save all other facts
if self.respond_to?("merge_facts")
self.merge_facts(facts.values)
# pre 0.25 it was called setfacts
else
self.setfacts(facts.values)
end
363141af Ohad Levy
# we are saving here with no validations, as we want this process to be as fast
# as possible, assuming we already have all the right settings in Foreman.
# If we don't (e.g. we never install the server via Foreman, we populate the fields from facts
# TODO: if it was installed by Foreman and there is a mismatch,
# we should probably send out an alert.
e22af92d Ohad Levy
self.save_with_validation(perform_validation = false)
363141af Ohad Levy
# we want to import other information only if this host was never installed via Foreman
installed_at.nil? ? self.populateFieldsFromFacts : true
e22af92d Ohad Levy
rescue
ea4fd101 Ohad Levy
logger.warn "Failed to save #{name}: #{errors.full_messages.join(", ")}"
cf2f7656 Ohad Levy
$stdout.puts $!
e22af92d Ohad Levy
end
end
end

ea4fd101 Ohad Levy
def fv name
749973b4 Ohad Levy
v=fact_values.find(:first, :select => "fact_values.value", :joins => :fact_name,
4b8adb21 Ohad Levy
:conditions => "fact_names.name = '#{name}'")
749973b4 Ohad Levy
v.value unless v.nil?
ea4fd101 Ohad Levy
end

e22af92d Ohad Levy
def populateFieldsFromFacts
ea4fd101 Ohad Levy
self.mac = fv(:macaddress)
self.ip = fv(:ipaddress) if ip.nil?
self.domain = Domain.find_or_create_by_name fv(:domain)
e22af92d Ohad Levy
# On solaris architecture fact is harwareisa
363141af Ohad Levy
if myarch=fv(:architecture) || fv(:hardwareisa)
self.arch=Architecture.find_or_create_by_name myarch
end
e22af92d Ohad Levy
# by default, puppet doesnt store an env name in the database
ea4fd101 Ohad Levy
env=fv(:environment) || "production"
self.environment = Environment.find_or_create_by_name env
e22af92d Ohad Levy
ea4fd101 Ohad Levy
os_name = fv(:operatingsystem)
363141af Ohad Levy
if orel = fv(:lsbdistrelease) || fv(:operatingsystemrelease)
major, minor = orel.split(".")
self.os = Operatingsystem.find_or_create_by_name_and_major_and_minor os_name, major, minor
end
# again we are saving without validations as input is required (e.g. partition tables)
self.save_with_validation(perform_validation = false)
e22af92d Ohad Levy
end

8613dec9 Ohad Levy
# Called by build link in the list
# Build is set
# The boot link and autosign entry are created
# Any existing puppet certificates are deleted
# Any facts are discarded
def setBuild
5617cefe Ohad Levy
clearFacts
clearReports
363141af Ohad Levy
#TODO move this stuff to be in the observer, as if the host changes after its being built this might invalidate the current settings
7098f87b Ohad Levy
return false unless GW::Tftp.create([mac, os.to_s.gsub(" ","-"), arch.name, serial])
5617cefe Ohad Levy
self.build = true
self.save
8613dec9 Ohad Levy
end

816465f2 Ohad Levy
# this method accepts a puppets external node yaml output and generate a node in our setup
# it is assumed that you already have the node (e.g. imported by one of the rack tasks)
def importNode nodeinfo
# puppet classes
nodeinfo["classes"].each do |klass|
if pc = Puppetclass.find_by_name(klass)
self.puppetclasses << pc unless puppetclasses.exists?(pc)
else
0da5bcf1 Ohad Levy
error = "Failed to import #{klass} for #{name}: doesn't exists in our database - ignoring"
722ba6f1 Ohad Levy
logger.warn error
cf2f7656 Ohad Levy
$stdout.puts error
816465f2 Ohad Levy
end
end

# parameters are a bit more tricky, as some classifiers provide the facts as parameters as well
# not sure what is puppet priority about it, but we ignore it if has a fact with the same name.
# additionally, we don't import any non strings values, as puppet don't know what to do with those as well.

myparams = self.info["parameters"]
nodeinfo["parameters"].each_pair do |param,value|
next if fact_names.exists? :name => param
next unless value.is_a?(String)

# we already have this parameter
next if myparams.has_key?(param) and myparams[param] == value

unless (hp = self.host_parameters.create(:name => param, :value => value))
logger.warn "Failed to import #{param}/#{value} for #{name}: #{hp.errors.full_messages.join(", ")}"
cf2f7656 Ohad Levy
$stdout.puts $!
816465f2 Ohad Levy
end
end

self.save
end

300c8b44 Ohad Levy
# counts each association of a given host
# e.g. how many hosts belongs to each os
# returns sorted hash
def self.count_distribution assocication
output = {}
2c7da330 Ohad Levy
count(:group => assocication).each do |k,v|
begin
output[k.to_label] = v unless v == 0
rescue
logger.info "skipped #{k} as it has has no label"
end
end
300c8b44 Ohad Levy
output
end

# counts each association of a given host for HABTM relationships
# TODO: Merge these two into one method
# e.g. how many hosts belongs to each os
# returns sorted hash
def self.count_habtm assocication
output = {}
Host.count(:include => assocication.pluralize, :group => "#{assocication}_id").to_a.each do |a|
#Ugly Ugly Ugly - I guess I'm missing something basic here
label = eval(assocication.camelize).send("find",a[0].to_i).to_label if a[0]
output[label] = a[1]
end
output
end

72e65b31 Ohad Levy
def graph(timerange = 1.day.ago)
c6f1b718 Ohad Levy
data = {}
data[:runtime] = []
data[:resources] = []
data[:runtime_labels] = [ ['datetime', "Time" ],['number', "Config Retrival"], ['number', 'Total']]
data[:resources_labels] = [ ['datetime','Time']] + Report::METRIC.map{|metric| ['number', metric] }
72e65b31 Ohad Levy
reports.recent(timerange).find_each do |r|
c6f1b718 Ohad Levy
data[:runtime] << [r.reported_at.getlocal, r.config_retrival, r.runtime ]
data[:resources] << [r.reported_at.getlocal, r.status.sort.map(&:last)].flatten
72e65b31 Ohad Levy
end
c6f1b718 Ohad Levy
return data
72e65b31 Ohad Levy
end
300c8b44 Ohad Levy
d3d91384 Ohad Levy
private
# align common mac and ip address input
ad36b317 Ohad Levy
def normalize_addresses
c22d6db2 Ohad Levy
# a helper for variable scoping
helper = []
[self.mac,self.sp_mac].each do |m|
d3d91384 Ohad Levy
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
c22d6db2 Ohad Levy
helper << m
d3d91384 Ohad Levy
end
c22d6db2 Ohad Levy
self.mac, self.sp_mac = helper
d3d91384 Ohad Levy
c22d6db2 Ohad Levy
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}/
d3d91384 Ohad Levy
end
c22d6db2 Ohad Levy
helper << i
d3d91384 Ohad Levy
end
c22d6db2 Ohad Levy
self.ip, self.sp_ip = helper
d3d91384 Ohad Levy
end

a26b8b66 Ohad Levy
# ensure that host name is fqdn
# if they user inputed short name, the domain name will be appended
8613dec9 Ohad Levy
# this is done to ensure compatibility with puppet storeconfigs
# if the user added a domain, and the domain doesn't exist, we add it dynamically.
a26b8b66 Ohad Levy
def normalize_hostname
dd2b33da Ohad Levy
# no hostname was given, since this is before validation we need to ignore it and let the validations to produce an error
ea4fd101 Ohad Levy
unless name.empty?
if name.count(".") == 0
self.name = name + "." + domain.name unless domain.nil?
dd2b33da Ohad Levy
else
ea4fd101 Ohad Levy
self.domain = Domain.find_or_create_by_name name.split(".")[1..-1].join(".") if domain.nil?
dd2b33da Ohad Levy
end
a26b8b66 Ohad Levy
end
end

d3d91384 Ohad Levy
end