Project

General

Profile

« Previous | Next » 

Revision 40df7dfb

Added by Daniel Lobato Garcia about 11 years ago

  • ID 40df7dfbfcde38f06d133fea97f0d30904155916

fixes #426 expose BMC information in foreman UI

Signed-off-by: Ohad Levy <>

This patch also adds an abstracted power management class for both VM
and BM

View differences:

app/assets/javascripts/application.js
$(div).load(url + "?operatingsystem_id=" + os_id + "&hostgroup_id=" + hostgroup_id + "&environment_id=" + env_id+"&provisioning="+build,
function(response, status, xhr) {
if (status == "error") {
$(div).html("<div class='alert alert-warning'><a class='close' data-di smiss='alert'>&times;</a><p>Sorry but no templates were configured.</p></div>");
$(div).html("<div class='alert alert-warning'><a class='close' data-dismiss='alert'>&times;</a><p>_('Sorry but no templates were configured.')</p></div>");
}
});
}
......
function foreman_url(path) {
return URL_PREFIX + path;
}
$(function() {
$('*[data-ajax-url]').each(function() {
var url = $(this).attr('data-ajax-url');
$(this).load(url, function(response, status, xhr) {
if (status == "error") {
$(this).closest(".tab-content").find("#spinner").html(_('Failed to fetch: ') + xhr.status + " " + xhr.statusText);
}
});
});
});
app/controllers/hosts_controller.rb
PUPPETMASTER_ACTIONS=[ :externalNodes, :lookup ]
SEARCHABLE_ACTIONS= %w[index active errors out_of_sync pending disabled ]
AJAX_REQUESTS=%w{compute_resource_selected hostgroup_or_environment_selected current_parameters puppetclass_parameters}
BOOT_DEVICES={ :disk => N_('Disk'), :cdrom => N_('CDROM'), :pxe => N_('PXE'), :bios => N_('BIOS') }
add_puppetmaster_filters PUPPETMASTER_ACTIONS
before_filter :ajax_request, :only => AJAX_REQUESTS
......
:update_multiple_environment, :submit_multiple_build, :submit_multiple_destroy, :update_multiple_puppetrun,
:multiple_puppetrun]
before_filter :find_by_name, :only => %w[show edit update destroy puppetrun setBuild cancelBuild
storeconfig_klasses clone pxe_config toggle_manage power console]
storeconfig_klasses clone pxe_config toggle_manage power console bmc ipmi_boot]
before_filter :taxonomy_scope, :only => [:hostgroup_or_environment_selected, :process_hostgroup]
before_filter :set_host_type, :only => [:update]
helper :hosts, :reports
......
end
def power
return unless @host.compute_resource && params[:power_action]
action = case params[:power_action]
when 'start'
:start
when 'stop'
:stop
else
logger.warn "invalid power state #{params[:power_action]}"
invalid_request and return
end
vm = @host.compute_resource.find_vm_by_uuid(@host.uuid)
return invalid_request if params[:power_action].blank?
@host.power.send(params[:power_action].to_sym)
process_success :success_redirect => :back, :success_msg => _("%{host} is now %{state}") % { :host => @host, :state => _(@host.power.state) }
rescue => e
process_error :redirect => :back, :error_msg => _("Failed to %{action} %{host}: %{e}") % { :action => _(params[:power_action]), :host => @host, :e => e }
end
def bmc
render :partial => 'bmc', :locals => { :host => @host }
rescue => e
#TODO: hack
error = e.try(:original_exception).try(:response) || e.to_s
logger.warn "failed to fetch bmc information: #{error}"
logger.debug e.backtrace
render :text => "Failure: #{error}"
end
def ipmi_boot
device = params[:ipmi_device]
begin
vm.send(action)
vm.reload
process_success :success_redirect => :back, :success_msg => _("%{vm} is now %{state}") % { :vm => vm, :state => vm.state.capitalize }
@host.ipmi_boot(device)
process_success :success_redirect => :back, :success_msg => _("%{host} now boots from %{device}") % { :host => @host.name, :device => _(BOOT_DEVICES[device.downcase.to_sym] || device) }
rescue => e
process_error :redirect => :back, :error_msg => _("Failed to %{action} %{vm}: %{e}") % { :action => action, :vm => vm, :e => e }
process_error :redirect => :back, :error_msg => _("Failed to configure %{host} to boot from %{device}: %{e}") % { :device => _(BOOT_DEVICES[device.downcase.to_sym] || device), :host => @host.name, :e => e }
end
end
app/helpers/bmc_helper.rb
module BmcHelper
def power_status s
if s.downcase == 'on'
"<span class='label label-success'>#{_('On')}</span>".html_safe
else
"<span class='label'>#{_('Off')}</span>".html_safe
end
end
def power_actions
action_buttons(
(PowerManager::SUPPORTED_ACTIONS - [:state]).map do |action|
display_link_if_authorized(_(action.to_s.capitalize), { :action => "power", :id => @host, :power_action => action},
:confirm => _('Are you sure?'), :method => :put)
end
)
end
def boot_actions
controller_options = { :action => "ipmi_boot", :id => @host }
confirm = _('Are you sure?')
links = HostsController::BOOT_DEVICES.map do |device,label|
display_link_if_authorized(_(label), controller_options.merge(:ipmi_device => device),
:confirm => confirm, :method => :put)
end
action_buttons("Select device", links)
end
end
app/helpers/hosts_helper.rb
include OperatingsystemsHelper
include HostsAndHostgroupsHelper
include ComputeResourcesVmsHelper
include BmcHelper
def last_report_column(record)
time = record.last_report? ? _("%s ago") % time_ago_in_words(record.last_report.getlocal): ""
app/models/host/managed.rb
new
end
def bmc_nic
interfaces.bmc.first
end
def sp_ip
bmc_nic.try(:ip)
end
......
tax_organization.import_missing_ids if organization
end
def bmc_proxy
@bmc_proxy ||= bmc_nic.proxy
end
def bmc_available?
ipmi = bmc_nic
return false if ipmi.nil?
ipmi.password.present? && ipmi.username.present? && ipmi.provider == 'IPMI'
end
def power
opts = {:host => self}
if compute_resource_id && uuid
VirtPowerManager.new(opts)
elsif bmc_available?
BMCPowerManager.new(opts)
else
raise ::Foreman::Exception.new(N_("Unknown power management support - can't continue"))
end
end
def ipmi_boot(booting_device)
bmc_proxy.boot({:function => 'bootdevice', :device => booting_device})
end
private
def lookup_keys_params
......
Classification.new(:host => self).enc
end
def bmc_nic
interfaces.bmc.first
end
# ensure that host name is fqdn
# if the user inputted short name, the domain name will be appended
# this is done to ensure compatibility with puppet storeconfigs
app/models/nic/bmc.rb
self.attrs[method] = value
end
end
def proxy
# try to find a bmc proxy in the same subnet as our bmc device
proxy = SmartProxy.bmc_proxies.joins(:subnets).where(['dhcp_id = ? or tftp_id = ?', subnet_id, subnet_id]).first if subnet_id
proxy ||= SmartProxy.bmc_proxies.first
raise Foreman::Exception.new(N_('Unable to find a proxy with BMC feature')) if proxy.nil?
ProxyAPI::BMC.new({ :host_ip => ip,
:url => proxy.url,
:user => username,
:password => password })
end
end
end
app/models/power_manager.rb
class PowerManager
SUPPORTED_ACTIONS = [N_('start'), N_('stop'), N_('poweroff'), N_('reboot'), N_('reset'), N_('state')]
def initialize(opts = {})
@host = opts[:host]
end
def self.method_missing(method, *args)
logger.warn "invalid power state request #{action} for host: #{host}"
raise ::Foreman::Exception.new(N_("Invalid power state request: %{action}, supported actions are %{supported}"), { :action => action, :supported => SUPPORTED_ACTIONS })
end
def state
N_("Unknown")
end
def logger
Rails.logger
end
private
attr_reader :host
end
app/models/power_manager/bmc_power_manager.rb
class BMCPowerManager < PowerManager
def initialize(opts = {})
super(opts)
@proxy = host.bmc_proxy
end
SUPPORTED_ACTIONS.each do |method|
define_method method do # def start
proxy.power(:action => action_map[method.to_sym]) # proxy.power(:action => 'on')
end # end
end
private
attr_reader :proxy
#TODO: consider moving this to the proxy code, so we can just delegate like as with Virt.
def action_map
{
:start => 'on',
:stop => 'off',
:poweroff => 'off',
:reboot => 'soft',
:reset => 'cycle',
:state => 'status'
}
end
end
app/models/power_manager/virt_power_manager.rb
class VirtPowerManager < PowerManager
def initialize(opts = {})
super(opts)
begin
timeout(15) do
@vm = host.compute_resource.find_vm_by_uuid(host.uuid)
end
rescue Timeout::Error
raise Foreman::Exception.new(N_("Timeout has occurred while communicating with %s"), host.compute_resource)
rescue => e
logger.warn "Error has occurred while communicating to #{host.compute_resource}: #{e}"
logger.debug e.backtrace
raise Foreman::Exception.new(N_("Error has occurred while communicating with %{cr}: %{e}"), { :cr => host.compute_resource, :e => e })
end
end
def state
# make sure we fetch latest vm status
vm.reload
vm.state
end
(SUPPORTED_ACTIONS - ['state']).each do |method|
define_method method do
vm.send(method.to_sym)
end
end
private
attr_reader :vm
end
app/views/hosts/_bmc.html.erb
<table class="table table-bordered table-striped">
<tr>
<th><%= _('Chassis') %></th>
</tr>
<tr>
<td><%= _('Status') %></td>
<td><%= power_status(@host.power.state) %></td>
</tr>
<tr>
<td><%= _('Power') %></td>
<td><%= power_actions %></td>
</tr>
<tr>
<td><%= _('Boot device') %></td>
<td><%= boot_actions %></td>
</tr>
</table>
<table class="table table-bordered table-striped">
<tr>
<th><%= _('Network') %></th>
</tr>
<tr>
<td><%= _('IP') %></td>
<td><%= @host.bmc_proxy.lan(:action => 'ip') %></td>
</tr>
<tr>
<td><%= _('Netmask') %></td>
<td><%= @host.bmc_proxy.lan(:action => 'netmask') %></td>
</tr>
<tr>
<td><%= _('MAC') %></td>
<td><%= @host.bmc_proxy.lan(:action => 'mac') %></td>
</tr>
<tr>
<td><%= _('Gateway') %></td>
<td><%= @host.bmc_proxy.lan(:action => 'gateway') %></td>
</tr>
</table>
app/views/hosts/show.html.erb
<% if @vm %>
<li><a href="#vm" data-toggle="tab"><%= _('VM') %></a></li>
<% end %>
<% if @host.bmc_available? %>
<li><a href="#bmc" data-toggle="tab"><%= _('BMC') %></a></li>
<% end %>
</ul>
<div id="myTabContent" class="tab-content">
<div class="tab-pane active in" id="properties">
......
<%= render("compute_resources_vms/show/#{@host.compute_resource.provider.downcase}") rescue nil %>
<% end %>
</div>
<% if @host.bmc_available? %>
<div id="bmc" class="tab-pane" data-ajax-url=<%= bmc_host_path(@host) %>>
<p id="spinner">
<%= image_tag '/assets/spinner.gif' %>
<%= _('Loading BMC information ...') %>
</p>
</div>
<% end %>
</div>
</div>
config/application.rb
# Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras)
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib"]
config.autoload_paths += Dir["#{config.root}/app/controllers/concerns"]
config.autoload_paths += %W(#{config.root}/models/**/*.rb)
config.autoload_paths += Dir[ Rails.root.join('app', 'models', 'power_manager') ]
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
config/routes.rb
post 'environment_selected'
put 'power'
get 'console'
get 'bmc'
put 'ipmi_boot'
end
collection do
get 'multiple_actions'
lib/fog_extensions/aws/server.rb
dns_name || private_dns_name
end
def poweroff
stop(true)
end
def reset
poweroff && start
end
end
end
end
lib/fog_extensions/libvirt/server.rb
attributes[:memory_size] = mem.to_i / 1024 if mem
end
def reset
poweroff
start
end
end
end
end
lib/fog_extensions/openstack/server.rb
def network
end
def reset
reboot('HARD')
end
end
end
end
lib/fog_extensions/ovirt/server.rb
def volumes_attributes=(attrs); end
def poweroff
service.vm_action(:id =>id, :action => :shutdown)
end
def reset
poweroff
start
end
end
end
end
lib/fog_extensions/vsphere/server.rb
def volumes_attributes=(attrs); end
def poweroff
stop(:force => true)
end
def reset
reboot(:force => true)
end
end
end
end
lib/foreman/access_permissions.rb
tasks_ajax_actions = [:show]
map.permission :view_hosts, {:hosts => [:index, :show, :errors, :active, :out_of_sync, :disabled, :pending,
:externalNodes, :pxe_config, :storeconfig_klasses, :auto_complete_search],
:externalNodes, :pxe_config, :storeconfig_klasses, :auto_complete_search, :bmc],
:dashboard => [:OutOfSync, :errors, :active],
:unattended => :template,
:"api/v1/hosts" => [:index, :show, :status],
......
:tasks => tasks_ajax_actions}
map.permission :power_hosts, {:hosts => [:power]}
map.permission :console_hosts, {:hosts => [:console]}
map.permission :ipmi_boot, {:hosts => [:ipmi_boot]}
map.permission :puppetrun_hosts, {:hosts => [:puppetrun, :multiple_puppetrun, :update_multiple_puppetrun]}
end
lib/proxy_api/bmc.rb
case args[:action]
when "on?", "off?", "status"
args[:action].chop! if args[:action].include?('?')
parse get(bmc_url_for('power',args[:action]), args)
response = parse(get(bmc_url_for('power',args[:action]), args))
response.is_a?(Hash) ? response['result'] : response
when "on", "off", "cycle", "soft"
res = parse put(args, bmc_url_for('power',args[:action]))
# This is a simple action, just return the result of the action
......
# get "/bmc/:host/lan/:action"
case args[:action]
when "ip", "netmask", "mac", "gateway"
parse get(bmc_url_for('lan',args[:action]), args)
response = parse(get(bmc_url_for('lan',args[:action]), args))
response.is_a?(Hash) ? response['result'] : response
else
raise NoMethodError
end

Also available in: Unified diff