Hello community,

here is the log from the commit of package yast2-cluster for openSUSE:Leap:15.2 
checked in at 2020-05-26 18:33:04
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Leap:15.2/yast2-cluster (Old)
 and      /work/SRC/openSUSE:Leap:15.2/.yast2-cluster.new.2738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "yast2-cluster"

Tue May 26 18:33:04 2020 rev:56 rq:808873 version:4.2.6

Changes:
--------
--- /work/SRC/openSUSE:Leap:15.2/yast2-cluster/yast2-cluster.changes    
2020-01-15 16:32:07.440878195 +0100
+++ /work/SRC/openSUSE:Leap:15.2/.yast2-cluster.new.2738/yast2-cluster.changes  
2020-05-26 18:33:09.237677181 +0200
@@ -1,0 +2,6 @@
+Mon Apr 13 06:49:43 UTC 2020 - nick wang <[email protected]>
+
+- jsc#SLE-12432, add qdevice heuristics support
+- Version 4.2.6
+
+-------------------------------------------------------------------

Old:
----
  yast2-cluster-4.2.5.tar.bz2

New:
----
  yast2-cluster-4.2.6.tar.bz2

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ yast2-cluster.spec ++++++
--- /var/tmp/diff_new_pack.mBnbby/_old  2020-05-26 18:33:09.561677889 +0200
+++ /var/tmp/diff_new_pack.mBnbby/_new  2020-05-26 18:33:09.565677898 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package yast2-cluster
 #
-# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2020 SUSE LINUX GmbH, Nuernberg, Germany.
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -19,12 +19,12 @@
 %define _fwdefdir %{_libexecdir}/firewalld/services
 
 Name:           yast2-cluster
-Version:        4.2.5
+Version:        4.2.6
 Release:        0
 Summary:        Configuration of cluster
 License:        GPL-2.0-only
 Group:          System/YaST
-Url:            https://github.com/yast/yast-cluster
+URL:            https://github.com/yast/yast-cluster
 
 Source0:        %{name}-%{version}.tar.bz2
 Source1:        cluster.firewalld.xml

++++++ yast2-cluster-4.2.5.tar.bz2 -> yast2-cluster-4.2.6.tar.bz2 ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast2-cluster-4.2.5/CONTRIBUTING.md 
new/yast2-cluster-4.2.6/CONTRIBUTING.md
--- old/yast2-cluster-4.2.5/CONTRIBUTING.md     2019-10-16 05:12:19.000000000 
+0200
+++ new/yast2-cluster-4.2.6/CONTRIBUTING.md     1970-01-01 01:00:00.000000000 
+0100
@@ -1,89 +0,0 @@
-YaST Contribution Guidelines
-============================
-
-YaST is an open source project and as such it welcomes all kinds of
-contributions. If you decide to contribute, please follow these guidelines to
-ensure the process is effective and pleasant both for you and the YaST 
maintainers.
-
-There are two main forms of contribution: reporting bugs and performing code
-changes.
-
-Bug Reports
------------
-
-If you find a problem, please report it either using
-[Bugzilla](https://bugzilla.suse.com/enter_bug.cgi?format=guided&product=openSUSE+Factory&component=YaST2)
-or [GitHub issues](../../issues). (For Bugzilla, use the [simplified
-registration](https://secure-www.novell.com/selfreg/jsp/createSimpleAccount.jsp)
-if you don't have an account yet.)
-
-When creating a bug report, please follow our [bug reporting
-guidelines](http://en.opensuse.org/openSUSE:Report_a_YaST_bug).
-
-We can't guarantee that every bug will be fixed, but we'll try.
-
-Code Changes
-------------
-
-We welcome all kinds of code contributions, from simple bug fixes to 
significant
-refactorings and implementation of new features. However, before making any
-non-trivial contribution, get in touch with us first — this can prevent wasted
-effort on both sides. Also, have a look at our [development
-documentation](http://en.opensuse.org/openSUSE:YaST_development).
-
-To send us your code change, use GitHub pull requests. The workflow is as
-follows:
-
-  1. Fork the project.
-
-  2. Create a topic branch based on `master`.
-
-  3. Implement your change, including tests (if possible). Make sure you adhere
-     to the [Ruby style
-     guide](https://github.com/SUSE/style-guides/blob/master/Ruby.md).
-
-  4. Update the package version (in `packages/*.spec`, usually by
-     `rake version:bump`) and add a new entry to the `package/*.changes` file
-     (by `osc vc package`).  
-     For bigger changes or changes which need longer discussion it is advised 
to
-     add this as a separate last commit so it can be easily updated when 
another
-     change is merged in the meantime.
-
-  5. Make sure your change didn't break anything by building the RPM package
-     (`rake osc:build`). The build process includes running the full testsuite.
-
-  6. Publish the branch and create a pull request.
-
-  7. YaST developers will review your change and possibly point out issues.
-     Adapt the code under their guidance until they are all resolved.
-
-  8. Finally, the pull request will get merged or rejected.
-
-See also [GitHub's guide on
-contributing](https://help.github.com/articles/fork-a-repo).
-
-If you want to do multiple unrelated changes, use separate branches and pull
-requests.
-
-### Commits
-
-Each commit in the pull request should do only one thing, which is clearly
-described by its commit message. Especially avoid mixing formatting changes and
-functional changes into one commit. When writing commit messages, adhere to
-[widely used
-conventions](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
-
-If your commit is related to a bug in Bugzilla or an issue on GitHub, make sure
-you mention it in the commit message for cross-reference. Use format like
-bnc#775814 or gh#yast/yast-foo#42. See also [GitHub
-autolinking](https://help.github.com/articles/github-flavored-markdown#references)
-and [openSUSE abbreviation
-reference](http://en.opensuse.org/openSUSE:Packaging_Patches_guidelines#Current_set_of_abbreviations).
-
-Additional Information
-----------------------
-
-If you have any question, feel free to ask at the [development mailing
-list](http://lists.opensuse.org/yast-devel/) or at the
-[#yast](http://webchat.freenode.net/?channels=%23yast) IRC channel on freenode.
-We'll do our best to provide a timely and accurate answer.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast2-cluster-4.2.5/package/yast2-cluster.changes 
new/yast2-cluster-4.2.6/package/yast2-cluster.changes
--- old/yast2-cluster-4.2.5/package/yast2-cluster.changes       2019-10-16 
05:12:19.000000000 +0200
+++ new/yast2-cluster-4.2.6/package/yast2-cluster.changes       2020-04-28 
10:12:21.000000000 +0200
@@ -1,4 +1,10 @@
 -------------------------------------------------------------------
+Mon Apr 13 06:49:43 UTC 2020 - nick wang <[email protected]>
+
+- jsc#SLE-12432, add qdevice heuristics support
+- Version 4.2.6
+
+-------------------------------------------------------------------
 Fri Oct 11 07:48:01 UTC 2019 - Yuan Ren <[email protected]>
 
 - bsc#1149089 corosync-qdevice service ready to be configured
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast2-cluster-4.2.5/package/yast2-cluster.spec 
new/yast2-cluster-4.2.6/package/yast2-cluster.spec
--- old/yast2-cluster-4.2.5/package/yast2-cluster.spec  2019-10-16 
05:12:19.000000000 +0200
+++ new/yast2-cluster-4.2.6/package/yast2-cluster.spec  2020-04-28 
10:12:21.000000000 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package yast2-cluster
 #
-# Copyright (c) 2019 SUSE LLC
+# Copyright (c) 2020 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,12 +18,12 @@
 %define _fwdefdir %{_libexecdir}/firewalld/services
 
 Name:           yast2-cluster
-Version:        4.2.5
+Version:        4.2.6
 Release:        0
 Summary:        Configuration of cluster
 License:        GPL-2.0-only
 Group:          System/YaST
-Url:            https://github.com/yast/yast-cluster
+URL:            https://github.com/yast/yast-cluster
 
 Source0:        %{name}-%{version}.tar.bz2
 Source1:        cluster.firewalld.xml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast2-cluster-4.2.5/src/include/cluster/dialogs.rb 
new/yast2-cluster-4.2.6/src/include/cluster/dialogs.rb
--- old/yast2-cluster-4.2.5/src/include/cluster/dialogs.rb      2019-10-16 
05:12:19.000000000 +0200
+++ new/yast2-cluster-4.2.6/src/include/cluster/dialogs.rb      2020-04-28 
10:12:21.000000000 +0200
@@ -735,6 +735,39 @@
       nil
     end
 
+    def heuristics_executables_input_dialog(name="", script="")
+      ret = nil
+
+      UI.OpenDialog(
+        MarginBox(
+          1,
+          1,
+          VBox(
+            HBox(
+            MinWidth(40, InputField(Id(:exec_name), _("Execute Name"), name)),
+            HSpacing(1),
+            MinWidth(100, InputField(Id(:exec_script), _("Execute Script"), 
script))
+            ),
+            VSpacing(1),
+            Right(
+              HBox(
+                PushButton(Id(:ok), _("OK")),
+                PushButton(Id(:cancel), _("Cancel"))
+              )
+            )
+          )
+        )
+      )
+
+      ret = UI.UserInput
+      if ret == :ok
+        ret = { UI.QueryWidget(:exec_name, :Value) => 
UI.QueryWidget(:exec_script, :Value) }
+      end
+      UI.CloseDialog
+
+      deep_copy(ret)
+    end
+
     def ValidateCorosyncQdevice
       if UI.QueryWidget(Id(:corosync_qdevice), :Value) == false
         return true
@@ -766,9 +799,35 @@
       end
 
       if UI.QueryWidget(Id(:corosync_qdevice), :Value) && 
Cluster.memberaddr.size <= 0
+        # Intent not return false since address is in another dialog.
         Popup.Message(_("Member Address is required when enable corosync 
qdevice"))
       end
 
+      if UI.QueryWidget(Id(:heuristics_mode), :Value) != "off"
+        if UI.QueryWidget(Id(:heuristics_timeout), :Value).to_i <= 0
+          Popup.Message(_("The qdevice heuristics timeout must be a positive 
integer"))
+          UI.SetFocus(Id(:heuristics_timeout))
+          return false
+        end
+
+        if UI.QueryWidget(Id(:heuristics_sync_timeout), :Value).to_i <= 0
+          Popup.Message(_("The qdevice heuristics sync timeout must be a 
positive integer"))
+          UI.SetFocus(Id(:heuristics_sync_timeout))
+          return false
+        end
+
+        if UI.QueryWidget(Id(:heuristics_interval), :Value).to_i <= 0
+          Popup.Message(_("The qdevice heuristics interval must be a positive 
integer"))
+          UI.SetFocus(Id(:heuristics_interval))
+          return false
+        end
+
+        if Cluster.heuristics_executables.size <= 0
+          Popup.Message(_("The heuristics executable script must config"))
+          return false
+        end
+      end
+
       true
     end
 
@@ -781,6 +840,12 @@
       Cluster.qdevice_tls = UI.QueryWidget(Id(:qdevice_tls), :Value)
       Cluster.qdevice_algorithm = UI.QueryWidget(Id(:qdevice_algorithm), 
:Value)
       Cluster.qdevice_tie_breaker = UI.QueryWidget(Id(:qdevice_tie_breaker), 
:Value)
+
+      Cluster.heuristics_mode = UI.QueryWidget(Id(:heuristics_mode), :Value)
+      Cluster.heuristics_timeout = UI.QueryWidget(Id(:heuristics_timeout), 
:Value).to_i
+      Cluster.heuristics_sync_timeout = 
UI.QueryWidget(Id(:heuristics_sync_timeout), :Value).to_i
+      Cluster.heuristics_interval = UI.QueryWidget(Id(:heuristics_interval), 
:Value).to_i
+
       nil
     end
 
@@ -811,6 +876,31 @@
         )
       )
 
+      qdevice_heuristics_section = VBox(
+        HBox(
+          Left(ComboBox(
+            Id(:heuristics_mode), Opt(:hstretch, :notify), _("Heuristics 
Mode:"),
+            ["off", "on", "sync"]
+          ))
+        ),
+        HBox(
+          Left(InputField(Id(:heuristics_timeout),Opt(:hstretch), 
_("Heuristics Timeout(milliseconds):"),"5000")),
+          HSpacing(1),
+          Left(InputField(Id(:heuristics_sync_timeout),Opt(:hstretch), 
_("Heuristics Sync_timeout(milliseconds):"),"15000")),
+          HSpacing(1),
+          Left(InputField(Id(:heuristics_interval),Opt(:hstretch), 
_("Heuristics Interval(milliseconds):"),"30000")),
+        ),
+        VBox(
+          Left(Label(_("Heuristics Executables:"))),
+          Table(Id(:heuristics_executables), Header(_("Name"), _("Value")), 
[]),
+          Right(HBox(
+            PushButton(Id(:executable_add), "Add"),
+            PushButton(Id(:executable_del), "Del"),
+            PushButton(Id(:executable_edit), "Edit"))
+          )
+        )
+      )
+
       contents = VBox(
         VSpacing(1),
         CheckBoxFrame(
@@ -821,6 +911,7 @@
           VBox(
             qdevice_section,
             qdevice_net_section,
+            qdevice_heuristics_section,
           )
         ),
         VStretch()
@@ -842,6 +933,11 @@
       end
       UI.ChangeWidget(Id(:qdevice_tie_breaker), :Value, 
Cluster.qdevice_tie_breaker)
 
+      UI.ChangeWidget(Id(:heuristics_mode), :Value, Cluster.heuristics_mode)
+      UI.ChangeWidget(Id(:heuristics_timeout), :Value, 
Cluster.heuristics_timeout)
+      UI.ChangeWidget(Id(:heuristics_sync_timeout), :Value, 
Cluster.heuristics_sync_timeout)
+      UI.ChangeWidget(Id(:heuristics_interval), :Value, 
Cluster.heuristics_interval)
+
       nil
     end
 
@@ -857,12 +953,42 @@
       nil
     end
 
+    def heuristics_switch
+      if !UI.QueryWidget(Id(:heuristics_mode), :Value) ||
+          UI.QueryWidget(Id(:heuristics_mode), :Value) == "off"
+        disable = false
+      else
+        disable = true
+      end
+
+      UI.ChangeWidget(Id(:heuristics_timeout), :Enabled, disable)
+      UI.ChangeWidget(Id(:heuristics_sync_timeout), :Enabled, disable)
+      UI.ChangeWidget(Id(:heuristics_interval), :Enabled, disable)
+      UI.ChangeWidget(Id(:heuristics_executables), :Enabled, disable)
+
+      nil
+    end
+
+    def fill_qdevice_heuristics_executables
+      items = []
+
+      Cluster.heuristics_executables.each do |name, value|
+        items.push(Item(Id(name.to_s), name.to_s, value.to_s))
+      end
+
+      UI.ChangeWidget(Id(:heuristics_executables), :Items, items)
+
+      nil
+    end
+
     def CorosyncQdeviceDialog
       ret = nil
 
       CorosyncQdeviceLayout()
 
       while true
+        fill_qdevice_heuristics_executables
+        heuristics_switch
 
         ret = UI.UserInput
 
@@ -872,6 +998,33 @@
           end
         end
 
+        if ret == :heuristics_mode
+          next
+        end
+
+        if ret == :executable_add
+          ret = heuristics_executables_input_dialog()
+          next if ret == :cancel
+          Cluster.heuristics_executables.merge!(ret)
+          next
+        end
+
+        if ret == :executable_edit
+          exec_name = UI.QueryWidget(:heuristics_executables, 
:CurrentItem).to_s
+          ret = heuristics_executables_input_dialog(exec_name,
+                                                    
Cluster.heuristics_executables[exec_name].to_s)
+          next if ret == :cancel
+          Cluster.heuristics_executables.delete(exec_name)
+          Cluster.heuristics_executables[ret.keys()[0]] = ret.values()[0]
+          next
+        end
+
+        if ret == :executable_del
+          exec_name = UI.QueryWidget(:heuristics_executables, 
:CurrentItem).to_s
+          Cluster.heuristics_executables.delete(exec_name)
+          next
+        end
+
         if ret == :next || ret == :back
           val = ValidateCorosyncQdevice()
           if val == true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast2-cluster-4.2.5/src/include/cluster/helps.rb 
new/yast2-cluster-4.2.6/src/include/cluster/helps.rb
--- old/yast2-cluster-4.2.5/src/include/cluster/helps.rb        2019-10-16 
05:12:19.000000000 +0200
+++ new/yast2-cluster-4.2.6/src/include/cluster/helps.rb        2020-04-28 
10:12:21.000000000 +0200
@@ -50,7 +50,32 @@
             "<p><b><big>Port</big></b><br>Specifies TCP port of qnetd server. 
Default is 5403.</p>\n" +
             "<p><b><big>TLS</big></b><br>Can be one of 'on', 'off' or 
'required' and specifies if tls should be used. 'on' means a connection with 
TLS is attempted first, but if the server doesn't advertise TLS support then 
non-TLS will be used. 'off' is used then TLS is not required and it's then not 
even tried. This mode is the only one which doesn't need a properly initialized 
NSS database. 'required' means TLS is required and if the server doesn't 
support TLS, qdevice will exit with error message. 'on' need manually change, 
refer to corosync-qdevice's man page for more details. Default is 'off' in 
yast.</p>\n" +
             "<p><b><big>Algorithm</big></b><br>Decision algorithm. Can be one 
of the 'ffsplit' or 'lms'.  (Actually there are also 'test' and '2nodelms', 
both of which are mainly for developers and shouldn't be used for production 
clusters, so yast will convert to 'ffsplit' automatically). For a description 
of what each algorithm means and how the algorithms differ see their individual 
sections.  Default value is ffsplit.</p>\n" +
-            "<p><b><big>Tie breaker</big></b><br>Can be one of 'lowest', 
'highest' or 'valid_node_id' (number) values. It's used as a fallback if 
qdevice has to decide between two or more equal partitions. 'lowest' means the 
partition with the lowest node id is chosen. 'highest' means the partition with 
highest node id is chosen. And 'valid_node_id' means that the partition 
containing the node with the given node id is chosen. Default is 
'lowest'.</p>\n"
+            "<p><b><big>Tie breaker</big></b><br>Can be one of 'lowest', 
'highest' or 'valid_node_id' (number) values. It's used as a fallback if 
qdevice has to decide between two or more equal partitions. 'lowest' means the 
partition with the lowest node id is chosen. 'highest' means the partition with 
highest node id is chosen. And 'valid_node_id' means that the partition 
containing the node with the given node id is chosen. Default is 
'lowest'.</p>\n" +
+            "<p><b><big>Qdevice Heuristics</big></b><br>Subsection of Qdevice. 
" \
+            "Heuristics are set of commands executed locally on startup, 
cluster membership change, " \
+            "successful connect to corosync-qnetd and optionally also at 
regular times. Commands are executed in parallel. " \
+            "When *all* commands finish successfully (their return error code 
is zero) on time, heuristics have passed, " \
+            "otherwise they have failed. The heuristics result is sent to 
corosync-qnetd and there it's used in calculations " \
+            "to determine which partition should be quorate.</p>\n" +
+            "<p><b><big>Heuristics Mode</big></b><br>Can be one of on, sync or 
off and specifies mode of operation of heuristics. " \
+            "Default is off, which  means  heuristics are disabled. When sync 
is set, heuristics are executed only during startup, " \
+            "membership change and when connection to corosync-qnetd is 
established. " \
+            "When heuristics should be running also on regular basis, this 
option should be set to on value.</p>\n" +
+            "<p><b><big>Heuristics Timeout</big></b><br>Specifies maximum time 
in milliseconds. " \
+            "How long corosync-qdevice waits till the heuristics commands 
finish. " \
+            "If some command doesn't finish before the timeout, it's killed 
and heuristics fail. " \
+            "This timeout is used for heuristics executed at regular times. " \
+            "Default value is half of the quorum.device.timeout, so 
5000.</p>\n" +
+            "<p><b><big>Heuristics Sync_timeout</big></b><br>Similar to 
quorum.device.heuristics.timeout but used during membership changes. " \
+            "Default value is half of the quorum.device.sync_timeout, so 
15000.</p>\n" +
+            "<p><b><big>Heuristics Interval</big></b><br>Specifies interval 
between two regular heuristics execution. " \
+            "Default value is 3 * quorum.device.timeout, so 30000.</p>\n" +
+            "<p><b><big>Heuristics exec_NAME</big></b><br>Defines executables. 
" \
+            "*NAME* can be arbitrary valid cmap key name string and it has no 
special meaning. " \
+            "The value of this variable must contain a command to execute. " \
+            "The value is parsed (split) into arguments similarly as Bourne 
shell would do. " \
+            "Quoting is possible by using backslash and double quotes. " \
+            "<br>For example, Name(exec_check_master), 
Value(/etc/corosync/qdevice/check_master.sh)</p>\n"
         ),
         "security"      => _(
           "\n" +
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast2-cluster-4.2.5/src/modules/Cluster.rb 
new/yast2-cluster-4.2.6/src/modules/Cluster.rb
--- old/yast2-cluster-4.2.5/src/modules/Cluster.rb      2019-10-16 
05:12:19.000000000 +0200
+++ new/yast2-cluster-4.2.6/src/modules/Cluster.rb      2020-04-28 
10:12:21.000000000 +0200
@@ -109,6 +109,13 @@
       @qdevice_algorithm = "ffsplit"
       @qdevice_tie_breaker = "lowest"
 
+      # qdevice heuristics
+      @heuristics_mode = "off"
+      @heuristics_timeout = "5000"
+      @heuristics_sync_timeout = "15000"
+      @heuristics_interval = "30000"
+      @heuristics_executables = {}
+
       @csync2_host = []
       @csync2_include = []
     end
@@ -164,6 +171,23 @@
       nil
     end
 
+    def LoadQdeviceHeuristicsExecutables
+      executables = {}
+      executables_str = 
SCR.Read(path(".openais.quorum.device.heuristics.executables"))
+
+      if executables_str
+        executables_ori = eval(executables_str)
+        # with eval(), key will have extra ""
+        # {'exec_check': '/tmp/check.sh'}
+        # {:exec_check=>"/tmp/check.sh"}
+        executables_ori.each do |key, value|
+          executables[key.to_s] = value.to_s
+        end
+      end
+
+      executables
+    end
+
     def LoadCorosyncQdeviceConfig
       if SCR.Read(path(".openais.quorum.device"))
         @corosync_qdevice = true
@@ -178,6 +202,14 @@
         @qdevice_tls = SCR.Read(path(".openais.quorum.device.net.tls"))
         @qdevice_algorithm = 
SCR.Read(path(".openais.quorum.device.net.algorithm"))
         @qdevice_tie_breaker = 
SCR.Read(path(".openais.quorum.device.net.tie_breaker"))
+
+        @heuristics_mode = 
SCR.Read(path(".openais.quorum.device.heuristics.mode"))
+        if @heuristics_mode
+          @heuristics_timeout = 
SCR.Read(path(".openais.quorum.device.heuristics.timeout")).to_s
+          @heuristics_sync_timeout = 
SCR.Read(path(".openais.quorum.device.heuristics.sync_timeout")).to_s
+          @heuristics_interval = 
SCR.Read(path(".openais.quorum.device.heuristics.interval")).to_s
+          @heuristics_executables = LoadQdeviceHeuristicsExecutables()
+        end
       end
 
       nil
@@ -299,6 +331,24 @@
       return address_string
     end
 
+    def generateDictString(obj)
+      str = "{"
+      first = true
+
+      obj.each do |k, v|
+        if not first
+          str << ", "
+        end
+
+        str << "'" + k.to_s + "'"
+        str << ": "
+        str << "'" + v.to_s + "'"
+        first = false
+      end
+
+      str << "}"
+    end
+
     def SaveCorosyncQdeviceConfig
       SCR.Write(path(".openais.quorum.device.model"), @qdevice_model)
       SCR.Write(path(".openais.quorum.device.votes"), @qdevice_votes)
@@ -309,6 +359,18 @@
       SCR.Write(path(".openais.quorum.device.net.algorithm"), 
@qdevice_algorithm)
       SCR.Write(path(".openais.quorum.device.net.tie_breaker"), 
@qdevice_tie_breaker)
 
+      if @heuristics_mode != "off"
+        SCR.Write(path(".openais.quorum.device.heuristics.mode"), 
@heuristics_mode)
+        # For @heuristics_xxx doesn't have suggested_value, skip record when 
empty?
+        SCR.Write(path(".openais.quorum.device.heuristics.timeout"), 
@heuristics_timeout)
+        SCR.Write(path(".openais.quorum.device.heuristics.sync_timeout"), 
@heuristics_sync_timeout)
+        SCR.Write(path(".openais.quorum.device.heuristics.interval"), 
@heuristics_interval)
+        executables_str = generateDictString(@heuristics_executables)
+        SCR.Write(path(".openais.quorum.device.heuristics.executables"), 
executables_str)
+      else
+        SCR.Write(path(".openais.quorum.device.heuristics"), "")
+      end
+
       nil
     end
 
@@ -686,6 +748,22 @@
       @mcastport2 = Ops.get_string(settings, "mcastport2", "")
       @autoid = Ops.get_boolean(settings, "autoid", true)
       @rrpmode = Ops.get_string(settings, "rrpmode", "")
+
+      @corosync_qdevice = settings["corosync_qdevice"] || false
+      @qdevice_model = settings["qdevice_model"] || "net"
+      @qdevice_votes = settings["qdevice_votes"] || ""
+      @qdevice_host = settings["qdevice_host"] || ""
+      @qdevice_port = settings["qdevice_port"] || "5403"
+      @qdevice_tls = settings["qdevice_tls"] || "off"
+      @qdevice_algorithm= settings["qdevice_algorithm"] || "ffsplit"
+      @qdevice_tie_breaker = settings["qdevice_tie_breaker"] || "lowest"
+
+      @heuristics_mode = settings["heuristics_mode"] || "off"
+      @heuristics_timeout = settings["heuristics_timeout"] || "5000"
+      @heuristics_sync_timeout = settings["heuristics_sync_timeout"] || "15000"
+      @heuristics_interval = settings["heuristics_interval"] || "30000"
+      @heuristics_executables = settings["heuristics_executables"] || {}
+
       @corokey = Ops.get_string(settings, "corokey", "")
       @csync2key = Ops.get_string(settings, "csync2key", "")
 
@@ -719,6 +797,22 @@
       Ops.set(result, "mcastport2", @mcastport2)
       Ops.set(result, "autoid", true)
       Ops.set(result, "rrpmode", @rrpmode)
+
+      result["corosync_qdevice"] = @corosync_qdevice
+      result["qdevice_model"] = @qdevice_model
+      result["qdevice_votes"] = @qdevice_votes
+      result["qdevice_host"] = @qdevice_host
+      result["qdevice_port"] = @qdevice_port
+      result["qdevice_tls"] = @qdevice_tls
+      result["qdevice_algorithm"] = @qdevice_algorithm
+      result["qdevice_tie_breaker"] = @qdevice_tie_breaker
+
+      result["heuristics_mode"] = @heuristics_mode
+      result["heuristics_timeout"] = @heuristics_timeout
+      result["heuristics_sync_timeout"] = @heuristics_sync_timeout
+      result["heuristics_interval"] = @heuristics_interval
+      result["heuristics_executables"] = @heuristics_executables
+
       Ops.set(result, "csync2_host", @csync2_host)
       Ops.set(result, "csync2_include", @csync2_include)
       if File.exist?("/etc/corosync/authkey")
@@ -813,6 +907,11 @@
     publish :variable => :qdevice_tls, :type => "string"
     publish :variable => :qdevice_algorithm, :type => "string"
     publish :variable => :qdevice_tie_breaker, :type => "string"
+    publish :variable => :heuristics_mode, :type => "string"
+    publish :variable => :heuristics_timeout, :type => "string"
+    publish :variable => :heuristics_sync_timeout, :type => "string"
+    publish :variable => :heuristics_interval, :type => "string"
+    publish :variable => :heuristics_executables, :type => "map <string, 
string>"
     publish :variable => :two_node, :type => "string"
     publish :variable => :config_format, :type => "string"
     publish :variable => :mcastport1, :type => "string"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/yast2-cluster-4.2.5/src/servers_non_y2/ag_openais 
new/yast2-cluster-4.2.6/src/servers_non_y2/ag_openais
--- old/yast2-cluster-4.2.5/src/servers_non_y2/ag_openais       2019-10-16 
05:12:19.000000000 +0200
+++ new/yast2-cluster-4.2.6/src/servers_non_y2/ag_openais       2020-04-28 
10:12:21.000000000 +0200
@@ -13,12 +13,12 @@
 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 #   See the GNU General Public License for more details.
-#   
+#
 #   This program is free software; you can redistribute it and/or
 #   modify it under the terms of the GNU General Public License
 #   as published by the Free Software Foundation; either version
 #   2 of the License, or (at your option) any later version.
-#   
+#
 #   You should have received a copy of the GNU General Public License
 #   along with this program; if not, write to the Free Software
 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
@@ -40,6 +40,10 @@
 
 #the option table is used to parse and write suggested_value if no options are 
read
 
+#type is used for verification of values. "string", "int" and "dict"
+#"dict" is for store the flexible key:value pair of a section. eg. 
quorum.device.heuristics.executables
+#       store in "other"(executables) dict, so doList(Dir) need to query the 
dict(executables)
+
 #default_value is used in file_parser, if input has the option, but failed to 
parse,
 #use the default_value.
 
@@ -108,9 +112,6 @@
        "ip_version":{"doc":"Specifies version of IP to use for communication. 
Value can be one of ipv4 or ipv6.", 
"type":"select[ipv4,ipv6]","default_value":"ipv4"},
 }
 
-
-
-
 interface_option_table = {
        "ringnumber":{"doc":"The ringnumber assigned to this interface 
setting", "default_value":0, "type":"int"},
        "bindnetaddr":{"doc":"Network Address to be bind for this interface 
setting", "default_value":0},
@@ -119,7 +120,6 @@
        "ttl":{"doc":"Time-to-live for cluster communication packets", 
"type":"int", "default_value":1}
        }
 
-
 #only has two rings at most
 node_option_table = {
                "ring0_addr":{"doc":"ring0 address", "type":"string", 
"default_value":"0"},
@@ -180,6 +180,14 @@
                "force_ip_version":{"doc":"Can be one of 0|4|6 and forces the 
software to use the given IP version.", "default_value":0, 
"type":"int","suggested_value":0}
                }
 
+quorum_qdevice_heuristics_option_table = {
+               "mode":{"doc":"Specifies the mode of operation of 
heuristics.","type":"string","default_value":"off","suggested_value":"off"},
+               "timeout":{"doc":"How long corosync-qdevice waits till the 
heuristics commands finish in milliseconds.","type":"int","default_value":5000},
+               "sync_timeout":{"doc":"How long corosync-qdevice waits during 
membership changes in milliseconds.","type":"int","default_value":15000},
+               "interval":{"doc":"Specifies interval between two regular 
heuristics execution in milliseconds.","type":"int","default_value":30000},
+               "executables":{"doc":"Executables. Scripts for heuristics 
check.","type":"dict","default_value":{}}
+               }
+
 qb_option_table = { "ipc_type":{"doc":"This specifies type of IPC to use",
                    "type":"string", "default_value":"native"}
                  }
@@ -200,7 +208,6 @@
        l = next(ff)
        return strip_comments_and_pending_space(l)
 
-
 def fulfill_suggested_logging_options ():
        for opt in logging_option_table.keys():
                if opt == "logger": continue
@@ -244,6 +251,13 @@
                if v == None and sv != None:
                        quorum_qdevice_options["net"][opt] = sv
 
+       if is_qdevice_heuristics_enabled():
+               for opt in ['mode', 'timeout', 'sync_timeout', 'interval']:
+                       sv = 
quorum_qdevice_heuristics_option_table[opt].get("suggested_value", None)
+                       v = quorum_qdevice_options["heuristics"].get(opt, None)
+                       if v == None and sv != None:
+                               quorum_qdevice_options["heuristics"][opt] = sv
+
 def print_quorum_qdevice_net_options(f, indent):
        '''
                indent means the level of sub-directive
@@ -254,6 +268,24 @@
                f.write("\t"*(indent+1)+"%s:\t%s\n\n" % (l, 
quorum_qdevice_options["net"][l]))
        f.write("\t"*indent+"}\n")
 
+def print_quorum_qdevice_heuristics_options(f, indent):
+       '''
+               indent means the level of sub-directive
+               sub-director of quorum.qdevice
+       '''
+       f.write("\t"*indent+"heuristics {\n")
+       for l in quorum_qdevice_options["heuristics"].keys():
+               if l != "executables":
+                       f.write("\t"*(indent+1)+"#%s\n" % 
(quorum_qdevice_heuristics_option_table[l]["doc"]))
+                       f.write("\t"*(indent+1)+"%s:\t%s\n\n" % (l, 
quorum_qdevice_options["heuristics"][l]))
+
+       f.write("\t"*(indent+1)+"#%s\n" % 
(quorum_qdevice_heuristics_option_table["executables"]["doc"]))
+       for key, value in 
quorum_qdevice_options["heuristics"]["executables"].items():
+               f.write("\t"*(indent+1)+"%s:\t%s\n" % (key, value))
+       f.write("\n")
+
+       f.write("\t"*indent+"}\n")
+
 def print_quorum_options(f):
        f.write("quorum {\n")
        for key in quorum_options.keys():
@@ -262,6 +294,9 @@
                        for l in quorum_options["device"].keys():
                                if l == "net":
                                        print_quorum_qdevice_net_options(f, 2)
+                               elif l == "heuristics":
+                                       if is_qdevice_heuristics_enabled():
+                                               
print_quorum_qdevice_heuristics_options(f, 2)
                                else:
                                        f.write("\t\t#%s\n" % 
(quorum_qdevice_option_table[l]["doc"]))
                                        f.write("\t\t%s:\t%s\n\n" % (l, 
quorum_qdevice_options[l]))
@@ -304,7 +339,15 @@
                return False
        else:
                return True
-                       
+
+def is_qdevice_heuristics_enabled():
+       if is_quorum_qdevice_configured():
+               if ("heuristics" not in quorum_qdevice_options) or \
+                               
(quorum_qdevice_options["heuristics"].get("mode", "off")) == "off":
+                       return False
+
+       return True
+
 def print_logging_options(f):
        f.write("logging {\n")
        for key in logging_options.keys():
@@ -383,11 +426,13 @@
                print(nodelist_options)
                print(qb_options)
                print(quorum_options)
-       
+
 def opt_parser(file, options, parent):
        global quorum_qdevice_options
 
        result = {}
+       others = {}
+
        i = ""
        while (i == ""):
                i = get_next_line(file)
@@ -407,12 +452,12 @@
                                members.append(opt_parser(file, 
node_option_table, "node"))
                                result["node"] = members
                        elif i.lstrip().split(" ")[0] == "device" and parent == 
"quorum":
-                               dev = result.get("device", {})
                                quorum_qdevice_options = opt_parser(file, 
quorum_qdevice_option_table, "qdevice")
                                result["device"] = quorum_qdevice_options
                        elif i.lstrip().split(" ")[0] == "net" and parent == 
"qdevice":
-                               net = result.get("net", {})
                                result["net"] = opt_parser(file, 
quorum_qdevice_net_option_table, "net")
+                       elif i.lstrip().split(" ")[0] == "heuristics" and 
parent == "qdevice":
+                               result["heuristics"] = opt_parser(file, 
quorum_qdevice_heuristics_option_table, "heuristics")
                        # member of interface is removed in the latest version
                        elif i.lstrip().split(" ")[0] == "member" and parent == 
"interface":
                                oldmembers = result.get("oldlist", [])
@@ -422,31 +467,42 @@
                                #y2warning("Unknown sub-directive %s found. 
Ignore it" % (i.lstrip().split(" ")[0]))
                                while (i[-1] != "}"):
                                        i = get_next_line(file)
-                               
+
                        i = get_next_line(file)
                        while ( i == ""):
                                i = get_next_line(file)
                        continue
-               
+
                tmp_opt = i.split(":")
                # In case string like IPv6 have ":" in value
-               opt = [tmp_opt[0], ":".join(tmp_opt[1:])]
+               opt = [tmp_opt[0].strip(), ":".join(tmp_opt[1:]).strip()]
                #opt = i.split(":")
 
                try:
-                       doc = options[opt[0].strip()]["doc"]
+                       doc = options[opt[0]]["doc"]
                except KeyError:
-                       #y2warning("Unknown options %s"%opt[0].strip())
-                       pass
+                       #y2warning("Unknown options %s"%opt[0])
+
+                       # If the keys of options have "dict"
+                       # Then set to the "dict" as key:value pair of it
+                       if not len(others):
+                               for key in options.keys():
+                                       if options[key].get("type", "string") 
== "dict":
+                                               result[key] = others
+
+                       others[opt[0]] = opt[1]
+
                else:
-                       if options[opt[0].strip()].get("type", "string") == 
"int":
+                       #Available types: "string", "int".
+                       if options[opt[0]].get("type", "string") == "int":
                                try:
-                                       result[opt[0].strip()] = 
int(opt[1].strip())
+                                       result[opt[0]] = int(opt[1])
                                except ValueError:
-                                       #y2warning("Invalid option %s found, 
default to %s" % (opt[0].strip(), options[opt[0].strip()]["default_value"]))
-                                       result[opt[0].strip()] = 
options[opt[0].strip()]["default_value"]
+                                       #y2warning("Invalid option %s found, 
default to %s" % (opt[0], options[opt[0]]["default_value"]))
+                                       result[opt[0]] = 
options[opt[0]]["default_value"]
                        else:
-                               result[opt[0].strip()] = opt[1].strip()
+                               result[opt[0]] = opt[1]
+
                i = ""
                while (i == ""):
                        i = get_next_line(file)
@@ -509,7 +565,7 @@
                if totem_options["interface"][i]["ringnumber"] == k:
                        del totem_options["interface"][i]
                        break
-               
+
 # obsolete setting. BNC-879596, pop up a message if config file is in old 
format.
 def check_conf_format():
        for x in range(len(totem_options["interface"])):
@@ -535,17 +591,26 @@
                except:
                        pass
 
+def safe_return_str(obj):
+       '''
+               return a str with all '"' replaced to '\"'
+               necessary when value may include '"'
+               For example:
+                 "nick"123l"nick" => "nick\"123l\"nick"
+                 "{'exec_ping': 'ping -q -c 1 "127.3.1.1"'}" => "{'exec_ping': 
'ping -q -c 1 \"127.3.1.1\"'}"
+       '''
+       return str(obj).replace('"', '\\"')
 
 class OpenAISConf_Parser(object):
        def __init__(self):
                load_ais_conf("/etc/corosync/corosync.conf")
-    
+
        def doList(self, path):
                #remove the leading dot
                path_arr = path
                if len(path_arr) == 0:
                        return '["allconfs"]'
-       
+
                if path_arr[0] == 'allconfs' and len(path_arr) == 1:
                        return '["quorum", "totem", "nodelist"]'
                elif path_arr[0] == 'totem':
@@ -583,11 +648,13 @@
                                return '["model", "timeout", "sync_timeout", 
"net", "votes"]'
                        elif len(path_arr) == 3 and path_arr[2] == "net":
                                return '["host", "port", "tls", "algorithm", 
"tie_breaker", "connect_timeout", "force_ip_version"]'
+                       elif len(path_arr) == 3 and path_arr[2] == "heuristics":
+                               return '["mode", "timeout", "sync_timeout", 
"interval", "executables"]'
                        else:
                                return '[]'
                else:
                        return 'nil\n'
-       
+
        def doRead(self, path):
                if path[0] == "":
                        return "nil\n"
@@ -604,6 +671,13 @@
                                path[1] == "device" and path[2] in 
quorum_qdevice_option_table.keys():
                                return '"%s"' % 
quorum_options["device"].get(path[2],
                                        
quorum_qdevice_option_table[path[2]]["default_value"])
+                       elif is_quorum_qdevice_configured() and 
is_qdevice_heuristics_enabled() and \
+                               len(path) == 4 and path[1] == "device" and 
path[2] == "heuristics":
+                               # "executables" will return a dict in str
+                               # eg. "{'exec_check': '/tmp/check.sh', 
'exec_ping': 'ping -q -c 1 \"127.0.0.1\"'}"
+                               if path[3] in 
quorum_qdevice_heuristics_option_table.keys():
+                                       return '"%s"' % 
safe_return_str(quorum_qdevice_options["heuristics"].get(path[3],
+                                               
quorum_qdevice_heuristics_option_table[path[3]]["default_value"]))
                        elif is_quorum_qdevice_configured() and len(path) == 4 
and \
                                path[1] == "device" and path[2] == "net" and \
                                path[3] in 
quorum_qdevice_net_option_table.keys():
@@ -744,6 +818,31 @@
                                        quorum_options["device"] = 
quorum_qdevice_options
                                quorum_qdevice_options["net"][path[3]] = args
                                return "true"
+                       elif len(path) == 3 and path[1] == "device" and \
+                               path[2] == "heuristics" and args == "":
+                               # May no "device" in quorum_options,
+                               # cause reset in file_parser
+                               if "device" in quorum_options and "heuristics" 
in quorum_options["device"]:
+                                       
del(quorum_options["device"]["heuristics"])
+                                       quorum_qdevice_options["heuristics"] = 
{}
+                               return "true"
+                       elif len(path) == 4 and path[1] == "device" and path[2] 
== "heuristics":
+                               if "heuristics" not in quorum_qdevice_options:
+                                       quorum_qdevice_options["heuristics"] = 
{"executables": {}}
+                                       quorum_options["device"] = 
quorum_qdevice_options
+
+                               if path[3] == "executables":
+                                       # always replaced by new dict
+                                       
quorum_qdevice_options["heuristics"]["executables"] = {}
+                                       # args example (str type):
+                                       # {'exec_testing': '/tmp/testing.sh', 
'exec_ping': 'ping -q -c 1 "127.0.0.1"'}
+                                       executables_dict = eval(args)
+                                       for key, value in 
executables_dict.items():
+                                               
quorum_qdevice_options["heuristics"]["executables"][key] = value
+                               elif path[3] in 
quorum_qdevice_heuristics_option_table.keys():
+                                       # key == executables run in the 
previous condition
+                                       
quorum_qdevice_options["heuristics"][path[3]] = args
+                               return "true"
                        else:
                                return "false"
                elif path[0] == "totem":
@@ -803,7 +902,7 @@
                                                        i = get_interface(1)
                                        else:
                                                i = None
-                                               
+
                                        if i != None:
                                                if path[3] == "bindnetaddr":
                                                        i["bindnetaddr"] = args
@@ -944,6 +1043,6 @@
                except:
                        break
 # <-- main
-    
+
 if __name__ == "__main__":
        main_entry()


Reply via email to