From 1a486023ad314e36fbcbf24af66f0cb1b48a1518 Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Tue, 9 May 2023 11:26:25 +0000 Subject: [PATCH] import pcs-0.11.4-7.el9_2 --- .gitignore | 6 +- .pcs.metadata | 6 +- ...97-01-fix-pcs-config-checkpoint-diff.patch | 121 +++ ...80704-01-fix-pcs-stonith-update-scsi.patch | 975 ++++++++++++++++++ ...180-01-fix-loading-with-fence-levels.patch | 89 ++ SPECS/pcs.spec | 22 +- 6 files changed, 1209 insertions(+), 10 deletions(-) create mode 100644 SOURCES/bz2180697-01-fix-pcs-config-checkpoint-diff.patch create mode 100644 SOURCES/bz2180704-01-fix-pcs-stonith-update-scsi.patch create mode 100644 SOURCES/bz2183180-01-fix-loading-with-fence-levels.patch diff --git a/.gitignore b/.gitignore index c9d343c..b0f1c26 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,10 @@ SOURCES/eventmachine-1.2.7.gem SOURCES/ffi-1.15.5.gem SOURCES/mustermann-3.0.0.gem SOURCES/pcs-0.11.4.tar.gz -SOURCES/pcs-web-ui-0.1.16.tar.gz -SOURCES/pcs-web-ui-node-modules-0.1.16.tar.xz +SOURCES/pcs-web-ui-0.1.16.1.tar.gz +SOURCES/pcs-web-ui-node-modules-0.1.16.1.tar.xz SOURCES/pyagentx-0.4.pcs.2.tar.gz -SOURCES/rack-2.2.5.gem +SOURCES/rack-2.2.6.4.gem SOURCES/rack-protection-3.0.5.gem SOURCES/rack-test-2.0.2.gem SOURCES/ruby2_keywords-0.0.5.gem diff --git a/.pcs.metadata b/.pcs.metadata index 90cd2c4..874f9be 100644 --- a/.pcs.metadata +++ b/.pcs.metadata @@ -7,10 +7,10 @@ 97632b7975067266c0b39596de0a4c86d9330658 SOURCES/ffi-1.15.5.gem e892678aaf02ccb27f3a6cd58482cda00aea6ce8 SOURCES/mustermann-3.0.0.gem b7aecf2f71777395b2b3bb79012de3e658383d4e SOURCES/pcs-0.11.4.tar.gz -62565f6f573d40a733662f5b9274caa4d275b0de SOURCES/pcs-web-ui-0.1.16.tar.gz -c079fc5427f91afcefec34c3dc5597eba916effc SOURCES/pcs-web-ui-node-modules-0.1.16.tar.xz +dba53fa53eb99770f9633fb15f19335fdb2530e2 SOURCES/pcs-web-ui-0.1.16.1.tar.gz +9a8a94313975247239df63f2842d17f9a526dd3f SOURCES/pcs-web-ui-node-modules-0.1.16.1.tar.xz 3176b2f2b332c2b6bf79fe882e83feecf3d3f011 SOURCES/pyagentx-0.4.pcs.2.tar.gz -3ad7b27b68d5dd893ce91f216bb2685ae6c9846a SOURCES/rack-2.2.5.gem +bbaa023e07bdc4143c5dd18d752c2543f254666f SOURCES/rack-2.2.6.4.gem b311f9d60fc3ac0e20078a5aca7c51efa404727c SOURCES/rack-protection-3.0.5.gem 3c669527ecbcb9f915a83983ec89320c356e1fe3 SOURCES/rack-test-2.0.2.gem d017b9e4d1978e0b3ccc3e2a31493809e4693cd3 SOURCES/ruby2_keywords-0.0.5.gem diff --git a/SOURCES/bz2180697-01-fix-pcs-config-checkpoint-diff.patch b/SOURCES/bz2180697-01-fix-pcs-config-checkpoint-diff.patch new file mode 100644 index 0000000..9127d8a --- /dev/null +++ b/SOURCES/bz2180697-01-fix-pcs-config-checkpoint-diff.patch @@ -0,0 +1,121 @@ +From d1a658601487175ec70054e56ade116f3dbcecf6 Mon Sep 17 00:00:00 2001 +From: Miroslav Lisik +Date: Mon, 6 Mar 2023 15:42:35 +0100 +Subject: [PATCH 1/2] fix `pcs config checkpoint diff` command + +--- + CHANGELOG.md | 26 -------------------------- + pcs/cli/common/lib_wrapper.py | 15 +-------------- + pcs/config.py | 3 +++ + 3 files changed, 4 insertions(+), 40 deletions(-) + +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 0945d727..7d3d606b 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -1,31 +1,5 @@ + # Change Log + +-## [Unreleased] +- +-### Added +-- Warning to `pcs resource|stonith update` commands about not using agent +- self-validation feature when the resource is already misconfigured +- ([rhbz#2151524]) +- +-### Fixed +-- Graceful stopping pcsd service using `systemctl stop pcsd` command +-- Displaying bool and integer values in `pcs resource config` command +- ([rhbz#2151164], [ghissue#604]) +-- Allow time values in stonith-watchdog-time property ([rhbz#2158790]) +- +-### Changed +-- Resource/stonith agent self-validation of instance attributes is now +- disabled by default, as many agents do not work with it properly. +- Use flag '--agent-validation' to enable it in supported commands. +- ([rhbz#2159454]) +- +-[ghissue#604]: https://github.com/ClusterLabs/pcs/issues/604 +-[rhbz#2151164]: https://bugzilla.redhat.com/show_bug.cgi?id=2151164 +-[rhbz#2151524]: https://bugzilla.redhat.com/show_bug.cgi?id=2151524 +-[rhbz#2159454]: https://bugzilla.redhat.com/show_bug.cgi?id=2159454 +-[rhbz#2158790]: https://bugzilla.redhat.com/show_bug.cgi?id=2158790 +- +- + ## [0.11.4] - 2022-11-21 + + ### Security +diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py +index 217bfe3e..e6411e3c 100644 +--- a/pcs/cli/common/lib_wrapper.py ++++ b/pcs/cli/common/lib_wrapper.py +@@ -1,9 +1,5 @@ + import logging + from collections import namedtuple +-from typing import ( +- Any, +- Dict, +-) + + from pcs import settings + from pcs.cli.common import middleware +@@ -36,9 +32,6 @@ from pcs.lib.commands.constraint import order as constraint_order + from pcs.lib.commands.constraint import ticket as constraint_ticket + from pcs.lib.env import LibraryEnvironment + +-# Note: not properly typed +-_CACHE: Dict[Any, Any] = {} +- + + def wrapper(dictionary): + return namedtuple("wrapper", dictionary.keys())(**dictionary) +@@ -106,12 +99,6 @@ def bind_all(env, run_with_middleware, dictionary): + ) + + +-def get_module(env, middleware_factory, name): +- if name not in _CACHE: +- _CACHE[name] = load_module(env, middleware_factory, name) +- return _CACHE[name] +- +- + def load_module(env, middleware_factory, name): + # pylint: disable=too-many-return-statements, too-many-branches + if name == "acl": +@@ -544,4 +531,4 @@ class Library: + self.middleware_factory = middleware_factory + + def __getattr__(self, name): +- return get_module(self.env, self.middleware_factory, name) ++ return load_module(self.env, self.middleware_factory, name) +diff --git a/pcs/config.py b/pcs/config.py +index e0d179f0..6da1151b 100644 +--- a/pcs/config.py ++++ b/pcs/config.py +@@ -691,6 +691,7 @@ def _checkpoint_to_lines(lib, checkpoint_number): + orig_usefile = utils.usefile + orig_filename = utils.filename + orig_middleware = lib.middleware_factory ++ orig_env = lib.env + # configure old code to read the CIB from a file + utils.usefile = True + utils.filename = os.path.join( +@@ -700,6 +701,7 @@ def _checkpoint_to_lines(lib, checkpoint_number): + lib.middleware_factory = orig_middleware._replace( + cib=middleware.cib(utils.filename, utils.touch_cib_file) + ) ++ lib.env = utils.get_cli_env() + # export the CIB to text + result = False, [] + if os.path.isfile(utils.filename): +@@ -708,6 +710,7 @@ def _checkpoint_to_lines(lib, checkpoint_number): + utils.usefile = orig_usefile + utils.filename = orig_filename + lib.middleware_factory = orig_middleware ++ lib.env = orig_env + return result + + +-- +2.39.2 + diff --git a/SOURCES/bz2180704-01-fix-pcs-stonith-update-scsi.patch b/SOURCES/bz2180704-01-fix-pcs-stonith-update-scsi.patch new file mode 100644 index 0000000..a7fe08b --- /dev/null +++ b/SOURCES/bz2180704-01-fix-pcs-stonith-update-scsi.patch @@ -0,0 +1,975 @@ +From 6841064bf1d06e16c9c5bf9a6ab42b3185d55afb Mon Sep 17 00:00:00 2001 +From: Miroslav Lisik +Date: Mon, 20 Mar 2023 10:35:34 +0100 +Subject: [PATCH 2/2] fix `pcs stonith update-scsi-devices` command + +--- + pcs/lib/cib/resource/stonith.py | 168 +++++- + .../test_stonith_update_scsi_devices.py | 571 ++++++++++++++---- + 2 files changed, 601 insertions(+), 138 deletions(-) + +diff --git a/pcs/lib/cib/resource/stonith.py b/pcs/lib/cib/resource/stonith.py +index 1f5bddff..07cffba6 100644 +--- a/pcs/lib/cib/resource/stonith.py ++++ b/pcs/lib/cib/resource/stonith.py +@@ -169,12 +169,64 @@ def get_node_key_map_for_mpath( + return node_key_map + + +-DIGEST_ATTRS = ["op-digest", "op-secure-digest", "op-restart-digest"] +-DIGEST_ATTR_TO_TYPE_MAP = { ++DIGEST_ATTR_TO_DIGEST_TYPE_MAP = { + "op-digest": "all", + "op-secure-digest": "nonprivate", + "op-restart-digest": "nonreloadable", + } ++TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP = { ++ "#digests-all": "all", ++ "#digests-secure": "nonprivate", ++} ++DIGEST_ATTRS = frozenset(DIGEST_ATTR_TO_DIGEST_TYPE_MAP.keys()) ++TRANSIENT_DIGEST_ATTRS = frozenset( ++ TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP.keys() ++) ++ ++ ++def _get_digest( ++ attr: str, ++ attr_to_type_map: Dict[str, str], ++ calculated_digests: Dict[str, Optional[str]], ++) -> str: ++ """ ++ Return digest of right type for the specified attribute. If missing, raise ++ an error. ++ ++ attr -- name of digest attribute ++ atttr_to_type_map -- map for attribute name to digest type conversion ++ calculated_digests -- digests calculated by pacemaker ++ """ ++ if attr not in attr_to_type_map: ++ raise AssertionError( ++ f"Key '{attr}' is missing in the attribute name to digest type map" ++ ) ++ digest = calculated_digests.get(attr_to_type_map[attr]) ++ if digest is None: ++ # this should not happen and when it does it is pacemaker fault ++ raise LibraryError( ++ ReportItem.error( ++ reports.messages.StonithRestartlessUpdateUnableToPerform( ++ f"necessary digest for '{attr}' attribute is missing" ++ ) ++ ) ++ ) ++ return digest ++ ++ ++def _get_transient_instance_attributes(cib: _Element) -> List[_Element]: ++ """ ++ Return list of instance_attributes elements which could contain digest ++ attributes. ++ ++ cib -- CIB root element ++ """ ++ return cast( ++ List[_Element], ++ cib.xpath( ++ "./status/node_state/transient_attributes/instance_attributes" ++ ), ++ ) + + + def _get_lrm_rsc_op_elements( +@@ -278,21 +330,89 @@ def _update_digest_attrs_in_lrm_rsc_op( + ) + ) + for attr in common_digests_attrs: +- new_digest = calculated_digests[DIGEST_ATTR_TO_TYPE_MAP[attr]] +- if new_digest is None: +- # this should not happen and when it does it is pacemaker fault ++ # update digest in cib ++ lrm_rsc_op.attrib[attr] = _get_digest( ++ attr, DIGEST_ATTR_TO_DIGEST_TYPE_MAP, calculated_digests ++ ) ++ ++ ++def _get_transient_digest_value( ++ old_value: str, stonith_id: str, stonith_type: str, digest: str ++) -> str: ++ """ ++ Return transient digest value with replaced digest. ++ ++ Value has comma separated format: ++ ::,... ++ ++ and we need to replace only digest for our currently updated stonith device. ++ ++ old_value -- value to be replaced ++ stonith_id -- id of stonith resource ++ stonith_type -- stonith resource type ++ digest -- digest for new value ++ """ ++ new_comma_values_list = [] ++ for comma_value in old_value.split(","): ++ if comma_value: ++ try: ++ _id, _type, _ = comma_value.split(":") ++ except ValueError as e: ++ raise LibraryError( ++ ReportItem.error( ++ reports.messages.StonithRestartlessUpdateUnableToPerform( ++ f"invalid digest attribute value: '{old_value}'" ++ ) ++ ) ++ ) from e ++ if _id == stonith_id and _type == stonith_type: ++ comma_value = ":".join([stonith_id, stonith_type, digest]) ++ new_comma_values_list.append(comma_value) ++ return ",".join(new_comma_values_list) ++ ++ ++def _update_digest_attrs_in_transient_instance_attributes( ++ nvset_el: _Element, ++ stonith_id: str, ++ stonith_type: str, ++ calculated_digests: Dict[str, Optional[str]], ++) -> None: ++ """ ++ Update digests attributes in transient instance attributes element. ++ ++ nvset_el -- instance_attributes element containing nvpairs with digests ++ attributes ++ stonith_id -- id of stonith resource being updated ++ stonith_type -- type of stonith resource being updated ++ calculated_digests -- digests calculated by pacemaker ++ """ ++ for attr in TRANSIENT_DIGEST_ATTRS: ++ nvpair_list = cast( ++ List[_Element], ++ nvset_el.xpath("./nvpair[@name=$name]", name=attr), ++ ) ++ if not nvpair_list: ++ continue ++ if len(nvpair_list) > 1: + raise LibraryError( + ReportItem.error( + reports.messages.StonithRestartlessUpdateUnableToPerform( +- ( +- f"necessary digest for '{attr}' attribute is " +- "missing" +- ) ++ f"multiple digests attributes: '{attr}'" + ) + ) + ) +- # update digest in cib +- lrm_rsc_op.attrib[attr] = new_digest ++ old_value = nvpair_list[0].attrib["value"] ++ if old_value: ++ nvpair_list[0].attrib["value"] = _get_transient_digest_value( ++ str(old_value), ++ stonith_id, ++ stonith_type, ++ _get_digest( ++ attr, ++ TRANSIENT_DIGEST_ATTR_TO_DIGEST_TYPE_MAP, ++ calculated_digests, ++ ), ++ ) + + + def update_scsi_devices_without_restart( +@@ -311,6 +431,8 @@ def update_scsi_devices_without_restart( + id_provider -- elements' ids generator + device_list -- list of updated scsi devices + """ ++ # pylint: disable=too-many-locals ++ cib = get_root(resource_el) + resource_id = resource_el.get("id", "") + roles_with_nodes = get_resource_state(cluster_state, resource_id) + if "Started" not in roles_with_nodes: +@@ -341,17 +463,14 @@ def update_scsi_devices_without_restart( + ) + + lrm_rsc_op_start_list = _get_lrm_rsc_op_elements( +- get_root(resource_el), resource_id, node_name, "start" ++ cib, resource_id, node_name, "start" ++ ) ++ new_instance_attrs_digests = get_resource_digests( ++ runner, resource_id, node_name, new_instance_attrs + ) + if len(lrm_rsc_op_start_list) == 1: + _update_digest_attrs_in_lrm_rsc_op( +- lrm_rsc_op_start_list[0], +- get_resource_digests( +- runner, +- resource_id, +- node_name, +- new_instance_attrs, +- ), ++ lrm_rsc_op_start_list[0], new_instance_attrs_digests + ) + else: + raise LibraryError( +@@ -364,7 +483,7 @@ def update_scsi_devices_without_restart( + + monitor_attrs_list = _get_monitor_attrs(resource_el) + lrm_rsc_op_monitor_list = _get_lrm_rsc_op_elements( +- get_root(resource_el), resource_id, node_name, "monitor" ++ cib, resource_id, node_name, "monitor" + ) + if len(lrm_rsc_op_monitor_list) != len(monitor_attrs_list): + raise LibraryError( +@@ -380,7 +499,7 @@ def update_scsi_devices_without_restart( + + for monitor_attrs in monitor_attrs_list: + lrm_rsc_op_list = _get_lrm_rsc_op_elements( +- get_root(resource_el), ++ cib, + resource_id, + node_name, + "monitor", +@@ -409,3 +528,10 @@ def update_scsi_devices_without_restart( + ) + ) + ) ++ for nvset_el in _get_transient_instance_attributes(cib): ++ _update_digest_attrs_in_transient_instance_attributes( ++ nvset_el, ++ resource_id, ++ resource_el.get("type", ""), ++ new_instance_attrs_digests, ++ ) +diff --git a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py +index 69ea097c..72c7dbcf 100644 +--- a/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py ++++ b/pcs_test/tier0/lib/commands/test_stonith_update_scsi_devices.py +@@ -38,6 +38,7 @@ DEFAULT_DIGEST = _DIGEST + "0" + ALL_DIGEST = _DIGEST + "1" + NONPRIVATE_DIGEST = _DIGEST + "2" + NONRELOADABLE_DIGEST = _DIGEST + "3" ++DIGEST_ATTR_VALUE_GOOD_FORMAT = f"stonith_id:stonith_type:{DEFAULT_DIGEST}," + DEV_1 = "/dev/sda" + DEV_2 = "/dev/sdb" + DEV_3 = "/dev/sdc" +@@ -151,33 +152,58 @@ def _fixture_lrm_rsc_start_ops(resource_id, lrm_start_ops): + return _fixture_lrm_rsc_ops("start", resource_id, lrm_start_ops) + + +-def _fixture_status_lrm_ops_base( +- resource_id, +- resource_type, +- lrm_ops, +-): ++def _fixture_status_lrm_ops(resource_id, resource_type, lrm_ops): + return f""" +- +- +- +- +- +- {lrm_ops} +- +- +- +- +- ++ ++ ++ ++ {lrm_ops} ++ ++ ++ ++ """ ++ ++ ++def _fixture_digest_nvpair(node_id, digest_name, digest_value): ++ return ( ++ f'' ++ ) ++ ++ ++def _fixture_transient_attributes(node_id, digests_nvpairs): ++ return f""" ++ ++ ++ ++ ++ {digests_nvpairs} ++ ++ ++ """ ++ ++ ++def _fixture_node_state(node_id, lrm_ops=None, transient_attrs=None): ++ if transient_attrs is None: ++ transient_attrs = "" ++ if lrm_ops is None: ++ lrm_ops = "" ++ return f""" ++ ++ {lrm_ops} ++ {transient_attrs} ++ + """ + + +-def _fixture_status_lrm_ops( ++def _fixture_status( + resource_id, + resource_type, + lrm_start_ops=DEFAULT_LRM_START_OPS, + lrm_monitor_ops=DEFAULT_LRM_MONITOR_OPS, ++ digests_attrs_list=None, + ): +- return _fixture_status_lrm_ops_base( ++ lrm_ops = _fixture_status_lrm_ops( + resource_id, + resource_type, + "\n".join( +@@ -185,18 +211,52 @@ def _fixture_status_lrm_ops( + + _fixture_lrm_rsc_monitor_ops(resource_id, lrm_monitor_ops) + ), + ) ++ node_states_list = [] ++ if not digests_attrs_list: ++ node_states_list.append( ++ _fixture_node_state("1", lrm_ops, transient_attrs=None) ++ ) ++ else: ++ for node_id, digests_attrs in enumerate(digests_attrs_list, start=1): ++ transient_attrs = _fixture_transient_attributes( ++ node_id, ++ "\n".join( ++ _fixture_digest_nvpair(node_id, name, value) ++ for name, value in digests_attrs ++ ), ++ ) ++ node_state = _fixture_node_state( ++ node_id, ++ lrm_ops=lrm_ops if node_id == 1 else None, ++ transient_attrs=transient_attrs, ++ ) ++ node_states_list.append(node_state) ++ node_states = "\n".join(node_states_list) ++ return f""" ++ ++ {node_states} ++ ++ """ ++ + ++def fixture_digests_xml(resource_id, node_name, devices="", nonprivate=True): ++ nonprivate_xml = ( ++ f""" ++ ++ ++ ++ """ ++ if nonprivate ++ else "" ++ ) + +-def fixture_digests_xml(resource_id, node_name, devices=""): + return f""" + + + + + +- +- +- ++ {nonprivate_xml} + + + +@@ -334,6 +394,8 @@ class UpdateScsiDevicesMixin: + nodes_running_on=1, + start_digests=True, + monitor_digests=True, ++ digests_attrs_list=None, ++ crm_digests_xml=None, + ): + # pylint: disable=too-many-arguments + # pylint: disable=too-many-locals +@@ -346,11 +408,12 @@ class UpdateScsiDevicesMixin: + resource_ops=resource_ops, + host_map=host_map, + ), +- status=_fixture_status_lrm_ops( ++ status=_fixture_status( + self.stonith_id, + self.stonith_type, + lrm_start_ops=lrm_start_ops, + lrm_monitor_ops=lrm_monitor_ops, ++ digests_attrs_list=digests_attrs_list, + ), + ) + self.config.runner.pcmk.is_resource_digests_supported() +@@ -363,14 +426,17 @@ class UpdateScsiDevicesMixin: + nodes=FIXTURE_CRM_MON_NODES, + ) + devices_opt = "devices={}".format(devices_value) ++ ++ if crm_digests_xml is None: ++ crm_digests_xml = fixture_digests_xml( ++ self.stonith_id, SCSI_NODE, devices=devices_value ++ ) + if start_digests: + self.config.runner.pcmk.resource_digests( + self.stonith_id, + SCSI_NODE, + name="start.op.digests", +- stdout=fixture_digests_xml( +- self.stonith_id, SCSI_NODE, devices=devices_value +- ), ++ stdout=crm_digests_xml, + args=[devices_opt], + ) + if monitor_digests: +@@ -394,11 +460,7 @@ class UpdateScsiDevicesMixin: + self.stonith_id, + SCSI_NODE, + name=f"{name}-{num}.op.digests", +- stdout=fixture_digests_xml( +- self.stonith_id, +- SCSI_NODE, +- devices=devices_value, +- ), ++ stdout=crm_digests_xml, + args=args, + ) + +@@ -406,14 +468,16 @@ class UpdateScsiDevicesMixin: + self, + devices_before=DEVICES_1, + devices_updated=DEVICES_2, +- devices_add=(), +- devices_remove=(), ++ devices_add=None, ++ devices_remove=None, + unfence=None, + resource_ops=DEFAULT_OPS, + lrm_monitor_ops=DEFAULT_LRM_MONITOR_OPS, + lrm_start_ops=DEFAULT_LRM_START_OPS, + lrm_monitor_ops_updated=DEFAULT_LRM_MONITOR_OPS_UPDATED, + lrm_start_ops_updated=DEFAULT_LRM_START_OPS_UPDATED, ++ digests_attrs_list=None, ++ digests_attrs_list_updated=None, + ): + # pylint: disable=too-many-arguments + self.config_cib( +@@ -422,6 +486,7 @@ class UpdateScsiDevicesMixin: + resource_ops=resource_ops, + lrm_monitor_ops=lrm_monitor_ops, + lrm_start_ops=lrm_start_ops, ++ digests_attrs_list=digests_attrs_list, + ) + if unfence: + self.config.corosync_conf.load_content( +@@ -445,20 +510,34 @@ class UpdateScsiDevicesMixin: + devices=devices_updated, + resource_ops=resource_ops, + ), +- status=_fixture_status_lrm_ops( ++ status=_fixture_status( + self.stonith_id, + self.stonith_type, + lrm_start_ops=lrm_start_ops_updated, + lrm_monitor_ops=lrm_monitor_ops_updated, ++ digests_attrs_list=digests_attrs_list_updated, + ), + ) +- self.command( +- devices_updated=devices_updated, +- devices_add=devices_add, +- devices_remove=devices_remove, +- )() ++ kwargs = dict(devices_updated=devices_updated) ++ if devices_add is not None: ++ kwargs["devices_add"] = devices_add ++ if devices_remove is not None: ++ kwargs["devices_remove"] = devices_remove ++ self.command(**kwargs)() + self.env_assist.assert_reports([]) + ++ def digest_attr_value_single(self, digest, last_comma=True): ++ comma = "," if last_comma else "" ++ return f"{self.stonith_id}:{self.stonith_type}:{digest}{comma}" ++ ++ def digest_attr_value_multiple(self, digest, last_comma=True): ++ if self.stonith_type == STONITH_TYPE_SCSI: ++ value = f"{STONITH_ID_MPATH}:{STONITH_TYPE_MPATH}:{DEFAULT_DIGEST}," ++ else: ++ value = f"{STONITH_ID_SCSI}:{STONITH_TYPE_SCSI}:{DEFAULT_DIGEST}," ++ ++ return f"{value}{self.digest_attr_value_single(digest, last_comma=last_comma)}" ++ + + class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): + def test_pcmk_doesnt_support_digests(self): +@@ -567,9 +646,7 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): + ) + + def test_no_lrm_start_op(self): +- self.config_cib( +- lrm_start_ops=(), start_digests=False, monitor_digests=False +- ) ++ self.config_cib(lrm_start_ops=(), monitor_digests=False) + self.env_assist.assert_raise_library_error( + self.command(), + [ +@@ -622,6 +699,59 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): + expected_in_processor=False, + ) + ++ def test_crm_resource_digests_missing_for_transient_digests_attrs(self): ++ self.config_cib( ++ digests_attrs_list=[ ++ [ ++ ( ++ "digests-secure", ++ self.digest_attr_value_single(ALL_DIGEST), ++ ), ++ ], ++ ], ++ crm_digests_xml=fixture_digests_xml( ++ self.stonith_id, SCSI_NODE, devices="", nonprivate=False ++ ), ++ ) ++ self.env_assist.assert_raise_library_error( ++ self.command(), ++ [ ++ fixture.error( ++ reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM, ++ reason=( ++ "necessary digest for '#digests-secure' attribute is " ++ "missing" ++ ), ++ reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER, ++ ) ++ ], ++ expected_in_processor=False, ++ ) ++ ++ def test_multiple_digests_attributes(self): ++ self.config_cib( ++ digests_attrs_list=[ ++ 2 ++ * [ ++ ( ++ "digests-all", ++ self.digest_attr_value_single(DEFAULT_DIGEST), ++ ), ++ ], ++ ], ++ ) ++ self.env_assist.assert_raise_library_error( ++ self.command(), ++ [ ++ fixture.error( ++ reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM, ++ reason=("multiple digests attributes: '#digests-all'"), ++ reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER, ++ ) ++ ], ++ expected_in_processor=False, ++ ) ++ + def test_monitor_ops_and_lrm_monitor_ops_do_not_match(self): + self.config_cib( + resource_ops=( +@@ -812,7 +942,7 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): + stonith_type=self.stonith_type, + devices=DEVICES_2, + ), +- status=_fixture_status_lrm_ops( ++ status=_fixture_status( + self.stonith_id, + self.stonith_type, + lrm_start_ops=DEFAULT_LRM_START_OPS_UPDATED, +@@ -959,6 +1089,28 @@ class UpdateScsiDevicesFailuresMixin(UpdateScsiDevicesMixin): + ] + ) + ++ def test_transient_digests_attrs_bad_value_format(self): ++ bad_format = f"{DIGEST_ATTR_VALUE_GOOD_FORMAT}id:type," ++ self.config_cib( ++ digests_attrs_list=[ ++ [ ++ ("digests-all", DIGEST_ATTR_VALUE_GOOD_FORMAT), ++ ("digests-secure", bad_format), ++ ] ++ ] ++ ) ++ self.env_assist.assert_raise_library_error( ++ self.command(), ++ [ ++ fixture.error( ++ reports.codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM, ++ reason=f"invalid digest attribute value: '{bad_format}'", ++ reason_type=reports.const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER, ++ ) ++ ], ++ expected_in_processor=False, ++ ) ++ + + class UpdateScsiDevicesSetBase(UpdateScsiDevicesMixin, CommandSetMixin): + def test_update_1_to_1_devices(self): +@@ -1002,80 +1154,6 @@ class UpdateScsiDevicesSetBase(UpdateScsiDevicesMixin, CommandSetMixin): + unfence=[DEV_3, DEV_4], + ) + +- def test_default_monitor(self): +- self.assert_command_success(unfence=[DEV_2]) +- +- def test_no_monitor_ops(self): +- self.assert_command_success( +- unfence=[DEV_2], +- resource_ops=(), +- lrm_monitor_ops=(), +- lrm_monitor_ops_updated=(), +- ) +- +- def test_1_monitor_with_timeout(self): +- self.assert_command_success( +- unfence=[DEV_2], +- resource_ops=(("monitor", "30s", "10s", None),), +- lrm_monitor_ops=(("30000", DEFAULT_DIGEST, None, None),), +- lrm_monitor_ops_updated=(("30000", ALL_DIGEST, None, None),), +- ) +- +- def test_2_monitor_ops_with_timeouts(self): +- self.assert_command_success( +- unfence=[DEV_2], +- resource_ops=( +- ("monitor", "30s", "10s", None), +- ("monitor", "40s", "20s", None), +- ), +- lrm_monitor_ops=( +- ("30000", DEFAULT_DIGEST, None, None), +- ("40000", DEFAULT_DIGEST, None, None), +- ), +- lrm_monitor_ops_updated=( +- ("30000", ALL_DIGEST, None, None), +- ("40000", ALL_DIGEST, None, None), +- ), +- ) +- +- def test_2_monitor_ops_with_one_timeout(self): +- self.assert_command_success( +- unfence=[DEV_2], +- resource_ops=( +- ("monitor", "30s", "10s", None), +- ("monitor", "60s", None, None), +- ), +- lrm_monitor_ops=( +- ("30000", DEFAULT_DIGEST, None, None), +- ("60000", DEFAULT_DIGEST, None, None), +- ), +- lrm_monitor_ops_updated=( +- ("30000", ALL_DIGEST, None, None), +- ("60000", ALL_DIGEST, None, None), +- ), +- ) +- +- def test_various_start_ops_one_lrm_start_op(self): +- self.assert_command_success( +- unfence=[DEV_2], +- resource_ops=( +- ("monitor", "60s", None, None), +- ("start", "0s", "40s", None), +- ("start", "0s", "30s", "1"), +- ("start", "10s", "5s", None), +- ("start", "20s", None, None), +- ), +- ) +- +- def test_1_nonrecurring_start_op_with_timeout(self): +- self.assert_command_success( +- unfence=[DEV_2], +- resource_ops=( +- ("monitor", "60s", None, None), +- ("start", "0s", "40s", None), +- ), +- ) +- + + class UpdateScsiDevicesAddRemoveBase( + UpdateScsiDevicesMixin, CommandAddRemoveMixin +@@ -1245,6 +1323,221 @@ class MpathFailuresMixin: + self.assert_failure("node1:1;node2=", ["node2", "node3"]) + + ++class UpdateScsiDevicesDigestsBase(UpdateScsiDevicesMixin): ++ def test_default_monitor(self): ++ self.assert_command_success(unfence=[DEV_2]) ++ ++ def test_no_monitor_ops(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ resource_ops=(), ++ lrm_monitor_ops=(), ++ lrm_monitor_ops_updated=(), ++ ) ++ ++ def test_1_monitor_with_timeout(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ resource_ops=(("monitor", "30s", "10s", None),), ++ lrm_monitor_ops=(("30000", DEFAULT_DIGEST, None, None),), ++ lrm_monitor_ops_updated=(("30000", ALL_DIGEST, None, None),), ++ ) ++ ++ def test_2_monitor_ops_with_timeouts(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ resource_ops=( ++ ("monitor", "30s", "10s", None), ++ ("monitor", "40s", "20s", None), ++ ), ++ lrm_monitor_ops=( ++ ("30000", DEFAULT_DIGEST, None, None), ++ ("40000", DEFAULT_DIGEST, None, None), ++ ), ++ lrm_monitor_ops_updated=( ++ ("30000", ALL_DIGEST, None, None), ++ ("40000", ALL_DIGEST, None, None), ++ ), ++ ) ++ ++ def test_2_monitor_ops_with_one_timeout(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ resource_ops=( ++ ("monitor", "30s", "10s", None), ++ ("monitor", "60s", None, None), ++ ), ++ lrm_monitor_ops=( ++ ("30000", DEFAULT_DIGEST, None, None), ++ ("60000", DEFAULT_DIGEST, None, None), ++ ), ++ lrm_monitor_ops_updated=( ++ ("30000", ALL_DIGEST, None, None), ++ ("60000", ALL_DIGEST, None, None), ++ ), ++ ) ++ ++ def test_various_start_ops_one_lrm_start_op(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ resource_ops=( ++ ("monitor", "60s", None, None), ++ ("start", "0s", "40s", None), ++ ("start", "0s", "30s", "1"), ++ ("start", "10s", "5s", None), ++ ("start", "20s", None, None), ++ ), ++ ) ++ ++ def test_1_nonrecurring_start_op_with_timeout(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ resource_ops=( ++ ("monitor", "60s", None, None), ++ ("start", "0s", "40s", None), ++ ), ++ ) ++ ++ def _digests_attrs_before(self, last_comma=True): ++ return [ ++ ( ++ "digests-all", ++ self.digest_attr_value_single(DEFAULT_DIGEST, last_comma), ++ ), ++ ( ++ "digests-secure", ++ self.digest_attr_value_single(DEFAULT_DIGEST, last_comma), ++ ), ++ ] ++ ++ def _digests_attrs_after(self, last_comma=True): ++ return [ ++ ( ++ "digests-all", ++ self.digest_attr_value_single(ALL_DIGEST, last_comma), ++ ), ++ ( ++ "digests-secure", ++ self.digest_attr_value_single(NONPRIVATE_DIGEST, last_comma), ++ ), ++ ] ++ ++ def _digests_attrs_before_multi(self, last_comma=True): ++ return [ ++ ( ++ "digests-all", ++ self.digest_attr_value_multiple(DEFAULT_DIGEST, last_comma), ++ ), ++ ( ++ "digests-secure", ++ self.digest_attr_value_multiple(DEFAULT_DIGEST, last_comma), ++ ), ++ ] ++ ++ def _digests_attrs_after_multi(self, last_comma=True): ++ return [ ++ ( ++ "digests-all", ++ self.digest_attr_value_multiple(ALL_DIGEST, last_comma), ++ ), ++ ( ++ "digests-secure", ++ self.digest_attr_value_multiple(NONPRIVATE_DIGEST, last_comma), ++ ), ++ ] ++ ++ def test_transient_digests_attrs_all_nodes(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=len(self.existing_nodes) ++ * [self._digests_attrs_before()], ++ digests_attrs_list_updated=len(self.existing_nodes) ++ * [self._digests_attrs_after()], ++ ) ++ ++ def test_transient_digests_attrs_not_on_all_nodes(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=[self._digests_attrs_before()], ++ digests_attrs_list_updated=[self._digests_attrs_after()], ++ ) ++ ++ def test_transient_digests_attrs_all_nodes_multi_value(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=len(self.existing_nodes) ++ * [self._digests_attrs_before_multi()], ++ digests_attrs_list_updated=len(self.existing_nodes) ++ * [self._digests_attrs_after_multi()], ++ ) ++ ++ def test_transient_digests_attrs_not_on_all_nodes_multi_value(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=[self._digests_attrs_before()], ++ digests_attrs_list_updated=[self._digests_attrs_after()], ++ ) ++ ++ def test_transient_digests_attrs_not_all_digest_types(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=len(self.existing_nodes) ++ * [self._digests_attrs_before()[0:1]], ++ digests_attrs_list_updated=len(self.existing_nodes) ++ * [self._digests_attrs_after()[0:1]], ++ ) ++ ++ def test_transient_digests_attrs_without_digests_attrs(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=len(self.existing_nodes) * [[]], ++ digests_attrs_list_updated=len(self.existing_nodes) * [[]], ++ ) ++ ++ def test_transient_digests_attrs_without_last_comma(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=[self._digests_attrs_before(last_comma=False)], ++ digests_attrs_list_updated=[ ++ self._digests_attrs_after(last_comma=False) ++ ], ++ ) ++ ++ def test_transient_digests_attrs_without_last_comma_multi_value(self): ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=[ ++ self._digests_attrs_before_multi(last_comma=False) ++ ], ++ digests_attrs_list_updated=[ ++ self._digests_attrs_after_multi(last_comma=False) ++ ], ++ ) ++ ++ def test_transient_digests_attrs_no_digest_for_our_stonith_id(self): ++ digests_attrs_list = len(self.existing_nodes) * [ ++ [ ++ ("digests-all", DIGEST_ATTR_VALUE_GOOD_FORMAT), ++ ("digests-secure", DIGEST_ATTR_VALUE_GOOD_FORMAT), ++ ] ++ ] ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=digests_attrs_list, ++ digests_attrs_list_updated=digests_attrs_list, ++ ) ++ ++ def test_transient_digests_attrs_digests_with_empty_value(self): ++ digests_attrs_list = len(self.existing_nodes) * [ ++ [("digests-all", ""), ("digests-secure", "")] ++ ] ++ self.assert_command_success( ++ unfence=[DEV_2], ++ digests_attrs_list=digests_attrs_list, ++ digests_attrs_list_updated=digests_attrs_list, ++ ) ++ ++ + @mock.patch.object( + settings, + "pacemaker_api_result_schema", +@@ -1337,3 +1630,47 @@ class TestUpdateScsiDevicesAddRemoveFailuresScsi( + UpdateScsiDevicesAddRemoveFailuresBaseMixin, ScsiMixin, TestCase + ): + pass ++ ++ ++@mock.patch.object( ++ settings, ++ "pacemaker_api_result_schema", ++ rc("pcmk_api_rng/api-result.rng"), ++) ++class TestUpdateScsiDevicesDigestsSetScsi( ++ UpdateScsiDevicesDigestsBase, ScsiMixin, CommandSetMixin, TestCase ++): ++ pass ++ ++ ++@mock.patch.object( ++ settings, ++ "pacemaker_api_result_schema", ++ rc("pcmk_api_rng/api-result.rng"), ++) ++class TestUpdateScsiDevicesDigestsAddRemoveScsi( ++ UpdateScsiDevicesDigestsBase, ScsiMixin, CommandAddRemoveMixin, TestCase ++): ++ pass ++ ++ ++@mock.patch.object( ++ settings, ++ "pacemaker_api_result_schema", ++ rc("pcmk_api_rng/api-result.rng"), ++) ++class TestUpdateScsiDevicesDigestsSetMpath( ++ UpdateScsiDevicesDigestsBase, MpathMixin, CommandSetMixin, TestCase ++): ++ pass ++ ++ ++@mock.patch.object( ++ settings, ++ "pacemaker_api_result_schema", ++ rc("pcmk_api_rng/api-result.rng"), ++) ++class TestUpdateScsiDevicesDigestsAddRemoveMpath( ++ UpdateScsiDevicesDigestsBase, MpathMixin, CommandAddRemoveMixin, TestCase ++): ++ pass +-- +2.39.2 + diff --git a/SOURCES/bz2183180-01-fix-loading-with-fence-levels.patch b/SOURCES/bz2183180-01-fix-loading-with-fence-levels.patch new file mode 100644 index 0000000..0b30882 --- /dev/null +++ b/SOURCES/bz2183180-01-fix-loading-with-fence-levels.patch @@ -0,0 +1,89 @@ +From 2403a2414f234a4025055e56f8202094caf1b655 Mon Sep 17 00:00:00 2001 +From: Ivan Devat +Date: Thu, 30 Mar 2023 17:03:06 +0200 +Subject: [PATCH] fix cluster-status/fence_levels shape expectation + +--- + jest.config.js | 1 + + .../endpoints/clusterStatus/shape/cluster.ts | 10 +++-- + .../cluster/displayAdvancedStatus.test.ts | 37 +++++++++++++++++++ + 3 files changed, 44 insertions(+), 4 deletions(-) + create mode 100644 src/test/scenes/cluster/displayAdvancedStatus.test.ts + +diff --git a/jest.config.js b/jest.config.js +index 08660443..c5c39dc5 100644 +--- a/jest.config.js ++++ b/jest.config.js +@@ -1,4 +1,5 @@ + module.exports = { + globalSetup: "./src/test/jest-preset.ts", + moduleDirectories: ["node_modules", "src"], ++ testTimeout: 10000, + }; +diff --git a/src/app/backend/endpoints/clusterStatus/shape/cluster.ts b/src/app/backend/endpoints/clusterStatus/shape/cluster.ts +index 97ec4f17..ea29470e 100644 +--- a/src/app/backend/endpoints/clusterStatus/shape/cluster.ts ++++ b/src/app/backend/endpoints/clusterStatus/shape/cluster.ts +@@ -13,10 +13,12 @@ The key of record is a target. + */ + const ApiFencingLevels = t.record( + t.string, +- t.type({ +- level: t.string, +- devices: t.array(t.string), +- }), ++ t.array( ++ t.type({ ++ level: t.string, ++ devices: t.string, ++ }), ++ ), + ); + + export const ApiClusterStatusFlag = t.keyof({ +diff --git a/src/test/scenes/cluster/displayAdvancedStatus.test.ts b/src/test/scenes/cluster/displayAdvancedStatus.test.ts +new file mode 100644 +index 00000000..78eb7dbe +--- /dev/null ++++ b/src/test/scenes/cluster/displayAdvancedStatus.test.ts +@@ -0,0 +1,37 @@ ++// Cluster status is pretty complex. Sometimes a discrepancy between frontend ++// and backend appears. This modules collect tests for discovered cases. ++ ++import * as t from "dev/responses/clusterStatus/tools"; ++ ++import {dt} from "test/tools/selectors"; ++import {location, shortcuts} from "test/tools"; ++ ++const clusterName = "test-cluster"; ++ ++// We want to see browser behavior with (for now) invalid status before fix. But ++// the typecheck tell us that it is wrong and dev build fails. So, we decive it. ++const deceiveTypeCheck = (maybeInvalidPart: ReturnType) => ++ JSON.parse(JSON.stringify(maybeInvalidPart)); ++ ++describe("Cluster with advanced status", () => { ++ it("accept fence levels", async () => { ++ shortcuts.interceptWithCluster({ ++ clusterStatus: t.cluster(clusterName, "ok", { ++ fence_levels: deceiveTypeCheck({ ++ "node-1": [ ++ { ++ level: "1", ++ devices: "fence-1", ++ }, ++ { ++ level: "2", ++ devices: "fence-2", ++ }, ++ ], ++ }), ++ }), ++ }); ++ await page.goto(location.cluster({clusterName})); ++ await page.waitForSelector(dt("cluster-overview")); ++ }); ++}); +-- +2.39.2 + diff --git a/SPECS/pcs.spec b/SPECS/pcs.spec index f151372..dafca9f 100644 --- a/SPECS/pcs.spec +++ b/SPECS/pcs.spec @@ -1,6 +1,6 @@ Name: pcs Version: 0.11.4 -Release: 6%{?dist} +Release: 7%{?dist} # https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/ # https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses # GPL-2.0-only: pcs @@ -24,8 +24,8 @@ ExclusiveArch: i686 x86_64 s390x ppc64le aarch64 %global pcs_source_name %{name}-%{version_or_commit} # ui_commit can be determined by hash, tag or branch -%global ui_commit 0.1.16 -%global ui_modules_version 0.1.16 +%global ui_commit 0.1.16.1 +%global ui_modules_version 0.1.16.1 %global ui_src_name pcs-web-ui-%{ui_commit} %global pcs_snmp_pkg_name pcs-snmp @@ -40,7 +40,7 @@ ExclusiveArch: i686 x86_64 s390x ppc64le aarch64 %global version_rubygem_eventmachine 1.2.7 %global version_rubygem_ffi 1.15.5 %global version_rubygem_mustermann 3.0.0 -%global version_rubygem_rack 2.2.5 +%global version_rubygem_rack 2.2.6.4 %global version_rubygem_rack_protection 3.0.5 %global version_rubygem_rack_test 2.0.2 %global version_rubygem_ruby2_keywords 0.0.5 @@ -115,9 +115,12 @@ Patch6: bz2151164-01-fix-displaying-bool-and-integer-values.patch Patch7: bz2159454-01-add-agent-validation-option.patch Patch8: bz2158790-01-fix-stonith-watchdog-timeout-validation.patch Patch9: bz2166249-01-fix-stonith-watchdog-timeout-offline-update.patch +Patch10: bz2180697-01-fix-pcs-config-checkpoint-diff.patch +Patch11: bz2180704-01-fix-pcs-stonith-update-scsi.patch # ui patches: >200 Patch201: bz2167471-01-fix-broken-typeahead-component.patch +Patch202: bz2183180-01-fix-loading-with-fence-levels.patch # git for patches BuildRequires: git-core @@ -297,6 +300,7 @@ update_times_patch(){ %autosetup -D -T -b 100 -a 101 -S git -n %{ui_src_name} -N %autopatch -p1 -m 201 update_times_patch %{PATCH201} +update_times_patch %{PATCH202} # patch pcs sources %autosetup -S git -n %{pcs_source_name} -N @@ -310,6 +314,8 @@ update_times_patch %{PATCH6} update_times_patch %{PATCH7} update_times_patch %{PATCH8} update_times_patch %{PATCH9} +update_times_patch %{PATCH10} +update_times_patch %{PATCH11} # prepare dirs/files necessary for building all bundles # ----------------------------------------------------- @@ -544,6 +550,14 @@ run_all_tests %license pyagentx_LICENSE.txt %changelog +* Tue Mar 28 2023 Michal Pospisil - 0.11.4-7 +- Fix displaying differences between configuration checkpoints in “pcs config checkpoint diff” command +- Fix “pcs stonith update-scsi-devices” command which was broken since Pacemaker-2.1.5-rc1 +- Fixed loading of cluster status in the web interface when fencing levels are configured +- Fixed a vulnerability in pcs-web-ui-node-modules +- Updated bundled rubygem rack +- Resolves: rhbz#2179901 rhbz#2180697 rhbz#2180704 rhbz#2180708 rhbz#2180978 rhbz#2183180 + * Mon Feb 13 2023 Michal Pospisil - 0.11.4-6 - Fixed broken filtering in create resource/fence device wizards in the web interface - Added BuildRequires: pam - needed for tier0 tests during build