hawk/app/models/node.rb
# Copyright (c) 2009-2015 Tim Serong <tserong@suse.com>
# See COPYING for license.
require 'invoker'
class Node < Tableless
class CommandError < StandardError
end
attr_accessor :xml
attribute :id, String
attribute :name, String
attribute :params, Hash
attribute :utilization, Hash
attribute :utilization_details, Hash
attribute :state, String
attribute :online, Boolean
attribute :standby, Boolean
attribute :ready, Boolean
attribute :remote, Boolean
attribute :host, String
attribute :maintenance, Boolean
attribute :fence, Boolean
attribute :fence_history, String
validates :id,
presence: { message: _('Node ID is required') },
format: { with: /\A[a-zA-Z0-9_]+\z/, message: _('Invalid Node ID') }
validates :name,
presence: { message: _('Name is required') },
format: { with: /\A[a-zA-Z0-9_-]+\z/, message: _('Invalid name') }
validate do |record|
record.utilization.each do |u|
errors.add(:base, "#{u[0]}: %s" % _("No utilization value set!")) if u[1].blank?
end
end
def online!
Invoker.instance.no_log { |i| i.crm("-F", "node", "online", name) } unless @host
end
def online
!standby
end
def standby!
Invoker.instance.no_log { |i| i.crm("-F", "node", "standby", name) } unless @host
end
def ready!
Invoker.instance.no_log { |i| i.crm("-F", "node", "ready", name) } unless @host
end
def maintenance!
Invoker.instance.no_log { |i| i.crm("-F", "node", "maintenance", name) } unless @host
end
def fence!
Invoker.instance.no_log { |i| i.crm("-F", "node", "fence", name) }
end
def clearstate!
Invoker.instance.no_log { |i| i.crm("-F", "node", "clearstate", name) }
end
def ready
!maintenance
end
def to_param
name
end
def help_text
{
id: {
type: "string",
shortdesc: _("Node ID"),
longdesc: _("Unique identifier for the node."),
default: ""
},
name: {
type: "string",
shortdesc: _("Node Name"),
longdesc: _("Name used to refer to the node in the cluster."),
default: ""
}
}
end
def mapping
{}.tap do |m|
m["standby"] = {
# TODO: Should be boolean, but pacemaker's crappy (yes|true|1) booleans don't map well to the attrlist boolean type :/
type: "string",
default: "off",
longdesc: _("Puts the node into standby mode. The specified node is no longer able to host resources. Any resources currently active on the node will be moved to another node.")
}
params.map do |key, _|
m[key] = {
type: "string",
default: "",
longdesc: ""
} unless m.key? key
end
utilization.map do |key, _|
m[key] = {
type: "integer",
default: "",
longdesc: ""
} unless m.key? key
end
end
end
protected
def update
if current_cib.match("//configuration//node[@id='#{id}']").empty?
errors.add :base, _('The ID "%{id}" does not exist') % { id: id }
return false
end
return false if xml.nil?
merge_nvpairs("instance_attributes", params)
merge_nvpairs("utilization", utilization)
# write new xml
begin
Invoker.instance.cibadmin_replace xml.to_s
rescue NotFoundError, SecurityError, RuntimeError => e
Rails.logger.debug e.backtrace
errors.add :base, "Error: #{e.message}"
return false
end
true
end
class << self
def instantiate(xml, state)
record = allocate
record.id = state[:id]
record.xml = xml
record.name = state[:name]
record.state = state[:state]
record.standby = state[:standby]
record.maintenance = state[:maintenance]
record.remote = state[:remote]
record.host = state[:host]
record.fence_history = state[:fence_history]
record.fence = state[:fence]
record.params = if xml && xml.elements['instance_attributes']
vals = xml.elements['instance_attributes'].elements.collect do |e|
[e.attributes['name'], e.attributes['value']]
end
Hash[vals.sort]
else
{}
end
record.utilization_details = {}
record.utilization = {}.tap do |util|
if xml && xml.elements['utilization']
xml.elements['utilization'].elements.each do |e|
val = e.attributes['value'].to_i
util[e.attributes['name']] = val
record.utilization_details[e.attributes['name']] = {
total: val.to_i,
used: 0,
percentage: 0
}
end
end
end
if record.utilization_details.any?
Util.safe_x('/usr/sbin/crm_simulate', '-LU').split("\n").each do |line|
m = line.match(/^Remaining:\s+([^\s]+)\s+capacity:\s+(.*)$/)
next unless m && m[1] == record.name
m[2].split(' ').each do |u|
name, value = u.split('=', 2)
if record.utilization_details.has_key? name
r = record.utilization_details[name]
remaining = 0
remaining = value.to_i unless value.nil?
r[:used] = r[:total] - remaining
begin
r[:percentage] = 100 - ((remaining.to_f / r[:total].to_f) * 100.0).to_i
rescue FloatDomainError
r[:percentage] = 0
end
end
end
end
end
record
end
def cib_type
:node
end
def ordered
all.sort do |a, b|
a.name.natcmp(b.name, true)
end
end
# Since pacemaker started using corosync node IDs as the node ID attribute,
# Record#find will fail when looking for nodes by their human-readable
# name, so have to override here
def find(id)
super(id)
rescue Cib::RecordNotFound
# Can't find by id attribute, try by name attribute
super(name, 'name')
end
end
def merge_nvpairs(list, attrs)
return if xml.nil?
if attrs.empty?
# No attributes to set, get rid of the list (if it exists)
xml.elements[list].remove if xml.elements[list]
else
# Get rid of any attributes that are no longer set
if xml.elements[list]
xml.elements[list].elements.each do |el|
el.remove unless attrs.keys.include? el.attributes['name']
end
else
xml.add_element(
list,
"id" => "#{element_id(xml)}-#{list}")
end
# Write new attributes or update existing ones
attrs.each do |n, v|
nvp = xml.elements["#{list}/nvpair[@name=\"#{n}\"]"]
if nvp
nvp.attributes["value"] = v
else
xml.elements[list].add_element(
"nvpair",
"id" => "#{element_id(xml.elements[list])}-#{n}",
"name" => n,
"value" => v)
end
end
end
end
def element_id(elem)
return elem.attributes['uname'] if elem.attributes['uname']
elem.attributes['id']
end
end