From e33ad27846cf978746b6abf496a387d62b895373 Mon Sep 17 00:00:00 2001
From: Petr Pavlu <petr.pavlu@suse.com>
Date: Mon, 9 Mar 2020 15:28:37 +0100
Subject: [PATCH] Fix omission of built-in stonith attributes (bsc#1165587)

* Refactor: Obtain the Pacemaker daemon directory during initialization

- Move the code in app/models/crm_config.rb that searches for a location
of Pacemaker daemons into config/application.rb where it is run during
initialization. This way the location can be also used by other code
besides crm_config.rb and the user can potentially modify it when using
a non-standard libexec directory.

- Pacemaker implements special instance attributes that can be set for any
stonith resource, for example, pcmk_host_list. Hawk uses
'crm_resource --show-metadata <ra>' to obtain parameters valid for a
specific resource agent but output from this command does not list these
common stonith attributes. They can be however obtained separately by
invoking 'pacemaker-fenced metadata'. The patch updates the logic that
queries agent's metadata to additionally invoke the latter command (if
the resource has the stonith class) and use its output to complete
agent's parameters.

Implement mechanism to switch binaries in case we have different OS version.
For SLE15sp1 and higher the binary has changed, where for lower version
the binary has another name and path
---
 hawk/app/controllers/agents_controller.rb |  2 +-
 hawk/app/models/crm_config.rb             | 81 ++++++++++++++-----------------
 hawk/app/models/primitive.rb              | 24 ++++++++-
 hawk/config/application.rb                | 19 ++++++++
 hawk/lib/util.rb                          | 46 ++++++++++++++++++
 5 files changed, 125 insertions(+), 47 deletions(-)

diff --git a/hawk/app/controllers/agents_controller.rb b/hawk/app/controllers/agents_controller.rb
index 5d32861b..2e82dedc 100644
--- a/hawk/app/controllers/agents_controller.rb
+++ b/hawk/app/controllers/agents_controller.rb
@@ -22,7 +22,7 @@ class AgentsController < ApplicationController
     else
       @name = params[:id]
     end
-    @agent = Hash.from_xml(Util.safe_x("/usr/sbin/crm_resource", "--show-metadata", @name))
+    @agent = Util.get_metadata_hash(@name)
 
     if @agent
       respond_to do |format|
diff --git a/hawk/app/models/crm_config.rb b/hawk/app/models/crm_config.rb
index 6b38e55e..12fc4c47 100644
--- a/hawk/app/models/crm_config.rb
+++ b/hawk/app/models/crm_config.rb
@@ -194,55 +194,46 @@ class CrmConfig < Tableless
               "crmd",
               "cib"
             ].each do |cmd|
-              [
-                "/usr/libexec/pacemaker/#{cmd}",
-                "/usr/lib64/pacemaker/#{cmd}",
-                "/usr/lib/pacemaker/#{cmd}",
-                "/usr/lib64/heartbeat/#{cmd}",
-                "/usr/lib/heartbeat/#{cmd}"
-              ].each do |path|
-                next unless File.executable? path
-
-                REXML::Document.new(%x[#{path} metadata 2>/dev/null]).tap do |xml|
-                  return unless xml.root
-
-                  xml.elements.each("//parameter") do |param|
-                    name = param.attributes["name"]
-                    content = param.elements["content"]
-                    shortdesc = param.elements["shortdesc[@lang=\"#{I18n.locale.to_s.gsub("-", "_")}\"]|shortdesc[@lang=\"en\"]"].text || ""
-                    longdesc  = param.elements["longdesc[@lang=\"#{I18n.locale.to_s.gsub("-", "_")}\"]|longdesc[@lang=\"en\"]"].text || ""
-
-                    type = content.attributes["type"]
-                    default = content.attributes["default"]
-
-                    advanced = shortdesc.match(/advanced use only/i) || longdesc.match(/advanced use only/i)
-
-                    crm_config[name] = {
-                      type: content.attributes["type"],
-                      readonly: false,
-                      shortdesc: shortdesc,
-                      longdesc: longdesc,
-                      advanced: advanced ? true : false,
-                      default: default
-                    }
-
-                    if type == "enum"
-                      match = longdesc.match(/Allowed values:(.*)/i)
-
-                      if match
-                        values = match[1].split(",").map do |value|
-                          value.strip
-                        end.reject do |value|
-                          value.empty?
-                        end
-
-                        crm_config[name][:values] = values unless values.empty?
+              path = "#{Rails.configuration.x.crm_daemon_dir}/#{cmd}"
+              next unless File.executable? path
+
+              REXML::Document.new(%x[#{path} metadata 2>/dev/null]).tap do |xml|
+                return unless xml.root
+
+                xml.elements.each("//parameter") do |param|
+                  name = param.attributes["name"]
+                  content = param.elements["content"]
+                  shortdesc = param.elements["shortdesc[@lang=\"#{I18n.locale.to_s.gsub("-", "_")}\"]|shortdesc[@lang=\"en\"]"].text || ""
+                  longdesc  = param.elements["longdesc[@lang=\"#{I18n.locale.to_s.gsub("-", "_")}\"]|longdesc[@lang=\"en\"]"].text || ""
+
+                  type = content.attributes["type"]
+                  default = content.attributes["default"]
+
+                  advanced = shortdesc.match(/advanced use only/i) || longdesc.match(/advanced use only/i)
+
+                  crm_config[name] = {
+                    type: content.attributes["type"],
+                    readonly: false,
+                    shortdesc: shortdesc,
+                    longdesc: longdesc,
+                    advanced: advanced ? true : false,
+                    default: default
+                  }
+
+                  if type == "enum"
+                    match = longdesc.match(/Allowed values:(.*)/i)
+
+                    if match
+                      values = match[1].split(",").map do |value|
+                        value.strip
+                      end.reject do |value|
+                        value.empty?
                       end
+
+                      crm_config[name][:values] = values unless values.empty?
                     end
                   end
                 end
-
-                break
               end
             end
 
diff --git a/hawk/app/models/primitive.rb b/hawk/app/models/primitive.rb
index 7320b700..e46763c7 100644
--- a/hawk/app/models/primitive.rb
+++ b/hawk/app/models/primitive.rb
@@ -24,7 +24,29 @@ class Primitive < Template
     end
   end
 
-  alias_method_chain :provider, :template
+  alias_method :provider_without_template, :provider
+  alias_method :provider, :provider_with_template
+
+  validate :validate_params
+
+  def validate_params
+    required_params = []
+    res = Util.get_metadata_hash(agent_name)
+    param_res = res["resource_agent"]["parameters"]["parameter"]
+    if param_res
+      param_res.each do |items|
+        if items.key?("required") && items["required"] == "1"
+          required_params << items["name"]
+        end
+      end
+    end
+
+    params.each do |param, value|
+      if value.blank? && required_params.include?(param)
+        errors.add(:base, "In Parameters, #{param}'s value is blank!")
+      end
+    end
+  end
 
   def type_with_template
     if template.present?
diff --git a/hawk/config/application.rb b/hawk/config/application.rb
index 6408fd08..86594a35 100644
--- a/hawk/config/application.rb
+++ b/hawk/config/application.rb
@@ -50,6 +50,25 @@ module Hawk
 
     config.x.hawk_is_sles = system("cat /etc/os-release | grep 'ID=.*sles' >/dev/null 2>&1")
 
+    def lookup_daemon_dir
+      [
+        "/usr/libexec/pacemaker",
+        "/usr/lib64/pacemaker",
+        "/usr/lib/pacemaker",
+        "/usr/lib64/heartbeat",
+        "/usr/lib/heartbeat"
+      ].each do |dir|
+        [
+          "crmd",
+          "pacemaker-controld",
+        ].each do |cmd|
+          return dir if File.executable? "#{dir}/#{cmd}"
+        end
+      end
+      "/usr/libexec/pacemaker"
+    end
+
     ::Sass::Script::Number.precision = [10, ::Sass::Script::Number.precision].max
+    config.x.crm_daemon_dir = lookup_daemon_dir
   end
 end
diff --git a/hawk/lib/util.rb b/hawk/lib/util.rb
index a5399f7d..f3808f86 100644
--- a/hawk/lib/util.rb
+++ b/hawk/lib/util.rb
@@ -349,6 +349,52 @@ module Util
   end
   module_function :acl_version
 
+  # Hash#dig from Ruby 2.3
+  # https://apidock.com/ruby/Hash/dig
+  def dig_hash(hash, *keys)
+    res = hash
+    keys.each do |key|
+      res = res[key]
+      return nil if res.nil?
+    end
+    res
+  end
+  module_function :dig_hash
+
+  def get_metadata_hash(agent)
+    sub_map = { 'type="string"' => 'type="text"',
+                '(type="boolean".*)default="(yes|1)"' => '\1default="true"',
+                '(type="boolean".*)default="(no|0)"' => '\1default="false"' }
+
+    xml = safe_x("/usr/sbin/crm_resource", "--show-metadata", agent)
+    sub_map.each { |k,v| xml.gsub!(/#{k}/i, v) }
+    res = Hash.from_xml(xml)
+
+    # For stonith agents, complete the instance attributes with the ones that
+    # are built into Pacemaker and are available for all stonith-class
+    # resources.
+    if agent.start_with?("stonith:") && res && (res_param = dig_hash(res, "resource_agent", "parameters", "parameter"))
+      # by default we assume we are using the latest stonith binary path (sles15sp1 and higher)
+      # if we are using lower version, we adapt the path to other binary old accordingly
+      stonith_bin = "#{Rails.configuration.x.crm_daemon_dir}/pacemaker-fenced"
+      unless File.exists? "#{Rails.configuration.x.crm_daemon_dir}/pacemaker-fenced"
+        stonith_bin = "/usr/lib/pacemaker/stonithd"
+      end
+      # execute binary and collect/append the paramters to the hash which will be visualised
+      stonith_xml = safe_x(stonith_bin, "metadata")
+      sub_map.each { |k,v| stonith_xml.gsub!(/#{k}/i, v) }
+      stonith = Hash.from_xml(stonith_xml)
+
+      if stonith && (stonith_param = dig_hash(stonith, "resource_agent", "parameters", "parameter"))
+          res_param.concat(stonith_param)
+      end
+    end
+
+    res
+  end
+  module_function :get_metadata_hash
+
+
   # get text child of xml element - returns empty string if elem is nil or
   # text child is empty.  trims leading and trailing whitespace
   def get_xml_text(elem)
-- 
2.16.4

