diff --git a/.fmf/version b/.fmf/version deleted file mode 100644 index d00491f..0000000 --- a/.fmf/version +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/.gitignore b/.gitignore index fbe8060..06c771f 100644 --- a/.gitignore +++ b/.gitignore @@ -108,20 +108,3 @@ /pcs-0.11.5.tar.gz /pcs-web-ui-0.1.16.1.tar.gz /pcs-web-ui-node-modules-0.1.16.1.tar.xz -/pcs-0.11.6.tar.gz -/pcs-web-ui-node-modules-0.1.17.tar.xz -/pcs-web-ui-0.1.17.tar.gz -/dacite-1.8.1.tar.gz -/pcs-0.11.7.tar.gz -/pcs-web-ui-0.1.18.tar.gz -/pcs-web-ui-node-modules-0.1.18.tar.xz -/pcs-0.11.8.tar.gz -/pcs-web-ui-0.1.20.tar.gz -/pcs-web-ui-node-modules-0.1.20.tar.xz -/pcs-0.12.0.tar.gz -/pcs-web-ui-0.1.22.tar.gz -/pcs-web-ui-node-modules-0.1.22.tar.xz -/dacite-1.9.2.tar.gz -/pcs-0.12.1.tar.gz -/pcs-web-ui-0.1.23.tar.gz -/pcs-web-ui-node-modules-0.1.23.tar.xz diff --git a/do-not-require-wheel.patch b/do-not-require-wheel.patch deleted file mode 100644 index 2aab9ce..0000000 --- a/do-not-require-wheel.patch +++ /dev/null @@ -1,63 +0,0 @@ -From 72588f150e89b6f06810321245a13157b7e00b09 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= -Date: Tue, 29 Jul 2025 23:42:00 +0200 -Subject: [PATCH] Do not require wheel when setuptools is new enough - -Current version of setuptools creates wheels on its own. ---- - README.md | 2 +- - configure.ac | 7 +++++-- - rpm/pcs.spec.in | 2 +- - 3 files changed, 7 insertions(+), 4 deletions(-) - -diff --git a/README.md b/README.md -index a06143eba..636485be6 100644 ---- a/README.md -+++ b/README.md -@@ -47,7 +47,7 @@ Apart from the dependencies listed above, these are also required for - installation: - - * python development files (packages python3-devel, python3-setuptools 66.1+, -- python3-pip, python3-wheel) -+ python3-pip, python3-wheel if python3-setuptools < 71) - * ruby development files (package ruby-devel) - * rubygems - * rubygem bundler (package rubygem-bundler or ruby-bundler or bundler) -diff --git a/configure.ac b/configure.ac -index 4e7d36599..0a782bfa7 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -347,12 +347,15 @@ AC_SUBST([PYAGENTX_VERSION]) - - # pip 19.0 required for PEP517 support - PCS_CHECK_PYMOD([pip], [>= 23.0]) --# pip builds a wheel first --PCS_CHECK_PYMOD([wheel]) - - if test "x$tests_only" != "xyes"; then - # setuptools 61.0.0 required for PEP621 support - PCS_CHECK_PYMOD([setuptools], [>= 66.1]) -+ # wheel is required if setuptools < 71 -+ AC_PIP_MODULE([setuptools], [>= 71], [], [], [need_wheel=yes]) -+ if test "x$need_wheel" = "xyes"; then -+ PCS_CHECK_PYMOD([wheel]) -+ fi - PCS_CHECK_PYMOD([cryptography]) - PCS_CHECK_PYMOD([lxml]) - PCS_CHECK_PYMOD([pyparsing], [>= 3.0.0]) -diff --git a/rpm/pcs.spec.in b/rpm/pcs.spec.in -index 55881a398..639a3e974 100644 ---- a/rpm/pcs.spec.in -+++ b/rpm/pcs.spec.in -@@ -86,7 +86,7 @@ BuildRequires: python%{python3_version}-pip >= 23 - BuildRequires: python%{python3_version}-setuptools >= 66.1 - - # for building wheel during make install --BuildRequires: python%{python3_version}-wheel -+BuildRequires: (python%{python3_version}-wheel if python%{python3_version}-setuptools < 71) - - # for bundling dateutil - %if "@cirpmworkarounds@" != "yes" --- -2.50.1 - diff --git a/fix-broken-typeahead-component.patch b/fix-broken-typeahead-component.patch new file mode 100644 index 0000000..d75e81f --- /dev/null +++ b/fix-broken-typeahead-component.patch @@ -0,0 +1,67 @@ +From b53feb9febd8be85ab8eb90fda02478e4d7e9fea Mon Sep 17 00:00:00 2001 +From: Ivan Devat +Date: Tue, 13 Dec 2022 12:58:00 +0100 +Subject: [PATCH 1/2] fix agents filter in resource/fence device create + +--- + .../cluster/fenceDevices/task/create/NameTypeTypeSelect.tsx | 4 ++-- + .../view/cluster/resources/task/create/NameTypeTypeSelect.tsx | 4 ++-- + src/app/view/share/form/Select.tsx | 2 +- + 3 files changed, 5 insertions(+), 5 deletions(-) + +diff --git a/src/app/view/cluster/fenceDevices/task/create/NameTypeTypeSelect.tsx b/src/app/view/cluster/fenceDevices/task/create/NameTypeTypeSelect.tsx +index 80327801..8d623e2b 100644 +--- a/src/app/view/cluster/fenceDevices/task/create/NameTypeTypeSelect.tsx ++++ b/src/app/view/cluster/fenceDevices/task/create/NameTypeTypeSelect.tsx +@@ -38,13 +38,13 @@ export const NameTypeTypeSelect = ({ + return ( + 0 ? agentName : undefined} + data-test="resource-agent" + > +diff --git a/src/app/view/share/form/Select.tsx b/src/app/view/share/form/Select.tsx +index d73f126c..e2b81ce2 100644 +--- a/src/app/view/share/form/Select.tsx ++++ b/src/app/view/share/form/Select.tsx +@@ -31,7 +31,7 @@ export const Select = ( + const filter = onFilter + ? (_event: React.ChangeEvent | null, value: string) => { + onFilter(value); +- return null as unknown as React.ReactElement[]; ++ return undefined; + } + : null; + +-- +2.39.2 + diff --git a/fix-cluster-status-fence-levels.patch b/fix-cluster-status-fence-levels.patch new file mode 100644 index 0000000..e21c253 --- /dev/null +++ b/fix-cluster-status-fence-levels.patch @@ -0,0 +1,89 @@ +From 9e960df30366a93c3b383b34612451817cc617d8 Mon Sep 17 00:00:00 2001 +From: Ivan Devat +Date: Thu, 30 Mar 2023 17:03:06 +0200 +Subject: [PATCH 2/2] 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/fix-pcs-config-checkpoint-diff.patch b/fix-pcs-config-checkpoint-diff.patch new file mode 100644 index 0000000..3046951 --- /dev/null +++ b/fix-pcs-config-checkpoint-diff.patch @@ -0,0 +1,84 @@ +From 22b275bd8571b7b949b23c5c39d05fbc9e812e12 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 + +--- + pcs/cli/common/lib_wrapper.py | 15 +-------------- + pcs/config.py | 3 +++ + 2 files changed, 4 insertions(+), 14 deletions(-) + +diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py +index dabc5fd2..1c9e2f4d 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": +@@ -547,4 +534,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/fix-pcs-stonith-update-scsi-devices.patch b/fix-pcs-stonith-update-scsi-devices.patch new file mode 100644 index 0000000..cb04eb7 --- /dev/null +++ b/fix-pcs-stonith-update-scsi-devices.patch @@ -0,0 +1,975 @@ +From 16cfc184b4544156df36fa66f7cb5fbf015b18f1 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 b730fbbf..6eec6611 100644 +--- a/pcs/lib/cib/resource/stonith.py ++++ b/pcs/lib/cib/resource/stonith.py +@@ -173,12 +173,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( +@@ -282,21 +334,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( +@@ -315,6 +435,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: +@@ -345,17 +467,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( +@@ -368,7 +487,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( +@@ -384,7 +503,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", +@@ -413,3 +532,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/fix-pcsd-not-starting-with-older-rack.patch b/fix-pcsd-not-starting-with-older-rack.patch deleted file mode 100644 index e18f4ef..0000000 --- a/fix-pcsd-not-starting-with-older-rack.patch +++ /dev/null @@ -1,32 +0,0 @@ -From a2899ef0e20bb638f47e74077281e73f4f18faf3 Mon Sep 17 00:00:00 2001 -From: Miroslav Lisik -Date: Thu, 10 Jul 2025 19:48:28 +0200 -Subject: [PATCH 2/2] fix ruby NameError exception in pcsd/pcsd.rb occuring - with older rack - ---- - pcsd/pcsd.rb | 8 +++++--- - 1 file changed, 5 insertions(+), 3 deletions(-) - -diff --git a/pcsd/pcsd.rb b/pcsd/pcsd.rb -index e8ae9f106..c9f668851 100644 ---- a/pcsd/pcsd.rb -+++ b/pcsd/pcsd.rb -@@ -74,9 +74,11 @@ configure do - CAPABILITIES_PCSD = capabilities_pcsd.freeze - end - --error Rack::QueryParser::QueryLimitError do -- $logger.warn(env['sinatra.error'].message) -- return 400, env['sinatra.error'].message -+if Rack.const_defined?(:QueryParser) and Rack::QueryParser.const_defined?(:QueryLimitError) -+ error Rack::QueryParser::QueryLimitError do -+ $logger.warn(env['sinatra.error'].message) -+ return 400, env['sinatra.error'].message -+ end - end - - get '/remote/?:command?' do --- -2.50.1 - diff --git a/pcs.spec b/pcs.spec index aa227b2..a3ec696 100644 --- a/pcs.spec +++ b/pcs.spec @@ -1,6 +1,6 @@ Name: pcs -Version: 0.12.1 -Release: 5%{?dist} +Version: 0.11.5 +Release: 2%{?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 @@ -11,52 +11,28 @@ Group: System Environment/Base Summary: Pacemaker/Corosync Configuration System BuildArch: noarch -# Remove a tilde used by RPM to get the correct upstream version -%global clean_version %(echo %{version} | sed 's/~//') - -# To build an official pcs release, comment out branch_or_commit -# Use long commit hash or branch name to build an unreleased version -# %%global branch_or_commit 1353dfbb3af82d77f4de17a3fa4cbde185bb2b2d -%global version_or_commit %{clean_version} -%if 0%{?branch_or_commit:1} - %global version_or_commit %{branch_or_commit} - %global tarball_version %{clean_version}+%(echo %{branch_or_commit} | head -c 8) -%endif +# When specifying a commit, use its long hash +%global version_or_commit %{version} +# %%global version_or_commit 10069ca47e5c9f4ac1abd8bc4cd99281ead047b7 %global pcs_source_name %{name}-%{version_or_commit} -# To build an official pcs-web-ui release, comment out ui_branch_or_commit -# Last tagged version, also used as fallback version for untagged tarballs -%global ui_version 0.1.23 -%global ui_modules_version 0.1.23 -# Use long commit hash or branch name to build an unreleased version -# %%global ui_branch_or_commit 34372d1268f065ed186546f55216aaa2d7e76b54 -%global ui_version_or_commit %{ui_version} -%if 0%{?ui_branch_or_commit:1} - %global ui_version_or_commit %{ui_branch_or_commit} - %global ui_tarball_version %{ui_version}-%(echo %{ui_branch_or_commit} | head -c 8) -%endif -%global ui_src_name pcs-web-ui-%{ui_version_or_commit} +# ui_commit can be determined by hash, tag or branch +%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 %global pyagentx_version 0.4.pcs.2 -%global dacite_version 1.9.2 +%global dacite_version 1.8.0 -%global required_pacemaker_version 3.0.0 +%global required_pacemaker_version 2.1.0 %global pcs_bundled_dir pcs_bundled -%global pcsd_webui_dir %{_prefix}/lib/pcsd/public/ui - -%global cockpit_dir %{_datadir}/cockpit/ -%global metainfo_dir %{_datadir}/metainfo -%global ui_metainfo_name org.clusterlabs.cockpit_pcs_web_ui.metainfo.xml -%global ui_metainfo %{metainfo_dir}/%{ui_metainfo_name} - -%global pkg_pcs_snmp pcs-snmp -%global pkg_pcs_web_ui pcs-web-ui -%global pkg_cockpit_ha_cluster cockpit-ha-cluster +%global pcsd_public_dir pcsd/public # prepend v for folder in GitHub link when using tagged tarball -%if "%{clean_version}" == "%{version_or_commit}" +%if "%{version}" == "%{version_or_commit}" %global v_prefix v %endif @@ -66,42 +42,25 @@ Source0: %{url}/archive/%{?v_prefix}%{version_or_commit}/%{pcs_source_name}.tar. Source41: https://github.com/ondrejmular/pyagentx/archive/v%{pyagentx_version}/pyagentx-%{pyagentx_version}.tar.gz Source42: https://github.com/konradhalas/dacite/archive/v%{dacite_version}/dacite-%{dacite_version}.tar.gz -Source100: https://github.com/ClusterLabs/pcs-web-ui/archive/%{ui_version_or_commit}/%{ui_src_name}.tar.gz -Source101: https://github.com/ClusterLabs/pcs-web-ui/releases/download/%{ui_version_or_commit}/pcs-web-ui-node-modules-%{ui_modules_version}.tar.xz - +Source100: https://github.com/ClusterLabs/pcs-web-ui/archive/%{ui_commit}/%{ui_src_name}.tar.gz +Source101: https://github.com/ClusterLabs/pcs-web-ui/releases/download/%{ui_commit}/pcs-web-ui-node-modules-%{ui_modules_version}.tar.xz # pcs patches: <= 200 -# Patch1: name.patch -Patch1: show-info-page-instead-of-webui.patch -Patch2: fix-pcsd-not-starting-with-older-rack.patch -Patch3: do-not-require-wheel.patch +# Patch0: name.patch +Patch0: fix-pcs-config-checkpoint-diff.patch +Patch1: fix-pcs-stonith-update-scsi-devices.patch # ui patches: >200 -# Patch201: name-web-ui.patch - - -# Split pcs to pcs and pcs-web-ui, all packages that replace pcs must obsolete -# the old monolithic package -# https://docs.fedoraproject.org/en-US/packaging-guidelines/#_one_to_many_replacement -Obsoletes: pcs < 0.12.0 -# Web UI is an add-on that doesn't need to be installed for pcs to function. -# Upgrades from before 0.12 will install it thanks to Obsoletes. But it will -# be possible to uninstall web UI to disable it and then it will not be -# installed during upgrades because it is a weak dependency. -Recommends: %{pkg_pcs_web_ui} == %{version}-%{release} - +Patch201: fix-broken-typeahead-component.patch +Patch202: fix-cluster-status-fence-levels.patch # git for patches BuildRequires: git-core -# for building pcs tarballs -BuildRequires: autoconf -BuildRequires: automake BuildRequires: make # printf from coreutils is used in makefile, head is used in spec BuildRequires: coreutils -# find is used in Makefile and also somewhere else -BuildRequires: findutils # python for pcs +BuildRequires: python3 >= 3.9 BuildRequires: python3-dateutil >= 2.7.0 BuildRequires: python3-devel BuildRequires: python3-setuptools @@ -112,8 +71,10 @@ BuildRequires: python3-tornado BuildRequires: python3-cryptography BuildRequires: python3-lxml # for building bundled python packages -# setuptools 71+ builds wheels by itself -BuildRequires: (python3-wheel if python3-setuptools < 71) +BuildRequires: python3-wheel +# for bundled python dateutil +BuildRequires: python3-setuptools_scm +BuildRequires: python3-distro # ruby and gems for pcsd BuildRequires: ruby >= 2.5.0 BuildRequires: ruby-devel @@ -124,52 +85,55 @@ BuildRequires: rubygem-ffi BuildRequires: rubygem-json BuildRequires: rubygem-mustermann BuildRequires: rubygem-puma -BuildRequires: (rubygem(rack) < 3 or (rubygem(rack) >= 3 and rubygem(rackup))) +BuildRequires: rubygem-rack BuildRequires: rubygem-rack-protection BuildRequires: rubygem-rack-test BuildRequires: rubygem-sinatra BuildRequires: rubygem-tilt -%if 0%{?fedora} || 0%{?rhel} >= 9 +%if 0%{?fedora} || 0%{?eln} || 0%{?rhel} >= 9 BuildRequires: rubygem(rexml) %endif # ruby libraries for tests BuildRequires: rubygem-test-unit # for touching patch files (sanitization function) BuildRequires: diffstat -# for systemd scriptlet macros -BuildRequires: systemd-rpm-macros +# for post, preun and postun macros +BuildRequires: systemd # pam is used for authentication inside daemon (python ctypes) # needed for tier0 tests during build BuildRequires: pam # for working with qdevice certificates (certutil) - used in configure.ac BuildRequires: nss-tools -# pcs now provides a pc file -BuildRequires: pkgconfig +# for creating the web ui favicon symlink to the Fedora logo +BuildRequires: fedora-logos +# for building web ui +%if 0%{?fedora} < 37 +BuildRequires: npm +%else +BuildRequires: nodejs-npm +%endif # cluster stack packages for pkg-config -# corosync has different package names on distributions but all provide -# corosync-devel -# corosync and pacemaker need versions and it's not working in virtual provides -BuildRequires: corosync-devel >= 3.0 +BuildRequires: booth +BuildRequires: corosync-qdevice-devel +BuildRequires: corosynclib-devel >= 3.0 +BuildRequires: fence-agents-common BuildRequires: pacemaker-libs-devel >= %{required_pacemaker_version} -BuildRequires: pkgconfig(booth) -BuildRequires: pkgconfig(corosync-qdevice) -BuildRequires: pkgconfig(sbd) - -# for validating cockpit-ha-cluster metainfo -BuildRequires: libappstream-glib - +BuildRequires: resource-agents +BuildRequires: sbd # python and libraries for pcs, setuptools for pcs entrypoint +Requires: python3 >= 3.9 Requires: python3-cryptography Requires: python3-dateutil >= 2.7.0 Requires: python3-lxml +Requires: python3-setuptools Requires: python3-pycurl Requires: python3-pyparsing Requires: python3-tornado # ruby and gems for pcsd -Requires: ruby >= 3.3.0 +Requires: ruby >= 2.5.0 Requires: rubygem-backports Requires: rubygem-childprocess Requires: rubygem-ethon @@ -177,11 +141,12 @@ Requires: rubygem-ffi Requires: rubygem-json Requires: rubygem-mustermann Requires: rubygem-puma -Requires: (rubygem(rack) < 3 or (rubygem(rack) >= 3 and rubygem(rackup))) +Requires: rubygem-rack Requires: rubygem-rack-protection +Requires: rubygem-rack-test Requires: rubygem-sinatra Requires: rubygem-tilt -%if 0%{?fedora} || 0%{?rhel} >= 9 +%if 0%{?fedora} || 0%{?eln} || 0%{?rhel} >= 9 Requires: rubygem(rexml) %endif # for killall @@ -193,6 +158,10 @@ Requires: (corosync >= 3.0 if pacemaker) # pcs enables corosync encryption by default so we require libknet1-plugins-all Requires: (libknet1-plugins-all if corosync) Requires: pacemaker-cli >= %{required_pacemaker_version} +# for post, preun and postun macros +Requires(post): systemd +Requires(preun): systemd +Requires(postun): systemd # pam is used for authentication inside daemon (python ctypes) # more details: https://bugzilla.redhat.com/show_bug.cgi?id=1717113 Requires: pam @@ -200,12 +169,18 @@ Requires: pam Requires: logrotate # for working with qdevice certificates (certutil) Requires: nss-tools +# for web ui favicon - symlink to the Fedora logo +Requires: fedora-logos Provides: bundled(dacite) = %{dacite_version} -# pcs-snmp subpackage definition -%package -n %{pkg_pcs_snmp} +%description +pcs is a corosync and pacemaker configuration tool. It permits users to +easily view, modify and create pacemaker based clusters. + +# pcs-snmp package definition +%package -n %{pcs_snmp_pkg_name} Group: System Environment/Base Summary: Pacemaker cluster SNMP agent # https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses @@ -224,60 +199,11 @@ Requires: net-snmp Provides: bundled(pyagentx) = %{pyagentx_version} -# pcs-web-ui subpackage definition -%package -n %{pkg_pcs_web_ui} -Summary: Standalone web UI for Pacemaker/Corosync Configuration System -# https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/ -# https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Good_Licenses -# GPL-2.0-only: pcs -License: GPL-2.0-only -URL: https://github.com/ClusterLabs/pcs-web-ui - -# Split pcs to pcs and pcs-web-ui, all packages that replace pcs must obsolete -# the old monolithic package -# https://docs.fedoraproject.org/en-US/packaging-guidelines/#_one_to_many_replacement -Obsoletes: pcs < 0.12.0 - -Requires: pcs = %{version}-%{release} - -Provides: bundled(pcs-web-ui) = %{!?ui_tarball_version:%{ui_version}}%{?ui_tarball_version} - -# cockpit-ha-cluster subpackage definition -%package -n %{pkg_cockpit_ha_cluster} -Group: System Environment/Base -Summary: Cockpit application for managing Pacemaker based clusters -License: GPL-2.0-only AND CC0-1.0 -URL: https://github.com/ClusterLabs/pcs-web-ui - -BuildRequires: make -BuildRequires: nodejs-npm - -Requires: pcs = %{version}-%{release} -Requires: cockpit-bridge - -Provides: bundled(pcs-web-ui) = %{!?ui_tarball_version:%{ui_version}}%{?ui_tarball_version} - - - -%description -pcs is a configuration tool for Corosync and Pacemaker. It permits users to -easily view, modify and create high availability clusters based on Pacemaker. -This package contains the pcs command-line utility and its server pcsd. - -%description -n %{pkg_pcs_web_ui} -Provides standalone web UI for Pacemaker/Corosync Configuration System (pcs). - -%description -n %{pkg_pcs_snmp} -SNMP agent that provides information about Pacemaker cluster to the main agent -(snmpd). - -%description -n %{pkg_cockpit_ha_cluster} -Cockpit application for managing Pacemaker based clusters. Uses -Pacemaker/Corosync Configuration System (pcs) in the background. - - +%description -n %{pcs_snmp_pkg_name} +SNMP agent that provides information about pacemaker cluster to the master agent (snmpd) %prep + # -- following is inspired by python-simplejon.el5 -- # Update timestamps on the files touched by a patch, to avoid non-equal # .pyc/.pyo files across the multilib peers within a build @@ -321,35 +247,23 @@ update_times_patch(){ # * http://ftp.rpm.org/max-rpm/s1-rpm-inside-macros.html # * https://rpm-software-management.github.io/rpm/manual/autosetup.html # patch web-ui sources -# -n — Set Name of Build Directory -# -T — Do Not Perform Default Archive Unpacking -# -b — Unpack The nth Sources Before Changing Directory -# -a — Unpack The nth Sources After Changing Directory -# -N — disables automatic patch application, use autopatch to apply patches -# -# 1. unpack sources (-b 0) -# 2. then cd into sources tree (the setup macro itself) -# 3. then unpack node_modules into sources tree (-a 1). -%autosetup -T -b 100 -a 101 -N -n %{ui_src_name} +%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 %{PATCH201} +update_times_patch %{PATCH202} # patch pcs sources %autosetup -S git -n %{pcs_source_name} -N %autopatch -p1 -M 200 -# update_times_patch %%{PATCH1} +# update_times_patch %%{PATCH0} +update_times_patch %{PATCH0} update_times_patch %{PATCH1} -update_times_patch %{PATCH2} -update_times_patch %{PATCH3} # generate .tarball-version if building from an untagged commit, not a released version # autogen uses git-version-gen which uses .tarball-version for generating version number -%if 0%{?tarball_version:1} - echo %{tarball_version} > %{_builddir}/%{pcs_source_name}/.tarball-version -%endif - -%if 0%{?ui_tarball_version:1} - echo %{ui_tarball_version} > %{_builddir}/%{ui_src_name}/.tarball-version +%if "%{version}" != "%{version_or_commit}" + echo "%version+$(echo "%{version_or_commit}" | head -c 8)" > %{_builddir}/%{pcs_source_name}/.tarball-version %endif # prepare dirs/files necessary for building python bundles @@ -357,58 +271,39 @@ mkdir -p %{pcs_bundled_dir}/src cp -f %SOURCE41 rpm/ cp -f %SOURCE42 rpm/ - - %build %define debug_package %{nil} -# We left off by setting up pcs, so we are in its directory now +# Booth authfile fix support +# Fedora 35, 36: set and unset +# Fedora 37, 38, ELN = RHEL10: unset only +# Fedora 39+: no booth build options +%if 0%{?fedora} <= 36 + %define booth_build_options --enable-booth-enable-authfile-set --enable-booth-enable-authfile-unset +%elif 0%{?fedora} <= 38 || 0%{?eln} + %define booth_build_options --enable-booth-enable-authfile-unset +%endif + ./autogen.sh -%{configure} --enable-local-build --enable-use-local-cache-only \ - --enable-individual-bundling --enable-webui \ - --with-pcsd-default-cipherlist='PROFILE=SYSTEM' \ - --with-pcs-lib-dir="%{_prefix}/lib" PYTHON=%{__python3} +%{configure} --enable-local-build --enable-use-local-cache-only --enable-individual-bundling %{?booth_build_options} --with-pcs-lib-dir="%{_prefix}/lib" PYTHON=%{__python3} make all -# Web UI installation -# Switch to web ui folder first -cd ../%{ui_src_name} -./autogen.sh -%{configure} \ - --with-pcsd-webui-dir=%{pcsd_webui_dir} \ - --with-cockpit-dir=%{cockpit_dir} \ - --with-metainfo-dir=%{metainfo_dir} -make all - - +# build pcs-web-ui +make -C %{_builddir}/%{ui_src_name} build BUILD_USE_EXISTING_NODE_MODULES=true %install -rm -rf %{buildroot} +rm -rf $RPM_BUILD_ROOT pwd -# Install cockpit pcs-web-ui -cd ../%{ui_src_name} %make_install -# prepare pcs-web-ui files (not needed for pcs as pcs installs them in Makefile) -mkdir -p %{buildroot}/%{_defaultlicensedir}/%{pkg_cockpit_ha_cluster} -mkdir -p %{buildroot}/%{_defaultlicensedir}/%{pkg_pcs_web_ui} +# install pcs-web-ui +# cp -r %%{_builddir}/%%{ui_src_name}/build ${RPM_BUILD_ROOT}%%{_prefix}/lib/%%{pcsd_public_dir}/ui +make -C %{_builddir}/%{ui_src_name} _install PCSD_DIR=${RPM_BUILD_ROOT}%{_prefix}/lib/pcsd -cp COPYING %{buildroot}/%{_defaultlicensedir}/%{pkg_cockpit_ha_cluster}/COPYING_WUI.md -mv COPYING %{buildroot}/%{_defaultlicensedir}/%{pkg_pcs_web_ui}/COPYING_WUI.md - -mkdir -p %{buildroot}/%{_docdir}/%{pkg_cockpit_ha_cluster} -mkdir -p %{buildroot}/%{_docdir}/%{pkg_pcs_web_ui} - -cp CHANGELOG.md %{buildroot}/%{_docdir}/%{pkg_cockpit_ha_cluster}/CHANGELOG_WUI.md -mv CHANGELOG.md %{buildroot}/%{_docdir}/%{pkg_pcs_web_ui}/CHANGELOG_WUI.md - -cp README.md %{buildroot}/%{_docdir}/%{pkg_cockpit_ha_cluster}/README_WUI.md -mv README.md %{buildroot}/%{_docdir}/%{pkg_pcs_web_ui}/README_WUI.md - -# Install pcs -cd ../%{pcs_source_name} -%make_install +# symlink favicon into pcsd directories +mkdir -p ${RPM_BUILD_ROOT}%{_prefix}/lib/%{pcsd_public_dir}/images/ +ln -fs /etc/favicon.png ${RPM_BUILD_ROOT}%{_prefix}/lib/%{pcsd_public_dir}/images/favicon.png # prepare license files cp %{pcs_bundled_dir}/src/pyagentx-*/LICENSE.txt pyagentx_LICENSE.txt @@ -418,12 +313,7 @@ cp %{pcs_bundled_dir}/src/pyagentx-*/README.md pyagentx_README.md cp %{pcs_bundled_dir}/src/dacite-*/LICENSE dacite_LICENSE cp %{pcs_bundled_dir}/src/dacite-*/README.md dacite_README.md - - %check -# Run validation of cockpit metainfo -appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/%{ui_metainfo_name} - # In the building environment LC_CTYPE is set to C which causes tests to fail # due to python prints a warning about it to stderr. The following environment # variable disables the warning. @@ -447,7 +337,7 @@ run_all_tests(){ #run pcsd tests and remove them ruby \ - -I%{buildroot}%{_prefix}/lib/pcsd \ + -I$RPM_BUILD_ROOT%{_prefix}/lib/pcsd \ -Ipcsd/test \ pcsd/test/test_all_suite.rb test_result_ruby=$? @@ -460,31 +350,34 @@ run_all_tests(){ run_all_tests - -# Mark pcsd and pcs_snmp_agent for restart after upgrade %posttrans -%systemd_posttrans_with_restart pcsd.service - -%posttrans -n %{pkg_pcs_snmp} -%systemd_posttrans_with_restart pcs_snmp_agent.service - -# Restart pcsd if it is running to reload the Tornado app so it detects -# presence or absence of the webui backend handler on install/update -# of pcs-web-ui that contains it -# Systemd will not pick-up on this change because pcs-web-ui doesn't contain -# a unit file that would mark it for restart -# https://fedoraproject.org/wiki/Changes/Restart_services_at_end_of_rpm_transaction -%posttrans -n %{pkg_pcs_web_ui} -systemctl try-restart pcsd.service - -# Runs only on pcs-web-ui uninstall -# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ -%postun -n %{pkg_pcs_web_ui} -if [ $1 -eq 0 ] ; then - systemctl try-restart pcsd.service -fi +# Make sure the new version of the daemon is running. +# Also, make sure to start pcsd-ruby if it hasn't been started or even +# installed before. This is done by restarting pcsd.service. +%{_bindir}/systemctl daemon-reload +%{_bindir}/systemctl try-restart pcsd.service +%post +%systemd_post pcsd.service +%systemd_post pcsd-ruby.service + +%post -n %{pcs_snmp_pkg_name} +%systemd_post pcs_snmp_agent.service + +%preun +%systemd_preun pcsd.service +%systemd_preun pcsd-ruby.service + +%preun -n %{pcs_snmp_pkg_name} +%systemd_preun pcs_snmp_agent.service + +%postun +%systemd_postun_with_restart pcsd.service +%systemd_postun_with_restart pcsd-ruby.service + +%postun -n %{pcs_snmp_pkg_name} +%systemd_postun_with_restart pcs_snmp_agent.service %files %doc CHANGELOG.md @@ -493,10 +386,9 @@ fi %license dacite_LICENSE %license COPYING %{python3_sitelib}/* -%{_bindir}/pcs -%{_bindir}/pcsd +%{_sbindir}/pcs +%{_sbindir}/pcsd %{_prefix}/lib/pcs/* -%{_prefix}/lib/pkgconfig/pcs.pc %{_prefix}/lib/pcsd/* %{_unitdir}/pcsd.service %{_unitdir}/pcsd-ruby.service @@ -517,21 +409,9 @@ fi %{_mandir}/man8/pcsd.* %exclude %{_prefix}/lib/pcs/pcs_snmp_agent %exclude %{_prefix}/lib/pcs/%{pcs_bundled_dir}/packages/pyagentx* -%exclude %{cockpit_dir} -%exclude %{ui_metainfo} -%exclude %{python3_sitelib}/pcs/daemon/app/webui -%exclude %{pcsd_webui_dir} -%files -n %{pkg_pcs_web_ui} -%doc CHANGELOG.md -%doc %{_docdir}/%{pkg_pcs_web_ui}/CHANGELOG_WUI.md -%doc %{_docdir}/%{pkg_pcs_web_ui}/README_WUI.md -%license COPYING -%license %{_defaultlicensedir}/%{pkg_pcs_web_ui}/COPYING_WUI.md -%{python3_sitelib}/pcs/daemon/app/webui -%{pcsd_webui_dir} -%files -n %{pkg_pcs_snmp} +%files -n %{pcs_snmp_pkg_name} %{_prefix}/lib/pcs/pcs_snmp_agent %{_prefix}/lib/pcs/%{pcs_bundled_dir}/packages/pyagentx* %{_unitdir}/pcs_snmp_agent.service @@ -544,86 +424,7 @@ fi %license COPYING %license pyagentx_LICENSE.txt -%files -n %{pkg_cockpit_ha_cluster} -%doc %{_docdir}/%{pkg_cockpit_ha_cluster}/CHANGELOG_WUI.md -%doc %{_docdir}/%{pkg_cockpit_ha_cluster}/README_WUI.md -%license %{_defaultlicensedir}/%{pkg_cockpit_ha_cluster}/COPYING_WUI.md -%{cockpit_dir} -%{ui_metainfo} - - - %changelog -* Fri Sep 19 2025 Python Maint - 0.12.1-5 -- Rebuilt for Python 3.14.0rc3 bytecode - -* Fri Aug 15 2025 Python Maint - 0.12.1-4 -- Rebuilt for Python 3.14.0rc2 bytecode - -* Wed Jul 30 2025 Michal Pospíšil - 0.12.1-3 -- Remove BuildRequires: python3-wheel when using setuptools 71+ - -* Fri Jul 25 2025 Fedora Release Engineering - 0.12.1-2 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_43_Mass_Rebuild - -* Fri Jul 18 2025 Michal Pospíšil - 0.12.1-1 -- Rebased pcs to the newest major version (see CHANGELOG.md) - Resolves: rhbz#2371770 -- Updated standalone web UI and HA Cluster Management Cockpit application to pcs-web-ui 0.1.23 (see CHANGELOG_WUI.md) -- There is now a changelog for the HA Cluster Management Cockpit application and the standalone web UI in both packages -- The upstream version of pcs-web-ui can now be queried through RPM in both packages - see bundled(pcs-web-ui) -- Updated bundled Python dependency dacite -- Compatibility with rubygem-rack >= 3 - Resolves: rhbz#2372462 - -* Tue Jun 03 2025 Python Maint - 0.12.0-2 -- Rebuilt for Python 3.14 - -* Mon Jan 27 2025 Michal Pospíšil - 0.12.0-1 -- Rebased pcs to the newest major version (see CHANGELOG.md) - Resolves: rhbz#2341012 -- New subpackage pcs-web-ui - enables standalone web UI when the subpackage is installed on the system (default), uninstall the subpackage to disable the web UI -- Updated standalone web UI and HA Cluster Management Cockpit application to pcs-web-ui 0.1.22 -- Fixes for compatibility with Ruby 3.4 - Resolves: rhbz#2331005 - -* Fri Jan 17 2025 Fedora Release Engineering - 0.11.8-2 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_42_Mass_Rebuild - -* Tue Aug 6 2024 Michal Pospíšil - 0.11.8-1 -- Rebased to the latest sources (see CHANGELOG.md) -- Updated pcs-web-ui to 0.1.20 - -* Thu Jul 18 2024 Fedora Release Engineering - 0.11.7-5 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_41_Mass_Rebuild - -* Sun Jun 09 2024 Python Maint - 0.11.7-4 -- Rebuilt for Python 3.13 - -* Mon Feb 5 2024 Michal Pospisil - 0.11.7-3 -- Fixed a bug preventing the Cockpit Application from being installed from Cockpit - -* Thu Jan 25 2024 Fedora Release Engineering - 0.11.7-2 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild - -* Mon Jan 8 2024 Michal Pospisil - 0.11.7-1 -- Rebased to the latest upstream sources (see CHANGELOG.md) -- Updated pcs-web-ui to 0.1.18 -- TLS cipher setting in pcsd now follows system-wide crypto policies by default -- Added cockpit-ha-cluster subpackage that adds pcs-web-ui as a Cockpit application - -* Thu Jul 20 2023 Fedora Release Engineering - 0.11.6-3 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild - -* Wed Jun 28 2023 Python Maint - 0.11.6-2 -- Rebuilt for Python 3.12 - -* Wed Jun 21 2023 Michal Pospisil - 0.11.6-1 -- Rebased to the latest upstream sources (see CHANGELOG.md) -- Updated pcs-web-ui -- Removed dependency fedora-logos - favicon is now correctly provided by pcs-web-ui -- Resolves: rhbz#2109852 rhbz#2170648 - * Wed Apr 12 2023 Michal Pospisil - 0.11.5-2 - 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 diff --git a/plan-gating.fmf b/plan-gating.fmf deleted file mode 100644 index 78d1116..0000000 --- a/plan-gating.fmf +++ /dev/null @@ -1,32 +0,0 @@ -summary: PCS gating test plan -description: Runs upstream tier0, tier1 and smoke tests - -discover: - dist-git-source: true - dist-git-install-builddeps: true - how: shell - tests: - - name: tier0 - test: | - ./prepare-tests.sh - $TMT_SOURCE_DIR/pcs-*/pcs_test/suite --tier0 -v --vanilla --installed - duration: 10m - tier: 0 - tag: fast - - name: tier1 - test: | - ./prepare-tests.sh - $TMT_SOURCE_DIR/pcs-*/pcs_test/suite --tier1 -v --vanilla --installed - duration: 2h - tier: 1 - - name: smoke - test: | - systemctl start pcsd - ./prepare-tests.sh - $TMT_SOURCE_DIR/pcs-*/pcs_test/smoke.sh - duration: 10m - tier: smoke - tag: fast - -execute: - how: tmt diff --git a/prepare-tests.sh b/prepare-tests.sh deleted file mode 100755 index aeb9a8b..0000000 --- a/prepare-tests.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -set -eo xtrace -cd $TMT_SOURCE_DIR -# Use stamp file to abort if this script already ran -if [ -e tests-prepared-stamp ]; then exit 0; fi -# RPM 4.20 changed the builddir structure - unpacked sources go to *-build but -# tmt copies them back to pcs-*, so the pcs-*-build folder is empty -# Remove pcs-web-ui, pcs-*-build for "cd pcs-*" to have exactly one match -rm -rf pcs-web-ui-* pcs-*-build -cd pcs-*/ -# Run autotools, use bundled dependencies from the system -export PYTHONPATH=/usr/lib/pcs/pcs_bundled/packages/ -export GEM_HOME=/usr/lib/pcsd/vendor/bundle/ -# We need to use cd pcs-* because when pcs-web-ui starts using autotools, running -# autogen and configure with expanded TMT_SOURCE_DIR will match that too -./autogen.sh -./configure --enable-webui --enable-local-build --enable-use-local-cache-only \ - --enable-individual-bundling --with-pcs-lib-dir=/usr/lib -# Remove pcs sources to make sure tests are not using any of those files -rm -rf pcs -touch ../tests-prepared-stamp diff --git a/show-info-page-instead-of-webui.patch b/show-info-page-instead-of-webui.patch deleted file mode 100644 index 9125cd1..0000000 --- a/show-info-page-instead-of-webui.patch +++ /dev/null @@ -1,148 +0,0 @@ -From e7fa056f582c992d3d2b289911be20440fb839ac Mon Sep 17 00:00:00 2001 -From: Ivan Devat -Date: Tue, 5 Nov 2024 16:35:02 +0100 -Subject: [PATCH 1/2] show info page instead of webui - ---- - pcs/Makefile.am | 1 + - pcs/daemon/app/webui_info_handler.py | 31 ++++++++++++++++++++++++++++ - pcs/daemon/run.py | 4 +++- - pcs_test/smoke.sh.in | 4 ++-- - pcsd/public/ui_instructions.html | 24 ++++++++++----------- - 5 files changed, 48 insertions(+), 16 deletions(-) - create mode 100644 pcs/daemon/app/webui_info_handler.py - -diff --git a/pcs/Makefile.am b/pcs/Makefile.am -index 1297473ff..8d4b93073 100644 ---- a/pcs/Makefile.am -+++ b/pcs/Makefile.am -@@ -219,6 +219,7 @@ EXTRA_DIST = \ - daemon/app/webui/core.py \ - daemon/app/webui/session.py \ - daemon/app/webui/sinatra_ui.py \ -+ daemon/app/webui_info_handler.py \ - daemon/async_tasks/__init__.py \ - daemon/async_tasks/scheduler.py \ - daemon/async_tasks/task.py \ -diff --git a/pcs/daemon/app/webui_info_handler.py b/pcs/daemon/app/webui_info_handler.py -new file mode 100644 -index 000000000..3ab8275bf ---- /dev/null -+++ b/pcs/daemon/app/webui_info_handler.py -@@ -0,0 +1,31 @@ -+from pcs.daemon.app.common import ( -+ BaseHandler, -+ RoutesType, -+) -+ -+ -+class _WebuiInfoHandler(BaseHandler): -+ __path = None -+ -+ def initialize(self, path): -+ self.__path = path -+ -+ def get(self): -+ self.set_status(404) -+ self.render(self.__path) -+ -+ -+def get_routes(path: str) -> RoutesType: -+ return [ -+ # The following two rules can be compressed into one: r"/(ui/?)?". -+ # However, the content of the parentheses used here should be captured -+ # and passed in to the handler’s get method as an argument. -+ # Unfortunately, it seems that tornado version 6.4.1 don't pass this -+ # argument (unlike version 6.3.3). Maybe it's a bug (it needs further -+ # inspection). These rules are a safe way to avoid surprises. -+ # Moreover, the captured parameter is irrelevant to the functionality -+ # of the handler. -+ (r"/", _WebuiInfoHandler, dict(path=path)), -+ (r"/ui/?", _WebuiInfoHandler, dict(path=path)), -+ (r"/ui/.*", _WebuiInfoHandler, dict(path=path)), -+ ] -diff --git a/pcs/daemon/run.py b/pcs/daemon/run.py -index 558315c23..cfe9999fc 100644 ---- a/pcs/daemon/run.py -+++ b/pcs/daemon/run.py -@@ -33,6 +33,7 @@ from pcs.daemon.app import ( - auth, - sinatra_remote, - sinatra_ui, -+ webui_info_handler, - ) - from pcs.daemon.app import capabilities as capabilities_app - -@@ -168,7 +169,8 @@ def configure_app( # noqa: PLR0913 - # Even with disabled (standalone) webui the following routes must be - # provided because they can be used via unix socket from cockpit. - routes.extend( -- sinatra_ui.get_routes(auth_provider, ruby_pcsd_wrapper) -+ webui_info_handler.get_routes(webui_fallback) -+ + sinatra_ui.get_routes(auth_provider, ruby_pcsd_wrapper) - ) - - return Application( -diff --git a/pcs_test/smoke.sh.in b/pcs_test/smoke.sh.in -index fdfe8be2c..a9bb83447 100755 ---- a/pcs_test/smoke.sh.in -+++ b/pcs_test/smoke.sh.in -@@ -71,10 +71,10 @@ if [ "$webui_http_code_response" = "200" ]; then - curl --insecure --cookie ${cookie_file} --header "X-Requested-With: XMLHttpRequest" --data "hidden[hidden_input]=&config[stonith-enabled]=false" https://localhost:2224/managec/${cluster_name}/update_cluster_settings > "${output_file}" - cat "${output_file}"; echo "" - [ "$(cat ${output_file})" = "Update Successful" ] --elif [ "$webui_http_code_response" = "401" ]; then -+elif [ "$webui_http_code_response" = "404" ]; then - curl --insecure https://localhost:2224/ui/ > "${output_file}" - cat "${output_file}"; echo "" -- [ "$(cat "${output_file}")" = '{"notauthorized":"true"}' ] -+ grep "HA cluster management has been moved" "${output_file}" - else - echo "Unexpected response from https://localhost:2224/ui/ - http code: '${webui_http_code_response}'" - exit 1 -diff --git a/pcsd/public/ui_instructions.html b/pcsd/public/ui_instructions.html -index a120ed3d4..b9c1c3152 100644 ---- a/pcsd/public/ui_instructions.html -+++ b/pcsd/public/ui_instructions.html -@@ -1,27 +1,25 @@ - - - -- Pcs WebUI instructions -+ Web UI is disabled - - - --

Pcs WebUI instructions

-+

Package pcs-web-ui is not installed

-

-- WebUI is not a part of pcs repository but it has its own -- repository. -+ Since Fedora 42, pcsd web UI is distributed with pcs as pcs-web-ui -+ package. This
package can be uninstalled to disable the web UI. -

-

-- You can clone WebUI repository -- and build the web application into pcs by: -+ To re-enable pcsd web UI, install the pcs-web-ui RPM package. -

--

--    $ npm install
--    $ npm run build
--    $ mv ./build [/path/to/]pcs/pcsd/public/ui
--  
-

-- For more details, see instructions in -- README.md. -+ Another option is to try pcsd web UI as an application for -+ Cockpit -+ - a web-based
graphical interface for servers. The HA Cluster Management -+ application can be
installed from within the Applications pane in -+ Cockpit or by installing the
cockpit-ha-cluster RPM -+ package. -

- - --- -2.50.1 - diff --git a/sources b/sources index c6784fc..c0b4a02 100644 --- a/sources +++ b/sources @@ -1,5 +1,5 @@ SHA512 (pyagentx-0.4.pcs.2.tar.gz) = d4194fec9a3e5fefe3793d49b7fec1feafef294c7e613a06046c2993daeefc5cb39d7c5b2b402ff83e49b2d976953f862264288c758c0be09d997b5323cc558a -SHA512 (dacite-1.9.2.tar.gz) = 69513bc1acd43e45a208ce4b4186072f92d9057a6a88c77d56f21405fbed7964d94fe4f6a37c99754a793caa2bd0584664dfab69169ef0d3564bb09d37769e59 -SHA512 (pcs-0.12.1.tar.gz) = 4f17a8d9c15b8c9208c94a264f5f79f03abc451e3f4ae4ba0fb338055a26407d4961466dd8f3565f6882c2c00f1d6a1ec8fd15dadcb0cf1a5586d37c56c77238 -SHA512 (pcs-web-ui-0.1.23.tar.gz) = 6bc709926f26a02d33fb3865d15a1cf15db51a07dd9b25c63f33277c427c3403c189e2ac5e7adc795b254ad0bdb50f1bc4be70f1385bc8522ca502142d41fc26 -SHA512 (pcs-web-ui-node-modules-0.1.23.tar.xz) = f39dd1fd391f10b874b5fec34da1a2f2496d66b31af736ee1dfc2ef6e0b518efd03053fd00312c84e10a33ffda492e12d48a898a21efb6241e5459828e500e80 +SHA512 (dacite-1.8.0.tar.gz) = 97816021b8285197bcd00d9323dd61b33b3a2e51424287ff8e63545082ae9c6b45a416cd2ff2daff41b271a0312192d6a0ec967f6f929402d3c6b8c12b809e08 +SHA512 (pcs-0.11.5.tar.gz) = 2c344537d41416c9af22a18057523b22a1b40b6354ad4b7c7061f1a1690828553ba1d7d10cff0c8adda3c7ef0bee5ce76ece8f2a05d80865f49b71c676fed6aa +SHA512 (pcs-web-ui-0.1.16.1.tar.gz) = 01427f35276cd5ee2926d6541ec9ccce7e86ec592d294dfb08b086ca701e6b937563ac09ba2b5e82b342b58234f41f6cec38bb22599a3b5181db96e0a0382004 +SHA512 (pcs-web-ui-node-modules-0.1.16.1.tar.xz) = 6263f14ba017ed98a17985ee2899f25eb97288f62ef2ded90b0217702bc30d0aa80238f1787e34f6ae6b276df2543451eda44422c1df7cade96617209de5c62d diff --git a/tests/tests.yml b/tests/tests.yml new file mode 100644 index 0000000..098eec7 --- /dev/null +++ b/tests/tests.yml @@ -0,0 +1,72 @@ +- hosts: localhost + tags: + - classic + vars: + # standard-test-basic directory for tests are relative to {{ tenv_workdir }} + tenv_workdir: /var/test + pre_tasks: + - import_role: + name: standard-test-source + vars: + fetch_only: True + - name: Copy files including source to test environment + synchronize: + src: "{{ playbook_dir }}/.." + dest: "{{ tenv_workdir }}" + mode: push + ssh_args: "-o UserKnownHostsFile=/dev/null" + - name: Start pcsd + systemd: + state: started + name: pcsd + daemon_reload: yes + + roles: + # roles documentation: + # https://pagure.io/standard-test-roles/blob/master/f/roles/standard-test-source + - role: standard-test-basic + required_packages: + - autoconf + - automake + - make + - rpm-build + - ruby-devel + - git-core + - booth-site + - fence-agents-apc + - fence-agents-ipmilan + - fence-agents-scsi + - fence-virt + - pcs + - python3-pip + - python3-setuptools_scm + - python3-wheel + - rubygem-test-unit + - wget + required_services: + - pcsd + tests: + # dir: . -> dot means tests dir in distgit + - prepare-source: + dir: ./ + run: rpmbuild -bp {{tenv_workdir}}/*.spec --nodeps --define "_sourcedir {{tenv_workdir}}" --define "_builddir {{tenv_workdir}}/source" + # pcs-web-ui sources are deleted because there are conflicts during move of sources + # pcs-web-ui sources aren't needed during testing + - flatten-source: + dir: ./ + run: rm -rf {{tenv_workdir}}/source/pcs-web-ui-*; shopt -s dotglob; mv {{tenv_workdir}}/source/*/* {{tenv_workdir}}/source + - build_sources: + dir: ./source + run: "export PYTHONPATH=/usr/lib/pcs/pcs_bundled/packages/; ./autogen.sh && ./configure --enable-tests-only --with-pcs-lib-dir=/usr/lib" + - remove_sources: + dir: ./source + run: rm -rfv pcs + - run_upstream_tier0_tests: + dir: ./source + run: pcs_test/suite --tier0 -v --vanilla --installed + - run_upstream_tier1_tests: + dir: ./source + run: pcs_test/suite --tier1 -v --vanilla --installed + - run_smoke_tests: + dir: ./source + run: pcs_test/smoke.sh