crowbar_framework/app/controllers/ucs_controller.rb
#
# Copyright 2011-2013, Dell
# Copyright 2013-2014, SUSE LINUX Products GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Things that still should be done:
# - apply SUSE OpenStack Cloud admin CSS to views
# - encrypt the password in xml document
require "net/http"
require "uri"
require "rexml/document"
require "cgi"
class XMLAPIRequestError < StandardError
def initialize(exception); @exception = exception end
def message; @exception.message end
alias_method :to_s, :message
end
class XMLAPIResponseFailure < StandardError
attr_reader :response
def initialize(xml_response)
@response = xml_response
end
def message
"%d %s" % [@response.code, @response.message]
end
alias_method :to_s, :message
end
class UcsController < ApplicationController
CREDENTIALS_XML_PATH = "/etc/crowbar/cisco-ucs/credentials.xml"
COMPUTE_SERVICE_PROFILE = "suse-cloud-compute"
STORAGE_SERVICE_PROFILE = "suse-cloud-storage"
before_filter :authenticate, only: [:edit, :update]
#before_filter :authenticate, :except => [ :settings, :login ]
def handle_exception(exception, log_message, ui_message)
logger.warn "Cisco UCS: #{log_message}: #{exception}"
redirect_to :back, notice: (ui_message % truncate(exception.to_s))
end
rescue_from SocketError, XMLAPIRequestError do |e|
handle_exception(e, e, "Failed to connect to UCS API server (%s)")
end
rescue_from XMLAPIResponseFailure do |e|
handle_exception(e, "HTTP request to XML API failed",
"Error receiving response from UCS API server (%s)")
end
rescue_from REXML::ParseException do |e|
handle_exception(e, "failed to parse response from XML API",
"Received invalid response from UCS API server (%s)")
end
# Render the login page, where the URL, username, and password can
# be changed.
def settings
if have_credentials?
read_credentials
else
@ucs_url = @username = @password = ""
end
end
# Store the provided credentials, then attempt to log in and get our
# session cookie. The id we are passing to the #show action can be
# changed as indicated in the notes for #edit.
def login
# Persist the credentials even before we know they're right, because
# if the user got them wrong, it's nicer to make them edit the incorrect
# settings than have to type them all out from scratch.
write_credentials(params[:ucs_url], params[:username], params[:password])
read_credentials
logger.debug "Cisco UCS: about to aaaLogin"
cookie = aaaLogin(@ucs_url, @username, @password)
logger.debug "Cisco UCS: cookie returned from aaaLogin: " + cookie.inspect
# ucs_login will issue a redirect if authentication failed.
return unless cookie
# Login succeeded
set_ucs_session_cookie(cookie)
redirect_to action: :edit
end
def logout
unless logged_in?
redirect_to ucs_settings_path, notice: "Already logged out from UCS."
return
end
if have_credentials?
read_credentials # need API endpoint
logoutDoc = sendXML("<aaaLogout inCookie='#{ucs_session_cookie}'/>")
logger.debug "Cisco UCS: logout: " + logoutDoc.root.inspect
else
logger.warn "Cisco UCS: logging out without credentials"
end
set_ucs_session_cookie(nil)
redirect_to ucs_settings_path, notice: "Logged out from UCS."
end
def edit
# N.B. the ls:Server class (in which 'ls' stands for
# logical server) encapsulates:
#
# - service profiles
# - service profile initial templates
# - service profile initial templates
#
# rather than what one might intuitively expect, which is for
# service profile templates to have a separate class to service
# profile instances. This can be seen by visiting the Cisco UCS
# web UI, clicking on the API Model Documentation, selecting
# "Classes" then "ls:Server", and scrolling down to the "type"
# attribute which references the "ls:Type" class, e.g.:
#
# http://192.168.124.26/docs/MO-lsServer.html#type
#
# ls:Server inherits from compute:Logical (c.f. compute:Physical
# below)
get_class_instances("lsServer").each do |element|
# filter out service profile instances, as per above
next unless element.attributes["type"] =~ /template/
# check policies for matches to "hardcoded" named values
case element.attributes["name"]
when STORAGE_SERVICE_PROFILE
@storage = true
logger.debug "Cisco UCS: found ls:Server instance named #{STORAGE_SERVICE_PROFILE}"
when COMPUTE_SERVICE_PROFILE
@compute = true
logger.debug "Cisco UCS: found ls:Server instance named #{COMPUTE_SERVICE_PROFILE}"
end
end
# compute:Physical is a superclass containing compute:RackUnit and compute:Blade,
# so we can get instances of both in a single API call:
@computePhysical = configResolveClass("computePhysical").elements
@rackUnits = @computePhysical.to_a("configResolveClass/outConfigs/computeRackUnit")
@blades = @computePhysical.to_a("configResolveClass/outConfigs/computeBlade")
# equipment:Chassis is in a different part of the class hierarchy
@chassisUnits = get_class_instances("equipmentChassis")
@storage_service_profile = STORAGE_SERVICE_PROFILE
@compute_service_profile = COMPUTE_SERVICE_PROFILE
end
# This will perform the update action and should redirect to edit once complete.
def update
@updateDoc = ""
case params[:updateAction]
when "compute"
action = COMPUTE_SERVICE_PROFILE
when "storage"
action = STORAGE_SERVICE_PROFILE
when "up"
action = "admin-up"
when "down"
action = "admin-down"
when "reboot"
action = "cycle-immediate"
else
logger.warn "Cisco UCS: update request had invalid action '#{params[:updateAction]}'"
redirect_to ucs_edit_path, notice: "You must choose an action."
return
end
if action == COMPUTE_SERVICE_PROFILE || action == STORAGE_SERVICE_PROFILE
match_count = instantiate_service_profile(action)
else
match_count = send_power_commands(action)
end
if match_count == 0
redirect_to ucs_edit_path, notice: "You must select at least one node."
return nil
end
@updateDoc = \
"<configConfMos inHierarchical='false' cookie='#{ucs_session_cookie}'><inConfigs>" +
@updateDoc +
"</inConfigs></configConfMos>"
serverResponseDoc = sendXML(@updateDoc)
redirect_to ucs_edit_path, notice: "Your update has been applied."
end
private
@@xml_formatter = REXML::Formatters::Pretty.new
@@xml_formatter.compact = true
def pp_xml(xml)
doc = REXML::Document.new(xml)
pp_element(doc.root)
end
def pp_element(element)
out = ""
@@xml_formatter.write(element, out)
out
end
def instantiate_service_profile(action)
logger.debug "Cisco UCS: will instantiate from #{action} template"
match_count = 0
get_class_instances("computePhysical").each do |element|
if params[element.attributes["dn"]] == "1"
match_count += 1
@instantiateNTemplate = sendXML(<<-EOXML)
<lsInstantiateNTemplate
cookie='#{ucs_session_cookie}'
dn='org-root/ls-#{action}'
inTargetOrg='org-root'
inServerNamePrefixOrEmpty='sc'
inNumberOf='1'
inHierarchical='false'>
</lsInstantiateNTemplate>
EOXML
@instantiateNTemplate.elements.each("lsInstantiateNTemplate/outConfigs/lsServer") do |currentPolicy|
@currentPolicyName = currentPolicy.attributes["dn"]
@currentPolicyXML = currentPolicy
end
@updateDoc = @updateDoc + <<-EOXML
<pair key='#{@currentPolicyName}/pn'>
<lsBinding pnDn='#{element.attributes["dn"]}'>
</lsBinding>
</pair>"
EOXML
end
end
match_count
end
def send_power_commands(action)
logger.debug "Cisco UCS: will send #{action} command"
match_count = 0
get_class_instances("computePhysical").each do |element|
if params[element.attributes["dn"]] == "1"
match_count += 1
@updateDoc = @updateDoc + <<-EOXML
<pair key='#{element.attributes["dn"]}'>
<#{element.name} adminPower='#{action}' dn='#{element.attributes["dn"]}'>
</#{element.name}>
</pair>
EOXML
end
end
match_count
end
def ucs_session_cookie
session[:ucs_cookie]
end
def set_ucs_session_cookie(cookie)
session[:ucs_cookie] = cookie
logger.debug "Cisco UCS: set session cookie to #{cookie}"
end
helper_method :logged_in?
def logged_in?
!! ucs_session_cookie
end
# Use this to protect error messages which are intended to go in the
# flash from causing a ActionDispatch::Cookies::CookieOverflow error
# (the session cookie has a limit of 4k) or making the web UI look
# ugly.
def truncate(message)
return message if message.size < 80
message.slice(0, 80) + "..."
end
def sendXML(xmlString = "")
uri = URI.parse(@ucs_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = false
if uri.scheme == "https"
http.use_ssl = true
### Make configurable
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
api_request = Net::HTTP::Post.new(uri.request_uri)
api_request.body = xmlString
begin
api_response = http.request(api_request)
rescue StandardError => e
raise XMLAPIRequestError, e
end
unless api_response.is_a? Net::HTTPSuccess
raise XMLAPIResponseFailure, api_response
end
return REXML::Document.new(api_response.body)
end
def aaaLogin(ucs_url, username, password)
if ucs_url.blank?
logger.debug "Cisco UCS: missing login URL"
redirect_to ucs_settings_path, alert: "You must provide a login URL"
return nil
elsif ! ucs_url.end_with? "/nuova"
logger.debug "Cisco UCS: login URL didn't have the correct '/nuova' ending"
redirect_to ucs_settings_path, alert: "Login URL should end in '/nuova'"
return nil
elsif username.blank?
logger.debug "Cisco UCS: missing login name"
redirect_to ucs_settings_path, alert: "You must provide a login name"
return nil
elsif password.blank?
logger.debug "Cisco UCS: missing login password"
redirect_to ucs_settings_path, alert: "You must provide a login password"
return nil
end
valid_uri = false
begin
uri = URI.parse(ucs_url)
valid_uri = uri.kind_of?(URI::HTTP)
rescue URI::InvalidURIError
# pass
end
unless valid_uri
logger.debug "Cisco UCS: login URL is not a HTTP/HTTPS URL"
redirect_to ucs_settings_path, alert: "Login URL should be a HTTP/HTTPS URL"
return nil
end
unless uri.host
logger.debug "Cisco UCS: login URL does not have a valid hostname"
redirect_to ucs_settings_path, alert: "Login URL should have a valid hostname"
return nil
end
logger.debug "Cisco UCS: credentials all present"
begin
loginDoc = sendXML("<aaaLogin inName='#{username}' inPassword='#{password}'></aaaLogin>")
rescue REXML::ParseException => e
logger.warn "Cisco UCS: REXML parse failure during aaaLogin: #{e}"
message = "Failed to parse response from UCS API server; did your API URL end in '/nuova'?"
redirect_to ucs_settings_path, notice: message
return nil
end
ucs_cookie = cookie_from_response(loginDoc)
unless ucs_cookie
# FIXME: improve cookie validation
redirect_to ucs_settings_path, notice: "Login failed to obtain session cookie from Cisco UCS"
return nil
end
return ucs_cookie
end
def cookie_from_response(response)
response ? response.root.attributes["outCookie"] : nil
end
def configResolveClass(classId)
ucsDoc = sendXML("<configResolveClass cookie='#{ucs_session_cookie}' classId='#{classId}'></configResolveClass>")
return ucsDoc
end
def get_class_instances(classId)
root = configResolveClass(classId)
root.elements.to_a("configResolveClass/outConfigs/*")
end
def authenticate
unless have_credentials?
redirect_to ucs_settings_path, notice: t("barclamp.ucs.login.provide_creds")
return
end
unless logged_in?
redirect_to ucs_settings_path, notice: t("barclamp.ucs.login.please_login")
return
end
read_credentials
end
def have_credentials?
File.exist?(CREDENTIALS_XML_PATH)
end
def read_credentials
File.open(CREDENTIALS_XML_PATH) do |file|
cloudDoc = REXML::Document.new(file)
cloudDoc.elements.each("ucs/cloud") do |element|
@ucs_url = element.attributes["url"]
@username = element.attributes["username"]
@password = element.attributes["password"]
end
end
end
def write_credentials(ucs_url, username, password)
File.open(CREDENTIALS_XML_PATH, "w", 0640) do |file|
file.puts "<ucs><cloud url='#{ucs_url}' username='#{username}' password='#{password}' /></ucs>"
end
end
end