Compare commits

..

No commits in common. "rawhide" and "f36" have entirely different histories.

14 changed files with 1412 additions and 639 deletions

View file

@ -1 +0,0 @@
1

17
.gitignore vendored
View file

@ -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

View file

@ -1,63 +0,0 @@
From 72588f150e89b6f06810321245a13157b7e00b09 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz>
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

View file

@ -0,0 +1,67 @@
From b53feb9febd8be85ab8eb90fda02478e4d7e9fea Mon Sep 17 00:00:00 2001
From: Ivan Devat <idevat@redhat.com>
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 (
<Select
variant="typeahead"
- typeAheadAriaLabel="Select a fence device"
+ typeAheadAriaLabel="Select a fence device agent"
+ placeholderText="Select a fence device agent"
onSelect={onSelect}
onClear={onClear}
onFilter={onFilter}
selections={agentName}
isGrouped
- hasInlineFilter
customBadgeText={agentName.length > 0 ? agentName : undefined}
optionsValues={filteredFenceAgentList}
data-test="fence-device-agent"
diff --git a/src/app/view/cluster/resources/task/create/NameTypeTypeSelect.tsx b/src/app/view/cluster/resources/task/create/NameTypeTypeSelect.tsx
index bd7807d8..b531e825 100644
--- a/src/app/view/cluster/resources/task/create/NameTypeTypeSelect.tsx
+++ b/src/app/view/cluster/resources/task/create/NameTypeTypeSelect.tsx
@@ -52,13 +52,13 @@ export const NameTypeTypeSelect = ({
return (
<Select
variant="typeahead"
- typeAheadAriaLabel="Select a state"
+ typeAheadAriaLabel="Select a resource agent"
+ placeholderText="Select a resource agent"
onSelect={onSelect}
onClear={onClear}
onFilter={onFilter}
selections={agentName}
isGrouped
- hasInlineFilter
customBadgeText={agentName.length > 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<HTMLInputElement> | null, value: string) => {
onFilter(value);
- return null as unknown as React.ReactElement[];
+ return undefined;
}
: null;
--
2.39.2

View file

@ -0,0 +1,89 @@
From 9e960df30366a93c3b383b34612451817cc617d8 Mon Sep 17 00:00:00 2001
From: Ivan Devat <idevat@redhat.com>
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<typeof JSON.parse>) =>
+ 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

View file

@ -0,0 +1,84 @@
From 22b275bd8571b7b949b23c5c39d05fbc9e812e12 Mon Sep 17 00:00:00 2001
From: Miroslav Lisik <mlisik@redhat.com>
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

View file

@ -0,0 +1,975 @@
From 16cfc184b4544156df36fa66f7cb5fbf015b18f1 Mon Sep 17 00:00:00 2001
From: Miroslav Lisik <mlisik@redhat.com>
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:
+ <stonith_id>:<stonith_type>:<digest>,...
+
+ 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"""
- <status>
- <node_state id="1" uname="node1">
- <lrm id="1">
- <lrm_resources>
- <lrm_resource id="{resource_id}" type="{resource_type}" class="stonith">
- {lrm_ops}
- </lrm_resource>
- </lrm_resources>
- </lrm>
- </node_state>
- </status>
+ <lrm id="1">
+ <lrm_resources>
+ <lrm_resource id="{resource_id}" type="{resource_type}" class="stonith">
+ {lrm_ops}
+ </lrm_resource>
+ </lrm_resources>
+ </lrm>
+ """
+
+
+def _fixture_digest_nvpair(node_id, digest_name, digest_value):
+ return (
+ f'<nvpair id="status-{node_id}-.{digest_name}" name="#{digest_name}" '
+ f'value="{digest_value}"/>'
+ )
+
+
+def _fixture_transient_attributes(node_id, digests_nvpairs):
+ return f"""
+ <transient_attributes id="{node_id}">
+ <instance_attributes id="status-{node_id}">
+ <nvpair id="status-{node_id}-.feature-set" name="#feature-set" value="3.16.2"/>
+ <nvpair id="status-{node_id}-.node-unfenced" name="#node-unfenced" value="1679319764"/>
+ {digests_nvpairs}
+ </instance_attributes>
+ </transient_attributes>
+ """
+
+
+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"""
+ <node_state id="{node_id}" uname="node{node_id}">
+ {lrm_ops}
+ {transient_attrs}
+ </node_state>
"""
-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"""
+ <status>
+ {node_states}
+ </status>
+ """
+
+def fixture_digests_xml(resource_id, node_name, devices="", nonprivate=True):
+ nonprivate_xml = (
+ f"""
+ <digest type="nonprivate" hash="{NONPRIVATE_DIGEST}">
+ <parameters devices="{devices}"/>
+ </digest>
+ """
+ if nonprivate
+ else ""
+ )
-def fixture_digests_xml(resource_id, node_name, devices=""):
return f"""
<pacemaker-result api-version="2.9" request="crm_resource --digests --resource {resource_id} --node {node_name} --output-as xml devices={devices}">
<digests resource="{resource_id}" node="{node_name}" task="stop" interval="0ms">
<digest type="all" hash="{ALL_DIGEST}">
<parameters devices="{devices}" pcmk_host_check="static-list" pcmk_host_list="node1 node2 node3" pcmk_reboot_action="off"/>
</digest>
- <digest type="nonprivate" hash="{NONPRIVATE_DIGEST}">
- <parameters devices="{devices}"/>
- </digest>
+ {nonprivate_xml}
</digests>
<status code="0" message="OK"/>
</pacemaker-result>
@@ -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

View file

@ -1,32 +0,0 @@
From a2899ef0e20bb638f47e74077281e73f4f18faf3 Mon Sep 17 00:00:00 2001
From: Miroslav Lisik <mlisik@redhat.com>
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

441
pcs.spec
View file

@ -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 <name> — Set Name of Build Directory
# -T — Do Not Perform Default Archive Unpacking
# -b <n> — Unpack The nth Sources Before Changing Directory
# -a <n> — 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 <python-maint@redhat.com> - 0.12.1-5
- Rebuilt for Python 3.14.0rc3 bytecode
* Fri Aug 15 2025 Python Maint <python-maint@redhat.com> - 0.12.1-4
- Rebuilt for Python 3.14.0rc2 bytecode
* Wed Jul 30 2025 Michal Pospíšil <mpospisi@redhat.com> - 0.12.1-3
- Remove BuildRequires: python3-wheel when using setuptools 71+
* Fri Jul 25 2025 Fedora Release Engineering <releng@fedoraproject.org> - 0.12.1-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_43_Mass_Rebuild
* Fri Jul 18 2025 Michal Pospíšil <mpospisi@redhat.com> - 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 <python-maint@redhat.com> - 0.12.0-2
- Rebuilt for Python 3.14
* Mon Jan 27 2025 Michal Pospíšil <mpospisi@redhat.com> - 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 <releng@fedoraproject.org> - 0.11.8-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_42_Mass_Rebuild
* Tue Aug 6 2024 Michal Pospíšil <mpospisi@redhat.com> - 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 <releng@fedoraproject.org> - 0.11.7-5
- Rebuilt for https://fedoraproject.org/wiki/Fedora_41_Mass_Rebuild
* Sun Jun 09 2024 Python Maint <python-maint@redhat.com> - 0.11.7-4
- Rebuilt for Python 3.13
* Mon Feb 5 2024 Michal Pospisil <mpospisi@redhat.com> - 0.11.7-3
- Fixed a bug preventing the Cockpit Application from being installed from Cockpit
* Thu Jan 25 2024 Fedora Release Engineering <releng@fedoraproject.org> - 0.11.7-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild
* Mon Jan 8 2024 Michal Pospisil <mpospisi@redhat.com> - 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 <releng@fedoraproject.org> - 0.11.6-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild
* Wed Jun 28 2023 Python Maint <python-maint@redhat.com> - 0.11.6-2
- Rebuilt for Python 3.12
* Wed Jun 21 2023 Michal Pospisil <mpospisi@redhat.com> - 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 <mpospisi@redhat.com> - 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

View file

@ -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

View file

@ -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

View file

@ -1,148 +0,0 @@
From e7fa056f582c992d3d2b289911be20440fb839ac Mon Sep 17 00:00:00 2001
From: Ivan Devat <idevat@redhat.com>
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 handlers 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 @@
<!DOCTYPE html>
<html>
<head>
- <title>Pcs WebUI instructions</title>
+ <title>Web UI is disabled</title>
<meta charset="utf-8" />
</head>
<body>
- <h1>Pcs WebUI instructions</h1>
+ <h1>Package <code>pcs-web-ui</code> is not installed</h1>
<p>
- WebUI is not a part of pcs repository but it has its own
- <a href="https://github.com/ClusterLabs/pcs-web-ui">repository</a>.
+ Since Fedora 42, pcsd web UI is distributed with pcs as <code>pcs-web-ui
+ </code> package. This<br>package can be uninstalled to disable the web UI.
</p>
<p>
- You can clone <a href="https://github.com/ClusterLabs/pcs-web-ui">WebUI repository</a>
- and build the web application into pcs by:
+ To re-enable pcsd web UI, install the <code>pcs-web-ui</code> RPM package.
</p>
- <pre><code>
- $ npm install
- $ npm run build
- $ mv ./build [/path/to/]pcs/pcsd/public/ui
- </code></pre>
<p>
- For more details, see instructions in
- <a href="https://github.com/ClusterLabs/pcs-web-ui/blob/main/README.md">README.md</a>.
+ Another option is to try pcsd web UI as an application for
+ <a href="https://cockpit-project.org/">Cockpit</a>
+ - a web-based<br>graphical interface for servers. The HA Cluster Management
+ application can be<br>installed from within the Applications pane in
+ Cockpit or by installing the<br><code>cockpit-ha-cluster</code> RPM
+ package.
</p>
</body>
</html>
--
2.50.1

View file

@ -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

72
tests/tests.yml Normal file
View file

@ -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