diff --git a/Gemfile b/Gemfile index 587f6c1719..e57c211837 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ group :development do gem "sprockets-standalone", "~> 1.2.1" gem "sprockets", "~> 2.11.0" gem "rspec", "~> 3.1.0" + gem "rest-client", "~> 2.0" end unless ENV["PACKAGING"] && ENV["PACKAGING"] == "yes" diff --git a/crowbar_framework/app/controllers/intelrsd_controller.rb b/crowbar_framework/app/controllers/intelrsd_controller.rb new file mode 100644 index 0000000000..5d649d97d5 --- /dev/null +++ b/crowbar_framework/app/controllers/intelrsd_controller.rb @@ -0,0 +1,222 @@ +# +# Copyright 2016, SUSE LINUX 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. +# + +require "json" + +# An important note on interfacing with Redfish APIS: +# +# Only specific top level URIs may be assumed, and even these +# may be absent based on the implementation. (for ex: there +# might be no /redfish/v1/Systems collection on something that +# doesn't have compute nodes ) +# +# The API will eventually be implemented on a system that breaks +# any data model and hence the URIs must be dynamically discovered +# The data model represented here using @node_object_list prepares +# a list of all the available Systems along with the properties +# and IDs of available resources in each system. This data model +# needs to be appropriately mapped to the Node object of the system +# where the data model is employed. +# + +class RsdController < ApplicationController + attr_reader :redfish_client, :logger + + # Client setup for the class + @host = ENV["CROWBAR_REDFISH_HOST"] || "localhost" + @port = ENV["CROWBAR_REDFISH_PORT"] || "8443" + + def settings + @host = @rsd_rest_server + @port = @rsd_server_port + Rails.logger.warn "RackScale Server: #{@host}, Port: #{@port}" + end + + def display + Rails.logger.warn "RackScale Server: #{@host}, Port: #{@port}" + @redfish_client = RedfishHelper::RedfishClient.new(@host, @port) + @title = "Welcome to RackScale Design" + sys_list = get_all_systems + @rsd_systems = "Systems not Available" + unless sys_list.empty? + @rsd_systems = sys_list + end + end + + def initialize + host = ENV["CROWBAR_REDFISH_HOST"] || "localhost" + port = ENV["CROWBAR_REDFISH_PORT"] || "8443" + @redfish_client = RedfishHelper::RedfishClient.new(host, port) + @node_object_list = [] + end + + def get_system_resource_list(sys_id, resource) + resource_list = [] + items = @redfish_client.get_resource("Systems/#{sys_id}/#{resource}") + items["Members"].each do |item| + item_odata_id = item["@odata.id"] + item_id = item_odata_id.split("/")[-1] + resource_item = @redfish_client.get_resource("Systems/#{sys_id}/#{resource}/#{item_id}") + resource_list.push(resource_item) + end + resource_list + end + + def make_node_object_for_system(sys_id) + nodeobject = Hash.new + nodeobject["System_Id"] = sys_id + ["Processors", "Memory", "MemoryChunks", + "EthernetInterfaces", "Adapters"].each do |resource| + nodeobject[resource.to_s] = get_system_resource_list(sys_id, resource) + end + nodeobject + end + + def get_systems + @systems = @redfish_client.get_resource("Systems") + sys_list = [] + + @systems["Members"].each do |member| + odata_id = member["@odata.id"] + sys_id = odata_id.split("/")[-1] + sys_list.push(sys_id) + end + sys_list + end + + def get_system_data(sys_id) + system_data = @redfish_client.get_resource("Systems/#{sys_id}") + system_object = make_node_object_for_system(sys_id) + ["Processors", "Memory", "MemoryChunks", + "EthernetInterfaces", "Adapters"].each do |resource| + system_data[resource.to_s] = system_object[resource.to_s] + end + system_data + end + + def get_rsd_nodes + system_list = get_systems + system_list.each do |system| + node_object = make_node_object_for_system(system) + @node_object_list.push(node_object) + end + @node_object_list + end + + def get_crowbar_node_object(sys_id) + system_object = get_system_data(sys_id) + node_name_prefix = "d" + node_name_prefix = "IRSD" if system_object["Oem"].key?("Intel_RackScale") + + # Pickin up the first IP address. This may not be always the correct address. + # It must be revisited when testing with Rackscale hardware. + eth_interface = system_object["EthernetInterfaces"].first + node_name = node_name_prefix + eth_interface["MACAddress"].tr(":", "-") + + node = NodeObject.create_new "#{node_name}.#{Crowbar::Settings.domain}".downcase + + node.set["name"] = node_name + # set a flag to identify this node as a rackscale one + node.set["rackscale"] = true + # track the rackscale id for this node + node.set["rackscale_id"] = sys_id + node.set["target_cpu"] = "" + node.set["target_vendor"] = "suse" + node.set["host_cpu"] = "" + node.set["host_vendor"] = "suse" + node.set["kernel"] = "" # Kernel modules and configurations + node.set["counters"] = "" # various network interfaces and other counters + node.set["hostname"] = node_name + node.set["fqdn"] = "#{node_name}.#{Crowbar::Settings.domain}" + node.set["domain"] = Crowbar::Settings.domain + ipaddress_data = eth_interface["IPv4Addresses"].first + node.set["ipaddress"] = ipaddress_data["Address"] + node.set["macaddress"] = eth_interface["MACAddress"] + ip6address_data = eth_interface["IPv6Addresses"].first + node.set["ip6address"] = ip6address_data["Address"] + node.set["uptime"] = "1 minute" + node.set["recipes"] = "" + + # Add other roles as seen fit + node.set["roles"] = [] + ["deployer-config-default", "network-config-default", "dns-config-default", + "logging-config-default", "ntp-config-default", + "provisioner-base", "provisioner-config-default"].each do |role_name| + node["roles"] << role_name + end + + node.set["run_list"] = ["role[crowbar-#{node_name}.#{Crowbar::Settings.domain.tr(".", "_")}]"] + node.set["keys"]["host"]["host_dsa_public"] = "" + node.set["keys"]["host"]["host_rsa_public"] = "" + node.set["keys"]["host"]["host_ecdsa_public"] = "" + node.set["virtualization"]["system"] = "kvm" + node.set["virtualization"]["role"] = "guest" + node.set["platform"] = "suse" + node.set["platform_version"] = "12.1" + node.set["dmi"]["bios"]["all_records"] = "" + node.set["dmi"]["bios"]["vendor"] = "" + node.set["dmi"]["bios"]["version"] = system_object["BiosVersion"] + node.set["dmi"]["bios"]["release_date"] = "" + node.set["dmi"]["bios"]["address"] = "" + node.set["dmi"]["bios"]["runtime_size"] = "" + node.set["dmi"]["bios"]["rom_size"] = "" + node.set["dmi"]["bios"]["bios_revision"] = "" + node.set["dmi"]["system"]["product_name"] = system_object["Model"] + node.set["dmi"]["system"]["manufacturer"] = "" + node.set["dmi"]["system"]["serial_number"] = "Not Specified" + node.set["dmi"]["system"]["uuid"] = "" + node.set["dmi"]["system"]["wake_up_type"] = "Power Switch" + node.set["dmi"]["system"]["sku_number"] = "Not Specified" + node.set["dmi"]["system"]["family"] = "Not Specified" + node.set["dmi"]["chassis"]["serial_number"] = system_object["SerialNumber"] + node.set["dmi"]["chassis"]["all_records"] = "" + node.set["dmi"]["chassis"]["manufacturer"] = "" + node.set["dmi"]["chassis"]["all_records"] = "" + node.set["dmi"]["chassis"]["boot_up_state"] = "Safe" + node.set["dmi"]["chassis"]["power_supply_state"] = "Safe" + # this is needed so its counted properly for the UI + node.set["block_device"]["sda"] = { removable: "0" } + node.set["memory"]["swap"] = "" + node.set["memory"]["buffers"] = "" + total_mem = 0 + system_object["Memory"].each do |m| + total_mem += m["CapacityMiB"].to_i + end + node.set["memory"]["total"] = "#{total_mem * 1024}kB" + + system_object["Processors"].each do |processor| + id = processor["Id"].to_i - 1 # API starts at 1, we start at 0 + node.set["cpu"][id.to_s]["manufacturer"] = processor["Manufacturer"] + node.set["cpu"][id.to_s]["model"] = processor["Model"] + node.set["cpu"][id.to_s]["family"] = processor["ProcessorArchitecture"] + node.set["cpu"][id.to_s]["family"] = "x86_64" if processor["InstructionSet"] == "x86-64" + node.set["cpu"][id.to_s]["flags"] = processor["Capabilities"] + end + + node.set["filesystem"]["sysfs"] = "" + node.save + node.allocate + node.set_state("ready") + end +end + +# run it on a thread to not block the UI at the start +Thread.new do + rsd_controller = IntelRSDController.new + node_list = rsd_controller.get_systems + first_node = node_list.first + rsd_controller.get_crowbar_node_object(first_node) +end diff --git a/crowbar_framework/app/helpers/redfish_helper.rb b/crowbar_framework/app/helpers/redfish_helper.rb new file mode 100644 index 0000000000..3efecd676d --- /dev/null +++ b/crowbar_framework/app/helpers/redfish_helper.rb @@ -0,0 +1,108 @@ +# +# Copyright 2016, SUSE LINUX 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. +# + +require "json" +require "uri" +require "net/http" +require "rest-client" + +module RedfishHelper + class RedfishClient + attr_reader :logger + + # Standard JSONRPC Error responses + INVALID_JSON = -32700 + INVALID_REQUEST = -32600 + INVALID_PARAMS = -32602 + METHOD_NOT_FOUND = -32601 + INTERNAL_ERROR = -32603 + + # RedFish-specific constants + REDFISH_VERSION = "redfish/v1/".freeze + + def initialize(host, port, insecure = true, client_cert = false) + @service_uri = "https://#{host}:#{port}/#{REDFISH_VERSION}" + @verify_ssl = OpenSSL::SSL::VERIFY_NONE if insecure + @ssl_client_cert = false unless client_cert + @reset_action = "ComputerSystem.Reset".freeze + end + + def post_action(resource, action = nil, payload = nil) + uri = @service_uri + resource + uri += "/Actions/#{action}" if action + payload = {} unless payload + + begin + response = RestClient::Request.execute(url: uri, + method: :post, + payload: payload.to_json, + headers: { content_type: :json }, + verify_ssl: @verify_ssl, + ssl_client_cert: @ssl_client_cert) + JSON.parse(response) + rescue RestClient::ExceptionWithResponse => e + Rails.logger.error("Error while trying to post #{payload} to #{uri}: #{e}") + false + end + end + + def restart(resource) + post_action("Systems/#{resource}", + @reset_action, + "ResetType" => "GracefulRestart") + end + + def shutdown(resource) + post_action("Systems/#{resource}", + @reset_action, + "ResetType" => "GracefulShutdown") + end + + def poweron(resource) + post_action("Systems/#{resource}", + @reset_action, + "ResetType" => "On") + end + + def powercycle(resource) + post_action("Systems/#{resource}", + @reset_action, + "ResetType" => "ForceRestart") + end + + def poweroff(resource) + post_action("Systems/#{resource}", + @reset_action, + "ResetType" => "ForceOff") + end + + def get_resource(resource) + uri = @service_uri + resource + Rails.logger.debug("QUERYING RESOURCE: #{uri}") + + begin + response = RestClient::Request.execute(url: uri, + method: :get, + verify_ssl: @verify_ssl, + ssl_client_cert: @ssl_client_cert) + return JSON.parse(response) + rescue RestClient::ExceptionWithResponse => e + Rails.logger.error(e) + JSON.parse(e.response) + end + end + end +end diff --git a/crowbar_framework/app/models/intelrsd.rb b/crowbar_framework/app/models/intelrsd.rb new file mode 100644 index 0000000000..7fb030ac7c --- /dev/null +++ b/crowbar_framework/app/models/intelrsd.rb @@ -0,0 +1,19 @@ +# +# Copyright 2016, SUSE LINUX 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. +# + +class IntelRSD < ActiveResource::Base + attr_accessor :url +end diff --git a/crowbar_framework/app/models/node_object.rb b/crowbar_framework/app/models/node_object.rb index 46175e161b..fec5d13d02 100644 --- a/crowbar_framework/app/models/node_object.rb +++ b/crowbar_framework/app/models/node_object.rb @@ -1161,6 +1161,43 @@ def bmc_cmd(cmd) [200, {}] end + def redfish_rest_cmd(cmd, rackscale_id) + host = ENV["CROWBAR_REDFISH_HOST"] || "localhost" + port = ENV["CROWBAR_REDFISH_PORT"] || "8443" + redfish_client = RedfishHelper::RedfishClient.new(host, port) + case cmd + when :reboot + unless redfish_client.restart(rackscale_id) + Rails.logger.warn("rackscale reboot rest cmd failed - node in unknown state") + [422, I18n.t("unknown_state", scope: "error")] + end + when :shutdown + unless redfish_client.shutdown(rackscale_id) + Rails.logger.warn("rackscale shutdown rest cmd failed - node in unknown state") + [422, I18n.t("unknown_state", scope: "error")] + end + when :poweron + unless redfish_client.poweron(rackscale_id) + Rails.logger.warn("rackscale poweron rest cmd failed - node in unknown state") + [422, I18n.t("unknown_state", scope: "error")] + end + when :powercycle + unless redfish_client.powercycle(rackscale_id) + Rails.logger.warn("rackscale powercycle rest cmd failed - node in unknown state") + [422, I18n.t("unknown_state", scope: "error")] + end + when :poweroff + unless redfish_client.poweroff(rackscale_id) + Rails.logger.warn("rackscale poweroff rest cmd failed - node in unknown state") + [422, I18n.t("unknown_state", scope: "error")] + end + else + Rails.logger.warn("Unknown command #{cmd} for #{@node.name}.") + [400, I18n.t("unknown_cmd", scope: "error", cmd: cmd)] + end + [200, {}] + end + def set_state(state) # use the real transition function for this cb = CrowbarService.new Rails.logger @@ -1261,6 +1298,8 @@ def reboot set_state("reboot") if @node[:platform_family] == "windows" net_rpc_cmd(:reboot) + elsif @node["rackscale"] + redfish_rest_cmd(:reboot, @node["rackscale_id"]) else ssh_cmd("/sbin/reboot") end @@ -1270,6 +1309,8 @@ def shutdown set_state("shutdown") if @node[:platform_family] == "windows" net_rpc_cmd(:shutdown) + elsif @node["rackscale"] + redfish_rest_cmd(:shutdown, @node["rackscale_id"]) else ssh_cmd("/sbin/poweroff") end @@ -1277,13 +1318,19 @@ def shutdown def poweron set_state("poweron") - bmc_cmd("power on") + if @node["rackscale"] + redfish_rest_cmd(:poweron, @node["rackscale_id"]) + else + bmc_cmd("power on") + end end def powercycle set_state("reboot") if @node[:platform_family] == "windows" net_rpc_cmd(:power_cycle) + elsif @node["rackscale"] + redfish_rest_cmd(:powercycle, @node["rackscale_id"]) else bmc_cmd("power cycle") end @@ -1293,6 +1340,8 @@ def poweroff set_state("shutdown") if @node[:platform_family] == "windows" net_rpc_cmd(:power_off) + elsif @node["rackscale"] + redfish_rest_cmd(:poweroff, @node["rackscale_id"]) else bmc_cmd("power off") end diff --git a/crowbar_framework/app/views/rsd/display.html.haml b/crowbar_framework/app/views/rsd/display.html.haml new file mode 100644 index 0000000000..2941897d3f --- /dev/null +++ b/crowbar_framework/app/views/rsd/display.html.haml @@ -0,0 +1,32 @@ +.row + .col-xs-12 + %h1.page-header + = t(".title") + +.row + .col-xs-12 + .panel.panel-default + .panel-body + .alert.alert-info + = t(".rsd_header") + += form_for :node, :url => rsd_allocate_path, :html => { :role => "form" } do |f| + = hidden_field_tag "return", @allocated + .panel.panel-default#accordion + %h2 + = t(".sys_header") + .panel-panel-body + %table.table.table-hover.table-middle{ :style => "border: 1px; width: 100%" } + %thead + %tr + %th + = t(".rsd_selection") + %th + = t(".system_id") + %tbody + - @rsd_systems.each do |rsd_system| + %tr + %td= check_box_tag "#{rsd_system['SystemId']}" + %td= link_to("System-#{rsd_system['SystemId']}") + .btn-group.pull-right + %input.btn.btn-default{ :type => "submit", :name => "allocate", :value => t(".allocate_switch") } diff --git a/crowbar_framework/app/views/rsd/settings.html.haml b/crowbar_framework/app/views/rsd/settings.html.haml new file mode 100644 index 0000000000..a4d4cb5619 --- /dev/null +++ b/crowbar_framework/app/views/rsd/settings.html.haml @@ -0,0 +1,24 @@ +.row + .col-xs-12 + %h1.page-header + = t(".title") + + .btn-group.pull-right + = link_to t(".show"), rsd_display_path, :class => "btn btn-default" + += form_tag rsd_display_path, :role => "form" do |f| + .panel.panel-default + .panel-body + .form-group + %label{ :for => :rsd_rest_server } + = t(".rsd_rest_server") + = text_field_tag :rsd_rest_server, @rsd_rest_server, :class => "form-control", :disabled => nil + + .form-group + %label{ :for => :rsd_server_port } + = t(".rsd_server_port") + = text_field_tag :rsd_server_port, @rsd_server_port, :class => "form-control", :disabled => nil + + .panel-footer.text-right + .btn-group + %input.btn.btn-default{ :type => "submit", :name => "show", :value => t(".show") } diff --git a/crowbar_framework/config/locales/intel_rsd/en.yml b/crowbar_framework/config/locales/intel_rsd/en.yml new file mode 100644 index 0000000000..1ea4f8bbda --- /dev/null +++ b/crowbar_framework/config/locales/intel_rsd/en.yml @@ -0,0 +1,41 @@ +# +# Copyright 2016, SUSE LINUX 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. +# + +en: + nav: + utils: + rsd: 'Intel RackScale' + rsd: + settings: + title: 'Intel Rackscale Server Details' + rsd_rest_server: 'PSME Rest Server IP' + rsd_server_port: 'PSME Rest Server Port' + show: 'Show' + show: + title: 'Intel Rackscale' + rsd_header: 'Lists Systems available from Intel RackScale server by talking to the Redfish APIs' + man_header: 'Managers' + sys_header: 'Systems' + sw_header: 'Switches' + ch_header: 'Chassis' + allocate_switch: 'Allocate' + rsd_selection: 'Selection' + system_id: 'System ID' + barclamp: + rsd: + login: + provide_creds: 'Please provide Rackscale login credentials.' + please_login: 'No active Rackscale session; please login.' diff --git a/crowbar_framework/config/navigation.rb b/crowbar_framework/config/navigation.rb index 38db393bb6..6634f47cc0 100644 --- a/crowbar_framework/config/navigation.rb +++ b/crowbar_framework/config/navigation.rb @@ -39,6 +39,7 @@ level2.item :repositories, t("nav.utils.repositories"), repositories_path level2.item :backup, t("nav.utils.backup"), backups_path level2.item :logs, t("nav.utils.logs"), utils_path + level2.item :rsd, t("nav.utils.rsd"), rsd_settings_path end end end diff --git a/crowbar_framework/config/routes.d/intel-rsd.routes b/crowbar_framework/config/routes.d/intel-rsd.routes new file mode 100644 index 0000000000..2ea39e527d --- /dev/null +++ b/crowbar_framework/config/routes.d/intel-rsd.routes @@ -0,0 +1,20 @@ +# +# Copyright 2016, SUSE LINUX 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. +# + +post 'rsd/display' => 'rsd#display', as: 'rsd_display' +get 'rsd/settings' => 'rsd#settings', as: 'rsd_settings' +post 'rsd/show' => 'rsd#show', as: 'rsd_show' +post 'rsd/allocate' => 'rsd#allocate', as: 'rsd_allocate' diff --git a/intelrsd.yml b/intelrsd.yml new file mode 100644 index 0000000000..4350b27ada --- /dev/null +++ b/intelrsd.yml @@ -0,0 +1,37 @@ +# +# Copyright 2016, SUSE LINUX 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. +# + +barclamp: + name: intelrsd + display: Intel RackScale Design + description: Integration with Intel Rack Scale Design + version: 1 + user_managed: false + member: + - crowbar + +crowbar: + layout: 1 + order: 112 + run_order: 112 + chef_order: 112 + proposal_schema_version: 3 + +nav: + utils: + rsd: + order: 91 + route: 'rsd_settings_path'