7606 lines
255 KiB
Diff
7606 lines
255 KiB
Diff
From ec4f8fc199891ad13235729272c0f115918cade9 Mon Sep 17 00:00:00 2001
|
|
From: Tomas Jelinek <tojeline@redhat.com>
|
|
Date: Thu, 21 May 2020 16:51:25 +0200
|
|
Subject: [PATCH 1/3] squash bz1817547 Resource and operation defaults that
|
|
apply to specific resource/operation types
|
|
|
|
add rule parser for rsc and op expressions
|
|
|
|
improvements to rule parser
|
|
|
|
make rule parts independent of the parser
|
|
|
|
export parsed rules into cib
|
|
|
|
add a command for adding new rsc and op defaults
|
|
|
|
display rsc and op defaults with multiple nvsets
|
|
|
|
fix parsing and processing of rsc_expression in rules
|
|
|
|
improve syntax for creating a new nvset
|
|
|
|
make the rule parser produce dataclasses
|
|
|
|
fix for pyparsing-2.4.0
|
|
|
|
add commands for removing rsc and op defaults sets
|
|
|
|
add commands for updating rsc and op defaults sets
|
|
|
|
update chagelog, capabilities
|
|
|
|
add tier1 tests for rules
|
|
|
|
various minor fixes
|
|
|
|
fix routing, create 'defaults update' command
|
|
|
|
better error messages for unallowed rule expressions
|
|
---
|
|
.gitlab-ci.yml | 3 +
|
|
README.md | 1 +
|
|
mypy.ini | 9 +
|
|
pcs.spec.in | 3 +
|
|
pcs/cli/common/lib_wrapper.py | 10 +-
|
|
pcs/cli/nvset.py | 53 ++
|
|
pcs/cli/reports/messages.py | 39 +
|
|
pcs/cli/routing/resource.py | 77 +-
|
|
pcs/cli/rule.py | 89 +++
|
|
pcs/common/interface/dto.py | 9 +-
|
|
pcs/common/pacemaker/nvset.py | 26 +
|
|
pcs/common/pacemaker/rule.py | 28 +
|
|
pcs/common/reports/codes.py | 3 +
|
|
pcs/common/reports/const.py | 6 +
|
|
pcs/common/reports/messages.py | 73 ++
|
|
pcs/common/reports/types.py | 1 +
|
|
pcs/common/str_tools.py | 32 +
|
|
pcs/common/types.py | 13 +
|
|
pcs/config.py | 20 +-
|
|
pcs/lib/cib/nvpair_multi.py | 323 +++++++++
|
|
pcs/lib/cib/rule/__init__.py | 8 +
|
|
pcs/lib/cib/rule/cib_to_dto.py | 185 +++++
|
|
pcs/lib/cib/rule/expression_part.py | 49 ++
|
|
pcs/lib/cib/rule/parsed_to_cib.py | 103 +++
|
|
pcs/lib/cib/rule/parser.py | 232 ++++++
|
|
pcs/lib/cib/rule/validator.py | 62 ++
|
|
pcs/lib/cib/tools.py | 8 +-
|
|
pcs/lib/commands/cib_options.py | 322 ++++++++-
|
|
pcs/lib/validate.py | 15 +
|
|
pcs/lib/xml_tools.py | 9 +-
|
|
pcs/pcs.8 | 86 ++-
|
|
pcs/resource.py | 258 ++++++-
|
|
pcs/usage.py | 94 ++-
|
|
pcs_test/resources/cib-empty-3.1.xml | 2 +-
|
|
pcs_test/resources/cib-empty-3.2.xml | 2 +-
|
|
pcs_test/resources/cib-empty-3.3.xml | 10 +
|
|
pcs_test/resources/cib-empty-3.4.xml | 10 +
|
|
pcs_test/resources/cib-empty.xml | 2 +-
|
|
pcs_test/tier0/cli/reports/test_messages.py | 29 +
|
|
pcs_test/tier0/cli/resource/test_defaults.py | 324 +++++++++
|
|
pcs_test/tier0/cli/test_nvset.py | 92 +++
|
|
pcs_test/tier0/cli/test_rule.py | 477 +++++++++++++
|
|
.../tier0/common/reports/test_messages.py | 55 +-
|
|
pcs_test/tier0/common/test_str_tools.py | 33 +
|
|
.../cib_options => cib/rule}/__init__.py | 0
|
|
.../tier0/lib/cib/rule/test_cib_to_dto.py | 593 ++++++++++++++++
|
|
.../tier0/lib/cib/rule/test_parsed_to_cib.py | 214 ++++++
|
|
pcs_test/tier0/lib/cib/rule/test_parser.py | 270 +++++++
|
|
pcs_test/tier0/lib/cib/rule/test_validator.py | 68 ++
|
|
pcs_test/tier0/lib/cib/test_nvpair_multi.py | 513 ++++++++++++++
|
|
pcs_test/tier0/lib/cib/test_tools.py | 13 +-
|
|
.../cib_options/test_operations_defaults.py | 120 ----
|
|
.../cib_options/test_resources_defaults.py | 120 ----
|
|
.../tier0/lib/commands/test_cib_options.py | 669 ++++++++++++++++++
|
|
pcs_test/tier0/lib/test_validate.py | 27 +
|
|
pcs_test/tier1/legacy/test_resource.py | 8 +-
|
|
pcs_test/tier1/legacy/test_stonith.py | 8 +-
|
|
pcs_test/tier1/test_cib_options.py | 571 +++++++++++++++
|
|
pcs_test/tier1/test_tag.py | 4 +-
|
|
pcs_test/tools/fixture.py | 4 +-
|
|
pcs_test/tools/misc.py | 61 +-
|
|
pcsd/capabilities.xml | 30 +
|
|
test/centos8/Dockerfile | 1 +
|
|
test/fedora30/Dockerfile | 1 +
|
|
test/fedora31/Dockerfile | 1 +
|
|
test/fedora32/Dockerfile | 1 +
|
|
66 files changed, 6216 insertions(+), 366 deletions(-)
|
|
create mode 100644 pcs/cli/nvset.py
|
|
create mode 100644 pcs/cli/rule.py
|
|
create mode 100644 pcs/common/pacemaker/nvset.py
|
|
create mode 100644 pcs/common/pacemaker/rule.py
|
|
create mode 100644 pcs/lib/cib/nvpair_multi.py
|
|
create mode 100644 pcs/lib/cib/rule/__init__.py
|
|
create mode 100644 pcs/lib/cib/rule/cib_to_dto.py
|
|
create mode 100644 pcs/lib/cib/rule/expression_part.py
|
|
create mode 100644 pcs/lib/cib/rule/parsed_to_cib.py
|
|
create mode 100644 pcs/lib/cib/rule/parser.py
|
|
create mode 100644 pcs/lib/cib/rule/validator.py
|
|
create mode 100644 pcs_test/resources/cib-empty-3.3.xml
|
|
create mode 100644 pcs_test/resources/cib-empty-3.4.xml
|
|
create mode 100644 pcs_test/tier0/cli/resource/test_defaults.py
|
|
create mode 100644 pcs_test/tier0/cli/test_nvset.py
|
|
create mode 100644 pcs_test/tier0/cli/test_rule.py
|
|
rename pcs_test/tier0/lib/{commands/cib_options => cib/rule}/__init__.py (100%)
|
|
create mode 100644 pcs_test/tier0/lib/cib/rule/test_cib_to_dto.py
|
|
create mode 100644 pcs_test/tier0/lib/cib/rule/test_parsed_to_cib.py
|
|
create mode 100644 pcs_test/tier0/lib/cib/rule/test_parser.py
|
|
create mode 100644 pcs_test/tier0/lib/cib/rule/test_validator.py
|
|
create mode 100644 pcs_test/tier0/lib/cib/test_nvpair_multi.py
|
|
delete mode 100644 pcs_test/tier0/lib/commands/cib_options/test_operations_defaults.py
|
|
delete mode 100644 pcs_test/tier0/lib/commands/cib_options/test_resources_defaults.py
|
|
create mode 100644 pcs_test/tier0/lib/commands/test_cib_options.py
|
|
create mode 100644 pcs_test/tier1/test_cib_options.py
|
|
|
|
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
|
|
index 83eba12d..24444b72 100644
|
|
--- a/.gitlab-ci.yml
|
|
+++ b/.gitlab-ci.yml
|
|
@@ -51,6 +51,7 @@ pylint:
|
|
python3-pip
|
|
python3-pycurl
|
|
python3-pyOpenSSL
|
|
+ python3-pyparsing
|
|
findutils
|
|
make
|
|
time
|
|
@@ -69,6 +70,7 @@ mypy:
|
|
python3-pip
|
|
python3-pycurl
|
|
python3-pyOpenSSL
|
|
+ python3-pyparsing
|
|
git
|
|
make
|
|
tar
|
|
@@ -112,6 +114,7 @@ python_tier0_tests:
|
|
python3-pip
|
|
python3-pycurl
|
|
python3-pyOpenSSL
|
|
+ python3-pyparsing
|
|
which
|
|
"
|
|
- make install_pip
|
|
diff --git a/README.md b/README.md
|
|
index f888da68..efb4d0d5 100644
|
|
--- a/README.md
|
|
+++ b/README.md
|
|
@@ -30,6 +30,7 @@ These are the runtime dependencies of pcs and pcsd:
|
|
* python3-pycurl
|
|
* python3-setuptools
|
|
* python3-pyOpenSSL (python3-openssl)
|
|
+* python3-pyparsing
|
|
* python3-tornado 6.x
|
|
* python dataclasses (`pip install dataclasses`; required only for python 3.6,
|
|
already included in 3.7+)
|
|
diff --git a/mypy.ini b/mypy.ini
|
|
index ad3d1f18..ac6789a9 100644
|
|
--- a/mypy.ini
|
|
+++ b/mypy.ini
|
|
@@ -8,12 +8,18 @@ disallow_untyped_defs = True
|
|
[mypy-pcs.lib.cib.resource.relations]
|
|
disallow_untyped_defs = True
|
|
|
|
+[mypy-pcs.lib.cib.rule]
|
|
+disallow_untyped_defs = True
|
|
+
|
|
[mypy-pcs.lib.cib.tag]
|
|
disallow_untyped_defs = True
|
|
|
|
[mypy-pcs.lib.commands.tag]
|
|
disallow_untyped_defs = True
|
|
|
|
+[mypy-pcs.lib.commands.cib_options]
|
|
+disallow_untyped_defs = True
|
|
+
|
|
[mypy-pcs.lib.dr.*]
|
|
disallow_untyped_defs = True
|
|
disallow_untyped_calls = True
|
|
@@ -84,3 +90,6 @@ ignore_missing_imports = True
|
|
|
|
[mypy-distro]
|
|
ignore_missing_imports = True
|
|
+
|
|
+[mypy-pyparsing]
|
|
+ignore_missing_imports = True
|
|
diff --git a/pcs.spec.in b/pcs.spec.in
|
|
index c52c2fe4..e292a708 100644
|
|
--- a/pcs.spec.in
|
|
+++ b/pcs.spec.in
|
|
@@ -122,6 +122,8 @@ BuildRequires: platform-python-setuptools
|
|
%endif
|
|
|
|
BuildRequires: python3-devel
|
|
+# for tier0 tests
|
|
+BuildRequires: python3-pyparsing
|
|
|
|
# gcc for compiling custom rubygems
|
|
BuildRequires: gcc
|
|
@@ -155,6 +157,7 @@ Requires: platform-python-setuptools
|
|
|
|
Requires: python3-lxml
|
|
Requires: python3-pycurl
|
|
+Requires: python3-pyparsing
|
|
# clufter and its dependencies
|
|
Requires: python3-clufter => 0.70.0
|
|
%if "%{python3_version}" != "3.6" && "%{python3_version}" != "3.7"
|
|
diff --git a/pcs/cli/common/lib_wrapper.py b/pcs/cli/common/lib_wrapper.py
|
|
index 9fd05ac0..192a3dac 100644
|
|
--- a/pcs/cli/common/lib_wrapper.py
|
|
+++ b/pcs/cli/common/lib_wrapper.py
|
|
@@ -388,8 +388,14 @@ def load_module(env, middleware_factory, name):
|
|
env,
|
|
middleware.build(middleware_factory.cib,),
|
|
{
|
|
- "set_operations_defaults": cib_options.set_operations_defaults,
|
|
- "set_resources_defaults": cib_options.set_resources_defaults,
|
|
+ "operation_defaults_config": cib_options.operation_defaults_config,
|
|
+ "operation_defaults_create": cib_options.operation_defaults_create,
|
|
+ "operation_defaults_remove": cib_options.operation_defaults_remove,
|
|
+ "operation_defaults_update": cib_options.operation_defaults_update,
|
|
+ "resource_defaults_config": cib_options.resource_defaults_config,
|
|
+ "resource_defaults_create": cib_options.resource_defaults_create,
|
|
+ "resource_defaults_remove": cib_options.resource_defaults_remove,
|
|
+ "resource_defaults_update": cib_options.resource_defaults_update,
|
|
},
|
|
)
|
|
|
|
diff --git a/pcs/cli/nvset.py b/pcs/cli/nvset.py
|
|
new file mode 100644
|
|
index 00000000..69442df3
|
|
--- /dev/null
|
|
+++ b/pcs/cli/nvset.py
|
|
@@ -0,0 +1,53 @@
|
|
+from typing import (
|
|
+ cast,
|
|
+ Iterable,
|
|
+ List,
|
|
+ Optional,
|
|
+)
|
|
+
|
|
+from pcs.cli.rule import rule_expression_dto_to_lines
|
|
+from pcs.common.pacemaker.nvset import CibNvsetDto
|
|
+from pcs.common.str_tools import (
|
|
+ format_name_value_list,
|
|
+ indent,
|
|
+)
|
|
+from pcs.common.types import CibNvsetType
|
|
+
|
|
+
|
|
+def nvset_dto_list_to_lines(
|
|
+ nvset_dto_list: Iterable[CibNvsetDto],
|
|
+ with_ids: bool = False,
|
|
+ text_if_empty: Optional[str] = None,
|
|
+) -> List[str]:
|
|
+ if not nvset_dto_list:
|
|
+ return [text_if_empty] if text_if_empty else []
|
|
+ return [
|
|
+ line
|
|
+ for nvset_dto in nvset_dto_list
|
|
+ for line in nvset_dto_to_lines(nvset_dto, with_ids=with_ids)
|
|
+ ]
|
|
+
|
|
+
|
|
+def nvset_dto_to_lines(nvset: CibNvsetDto, with_ids: bool = False) -> List[str]:
|
|
+ nvset_label = _nvset_type_to_label.get(nvset.type, "Options Set")
|
|
+ heading_parts = [f"{nvset_label}: {nvset.id}"]
|
|
+ if nvset.options:
|
|
+ heading_parts.append(
|
|
+ " ".join(format_name_value_list(sorted(nvset.options.items())))
|
|
+ )
|
|
+
|
|
+ lines = format_name_value_list(
|
|
+ sorted([(nvpair.name, nvpair.value) for nvpair in nvset.nvpairs])
|
|
+ )
|
|
+ if nvset.rule:
|
|
+ lines.extend(
|
|
+ rule_expression_dto_to_lines(nvset.rule, with_ids=with_ids)
|
|
+ )
|
|
+
|
|
+ return [" ".join(heading_parts)] + indent(lines)
|
|
+
|
|
+
|
|
+_nvset_type_to_label = {
|
|
+ cast(str, CibNvsetType.INSTANCE): "Attributes",
|
|
+ cast(str, CibNvsetType.META): "Meta Attrs",
|
|
+}
|
|
diff --git a/pcs/cli/reports/messages.py b/pcs/cli/reports/messages.py
|
|
index 36f00a9e..7ccc8ab0 100644
|
|
--- a/pcs/cli/reports/messages.py
|
|
+++ b/pcs/cli/reports/messages.py
|
|
@@ -402,6 +402,45 @@ class TagCannotRemoveReferencesWithoutRemovingTag(CliReportMessageCustom):
|
|
)
|
|
|
|
|
|
+class RuleExpressionParseError(CliReportMessageCustom):
|
|
+ _obj: messages.RuleExpressionParseError
|
|
+
|
|
+ @property
|
|
+ def message(self) -> str:
|
|
+ # Messages coming from the parser are not very useful and readable,
|
|
+ # they mostly contain one line grammar expression covering the whole
|
|
+ # rule. No user would be able to parse that. Therefore we omit the
|
|
+ # messages.
|
|
+ marker = "-" * (self._obj.column_number - 1) + "^"
|
|
+ return (
|
|
+ f"'{self._obj.rule_string}' is not a valid rule expression, parse "
|
|
+ f"error near or after line {self._obj.line_number} column "
|
|
+ f"{self._obj.column_number}\n"
|
|
+ f" {self._obj.rule_line}\n"
|
|
+ f" {marker}"
|
|
+ )
|
|
+
|
|
+
|
|
+class CibNvsetAmbiguousProvideNvsetId(CliReportMessageCustom):
|
|
+ _obj: messages.CibNvsetAmbiguousProvideNvsetId
|
|
+
|
|
+ @property
|
|
+ def message(self) -> str:
|
|
+ command_map = {
|
|
+ const.PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE: (
|
|
+ "pcs resource defaults set update"
|
|
+ ),
|
|
+ const.PCS_COMMAND_OPERATION_DEFAULTS_UPDATE: (
|
|
+ "pcs resource op defaults set update"
|
|
+ ),
|
|
+ }
|
|
+ command = command_map.get(self._obj.pcs_command, "")
|
|
+ return (
|
|
+ f"Several options sets exist, please use the '{command}' command "
|
|
+ "and specify an option set ID"
|
|
+ )
|
|
+
|
|
+
|
|
def _create_report_msg_map() -> Dict[str, type]:
|
|
result: Dict[str, type] = {}
|
|
for report_msg_cls in get_all_subclasses(CliReportMessageCustom):
|
|
diff --git a/pcs/cli/routing/resource.py b/pcs/cli/routing/resource.py
|
|
index 28bb3d5e..0706f43b 100644
|
|
--- a/pcs/cli/routing/resource.py
|
|
+++ b/pcs/cli/routing/resource.py
|
|
@@ -1,15 +1,88 @@
|
|
from functools import partial
|
|
+from typing import (
|
|
+ Any,
|
|
+ List,
|
|
+)
|
|
|
|
from pcs import (
|
|
resource,
|
|
usage,
|
|
)
|
|
from pcs.cli.common.errors import raise_command_replaced
|
|
+from pcs.cli.common.parse_args import InputModifiers
|
|
from pcs.cli.common.routing import create_router
|
|
|
|
from pcs.cli.resource.relations import show_resource_relations_cmd
|
|
|
|
|
|
+def resource_defaults_cmd(
|
|
+ lib: Any, argv: List[str], modifiers: InputModifiers
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ * --force - allow unknown options
|
|
+ """
|
|
+ if argv and "=" in argv[0]:
|
|
+ # DEPRECATED legacy command
|
|
+ return resource.resource_defaults_legacy_cmd(
|
|
+ lib, argv, modifiers, deprecated_syntax_used=True
|
|
+ )
|
|
+
|
|
+ router = create_router(
|
|
+ {
|
|
+ "config": resource.resource_defaults_config_cmd,
|
|
+ "set": create_router(
|
|
+ {
|
|
+ "create": resource.resource_defaults_set_create_cmd,
|
|
+ "delete": resource.resource_defaults_set_remove_cmd,
|
|
+ "remove": resource.resource_defaults_set_remove_cmd,
|
|
+ "update": resource.resource_defaults_set_update_cmd,
|
|
+ },
|
|
+ ["resource", "defaults", "set"],
|
|
+ ),
|
|
+ "update": resource.resource_defaults_legacy_cmd,
|
|
+ },
|
|
+ ["resource", "defaults"],
|
|
+ default_cmd="config",
|
|
+ )
|
|
+ return router(lib, argv, modifiers)
|
|
+
|
|
+
|
|
+def resource_op_defaults_cmd(
|
|
+ lib: Any, argv: List[str], modifiers: InputModifiers
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ * --force - allow unknown options
|
|
+ """
|
|
+ if argv and "=" in argv[0]:
|
|
+ # DEPRECATED legacy command
|
|
+ return resource.resource_op_defaults_legacy_cmd(
|
|
+ lib, argv, modifiers, deprecated_syntax_used=True
|
|
+ )
|
|
+
|
|
+ router = create_router(
|
|
+ {
|
|
+ "config": resource.resource_op_defaults_config_cmd,
|
|
+ "set": create_router(
|
|
+ {
|
|
+ "create": resource.resource_op_defaults_set_create_cmd,
|
|
+ "delete": resource.resource_op_defaults_set_remove_cmd,
|
|
+ "remove": resource.resource_op_defaults_set_remove_cmd,
|
|
+ "update": resource.resource_op_defaults_set_update_cmd,
|
|
+ },
|
|
+ ["resource", "op", "defaults", "set"],
|
|
+ ),
|
|
+ "update": resource.resource_op_defaults_legacy_cmd,
|
|
+ },
|
|
+ ["resource", "op", "defaults"],
|
|
+ default_cmd="config",
|
|
+ )
|
|
+ return router(lib, argv, modifiers)
|
|
+
|
|
+
|
|
resource_cmd = create_router(
|
|
{
|
|
"help": lambda lib, argv, modifiers: usage.resource(argv),
|
|
@@ -68,14 +141,14 @@ resource_cmd = create_router(
|
|
"failcount": resource.resource_failcount,
|
|
"op": create_router(
|
|
{
|
|
- "defaults": resource.resource_op_defaults_cmd,
|
|
+ "defaults": resource_op_defaults_cmd,
|
|
"add": resource.resource_op_add_cmd,
|
|
"remove": resource.resource_op_delete_cmd,
|
|
"delete": resource.resource_op_delete_cmd,
|
|
},
|
|
["resource", "op"],
|
|
),
|
|
- "defaults": resource.resource_defaults_cmd,
|
|
+ "defaults": resource_defaults_cmd,
|
|
"cleanup": resource.resource_cleanup,
|
|
"refresh": resource.resource_refresh,
|
|
"relocate": create_router(
|
|
diff --git a/pcs/cli/rule.py b/pcs/cli/rule.py
|
|
new file mode 100644
|
|
index 00000000..c1149fff
|
|
--- /dev/null
|
|
+++ b/pcs/cli/rule.py
|
|
@@ -0,0 +1,89 @@
|
|
+from typing import List
|
|
+
|
|
+from pcs.common.pacemaker.rule import CibRuleExpressionDto
|
|
+from pcs.common.str_tools import (
|
|
+ format_name_value_list,
|
|
+ indent,
|
|
+)
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
+
|
|
+
|
|
+def rule_expression_dto_to_lines(
|
|
+ rule_expr: CibRuleExpressionDto, with_ids: bool = False
|
|
+) -> List[str]:
|
|
+ if rule_expr.type == CibRuleExpressionType.RULE:
|
|
+ return _rule_dto_to_lines(rule_expr, with_ids)
|
|
+ if rule_expr.type == CibRuleExpressionType.DATE_EXPRESSION:
|
|
+ return _date_dto_to_lines(rule_expr, with_ids)
|
|
+ return _simple_expr_to_lines(rule_expr, with_ids)
|
|
+
|
|
+
|
|
+def _rule_dto_to_lines(
|
|
+ rule_expr: CibRuleExpressionDto, with_ids: bool = False
|
|
+) -> List[str]:
|
|
+ heading_parts = [
|
|
+ "Rule{0}:".format(" (expired)" if rule_expr.is_expired else "")
|
|
+ ]
|
|
+ heading_parts.extend(
|
|
+ format_name_value_list(sorted(rule_expr.options.items()))
|
|
+ )
|
|
+ if with_ids:
|
|
+ heading_parts.append(f"(id:{rule_expr.id})")
|
|
+
|
|
+ lines = []
|
|
+ for child in rule_expr.expressions:
|
|
+ lines.extend(rule_expression_dto_to_lines(child, with_ids))
|
|
+
|
|
+ return [" ".join(heading_parts)] + indent(lines)
|
|
+
|
|
+
|
|
+def _date_dto_to_lines(
|
|
+ rule_expr: CibRuleExpressionDto, with_ids: bool = False
|
|
+) -> List[str]:
|
|
+ # pylint: disable=too-many-branches
|
|
+ operation = rule_expr.options.get("operation", None)
|
|
+
|
|
+ if operation == "date_spec":
|
|
+ heading_parts = ["Expression:"]
|
|
+ if with_ids:
|
|
+ heading_parts.append(f"(id:{rule_expr.id})")
|
|
+ line_parts = ["Date Spec:"]
|
|
+ if rule_expr.date_spec:
|
|
+ line_parts.extend(
|
|
+ format_name_value_list(
|
|
+ sorted(rule_expr.date_spec.options.items())
|
|
+ )
|
|
+ )
|
|
+ if with_ids:
|
|
+ line_parts.append(f"(id:{rule_expr.date_spec.id})")
|
|
+ return [" ".join(heading_parts)] + indent([" ".join(line_parts)])
|
|
+
|
|
+ if operation == "in_range" and rule_expr.duration:
|
|
+ heading_parts = ["Expression:", "date", "in_range"]
|
|
+ if "start" in rule_expr.options:
|
|
+ heading_parts.append(rule_expr.options["start"])
|
|
+ heading_parts.extend(["to", "duration"])
|
|
+ if with_ids:
|
|
+ heading_parts.append(f"(id:{rule_expr.id})")
|
|
+ lines = [" ".join(heading_parts)]
|
|
+
|
|
+ line_parts = ["Duration:"]
|
|
+ line_parts.extend(
|
|
+ format_name_value_list(sorted(rule_expr.duration.options.items()))
|
|
+ )
|
|
+ if with_ids:
|
|
+ line_parts.append(f"(id:{rule_expr.duration.id})")
|
|
+ lines.extend(indent([" ".join(line_parts)]))
|
|
+
|
|
+ return lines
|
|
+
|
|
+ return _simple_expr_to_lines(rule_expr, with_ids=with_ids)
|
|
+
|
|
+
|
|
+def _simple_expr_to_lines(
|
|
+ rule_expr: CibRuleExpressionDto, with_ids: bool = False
|
|
+) -> List[str]:
|
|
+ parts = ["Expression:", rule_expr.as_string]
|
|
+ if with_ids:
|
|
+ parts.append(f"(id:{rule_expr.id})")
|
|
+ return [" ".join(parts)]
|
|
diff --git a/pcs/common/interface/dto.py b/pcs/common/interface/dto.py
|
|
index fb40fc5e..768156d6 100644
|
|
--- a/pcs/common/interface/dto.py
|
|
+++ b/pcs/common/interface/dto.py
|
|
@@ -42,7 +42,14 @@ def from_dict(cls: Type[DtoType], data: DtoPayload) -> DtoType:
|
|
data=data,
|
|
# NOTE: all enum types has to be listed here in key cast
|
|
# see: https://github.com/konradhalas/dacite#casting
|
|
- config=dacite.Config(cast=[types.DrRole, types.ResourceRelationType,],),
|
|
+ config=dacite.Config(
|
|
+ cast=[
|
|
+ types.CibNvsetType,
|
|
+ types.CibRuleExpressionType,
|
|
+ types.DrRole,
|
|
+ types.ResourceRelationType,
|
|
+ ]
|
|
+ ),
|
|
)
|
|
|
|
|
|
diff --git a/pcs/common/pacemaker/nvset.py b/pcs/common/pacemaker/nvset.py
|
|
new file mode 100644
|
|
index 00000000..6d72c787
|
|
--- /dev/null
|
|
+++ b/pcs/common/pacemaker/nvset.py
|
|
@@ -0,0 +1,26 @@
|
|
+from dataclasses import dataclass
|
|
+from typing import (
|
|
+ Mapping,
|
|
+ Optional,
|
|
+ Sequence,
|
|
+)
|
|
+
|
|
+from pcs.common.interface.dto import DataTransferObject
|
|
+from pcs.common.pacemaker.rule import CibRuleExpressionDto
|
|
+from pcs.common.types import CibNvsetType
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class CibNvpairDto(DataTransferObject):
|
|
+ id: str # pylint: disable=invalid-name
|
|
+ name: str
|
|
+ value: str
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class CibNvsetDto(DataTransferObject):
|
|
+ id: str # pylint: disable=invalid-name
|
|
+ type: CibNvsetType
|
|
+ options: Mapping[str, str]
|
|
+ rule: Optional[CibRuleExpressionDto]
|
|
+ nvpairs: Sequence[CibNvpairDto]
|
|
diff --git a/pcs/common/pacemaker/rule.py b/pcs/common/pacemaker/rule.py
|
|
new file mode 100644
|
|
index 00000000..306e65e6
|
|
--- /dev/null
|
|
+++ b/pcs/common/pacemaker/rule.py
|
|
@@ -0,0 +1,28 @@
|
|
+from dataclasses import dataclass
|
|
+from typing import (
|
|
+ Mapping,
|
|
+ Optional,
|
|
+ Sequence,
|
|
+)
|
|
+
|
|
+from pcs.common.interface.dto import DataTransferObject
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class CibRuleDateCommonDto(DataTransferObject):
|
|
+ id: str # pylint: disable=invalid-name
|
|
+ options: Mapping[str, str]
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class CibRuleExpressionDto(DataTransferObject):
|
|
+ # pylint: disable=too-many-instance-attributes
|
|
+ id: str # pylint: disable=invalid-name
|
|
+ type: CibRuleExpressionType
|
|
+ is_expired: bool # only valid for type==rule
|
|
+ options: Mapping[str, str]
|
|
+ date_spec: Optional[CibRuleDateCommonDto]
|
|
+ duration: Optional[CibRuleDateCommonDto]
|
|
+ expressions: Sequence["CibRuleExpressionDto"]
|
|
+ as_string: str
|
|
diff --git a/pcs/common/reports/codes.py b/pcs/common/reports/codes.py
|
|
index 26eb8b51..8bcabfab 100644
|
|
--- a/pcs/common/reports/codes.py
|
|
+++ b/pcs/common/reports/codes.py
|
|
@@ -123,6 +123,7 @@ CIB_LOAD_ERROR = M("CIB_LOAD_ERROR")
|
|
CIB_LOAD_ERROR_GET_NODES_FOR_VALIDATION = M(
|
|
"CIB_LOAD_ERROR_GET_NODES_FOR_VALIDATION"
|
|
)
|
|
+CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID = M("CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID")
|
|
CIB_LOAD_ERROR_SCOPE_MISSING = M("CIB_LOAD_ERROR_SCOPE_MISSING")
|
|
CIB_PUSH_FORCED_FULL_DUE_TO_CRM_FEATURE_SET = M(
|
|
"CIB_PUSH_FORCED_FULL_DUE_TO_CRM_FEATURE_SET"
|
|
@@ -405,6 +406,8 @@ RESOURCE_UNMOVE_UNBAN_PCMK_SUCCESS = M("RESOURCE_UNMOVE_UNBAN_PCMK_SUCCESS")
|
|
RESOURCE_UNMOVE_UNBAN_PCMK_EXPIRED_NOT_SUPPORTED = M(
|
|
"RESOURCE_UNMOVE_UNBAN_PCMK_EXPIRED_NOT_SUPPORTED"
|
|
)
|
|
+RULE_EXPRESSION_PARSE_ERROR = M("RULE_EXPRESSION_PARSE_ERROR")
|
|
+RULE_EXPRESSION_NOT_ALLOWED = M("RULE_EXPRESSION_NOT_ALLOWED")
|
|
RUN_EXTERNAL_PROCESS_ERROR = M("RUN_EXTERNAL_PROCESS_ERROR")
|
|
RUN_EXTERNAL_PROCESS_FINISHED = M("RUN_EXTERNAL_PROCESS_FINISHED")
|
|
RUN_EXTERNAL_PROCESS_STARTED = M("RUN_EXTERNAL_PROCESS_STARTED")
|
|
diff --git a/pcs/common/reports/const.py b/pcs/common/reports/const.py
|
|
index aeb593ee..fa2122d0 100644
|
|
--- a/pcs/common/reports/const.py
|
|
+++ b/pcs/common/reports/const.py
|
|
@@ -1,9 +1,15 @@
|
|
from .types import (
|
|
DefaultAddressSource,
|
|
+ PcsCommand,
|
|
ReasonType,
|
|
ServiceAction,
|
|
)
|
|
|
|
+PCS_COMMAND_OPERATION_DEFAULTS_UPDATE = PcsCommand(
|
|
+ "resource op defaults update"
|
|
+)
|
|
+PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE = PcsCommand("resource defaults update")
|
|
+
|
|
SERVICE_ACTION_START = ServiceAction("START")
|
|
SERVICE_ACTION_STOP = ServiceAction("STOP")
|
|
SERVICE_ACTION_ENABLE = ServiceAction("ENABLE")
|
|
diff --git a/pcs/common/reports/messages.py b/pcs/common/reports/messages.py
|
|
index 540e8c69..f04d8632 100644
|
|
--- a/pcs/common/reports/messages.py
|
|
+++ b/pcs/common/reports/messages.py
|
|
@@ -27,6 +27,7 @@ from pcs.common.str_tools import (
|
|
indent,
|
|
is_iterable_not_str,
|
|
)
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
|
|
from . import (
|
|
codes,
|
|
@@ -120,6 +121,7 @@ _type_articles = {
|
|
"ACL user": "an",
|
|
"ACL role": "an",
|
|
"ACL permission": "an",
|
|
+ "options set": "an",
|
|
}
|
|
|
|
|
|
@@ -6399,3 +6401,74 @@ class TagIdsNotInTheTag(ReportItemMessage):
|
|
ids=format_plural(self.id_list, "id"),
|
|
id_list=format_list(self.id_list),
|
|
)
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class RuleExpressionParseError(ReportItemMessage):
|
|
+ """
|
|
+ Unable to parse pacemaker cib rule expression string
|
|
+
|
|
+ rule_string -- the whole rule expression string
|
|
+ reason -- error message from rule parser
|
|
+ rule_line -- part of rule_string - the line where the error occurred
|
|
+ line_number -- the line where parsing failed
|
|
+ column_number -- the column where parsing failed
|
|
+ position -- the start index where parsing failed
|
|
+ """
|
|
+
|
|
+ rule_string: str
|
|
+ reason: str
|
|
+ rule_line: str
|
|
+ line_number: int
|
|
+ column_number: int
|
|
+ position: int
|
|
+ _code = codes.RULE_EXPRESSION_PARSE_ERROR
|
|
+
|
|
+ @property
|
|
+ def message(self) -> str:
|
|
+ # Messages coming from the parser are not very useful and readable,
|
|
+ # they mostly contain one line grammar expression covering the whole
|
|
+ # rule. No user would be able to parse that. Therefore we omit the
|
|
+ # messages.
|
|
+ return (
|
|
+ f"'{self.rule_string}' is not a valid rule expression, parse error "
|
|
+ f"near or after line {self.line_number} column {self.column_number}"
|
|
+ )
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class RuleExpressionNotAllowed(ReportItemMessage):
|
|
+ """
|
|
+ Used rule expression is not allowed in current context
|
|
+
|
|
+ expression_type -- disallowed expression type
|
|
+ """
|
|
+
|
|
+ expression_type: CibRuleExpressionType
|
|
+ _code = codes.RULE_EXPRESSION_NOT_ALLOWED
|
|
+
|
|
+ @property
|
|
+ def message(self) -> str:
|
|
+ type_map = {
|
|
+ CibRuleExpressionType.OP_EXPRESSION: "op",
|
|
+ CibRuleExpressionType.RSC_EXPRESSION: "resource",
|
|
+ }
|
|
+ return (
|
|
+ f"Keyword '{type_map[self.expression_type]}' cannot be used "
|
|
+ "in a rule in this command"
|
|
+ )
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class CibNvsetAmbiguousProvideNvsetId(ReportItemMessage):
|
|
+ """
|
|
+ An old command supporting only one nvset have been used when several nvsets
|
|
+ exist. We require an nvset ID the command should work with to be specified.
|
|
+ """
|
|
+
|
|
+ pcs_command: types.PcsCommand
|
|
+ _code = codes.CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID
|
|
+
|
|
+ @property
|
|
+ def message(self) -> str:
|
|
+ return "Several options sets exist, please specify an option set ID"
|
|
diff --git a/pcs/common/reports/types.py b/pcs/common/reports/types.py
|
|
index 5973279e..541046ea 100644
|
|
--- a/pcs/common/reports/types.py
|
|
+++ b/pcs/common/reports/types.py
|
|
@@ -3,6 +3,7 @@ from typing import NewType
|
|
DefaultAddressSource = NewType("DefaultAddressSource", str)
|
|
ForceCode = NewType("ForceCode", str)
|
|
MessageCode = NewType("MessageCode", str)
|
|
+PcsCommand = NewType("PcsCommand", str)
|
|
ReasonType = NewType("ReasonType", str)
|
|
ServiceAction = NewType("ServiceAction", str)
|
|
SeverityLevel = NewType("SeverityLevel", str)
|
|
diff --git a/pcs/common/str_tools.py b/pcs/common/str_tools.py
|
|
index deb38799..80864b50 100644
|
|
--- a/pcs/common/str_tools.py
|
|
+++ b/pcs/common/str_tools.py
|
|
@@ -3,6 +3,8 @@ from typing import (
|
|
Any,
|
|
List,
|
|
Mapping,
|
|
+ Sequence,
|
|
+ Tuple,
|
|
TypeVar,
|
|
)
|
|
|
|
@@ -49,6 +51,36 @@ def format_list_custom_last_separator(
|
|
)
|
|
|
|
|
|
+# For now, Tuple[str, str] is sufficient. Feel free to change it if needed,
|
|
+# e.g. when values can be integers.
|
|
+def format_name_value_list(item_list: Sequence[Tuple[str, str]]) -> List[str]:
|
|
+ """
|
|
+ Turn 2-tuples to 'name=value' strings with standard quoting
|
|
+ """
|
|
+ output = []
|
|
+ for name, value in item_list:
|
|
+ name = quote(name, "= ")
|
|
+ value = quote(value, "= ")
|
|
+ output.append(f"{name}={value}")
|
|
+ return output
|
|
+
|
|
+
|
|
+def quote(string: str, chars_to_quote: str) -> str:
|
|
+ """
|
|
+ Quote a string if it contains specified characters
|
|
+
|
|
+ string -- the string to be processed
|
|
+ chars_to_quote -- the characters causing quoting
|
|
+ """
|
|
+ if not frozenset(chars_to_quote) & frozenset(string):
|
|
+ return string
|
|
+ if '"' not in string:
|
|
+ return f'"{string}"'
|
|
+ if "'" not in string:
|
|
+ return f"'{string}'"
|
|
+ return '"{string}"'.format(string=string.replace('"', '\\"'))
|
|
+
|
|
+
|
|
def join_multilines(strings):
|
|
return "\n".join([a.strip() for a in strings if a.strip()])
|
|
|
|
diff --git a/pcs/common/types.py b/pcs/common/types.py
|
|
index dace6f6d..0b656cc0 100644
|
|
--- a/pcs/common/types.py
|
|
+++ b/pcs/common/types.py
|
|
@@ -3,6 +3,19 @@ from enum import auto
|
|
from pcs.common.tools import AutoNameEnum
|
|
|
|
|
|
+class CibNvsetType(AutoNameEnum):
|
|
+ INSTANCE = auto()
|
|
+ META = auto()
|
|
+
|
|
+
|
|
+class CibRuleExpressionType(AutoNameEnum):
|
|
+ RULE = auto()
|
|
+ EXPRESSION = auto()
|
|
+ DATE_EXPRESSION = auto()
|
|
+ OP_EXPRESSION = auto()
|
|
+ RSC_EXPRESSION = auto()
|
|
+
|
|
+
|
|
class ResourceRelationType(AutoNameEnum):
|
|
ORDER = auto()
|
|
ORDER_SET = auto()
|
|
diff --git a/pcs/config.py b/pcs/config.py
|
|
index 058ec55a..67aa6e0e 100644
|
|
--- a/pcs/config.py
|
|
+++ b/pcs/config.py
|
|
@@ -48,6 +48,7 @@ from pcs import (
|
|
from pcs.cli.common import middleware
|
|
from pcs.cli.common.errors import CmdLineInputError
|
|
from pcs.cli.constraint import command as constraint_command
|
|
+from pcs.cli.nvset import nvset_dto_list_to_lines
|
|
from pcs.cli.reports import process_library_reports
|
|
from pcs.common.reports import constraints as constraints_reports
|
|
from pcs.common.str_tools import indent
|
|
@@ -96,7 +97,8 @@ def _config_show_cib_lines(lib):
|
|
Commandline options:
|
|
* -f - CIB file
|
|
"""
|
|
- # update of pcs_options will change output of constraint show
|
|
+ # update of pcs_options will change output of constraint show and
|
|
+ # displaying resources and operations defaults
|
|
utils.pcs_options["--full"] = 1
|
|
# get latest modifiers object after updating pcs_options
|
|
modifiers = utils.get_input_modifiers()
|
|
@@ -172,11 +174,23 @@ def _config_show_cib_lines(lib):
|
|
all_lines.append("")
|
|
all_lines.append("Resources Defaults:")
|
|
all_lines.extend(
|
|
- indent(resource.show_defaults(cib_dom, "rsc_defaults"), indent_step=1)
|
|
+ indent(
|
|
+ nvset_dto_list_to_lines(
|
|
+ lib.cib_options.resource_defaults_config(),
|
|
+ with_ids=modifiers.get("--full"),
|
|
+ text_if_empty="No defaults set",
|
|
+ )
|
|
+ )
|
|
)
|
|
all_lines.append("Operations Defaults:")
|
|
all_lines.extend(
|
|
- indent(resource.show_defaults(cib_dom, "op_defaults"), indent_step=1)
|
|
+ indent(
|
|
+ nvset_dto_list_to_lines(
|
|
+ lib.cib_options.operation_defaults_config(),
|
|
+ with_ids=modifiers.get("--full"),
|
|
+ text_if_empty="No defaults set",
|
|
+ )
|
|
+ )
|
|
)
|
|
|
|
all_lines.append("")
|
|
diff --git a/pcs/lib/cib/nvpair_multi.py b/pcs/lib/cib/nvpair_multi.py
|
|
new file mode 100644
|
|
index 00000000..7bdc2f55
|
|
--- /dev/null
|
|
+++ b/pcs/lib/cib/nvpair_multi.py
|
|
@@ -0,0 +1,323 @@
|
|
+from typing import (
|
|
+ cast,
|
|
+ Iterable,
|
|
+ List,
|
|
+ Mapping,
|
|
+ NewType,
|
|
+ Optional,
|
|
+ Tuple,
|
|
+)
|
|
+from xml.etree.ElementTree import Element
|
|
+
|
|
+from lxml import etree
|
|
+from lxml.etree import _Element
|
|
+
|
|
+from pcs.common import reports
|
|
+from pcs.common.pacemaker.nvset import (
|
|
+ CibNvpairDto,
|
|
+ CibNvsetDto,
|
|
+)
|
|
+from pcs.common.reports import ReportItemList
|
|
+from pcs.common.types import CibNvsetType
|
|
+from pcs.lib import validate
|
|
+from pcs.lib.cib.rule import (
|
|
+ RuleParseError,
|
|
+ RuleRoot,
|
|
+ RuleValidator,
|
|
+ parse_rule,
|
|
+ rule_element_to_dto,
|
|
+ rule_to_cib,
|
|
+)
|
|
+from pcs.lib.cib.tools import (
|
|
+ ElementSearcher,
|
|
+ IdProvider,
|
|
+ create_subelement_id,
|
|
+)
|
|
+from pcs.lib.xml_tools import (
|
|
+ export_attributes,
|
|
+ remove_one_element,
|
|
+)
|
|
+
|
|
+
|
|
+NvsetTag = NewType("NvsetTag", str)
|
|
+NVSET_INSTANCE = NvsetTag("instance_attributes")
|
|
+NVSET_META = NvsetTag("meta_attributes")
|
|
+
|
|
+_tag_to_type = {
|
|
+ str(NVSET_META): CibNvsetType.META,
|
|
+ str(NVSET_INSTANCE): CibNvsetType.INSTANCE,
|
|
+}
|
|
+
|
|
+
|
|
+def nvpair_element_to_dto(nvpair_el: Element) -> CibNvpairDto:
|
|
+ """
|
|
+ Export an nvpair xml element to its DTO
|
|
+ """
|
|
+ return CibNvpairDto(
|
|
+ nvpair_el.get("id", ""),
|
|
+ nvpair_el.get("name", ""),
|
|
+ nvpair_el.get("value", ""),
|
|
+ )
|
|
+
|
|
+
|
|
+def nvset_element_to_dto(nvset_el: Element) -> CibNvsetDto:
|
|
+ """
|
|
+ Export an nvset xml element to its DTO
|
|
+ """
|
|
+ rule_el = nvset_el.find("./rule")
|
|
+ return CibNvsetDto(
|
|
+ nvset_el.get("id", ""),
|
|
+ _tag_to_type[nvset_el.tag],
|
|
+ export_attributes(nvset_el, with_id=False),
|
|
+ None if rule_el is None else rule_element_to_dto(rule_el),
|
|
+ [
|
|
+ nvpair_element_to_dto(nvpair_el)
|
|
+ for nvpair_el in nvset_el.iterfind("./nvpair")
|
|
+ ],
|
|
+ )
|
|
+
|
|
+
|
|
+def find_nvsets(parent_element: Element) -> List[Element]:
|
|
+ """
|
|
+ Get all nvset xml elements in the given parent element
|
|
+
|
|
+ parent_element -- an element to look for nvsets in
|
|
+ """
|
|
+ return cast(
|
|
+ # The xpath method has a complicated return value, but we know our xpath
|
|
+ # expression returns only elements.
|
|
+ List[Element],
|
|
+ cast(_Element, parent_element).xpath(
|
|
+ "./*[{nvset_tags}]".format(
|
|
+ nvset_tags=" or ".join(f"self::{tag}" for tag in _tag_to_type)
|
|
+ )
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+def find_nvsets_by_ids(
|
|
+ parent_element: Element, id_list: Iterable[str]
|
|
+) -> Tuple[List[Element], ReportItemList]:
|
|
+ """
|
|
+ Find nvset elements by their IDs and return them with non-empty report
|
|
+ list in case of errors.
|
|
+
|
|
+ parent_element -- an element to look for nvsets in
|
|
+ id_list -- nvset IDs to be looked for
|
|
+ """
|
|
+ element_list = []
|
|
+ report_list: ReportItemList = []
|
|
+ for nvset_id in id_list:
|
|
+ searcher = ElementSearcher(
|
|
+ _tag_to_type.keys(),
|
|
+ nvset_id,
|
|
+ parent_element,
|
|
+ element_type_desc="options set",
|
|
+ )
|
|
+ if searcher.element_found():
|
|
+ element_list.append(searcher.get_element())
|
|
+ else:
|
|
+ report_list.extend(searcher.get_errors())
|
|
+ return element_list, report_list
|
|
+
|
|
+
|
|
+class ValidateNvsetAppendNew:
|
|
+ """
|
|
+ Validator for creating new nvset and appending it to CIB
|
|
+ """
|
|
+
|
|
+ def __init__(
|
|
+ self,
|
|
+ id_provider: IdProvider,
|
|
+ nvpair_dict: Mapping[str, str],
|
|
+ nvset_options: Mapping[str, str],
|
|
+ nvset_rule: Optional[str] = None,
|
|
+ rule_allows_rsc_expr: bool = False,
|
|
+ rule_allows_op_expr: bool = False,
|
|
+ ):
|
|
+ """
|
|
+ id_provider -- elements' ids generator
|
|
+ nvpair_dict -- nvpairs to be put into the new nvset
|
|
+ nvset_options -- additional attributes of the created nvset
|
|
+ nvset_rule -- optional rule describing when the created nvset applies
|
|
+ rule_allows_rsc_expr -- is rsc_expression element allowed in nvset_rule?
|
|
+ rule_allows_op_expr -- is op_expression element allowed in nvset_rule?
|
|
+ """
|
|
+ self._id_provider = id_provider
|
|
+ self._nvpair_dict = nvpair_dict
|
|
+ self._nvset_options = nvset_options
|
|
+ self._nvset_rule = nvset_rule
|
|
+ self._allow_rsc_expr = rule_allows_rsc_expr
|
|
+ self._allow_op_expr = rule_allows_op_expr
|
|
+ self._nvset_rule_parsed: Optional[RuleRoot] = None
|
|
+
|
|
+ def validate(self, force_options: bool = False) -> reports.ReportItemList:
|
|
+ report_list: reports.ReportItemList = []
|
|
+
|
|
+ # Nvpair dict is intentionally not validated: it may contain any keys
|
|
+ # and values. This can change in the future and then we add a
|
|
+ # validation. Until then there is really nothing to validate there.
|
|
+
|
|
+ # validate nvset options
|
|
+ validators = [
|
|
+ validate.NamesIn(
|
|
+ ("id", "score"),
|
|
+ **validate.set_warning(
|
|
+ reports.codes.FORCE_OPTIONS, force_options
|
|
+ ),
|
|
+ ),
|
|
+ # with id_provider it validates that the id is available as well
|
|
+ validate.ValueId(
|
|
+ "id", option_name_for_report="id", id_provider=self._id_provider
|
|
+ ),
|
|
+ validate.ValueScore("score"),
|
|
+ ]
|
|
+ report_list.extend(
|
|
+ validate.ValidatorAll(validators).validate(self._nvset_options)
|
|
+ )
|
|
+
|
|
+ # parse and validate rule
|
|
+ # TODO write and call parsed rule validation and cleanup and tests
|
|
+ if self._nvset_rule:
|
|
+ try:
|
|
+ # Allow flags are set to True always, the parsed rule tree is
|
|
+ # checked in the validator instead. That gives us better error
|
|
+ # messages, such as "op expression cannot be used in this
|
|
+ # context" instead of a universal "parse error".
|
|
+ self._nvset_rule_parsed = parse_rule(
|
|
+ self._nvset_rule, allow_rsc_expr=True, allow_op_expr=True
|
|
+ )
|
|
+ report_list.extend(
|
|
+ RuleValidator(
|
|
+ self._nvset_rule_parsed,
|
|
+ allow_rsc_expr=self._allow_rsc_expr,
|
|
+ allow_op_expr=self._allow_op_expr,
|
|
+ ).get_reports()
|
|
+ )
|
|
+ except RuleParseError as e:
|
|
+ report_list.append(
|
|
+ reports.ReportItem.error(
|
|
+ reports.messages.RuleExpressionParseError(
|
|
+ e.rule_string,
|
|
+ e.msg,
|
|
+ e.rule_line,
|
|
+ e.lineno,
|
|
+ e.colno,
|
|
+ e.pos,
|
|
+ )
|
|
+ )
|
|
+ )
|
|
+
|
|
+ return report_list
|
|
+
|
|
+ def get_parsed_rule(self) -> Optional[RuleRoot]:
|
|
+ return self._nvset_rule_parsed
|
|
+
|
|
+
|
|
+def nvset_append_new(
|
|
+ parent_element: Element,
|
|
+ id_provider: IdProvider,
|
|
+ nvset_tag: NvsetTag,
|
|
+ nvpair_dict: Mapping[str, str],
|
|
+ nvset_options: Mapping[str, str],
|
|
+ nvset_rule: Optional[RuleRoot] = None,
|
|
+) -> Element:
|
|
+ """
|
|
+ Create new nvset and append it to CIB
|
|
+
|
|
+ parent_element -- the created nvset will be appended into this element
|
|
+ id_provider -- elements' ids generator
|
|
+ nvset_tag -- type and actual tag of the nvset
|
|
+ nvpair_dict -- nvpairs to be put into the new nvset
|
|
+ nvset_options -- additional attributes of the created nvset
|
|
+ nvset_rule -- optional rule describing when the created nvset applies
|
|
+ """
|
|
+ nvset_options = dict(nvset_options) # make a copy which we can modify
|
|
+ if "id" not in nvset_options or not nvset_options["id"]:
|
|
+ nvset_options["id"] = create_subelement_id(
|
|
+ parent_element, nvset_tag, id_provider
|
|
+ )
|
|
+
|
|
+ nvset_el = etree.SubElement(cast(_Element, parent_element), nvset_tag)
|
|
+ for name, value in nvset_options.items():
|
|
+ if value != "":
|
|
+ nvset_el.attrib[name] = value
|
|
+ if nvset_rule:
|
|
+ rule_to_cib(cast(Element, nvset_el), id_provider, nvset_rule)
|
|
+ for name, value in nvpair_dict.items():
|
|
+ _set_nvpair(cast(Element, nvset_el), id_provider, name, value)
|
|
+ return cast(Element, nvset_el)
|
|
+
|
|
+
|
|
+def nvset_remove(nvset_el_list: Iterable[Element]) -> None:
|
|
+ """
|
|
+ Remove given nvset elements from CIB
|
|
+
|
|
+ nvset_el_list -- nvset elements to be removed
|
|
+ """
|
|
+ for nvset_el in nvset_el_list:
|
|
+ remove_one_element(nvset_el)
|
|
+
|
|
+
|
|
+def nvset_update(
|
|
+ nvset_el: Element, id_provider: IdProvider, nvpair_dict: Mapping[str, str],
|
|
+) -> None:
|
|
+ """
|
|
+ Update an existing nvset
|
|
+
|
|
+ nvset_el -- nvset to be updated
|
|
+ id_provider -- elements' ids generator
|
|
+ nvpair_dict -- nvpairs to be put into the nvset
|
|
+ """
|
|
+ # Do not ever remove the nvset element, even if it is empty. There may be
|
|
+ # ACLs set in pacemaker which allow "write" for nvpairs (adding, changing
|
|
+ # and removing) but not nvsets. In such a case, removing the nvset would
|
|
+ # cause the whole change to be rejected by pacemaker with a "permission
|
|
+ # denied" message.
|
|
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
|
|
+ for name, value in nvpair_dict.items():
|
|
+ _set_nvpair(nvset_el, id_provider, name, value)
|
|
+
|
|
+
|
|
+def _set_nvpair(
|
|
+ nvset_element: Element, id_provider: IdProvider, name: str, value: str
|
|
+):
|
|
+ """
|
|
+ Ensure name-value pair is set / removed in specified nvset
|
|
+
|
|
+ nvset_element -- container for nvpair elements to update
|
|
+ id_provider -- elements' ids generator
|
|
+ name -- name of the nvpair to be set
|
|
+ value -- value of the nvpair to be set, if "" the nvpair will be removed
|
|
+ """
|
|
+ nvpair_el_list = cast(
|
|
+ # The xpath method has a complicated return value, but we know our xpath
|
|
+ # expression returns only elements.
|
|
+ List[Element],
|
|
+ cast(_Element, nvset_element).xpath("./nvpair[@name=$name]", name=name),
|
|
+ )
|
|
+
|
|
+ if not nvpair_el_list:
|
|
+ if value != "":
|
|
+ etree.SubElement(
|
|
+ cast(_Element, nvset_element),
|
|
+ "nvpair",
|
|
+ {
|
|
+ "id": create_subelement_id(
|
|
+ nvset_element,
|
|
+ # limit id length to prevent excessively long ids
|
|
+ name[:20],
|
|
+ id_provider,
|
|
+ ),
|
|
+ "name": name,
|
|
+ "value": value,
|
|
+ },
|
|
+ )
|
|
+ return
|
|
+
|
|
+ if value != "":
|
|
+ nvpair_el_list[0].set("value", value)
|
|
+ else:
|
|
+ nvset_element.remove(nvpair_el_list[0])
|
|
+ for nvpair_el in nvpair_el_list[1:]:
|
|
+ nvset_element.remove(nvpair_el)
|
|
diff --git a/pcs/lib/cib/rule/__init__.py b/pcs/lib/cib/rule/__init__.py
|
|
new file mode 100644
|
|
index 00000000..94228572
|
|
--- /dev/null
|
|
+++ b/pcs/lib/cib/rule/__init__.py
|
|
@@ -0,0 +1,8 @@
|
|
+from .cib_to_dto import rule_element_to_dto
|
|
+from .expression_part import BoolExpr as RuleRoot
|
|
+from .parser import (
|
|
+ parse_rule,
|
|
+ RuleParseError,
|
|
+)
|
|
+from .parsed_to_cib import export as rule_to_cib
|
|
+from .validator import Validator as RuleValidator
|
|
diff --git a/pcs/lib/cib/rule/cib_to_dto.py b/pcs/lib/cib/rule/cib_to_dto.py
|
|
new file mode 100644
|
|
index 00000000..d8198e0c
|
|
--- /dev/null
|
|
+++ b/pcs/lib/cib/rule/cib_to_dto.py
|
|
@@ -0,0 +1,185 @@
|
|
+from typing import cast
|
|
+from xml.etree.ElementTree import Element
|
|
+
|
|
+from lxml.etree import _Element
|
|
+
|
|
+from pcs.common.pacemaker.rule import (
|
|
+ CibRuleDateCommonDto,
|
|
+ CibRuleExpressionDto,
|
|
+)
|
|
+from pcs.common.str_tools import (
|
|
+ format_name_value_list,
|
|
+ quote,
|
|
+)
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
+from pcs.lib.xml_tools import export_attributes
|
|
+
|
|
+
|
|
+def rule_element_to_dto(rule_el: Element) -> CibRuleExpressionDto:
|
|
+ """
|
|
+ Export a rule xml element including its children to their DTOs
|
|
+ """
|
|
+ return _tag_to_export[rule_el.tag](rule_el)
|
|
+
|
|
+
|
|
+def _attrs_to_str(el: Element) -> str:
|
|
+ return " ".join(
|
|
+ format_name_value_list(
|
|
+ sorted(export_attributes(el, with_id=False).items())
|
|
+ )
|
|
+ )
|
|
+
|
|
+
|
|
+def _rule_to_dto(rule_el: Element) -> CibRuleExpressionDto:
|
|
+ children_dto_list = [
|
|
+ _tag_to_export[child.tag](child)
|
|
+ # The xpath method has a complicated return value, but we know our xpath
|
|
+ # expression only returns elements.
|
|
+ for child in cast(
|
|
+ Element, cast(_Element, rule_el).xpath(_xpath_for_export)
|
|
+ )
|
|
+ ]
|
|
+ # "and" is a documented pacemaker default
|
|
+ # https://clusterlabs.org/pacemaker/doc/en-US/Pacemaker/2.0/html-single/Pacemaker_Explained/index.html#_rule_properties
|
|
+ boolean_op = rule_el.get("boolean-op", "and")
|
|
+ string_parts = []
|
|
+ for child_dto in children_dto_list:
|
|
+ if child_dto.type == CibRuleExpressionType.RULE:
|
|
+ string_parts.append(f"({child_dto.as_string})")
|
|
+ else:
|
|
+ string_parts.append(child_dto.as_string)
|
|
+ return CibRuleExpressionDto(
|
|
+ rule_el.get("id", ""),
|
|
+ _tag_to_type[rule_el.tag],
|
|
+ False, # TODO implement is_expired
|
|
+ export_attributes(rule_el, with_id=False),
|
|
+ None,
|
|
+ None,
|
|
+ children_dto_list,
|
|
+ f" {boolean_op} ".join(string_parts),
|
|
+ )
|
|
+
|
|
+
|
|
+def _common_expr_to_dto(
|
|
+ expr_el: Element, as_string: str
|
|
+) -> CibRuleExpressionDto:
|
|
+ return CibRuleExpressionDto(
|
|
+ expr_el.get("id", ""),
|
|
+ _tag_to_type[expr_el.tag],
|
|
+ False,
|
|
+ export_attributes(expr_el, with_id=False),
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ as_string,
|
|
+ )
|
|
+
|
|
+
|
|
+def _simple_expr_to_dto(expr_el: Element) -> CibRuleExpressionDto:
|
|
+ string_parts = []
|
|
+ if "value" in expr_el.attrib:
|
|
+ # "attribute" and "operation" are defined as mandatory in CIB schema
|
|
+ string_parts.extend(
|
|
+ [expr_el.get("attribute", ""), expr_el.get("operation", "")]
|
|
+ )
|
|
+ if "type" in expr_el.attrib:
|
|
+ string_parts.append(expr_el.get("type", ""))
|
|
+ string_parts.append(quote(expr_el.get("value", ""), " "))
|
|
+ else:
|
|
+ # "attribute" and "operation" are defined as mandatory in CIB schema
|
|
+ string_parts.extend(
|
|
+ [expr_el.get("operation", ""), expr_el.get("attribute", "")]
|
|
+ )
|
|
+ return _common_expr_to_dto(expr_el, " ".join(string_parts))
|
|
+
|
|
+
|
|
+def _date_common_to_dto(expr_el: Element) -> CibRuleDateCommonDto:
|
|
+ return CibRuleDateCommonDto(
|
|
+ expr_el.get("id", ""), export_attributes(expr_el, with_id=False),
|
|
+ )
|
|
+
|
|
+
|
|
+def _date_expr_to_dto(expr_el: Element) -> CibRuleExpressionDto:
|
|
+ date_spec = expr_el.find("./date_spec")
|
|
+ duration = expr_el.find("./duration")
|
|
+
|
|
+ string_parts = []
|
|
+ # "operation" is defined as mandatory in CIB schema
|
|
+ operation = expr_el.get("operation", "")
|
|
+ if operation == "date_spec":
|
|
+ string_parts.append("date-spec")
|
|
+ if date_spec is not None:
|
|
+ string_parts.append(_attrs_to_str(date_spec))
|
|
+ elif operation == "in_range":
|
|
+ string_parts.extend(["date", "in_range"])
|
|
+ # CIB schema allows "start" + "duration" or optional "start" + "end"
|
|
+ if "start" in expr_el.attrib:
|
|
+ string_parts.extend([expr_el.get("start", ""), "to"])
|
|
+ if "end" in expr_el.attrib:
|
|
+ string_parts.append(expr_el.get("end", ""))
|
|
+ if duration is not None:
|
|
+ string_parts.append("duration")
|
|
+ string_parts.append(_attrs_to_str(duration))
|
|
+ else:
|
|
+ # CIB schema allows operation=="gt" + "start" or operation=="lt" + "end"
|
|
+ string_parts.extend(["date", expr_el.get("operation", "")])
|
|
+ if "start" in expr_el.attrib:
|
|
+ string_parts.append(expr_el.get("start", ""))
|
|
+ if "end" in expr_el.attrib:
|
|
+ string_parts.append(expr_el.get("end", ""))
|
|
+
|
|
+ return CibRuleExpressionDto(
|
|
+ expr_el.get("id", ""),
|
|
+ _tag_to_type[expr_el.tag],
|
|
+ False,
|
|
+ export_attributes(expr_el, with_id=False),
|
|
+ None if date_spec is None else _date_common_to_dto(date_spec),
|
|
+ None if duration is None else _date_common_to_dto(duration),
|
|
+ [],
|
|
+ " ".join(string_parts),
|
|
+ )
|
|
+
|
|
+
|
|
+def _op_expr_to_dto(expr_el: Element) -> CibRuleExpressionDto:
|
|
+ string_parts = ["op"]
|
|
+ string_parts.append(expr_el.get("name", ""))
|
|
+ if "interval" in expr_el.attrib:
|
|
+ string_parts.append(
|
|
+ "interval={interval}".format(interval=expr_el.get("interval", ""))
|
|
+ )
|
|
+ return _common_expr_to_dto(expr_el, " ".join(string_parts))
|
|
+
|
|
+
|
|
+def _rsc_expr_to_dto(expr_el: Element) -> CibRuleExpressionDto:
|
|
+ return _common_expr_to_dto(
|
|
+ expr_el,
|
|
+ (
|
|
+ "resource "
|
|
+ + ":".join(
|
|
+ [
|
|
+ expr_el.get(attr, "")
|
|
+ for attr in ["class", "provider", "type"]
|
|
+ ]
|
|
+ )
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+_tag_to_type = {
|
|
+ "rule": CibRuleExpressionType.RULE,
|
|
+ "expression": CibRuleExpressionType.EXPRESSION,
|
|
+ "date_expression": CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ "op_expression": CibRuleExpressionType.OP_EXPRESSION,
|
|
+ "rsc_expression": CibRuleExpressionType.RSC_EXPRESSION,
|
|
+}
|
|
+
|
|
+_tag_to_export = {
|
|
+ "rule": _rule_to_dto,
|
|
+ "expression": _simple_expr_to_dto,
|
|
+ "date_expression": _date_expr_to_dto,
|
|
+ "op_expression": _op_expr_to_dto,
|
|
+ "rsc_expression": _rsc_expr_to_dto,
|
|
+}
|
|
+_xpath_for_export = "./*[{export_tags}]".format(
|
|
+ export_tags=" or ".join(f"self::{tag}" for tag in _tag_to_export)
|
|
+)
|
|
diff --git a/pcs/lib/cib/rule/expression_part.py b/pcs/lib/cib/rule/expression_part.py
|
|
new file mode 100644
|
|
index 00000000..3ba63aa2
|
|
--- /dev/null
|
|
+++ b/pcs/lib/cib/rule/expression_part.py
|
|
@@ -0,0 +1,49 @@
|
|
+"""
|
|
+Provides classes used as nodes of a semantic tree of a parsed rule expression.
|
|
+"""
|
|
+from dataclasses import dataclass
|
|
+from typing import (
|
|
+ NewType,
|
|
+ Optional,
|
|
+ Sequence,
|
|
+)
|
|
+
|
|
+
|
|
+class RuleExprPart:
|
|
+ pass
|
|
+
|
|
+
|
|
+BoolOperator = NewType("BoolOperator", str)
|
|
+BOOL_AND = BoolOperator("AND")
|
|
+BOOL_OR = BoolOperator("OR")
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class BoolExpr(RuleExprPart):
|
|
+ """
|
|
+ Represents a rule combining RuleExprPart objects by AND or OR operation.
|
|
+ """
|
|
+
|
|
+ operator: BoolOperator
|
|
+ children: Sequence[RuleExprPart]
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class RscExpr(RuleExprPart):
|
|
+ """
|
|
+ Represents a resource expression in a rule.
|
|
+ """
|
|
+
|
|
+ standard: Optional[str]
|
|
+ provider: Optional[str]
|
|
+ type: Optional[str]
|
|
+
|
|
+
|
|
+@dataclass(frozen=True)
|
|
+class OpExpr(RuleExprPart):
|
|
+ """
|
|
+ Represents an op expression in a rule.
|
|
+ """
|
|
+
|
|
+ name: str
|
|
+ interval: Optional[str]
|
|
diff --git a/pcs/lib/cib/rule/parsed_to_cib.py b/pcs/lib/cib/rule/parsed_to_cib.py
|
|
new file mode 100644
|
|
index 00000000..0fcae4f1
|
|
--- /dev/null
|
|
+++ b/pcs/lib/cib/rule/parsed_to_cib.py
|
|
@@ -0,0 +1,103 @@
|
|
+from typing import cast
|
|
+from xml.etree.ElementTree import Element
|
|
+
|
|
+from lxml import etree
|
|
+from lxml.etree import _Element
|
|
+
|
|
+from pcs.lib.cib.tools import (
|
|
+ IdProvider,
|
|
+ create_subelement_id,
|
|
+)
|
|
+
|
|
+from .expression_part import (
|
|
+ BoolExpr,
|
|
+ OpExpr,
|
|
+ RscExpr,
|
|
+ RuleExprPart,
|
|
+)
|
|
+
|
|
+
|
|
+def export(
|
|
+ parent_el: Element, id_provider: IdProvider, expr_tree: BoolExpr,
|
|
+) -> Element:
|
|
+ """
|
|
+ Export parsed rule to a CIB element
|
|
+
|
|
+ parent_el -- element to place the rule into
|
|
+ id_provider -- elements' ids generator
|
|
+ expr_tree -- parsed rule tree root
|
|
+ """
|
|
+ element = __export_part(parent_el, expr_tree, id_provider)
|
|
+ # Add score only to the top level rule element (which is represented by
|
|
+ # BoolExpr class). This is achieved by this function not being called for
|
|
+ # child nodes.
|
|
+ # TODO This was implemented originaly only for rules in resource and
|
|
+ # operation defaults. In those cases, score is the only rule attribute and
|
|
+ # it is always INFINITY. Once this code is used for other rules, modify
|
|
+ # this behavior as needed.
|
|
+ if isinstance(expr_tree, BoolExpr):
|
|
+ element.set("score", "INFINITY")
|
|
+ return element
|
|
+
|
|
+
|
|
+def __export_part(
|
|
+ parent_el: Element, expr_tree: RuleExprPart, id_provider: IdProvider
|
|
+) -> Element:
|
|
+ part_export_map = {
|
|
+ BoolExpr: __export_bool,
|
|
+ OpExpr: __export_op,
|
|
+ RscExpr: __export_rsc,
|
|
+ }
|
|
+ func = part_export_map[type(expr_tree)]
|
|
+ # mypy doesn't handle this dynamic call
|
|
+ return func(parent_el, expr_tree, id_provider) # type: ignore
|
|
+
|
|
+
|
|
+def __export_bool(
|
|
+ parent_el: Element, boolean: BoolExpr, id_provider: IdProvider
|
|
+) -> Element:
|
|
+ element = etree.SubElement(
|
|
+ cast(_Element, parent_el),
|
|
+ "rule",
|
|
+ {
|
|
+ "id": create_subelement_id(parent_el, "rule", id_provider),
|
|
+ "boolean-op": boolean.operator.lower(),
|
|
+ },
|
|
+ )
|
|
+ for child in boolean.children:
|
|
+ __export_part(cast(Element, element), child, id_provider)
|
|
+ return cast(Element, element)
|
|
+
|
|
+
|
|
+def __export_op(
|
|
+ parent_el: Element, op: OpExpr, id_provider: IdProvider
|
|
+) -> Element:
|
|
+ element = etree.SubElement(
|
|
+ cast(_Element, parent_el),
|
|
+ "op_expression",
|
|
+ {
|
|
+ "id": create_subelement_id(parent_el, f"op-{op.name}", id_provider),
|
|
+ "name": op.name,
|
|
+ },
|
|
+ )
|
|
+ if op.interval:
|
|
+ element.attrib["interval"] = op.interval
|
|
+ return cast(Element, element)
|
|
+
|
|
+
|
|
+def __export_rsc(
|
|
+ parent_el: Element, rsc: RscExpr, id_provider: IdProvider
|
|
+) -> Element:
|
|
+ id_part = "-".join(filter(None, [rsc.standard, rsc.provider, rsc.type]))
|
|
+ element = etree.SubElement(
|
|
+ cast(_Element, parent_el),
|
|
+ "rsc_expression",
|
|
+ {"id": create_subelement_id(parent_el, f"rsc-{id_part}", id_provider)},
|
|
+ )
|
|
+ if rsc.standard:
|
|
+ element.attrib["class"] = rsc.standard
|
|
+ if rsc.provider:
|
|
+ element.attrib["provider"] = rsc.provider
|
|
+ if rsc.type:
|
|
+ element.attrib["type"] = rsc.type
|
|
+ return cast(Element, element)
|
|
diff --git a/pcs/lib/cib/rule/parser.py b/pcs/lib/cib/rule/parser.py
|
|
new file mode 100644
|
|
index 00000000..2215c524
|
|
--- /dev/null
|
|
+++ b/pcs/lib/cib/rule/parser.py
|
|
@@ -0,0 +1,232 @@
|
|
+from typing import (
|
|
+ Any,
|
|
+ Iterator,
|
|
+ Optional,
|
|
+ Tuple,
|
|
+)
|
|
+
|
|
+import pyparsing
|
|
+
|
|
+from .expression_part import (
|
|
+ BOOL_AND,
|
|
+ BOOL_OR,
|
|
+ BoolExpr,
|
|
+ OpExpr,
|
|
+ RscExpr,
|
|
+ RuleExprPart,
|
|
+)
|
|
+
|
|
+pyparsing.ParserElement.enablePackrat()
|
|
+
|
|
+
|
|
+class RuleParseError(Exception):
|
|
+ def __init__(
|
|
+ self,
|
|
+ rule_string: str,
|
|
+ rule_line: str,
|
|
+ lineno: int,
|
|
+ colno: int,
|
|
+ pos: int,
|
|
+ msg: str,
|
|
+ ):
|
|
+ super().__init__()
|
|
+ self.rule_string = rule_string
|
|
+ self.rule_line = rule_line
|
|
+ self.lineno = lineno
|
|
+ self.colno = colno
|
|
+ self.pos = pos
|
|
+ self.msg = msg
|
|
+
|
|
+
|
|
+def parse_rule(
|
|
+ rule_string: str, allow_rsc_expr: bool = False, allow_op_expr: bool = False
|
|
+) -> BoolExpr:
|
|
+ """
|
|
+ Parse a rule string and return a corresponding semantic tree
|
|
+
|
|
+ rule_string -- the whole rule expression
|
|
+ allow_rsc_expr -- allow resource expressions in the rule
|
|
+ allow_op_expr -- allow resource operation expressions in the rule
|
|
+ """
|
|
+ if not rule_string:
|
|
+ return BoolExpr(BOOL_AND, [])
|
|
+
|
|
+ try:
|
|
+ parsed = __get_rule_parser(
|
|
+ allow_rsc_expr=allow_rsc_expr, allow_op_expr=allow_op_expr
|
|
+ ).parseString(rule_string, parseAll=True)[0]
|
|
+ except pyparsing.ParseException as e:
|
|
+ raise RuleParseError(
|
|
+ rule_string, e.line, e.lineno, e.col, e.loc, e.args[2],
|
|
+ )
|
|
+
|
|
+ if not isinstance(parsed, BoolExpr):
|
|
+ # If we only got a representation on an inner rule element instead of a
|
|
+ # rule element itself, wrap the result in a default AND-rule. (There is
|
|
+ # only one expression so "and" vs. "or" doesn't really matter.)
|
|
+ parsed = BoolExpr(BOOL_AND, [parsed])
|
|
+
|
|
+ return parsed
|
|
+
|
|
+
|
|
+def __operator_operands(
|
|
+ token_list: pyparsing.ParseResults,
|
|
+) -> Iterator[Tuple[Any, Any]]:
|
|
+ # See pyparsing examples
|
|
+ # https://github.com/pyparsing/pyparsing/blob/master/examples/eval_arith.py
|
|
+ token_iterator = iter(token_list)
|
|
+ while True:
|
|
+ try:
|
|
+ yield (next(token_iterator), next(token_iterator))
|
|
+ except StopIteration:
|
|
+ break
|
|
+
|
|
+
|
|
+def __build_bool_tree(token_list: pyparsing.ParseResults) -> RuleExprPart:
|
|
+ # See pyparsing examples
|
|
+ # https://github.com/pyparsing/pyparsing/blob/master/examples/eval_arith.py
|
|
+ token_to_operator = {
|
|
+ "and": BOOL_AND,
|
|
+ "or": BOOL_OR,
|
|
+ }
|
|
+ operand_left = token_list[0][0]
|
|
+ last_operator: Optional[str] = None
|
|
+ operand_list = []
|
|
+ for operator, operand_right in __operator_operands(token_list[0][1:]):
|
|
+ # In each iteration, we get a bool_op ("and" or "or") and the right
|
|
+ # operand.
|
|
+ if last_operator == operator or last_operator is None:
|
|
+ # If we got the same operator as last time (or this is the first
|
|
+ # one), stack all the operads so we can put them all into one
|
|
+ # BoolExpr class.
|
|
+ operand_list.append(operand_right)
|
|
+ else:
|
|
+ # The operator has changed. Put all the stacked operands into the
|
|
+ # correct BoolExpr class and start the stacking again. The created
|
|
+ # class is the left operand of the current operator.
|
|
+ operand_left = BoolExpr(
|
|
+ token_to_operator[last_operator], [operand_left] + operand_list
|
|
+ )
|
|
+ operand_list = [operand_right]
|
|
+ last_operator = operator
|
|
+ if operand_list and last_operator:
|
|
+ # Use any of the remaining stacked operands.
|
|
+ operand_left = BoolExpr(
|
|
+ token_to_operator[last_operator], [operand_left] + operand_list
|
|
+ )
|
|
+ return operand_left
|
|
+
|
|
+
|
|
+def __build_op_expr(parse_result: pyparsing.ParseResults) -> RuleExprPart:
|
|
+ # Those attr are defined by setResultsName in op_expr grammar rule
|
|
+ return OpExpr(
|
|
+ parse_result.name,
|
|
+ # pyparsing-2.1.0 puts "interval_value" into parse_result.interval as
|
|
+ # defined in the grammar AND it also puts "interval_value" into
|
|
+ # parse_result. pyparsing-2.4.0 only puts "interval_value" into
|
|
+ # parse_result. Not sure why, maybe it's a bug, maybe it's intentional.
|
|
+ parse_result.interval_value if parse_result.interval_value else None,
|
|
+ )
|
|
+
|
|
+
|
|
+def __build_rsc_expr(parse_result: pyparsing.ParseResults) -> RuleExprPart:
|
|
+ # Those attrs are defined by the regexp in rsc_expr grammar rule
|
|
+ return RscExpr(
|
|
+ parse_result.standard, parse_result.provider, parse_result.type
|
|
+ )
|
|
+
|
|
+
|
|
+def __get_rule_parser(
|
|
+ allow_rsc_expr: bool = False, allow_op_expr: bool = False
|
|
+) -> pyparsing.ParserElement:
|
|
+ # This function defines the rule grammar
|
|
+
|
|
+ # It was created for 'pcs resource [op] defaults' commands to be able to
|
|
+ # set defaults for specified resources and/or operation using rules. When
|
|
+ # implementing that feature, there was no time to reimplement all the other
|
|
+ # rule expressions from old code. The plan is to move old rule parser code
|
|
+ # here once there is time / need to do it.
|
|
+ # How to add other rule expressions:
|
|
+ # 1 Create new grammar rules in a way similar to existing rsc_expr and
|
|
+ # op_expr. Use setName for better description of a grammar when printed.
|
|
+ # Use setResultsName for an easy access to parsed parts.
|
|
+ # 2 Create new classes in expression_part module, probably one for each
|
|
+ # type of expression. Those are data containers holding the parsed data
|
|
+ # independent of the parser.
|
|
+ # 3 Create builders for the new classes and connect them to created
|
|
+ # grammar rules using setParseAction.
|
|
+ # 4 Add the new expressions into simple_expr_list.
|
|
+ # 5 Test and debug the whole thing.
|
|
+
|
|
+ rsc_expr = pyparsing.And(
|
|
+ [
|
|
+ pyparsing.CaselessKeyword("resource"),
|
|
+ # resource name
|
|
+ # Up to three parts seperated by ":". The parts can contain any
|
|
+ # characters except whitespace (token separator), ":" (parts
|
|
+ # separator) and "()" (brackets).
|
|
+ pyparsing.Regex(
|
|
+ r"(?P<standard>[^\s:()]+)?:(?P<provider>[^\s:()]+)?:(?P<type>[^\s:()]+)?"
|
|
+ ).setName("<resource name>"),
|
|
+ ]
|
|
+ )
|
|
+ rsc_expr.setParseAction(__build_rsc_expr)
|
|
+
|
|
+ op_interval = pyparsing.And(
|
|
+ [
|
|
+ pyparsing.CaselessKeyword("interval"),
|
|
+ # no spaces allowed around the "="
|
|
+ pyparsing.Literal("=").leaveWhitespace(),
|
|
+ # interval value: number followed by a time unit, no spaces allowed
|
|
+ # between the number and the unit thanks to Combine being used
|
|
+ pyparsing.Combine(
|
|
+ pyparsing.And(
|
|
+ [
|
|
+ pyparsing.Word(pyparsing.nums),
|
|
+ pyparsing.Optional(pyparsing.Word(pyparsing.alphas)),
|
|
+ ]
|
|
+ )
|
|
+ )
|
|
+ .setName("<integer>[<time unit>]")
|
|
+ .setResultsName("interval_value"),
|
|
+ ]
|
|
+ )
|
|
+ op_expr = pyparsing.And(
|
|
+ [
|
|
+ pyparsing.CaselessKeyword("op"),
|
|
+ # operation name
|
|
+ # It can by any string containing any characters except whitespace
|
|
+ # (token separator) and "()" (brackets). Operations are defined in
|
|
+ # agents' metadata which we do not have access to (e.g. when the
|
|
+ # user sets operation "my_check" and doesn't even specify agent's
|
|
+ # name).
|
|
+ pyparsing.Regex(r"[^\s()]+")
|
|
+ .setName("<operation name>")
|
|
+ .setResultsName("name"),
|
|
+ pyparsing.Optional(op_interval).setResultsName("interval"),
|
|
+ ]
|
|
+ )
|
|
+ op_expr.setParseAction(__build_op_expr)
|
|
+
|
|
+ simple_expr_list = []
|
|
+ if allow_rsc_expr:
|
|
+ simple_expr_list.append(rsc_expr)
|
|
+ if allow_op_expr:
|
|
+ simple_expr_list.append(op_expr)
|
|
+ simple_expr = pyparsing.Or(simple_expr_list)
|
|
+
|
|
+ # See pyparsing examples
|
|
+ # https://github.com/pyparsing/pyparsing/blob/master/examples/simpleBool.py
|
|
+ # https://github.com/pyparsing/pyparsing/blob/master/examples/eval_arith.py
|
|
+ bool_operator = pyparsing.Or(
|
|
+ [pyparsing.CaselessKeyword("and"), pyparsing.CaselessKeyword("or")]
|
|
+ )
|
|
+ bool_expr = pyparsing.infixNotation(
|
|
+ simple_expr,
|
|
+ # By putting both "and" and "or" in one tuple we say they have the same
|
|
+ # priority. This is consistent with legacy pcs parsers. And it is how
|
|
+ # it should be, they work as a glue between "simple_expr"s.
|
|
+ [(bool_operator, 2, pyparsing.opAssoc.LEFT, __build_bool_tree)],
|
|
+ )
|
|
+
|
|
+ return pyparsing.Or([bool_expr, simple_expr])
|
|
diff --git a/pcs/lib/cib/rule/validator.py b/pcs/lib/cib/rule/validator.py
|
|
new file mode 100644
|
|
index 00000000..c733ad96
|
|
--- /dev/null
|
|
+++ b/pcs/lib/cib/rule/validator.py
|
|
@@ -0,0 +1,62 @@
|
|
+from typing import Set
|
|
+
|
|
+from pcs.common import reports
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
+
|
|
+from .expression_part import (
|
|
+ BoolExpr,
|
|
+ OpExpr,
|
|
+ RscExpr,
|
|
+)
|
|
+
|
|
+
|
|
+class Validator:
|
|
+ # TODO For now we only check allowed expressions. Other checks and
|
|
+ # validations can be added if needed.
|
|
+ def __init__(
|
|
+ self,
|
|
+ parsed_rule: BoolExpr,
|
|
+ allow_rsc_expr: bool = False,
|
|
+ allow_op_expr: bool = False,
|
|
+ ):
|
|
+ """
|
|
+ parsed_rule -- a rule to be validated
|
|
+ allow_op_expr -- are op expressions allowed in the rule?
|
|
+ allow_rsc_expr -- are resource expressions allowed in the rule?
|
|
+ """
|
|
+ self._rule = parsed_rule
|
|
+ self._allow_op_expr = allow_op_expr
|
|
+ self._allow_rsc_expr = allow_rsc_expr
|
|
+ self._disallowed_expr_list: Set[CibRuleExpressionType] = set()
|
|
+
|
|
+ self._method_map = {
|
|
+ BoolExpr: self._validate_bool_expr,
|
|
+ OpExpr: self._validate_op_expr,
|
|
+ RscExpr: self._validate_rsc_expr,
|
|
+ }
|
|
+
|
|
+ def get_reports(self) -> reports.ReportItemList:
|
|
+ self._method_map[type(self._rule)](self._rule)
|
|
+ report_list = []
|
|
+ for expr_type in self._disallowed_expr_list:
|
|
+ report_list.append(
|
|
+ reports.ReportItem.error(
|
|
+ reports.messages.RuleExpressionNotAllowed(expr_type)
|
|
+ )
|
|
+ )
|
|
+ return report_list
|
|
+
|
|
+ def _validate_bool_expr(self, expr: BoolExpr):
|
|
+ for child in expr.children:
|
|
+ if type(child) in self._method_map:
|
|
+ self._method_map[type(child)](child)
|
|
+
|
|
+ def _validate_op_expr(self, expr):
|
|
+ del expr
|
|
+ if not self._allow_op_expr:
|
|
+ self._disallowed_expr_list.add(CibRuleExpressionType.OP_EXPRESSION)
|
|
+
|
|
+ def _validate_rsc_expr(self, expr):
|
|
+ del expr
|
|
+ if not self._allow_rsc_expr:
|
|
+ self._disallowed_expr_list.add(CibRuleExpressionType.RSC_EXPRESSION)
|
|
diff --git a/pcs/lib/cib/tools.py b/pcs/lib/cib/tools.py
|
|
index 920b7442..cfc5ba59 100644
|
|
--- a/pcs/lib/cib/tools.py
|
|
+++ b/pcs/lib/cib/tools.py
|
|
@@ -28,7 +28,7 @@ class IdProvider:
|
|
self._cib = get_root(cib_element)
|
|
self._booked_ids = set()
|
|
|
|
- def allocate_id(self, proposed_id):
|
|
+ def allocate_id(self, proposed_id: str) -> str:
|
|
"""
|
|
Generate a new unique id based on the proposal and keep track of it
|
|
string proposed_id -- requested id
|
|
@@ -294,9 +294,11 @@ def find_element_by_tag_and_id(
|
|
return None
|
|
|
|
|
|
-def create_subelement_id(context_element, suffix, id_provider):
|
|
+def create_subelement_id(
|
|
+ context_element: Element, suffix: str, id_provider: IdProvider
|
|
+) -> str:
|
|
proposed_id = sanitize_id(
|
|
- "{0}-{1}".format(context_element.get("id"), suffix)
|
|
+ "{0}-{1}".format(context_element.get("id", context_element.tag), suffix)
|
|
)
|
|
return id_provider.allocate_id(proposed_id)
|
|
|
|
diff --git a/pcs/lib/commands/cib_options.py b/pcs/lib/commands/cib_options.py
|
|
index 713644ca..368ce409 100644
|
|
--- a/pcs/lib/commands/cib_options.py
|
|
+++ b/pcs/lib/commands/cib_options.py
|
|
@@ -1,54 +1,312 @@
|
|
-from functools import partial
|
|
+from typing import (
|
|
+ Any,
|
|
+ Container,
|
|
+ Iterable,
|
|
+ List,
|
|
+ Mapping,
|
|
+ Optional,
|
|
+)
|
|
|
|
from pcs.common import reports
|
|
+from pcs.common.pacemaker.nvset import CibNvsetDto
|
|
from pcs.common.reports.item import ReportItem
|
|
-from pcs.lib.cib import sections
|
|
-from pcs.lib.cib.nvpair import arrange_first_meta_attributes
|
|
+from pcs.common.tools import Version
|
|
+from pcs.lib.cib import (
|
|
+ nvpair_multi,
|
|
+ sections,
|
|
+)
|
|
from pcs.lib.cib.tools import IdProvider
|
|
from pcs.lib.env import LibraryEnvironment
|
|
+from pcs.lib.errors import LibraryError
|
|
|
|
|
|
-def _set_any_defaults(section_name, env: LibraryEnvironment, options):
|
|
+def resource_defaults_create(
|
|
+ env: LibraryEnvironment,
|
|
+ nvpairs: Mapping[str, str],
|
|
+ nvset_options: Mapping[str, str],
|
|
+ nvset_rule: Optional[str] = None,
|
|
+ force_flags: Optional[Container] = None,
|
|
+) -> None:
|
|
"""
|
|
- string section_name -- determine the section of defaults
|
|
- env -- provides access to outside environment
|
|
- dict options -- are desired options with its values; when value is empty the
|
|
- option have to be removed
|
|
+ Create new resource defaults nvset
|
|
+
|
|
+ env --
|
|
+ nvpairs -- name-value pairs to be put into the new nvset
|
|
+ nvset_options -- additional attributes of the created nvset
|
|
+ nvset_rule -- optional rule describing when the created nvset applies
|
|
+ force_flags -- list of flags codes
|
|
+ """
|
|
+ return _defaults_create(
|
|
+ env,
|
|
+ sections.RSC_DEFAULTS,
|
|
+ dict(rule_allows_rsc_expr=True, rule_allows_op_expr=False),
|
|
+ nvpairs,
|
|
+ nvset_options,
|
|
+ nvset_rule=nvset_rule,
|
|
+ force_flags=force_flags,
|
|
+ )
|
|
+
|
|
+
|
|
+def operation_defaults_create(
|
|
+ env: LibraryEnvironment,
|
|
+ nvpairs: Mapping[str, str],
|
|
+ nvset_options: Mapping[str, str],
|
|
+ nvset_rule: Optional[str] = None,
|
|
+ force_flags: Optional[Container] = None,
|
|
+) -> None:
|
|
+ """
|
|
+ Create new operation defaults nvset
|
|
+
|
|
+ env --
|
|
+ nvpairs -- name-value pairs to be put into the new nvset
|
|
+ nvset_options -- additional attributes of the created nvset
|
|
+ nvset_rule -- optional rule describing when the created nvset applies
|
|
+ force_flags -- list of flags codes
|
|
"""
|
|
- # Do not ever remove the nvset element, even if it is empty. There may be
|
|
- # ACLs set in pacemaker which allow "write" for nvpairs (adding, changing
|
|
- # and removing) but not nvsets. In such a case, removing the nvset would
|
|
- # cause the whole change to be rejected by pacemaker with a "permission
|
|
- # denied" message.
|
|
- # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
|
|
+ return _defaults_create(
|
|
+ env,
|
|
+ sections.OP_DEFAULTS,
|
|
+ dict(rule_allows_rsc_expr=True, rule_allows_op_expr=True),
|
|
+ nvpairs,
|
|
+ nvset_options,
|
|
+ nvset_rule=nvset_rule,
|
|
+ force_flags=force_flags,
|
|
+ )
|
|
+
|
|
+
|
|
+def _defaults_create(
|
|
+ env: LibraryEnvironment,
|
|
+ cib_section_name: str,
|
|
+ validator_options: Mapping[str, Any],
|
|
+ nvpairs: Mapping[str, str],
|
|
+ nvset_options: Mapping[str, str],
|
|
+ nvset_rule: Optional[str] = None,
|
|
+ force_flags: Optional[Container] = None,
|
|
+) -> None:
|
|
+ if force_flags is None:
|
|
+ force_flags = set()
|
|
+ force = (reports.codes.FORCE in force_flags) or (
|
|
+ reports.codes.FORCE_OPTIONS in force_flags
|
|
+ )
|
|
+
|
|
+ required_cib_version = None
|
|
+ if nvset_rule:
|
|
+ required_cib_version = Version(3, 4, 0)
|
|
+ cib = env.get_cib(required_cib_version)
|
|
+ id_provider = IdProvider(cib)
|
|
+
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ id_provider,
|
|
+ nvpairs,
|
|
+ nvset_options,
|
|
+ nvset_rule=nvset_rule,
|
|
+ **validator_options,
|
|
+ )
|
|
+ if env.report_processor.report_list(
|
|
+ validator.validate(force_options=force)
|
|
+ ).has_errors:
|
|
+ raise LibraryError()
|
|
+
|
|
+ nvpair_multi.nvset_append_new(
|
|
+ sections.get(cib, cib_section_name),
|
|
+ id_provider,
|
|
+ nvpair_multi.NVSET_META,
|
|
+ nvpairs,
|
|
+ nvset_options,
|
|
+ nvset_rule=validator.get_parsed_rule(),
|
|
+ )
|
|
+
|
|
env.report_processor.report(
|
|
ReportItem.warning(reports.messages.DefaultsCanBeOverriden())
|
|
)
|
|
+ env.push_cib()
|
|
+
|
|
+
|
|
+def resource_defaults_config(env: LibraryEnvironment) -> List[CibNvsetDto]:
|
|
+ """
|
|
+ List all resource defaults nvsets
|
|
+ """
|
|
+ return _defaults_config(env, sections.RSC_DEFAULTS)
|
|
+
|
|
+
|
|
+def operation_defaults_config(env: LibraryEnvironment) -> List[CibNvsetDto]:
|
|
+ """
|
|
+ List all operation defaults nvsets
|
|
+ """
|
|
+ return _defaults_config(env, sections.OP_DEFAULTS)
|
|
+
|
|
+
|
|
+def _defaults_config(
|
|
+ env: LibraryEnvironment, cib_section_name: str,
|
|
+) -> List[CibNvsetDto]:
|
|
+ return [
|
|
+ nvpair_multi.nvset_element_to_dto(nvset_el)
|
|
+ for nvset_el in nvpair_multi.find_nvsets(
|
|
+ sections.get(env.get_cib(), cib_section_name)
|
|
+ )
|
|
+ ]
|
|
+
|
|
+
|
|
+def resource_defaults_remove(
|
|
+ env: LibraryEnvironment, nvset_id_list: Iterable[str]
|
|
+) -> None:
|
|
+ """
|
|
+ Remove specified resource defaults nvsets
|
|
+
|
|
+ env --
|
|
+ nvset_id_list -- nvset IDs to be removed
|
|
+ """
|
|
+ return _defaults_remove(env, sections.RSC_DEFAULTS, nvset_id_list)
|
|
+
|
|
+
|
|
+def operation_defaults_remove(
|
|
+ env: LibraryEnvironment, nvset_id_list: Iterable[str]
|
|
+) -> None:
|
|
+ """
|
|
+ Remove specified operation defaults nvsets
|
|
|
|
- if not options:
|
|
+ env --
|
|
+ nvset_id_list -- nvset IDs to be removed
|
|
+ """
|
|
+ return _defaults_remove(env, sections.OP_DEFAULTS, nvset_id_list)
|
|
+
|
|
+
|
|
+def _defaults_remove(
|
|
+ env: LibraryEnvironment, cib_section_name: str, nvset_id_list: Iterable[str]
|
|
+) -> None:
|
|
+ if not nvset_id_list:
|
|
return
|
|
+ nvset_elements, report_list = nvpair_multi.find_nvsets_by_ids(
|
|
+ sections.get(env.get_cib(), cib_section_name), nvset_id_list
|
|
+ )
|
|
+ if env.report_processor.report_list(report_list).has_errors:
|
|
+ raise LibraryError()
|
|
+ nvpair_multi.nvset_remove(nvset_elements)
|
|
+ env.push_cib()
|
|
+
|
|
+
|
|
+def resource_defaults_update(
|
|
+ env: LibraryEnvironment,
|
|
+ nvset_id: Optional[str],
|
|
+ nvpairs: Mapping[str, str],
|
|
+) -> None:
|
|
+ """
|
|
+ Update specified resource defaults nvset
|
|
+
|
|
+ env --
|
|
+ nvset_id -- nvset ID to be updated; if None, update an existing nvset if
|
|
+ there is only one
|
|
+ nvpairs -- name-value pairs to be put into the nvset
|
|
+ """
|
|
+ return _defaults_update(
|
|
+ env,
|
|
+ sections.RSC_DEFAULTS,
|
|
+ nvset_id,
|
|
+ nvpairs,
|
|
+ reports.const.PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE,
|
|
+ )
|
|
+
|
|
|
|
+def operation_defaults_update(
|
|
+ env: LibraryEnvironment,
|
|
+ nvset_id: Optional[str],
|
|
+ nvpairs: Mapping[str, str],
|
|
+) -> None:
|
|
+ """
|
|
+ Update specified operation defaults nvset
|
|
+
|
|
+ env --
|
|
+ nvset_id -- nvset ID to be updated; if None, update an existing nvset if
|
|
+ there is only one
|
|
+ nvpairs -- name-value pairs to be put into the nvset
|
|
+ """
|
|
+ return _defaults_update(
|
|
+ env,
|
|
+ sections.OP_DEFAULTS,
|
|
+ nvset_id,
|
|
+ nvpairs,
|
|
+ reports.const.PCS_COMMAND_OPERATION_DEFAULTS_UPDATE,
|
|
+ )
|
|
+
|
|
+
|
|
+def _defaults_update(
|
|
+ env: LibraryEnvironment,
|
|
+ cib_section_name: str,
|
|
+ nvset_id: Optional[str],
|
|
+ nvpairs: Mapping[str, str],
|
|
+ pcs_command: reports.types.PcsCommand,
|
|
+) -> None:
|
|
cib = env.get_cib()
|
|
+ id_provider = IdProvider(cib)
|
|
+
|
|
+ if nvset_id is None:
|
|
+ # Backward compatibility code to support an old use case where no id
|
|
+ # was requested and provided and the first meta_attributes nvset was
|
|
+ # created / updated. However, we check that there is only one nvset
|
|
+ # present in the CIB to prevent breaking the configuration with
|
|
+ # multiple nvsets in place.
|
|
+
|
|
+ # This is to be supported as it provides means of easily managing
|
|
+ # defaults if only one set of defaults is needed.
|
|
|
|
- # Do not create new defaults element if we are only removing values from it.
|
|
- only_removing = True
|
|
- for value in options.values():
|
|
- if value != "":
|
|
- only_removing = False
|
|
- break
|
|
- if only_removing and not sections.exists(cib, section_name):
|
|
+ # TODO move this to a separate lib command.
|
|
+
|
|
+ if not nvpairs:
|
|
+ return
|
|
+
|
|
+ # Do not create new defaults element if we are only removing values
|
|
+ # from it.
|
|
+ only_removing = True
|
|
+ for value in nvpairs.values():
|
|
+ if value != "":
|
|
+ only_removing = False
|
|
+ break
|
|
+ if only_removing and not sections.exists(cib, cib_section_name):
|
|
+ env.report_processor.report(
|
|
+ ReportItem.warning(reports.messages.DefaultsCanBeOverriden())
|
|
+ )
|
|
+ return
|
|
+
|
|
+ nvset_elements = nvpair_multi.find_nvsets(
|
|
+ sections.get(cib, cib_section_name)
|
|
+ )
|
|
+ if len(nvset_elements) > 1:
|
|
+ env.report_processor.report(
|
|
+ reports.item.ReportItem.error(
|
|
+ reports.messages.CibNvsetAmbiguousProvideNvsetId(
|
|
+ pcs_command
|
|
+ )
|
|
+ )
|
|
+ )
|
|
+ raise LibraryError()
|
|
+ env.report_processor.report(
|
|
+ ReportItem.warning(reports.messages.DefaultsCanBeOverriden())
|
|
+ )
|
|
+ if len(nvset_elements) == 1:
|
|
+ nvpair_multi.nvset_update(nvset_elements[0], id_provider, nvpairs)
|
|
+ elif only_removing:
|
|
+ # do not create new nvset if there is none and we are only removing
|
|
+ # nvpairs
|
|
+ return
|
|
+ else:
|
|
+ nvpair_multi.nvset_append_new(
|
|
+ sections.get(cib, cib_section_name),
|
|
+ id_provider,
|
|
+ nvpair_multi.NVSET_META,
|
|
+ nvpairs,
|
|
+ {},
|
|
+ )
|
|
+ env.push_cib()
|
|
return
|
|
|
|
- defaults_section = sections.get(cib, section_name)
|
|
- arrange_first_meta_attributes(
|
|
- defaults_section,
|
|
- options,
|
|
- IdProvider(cib),
|
|
- new_id="{0}-options".format(section_name),
|
|
+ nvset_elements, report_list = nvpair_multi.find_nvsets_by_ids(
|
|
+ sections.get(cib, cib_section_name), [nvset_id]
|
|
)
|
|
+ if env.report_processor.report_list(report_list).has_errors:
|
|
+ raise LibraryError()
|
|
|
|
+ nvpair_multi.nvset_update(nvset_elements[0], id_provider, nvpairs)
|
|
+ env.report_processor.report(
|
|
+ ReportItem.warning(reports.messages.DefaultsCanBeOverriden())
|
|
+ )
|
|
env.push_cib()
|
|
-
|
|
-
|
|
-set_operations_defaults = partial(_set_any_defaults, sections.OP_DEFAULTS)
|
|
-set_resources_defaults = partial(_set_any_defaults, sections.RSC_DEFAULTS)
|
|
diff --git a/pcs/lib/validate.py b/pcs/lib/validate.py
|
|
index 2edf8b31..7890585a 100644
|
|
--- a/pcs/lib/validate.py
|
|
+++ b/pcs/lib/validate.py
|
|
@@ -39,6 +39,7 @@ from pcs.common.reports import (
|
|
)
|
|
from pcs.lib.corosync import constants as corosync_constants
|
|
from pcs.lib.pacemaker.values import (
|
|
+ is_score,
|
|
timeout_to_seconds,
|
|
validate_id,
|
|
)
|
|
@@ -676,6 +677,20 @@ class ValuePositiveInteger(ValuePredicateBase):
|
|
return "a positive integer"
|
|
|
|
|
|
+class ValueScore(ValueValidator):
|
|
+ """
|
|
+ Report INVALID_SCORE if the value is not a valid CIB score
|
|
+ """
|
|
+
|
|
+ def _validate_value(self, value):
|
|
+ report_list = []
|
|
+ if not is_score(value.normalized):
|
|
+ report_list.append(
|
|
+ ReportItem.error(reports.messages.InvalidScore(value.original))
|
|
+ )
|
|
+ return report_list
|
|
+
|
|
+
|
|
class ValueTimeInterval(ValuePredicateBase):
|
|
"""
|
|
Report INVALID_OPTION_VALUE when the value is not a time interval
|
|
diff --git a/pcs/lib/xml_tools.py b/pcs/lib/xml_tools.py
|
|
index a463c418..b7d778a3 100644
|
|
--- a/pcs/lib/xml_tools.py
|
|
+++ b/pcs/lib/xml_tools.py
|
|
@@ -1,4 +1,4 @@
|
|
-from typing import cast, Iterable
|
|
+from typing import cast, Dict, Iterable
|
|
from xml.etree.ElementTree import Element
|
|
|
|
from lxml import etree
|
|
@@ -56,8 +56,11 @@ def get_sub_element(
|
|
return sub_element
|
|
|
|
|
|
-def export_attributes(element):
|
|
- return dict((key, value) for key, value in element.attrib.items())
|
|
+def export_attributes(element: Element, with_id: bool = True) -> Dict[str, str]:
|
|
+ result = dict((key, value) for key, value in element.attrib.items())
|
|
+ if not with_id:
|
|
+ result.pop("id", None)
|
|
+ return result
|
|
|
|
|
|
def update_attribute_remove_empty(element, name, value):
|
|
diff --git a/pcs/pcs.8 b/pcs/pcs.8
|
|
index 85c6adb1..c887d332 100644
|
|
--- a/pcs/pcs.8
|
|
+++ b/pcs/pcs.8
|
|
@@ -185,8 +185,48 @@ Remove specified operation (note: you must specify the exact operation propertie
|
|
op remove <operation id>
|
|
Remove the specified operation id.
|
|
.TP
|
|
-op defaults [options]
|
|
-Set default values for operations, if no options are passed, lists currently configured defaults. Defaults do not apply to resources which override them with their own defined operations.
|
|
+op defaults [config] [\fB\-\-full\fR]
|
|
+List currently configured default values for operations. If \fB\-\-full\fR is specified, also list ids.
|
|
+.TP
|
|
+op defaults <name>=<value>
|
|
+Set default values for operations.
|
|
+.br
|
|
+NOTE: Defaults do not apply to resources which override them with their own defined values.
|
|
+.TP
|
|
+op defaults set create [<set options>] [meta [<name>=<value>]...] [rule [<expression>]]
|
|
+Create a new set of default values for resource operations. You may specify a rule describing resources and / or operations to which the set applies.
|
|
+.br
|
|
+Set options are: id, score
|
|
+.br
|
|
+Expression looks like one of the following:
|
|
+.br
|
|
+ op <operation name> [interval=<interval>]
|
|
+.br
|
|
+ resource [<standard>]:[<provider>]:[<type>]
|
|
+.br
|
|
+ <expression> and|or <expression>
|
|
+.br
|
|
+ ( <expression> )
|
|
+.br
|
|
+You may specify all or any of 'standard', 'provider' and 'type' in a resource expression. For example: 'resource ocf::' matches all resources of 'ocf' standard, while 'resource ::Dummy' matches all resources of 'Dummy' type regardless of their standard and provider.
|
|
+.br
|
|
+NOTE: Defaults do not apply to resources which override them with their own defined values.
|
|
+.TP
|
|
+op defaults set delete [<set id>]...
|
|
+Delete specified options sets.
|
|
+.TP
|
|
+op defaults set remove [<set id>]...
|
|
+Delete specified options sets.
|
|
+.TP
|
|
+op defaults set update <set id> [meta [<name>=<value>]...]
|
|
+Add, remove or change values in specified set of default values for resource operations.
|
|
+.br
|
|
+NOTE: Defaults do not apply to resources which override them with their own defined values.
|
|
+.TP
|
|
+op defaults update <name>=<value>...
|
|
+Set default values for operations. This is a simplified command useful for cases when you only manage one set of default values.
|
|
+.br
|
|
+NOTE: Defaults do not apply to resources which override them with their own defined values.
|
|
.TP
|
|
meta <resource id | group id | clone id> <meta options> [\fB\-\-wait\fR[=n]]
|
|
Add specified options to the specified resource, group or clone. Meta options should be in the format of name=value, options may be removed by setting an option without a value. If \fB\-\-wait\fR is specified, pcs will wait up to 'n' seconds for the changes to take effect and then return 0 if the changes have been processed or 1 otherwise. If 'n' is not specified it defaults to 60 minutes.
|
|
@@ -232,8 +272,46 @@ Set resources listed to managed mode (default). If \fB\-\-monitor\fR is specifie
|
|
unmanage <resource id | tag id>... [\fB\-\-monitor\fR]
|
|
Set resources listed to unmanaged mode. When a resource is in unmanaged mode, the cluster is not allowed to start nor stop the resource. If \fB\-\-monitor\fR is specified, disable all monitor operations of the resources.
|
|
.TP
|
|
-defaults [options]
|
|
-Set default values for resources, if no options are passed, lists currently configured defaults. Defaults do not apply to resources which override them with their own defined values.
|
|
+defaults [config] [\fB\-\-full\fR]
|
|
+List currently configured default values for resources. If \fB\-\-full\fR is specified, also list ids.
|
|
+.TP
|
|
+defaults <name>=<value>
|
|
+Set default values for resources.
|
|
+.br
|
|
+NOTE: Defaults do not apply to resources which override them with their own defined values.
|
|
+.TP
|
|
+defaults set create [<set options>] [meta [<name>=<value>]...] [rule [<expression>]]
|
|
+Create a new set of default values for resources. You may specify a rule describing resources to which the set applies.
|
|
+.br
|
|
+Set options are: id, score
|
|
+.br
|
|
+Expression looks like one of the following:
|
|
+.br
|
|
+ resource [<standard>]:[<provider>]:[<type>]
|
|
+.br
|
|
+ <expression> and|or <expression>
|
|
+.br
|
|
+ ( <expression> )
|
|
+.br
|
|
+You may specify all or any of 'standard', 'provider' and 'type' in a resource expression. For example: 'resource ocf::' matches all resources of 'ocf' standard, while 'resource ::Dummy' matches all resources of 'Dummy' type regardless of their standard and provider.
|
|
+.br
|
|
+NOTE: Defaults do not apply to resources which override them with their own defined values.
|
|
+.TP
|
|
+defaults set delete [<set id>]...
|
|
+Delete specified options sets.
|
|
+.TP
|
|
+defaults set remove [<set id>]...
|
|
+Delete specified options sets.
|
|
+.TP
|
|
+defaults set update <set id> [meta [<name>=<value>]...]
|
|
+Add, remove or change values in specified set of default values for resources.
|
|
+.br
|
|
+NOTE: Defaults do not apply to resources which override them with their own defined values.
|
|
+.TP
|
|
+defaults update <name>=<value>...
|
|
+Set default values for resources. This is a simplified command useful for cases when you only manage one set of default values.
|
|
+.br
|
|
+NOTE: Defaults do not apply to resources which override them with their own defined values.
|
|
.TP
|
|
cleanup [<resource id>] [node=<node>] [operation=<operation> [interval=<interval>]] [\fB\-\-strict\fR]
|
|
Make the cluster forget failed operations from history of the resource and re\-detect its current state. This can be useful to purge knowledge of past failures that have since been resolved.
|
|
diff --git a/pcs/resource.py b/pcs/resource.py
|
|
index dd199bea..e835fc99 100644
|
|
--- a/pcs/resource.py
|
|
+++ b/pcs/resource.py
|
|
@@ -6,7 +6,12 @@ import textwrap
|
|
import time
|
|
import json
|
|
|
|
-from typing import Any, List
|
|
+from typing import (
|
|
+ Any,
|
|
+ Callable,
|
|
+ List,
|
|
+ Sequence,
|
|
+)
|
|
|
|
from pcs import (
|
|
usage,
|
|
@@ -19,10 +24,12 @@ from pcs.settings import (
|
|
)
|
|
from pcs.cli.common.errors import CmdLineInputError, raise_command_replaced
|
|
from pcs.cli.common.parse_args import (
|
|
+ group_by_keywords,
|
|
prepare_options,
|
|
prepare_options_allowed,
|
|
InputModifiers,
|
|
)
|
|
+from pcs.cli.nvset import nvset_dto_list_to_lines
|
|
from pcs.cli.reports import process_library_reports
|
|
from pcs.cli.reports.output import error, warn
|
|
from pcs.cli.resource.parse_args import (
|
|
@@ -31,8 +38,8 @@ from pcs.cli.resource.parse_args import (
|
|
parse_bundle_update_options,
|
|
parse_create as parse_create_args,
|
|
)
|
|
+from pcs.common import reports
|
|
from pcs.common.str_tools import indent
|
|
-from pcs.common.reports import ReportItemSeverity
|
|
import pcs.lib.cib.acl as lib_acl
|
|
from pcs.lib.cib.resource import (
|
|
bundle,
|
|
@@ -113,28 +120,228 @@ def resource_utilization_cmd(lib, argv, modifiers):
|
|
set_resource_utilization(argv.pop(0), argv)
|
|
|
|
|
|
-def resource_defaults_cmd(lib, argv, modifiers):
|
|
+def _defaults_set_create_cmd(
|
|
+ lib_command: Callable[..., Any],
|
|
+ argv: Sequence[str],
|
|
+ modifiers: InputModifiers,
|
|
+):
|
|
+ modifiers.ensure_only_supported("-f", "--force")
|
|
+
|
|
+ groups = group_by_keywords(
|
|
+ argv,
|
|
+ set(["meta", "rule"]),
|
|
+ implicit_first_group_key="options",
|
|
+ keyword_repeat_allowed=False,
|
|
+ )
|
|
+ force_flags = set()
|
|
+ if modifiers.get("--force"):
|
|
+ force_flags.add(reports.codes.FORCE)
|
|
+
|
|
+ lib_command(
|
|
+ prepare_options(groups["meta"]),
|
|
+ prepare_options(groups["options"]),
|
|
+ nvset_rule=(" ".join(groups["rule"]) if groups["rule"] else None),
|
|
+ force_flags=force_flags,
|
|
+ )
|
|
+
|
|
+
|
|
+def resource_defaults_set_create_cmd(
|
|
+ lib: Any, argv: Sequence[str], modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ * --force - allow unknown options
|
|
+ """
|
|
+ return _defaults_set_create_cmd(
|
|
+ lib.cib_options.resource_defaults_create, argv, modifiers
|
|
+ )
|
|
+
|
|
+
|
|
+def resource_op_defaults_set_create_cmd(
|
|
+ lib: Any, argv: Sequence[str], modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ * --force - allow unknown options
|
|
+ """
|
|
+ return _defaults_set_create_cmd(
|
|
+ lib.cib_options.operation_defaults_create, argv, modifiers
|
|
+ )
|
|
+
|
|
+
|
|
+def _defaults_config_cmd(
|
|
+ lib_command: Callable[..., Any],
|
|
+ argv: Sequence[str],
|
|
+ modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ * --full - verbose output
|
|
+ """
|
|
+ if argv:
|
|
+ raise CmdLineInputError()
|
|
+ modifiers.ensure_only_supported("-f", "--full")
|
|
+ print(
|
|
+ "\n".join(
|
|
+ nvset_dto_list_to_lines(
|
|
+ lib_command(),
|
|
+ with_ids=modifiers.get("--full"),
|
|
+ text_if_empty="No defaults set",
|
|
+ )
|
|
+ )
|
|
+ )
|
|
+
|
|
+
|
|
+def resource_defaults_config_cmd(
|
|
+ lib: Any, argv: Sequence[str], modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ * --full - verbose output
|
|
+ """
|
|
+ return _defaults_config_cmd(
|
|
+ lib.cib_options.resource_defaults_config, argv, modifiers
|
|
+ )
|
|
+
|
|
+
|
|
+def resource_op_defaults_config_cmd(
|
|
+ lib: Any, argv: Sequence[str], modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ * --full - verbose output
|
|
+ """
|
|
+ return _defaults_config_cmd(
|
|
+ lib.cib_options.operation_defaults_config, argv, modifiers
|
|
+ )
|
|
+
|
|
+
|
|
+def _defaults_set_remove_cmd(
|
|
+ lib_command: Callable[..., Any],
|
|
+ argv: Sequence[str],
|
|
+ modifiers: InputModifiers,
|
|
+) -> None:
|
|
"""
|
|
Options:
|
|
* -f - CIB file
|
|
"""
|
|
modifiers.ensure_only_supported("-f")
|
|
- if not argv:
|
|
- print("\n".join(show_defaults(utils.get_cib_dom(), "rsc_defaults")))
|
|
- else:
|
|
- lib.cib_options.set_resources_defaults(prepare_options(argv))
|
|
+ lib_command(argv)
|
|
|
|
|
|
-def resource_op_defaults_cmd(lib, argv, modifiers):
|
|
+def resource_defaults_set_remove_cmd(
|
|
+ lib: Any, argv: Sequence[str], modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ """
|
|
+ return _defaults_set_remove_cmd(
|
|
+ lib.cib_options.resource_defaults_remove, argv, modifiers
|
|
+ )
|
|
+
|
|
+
|
|
+def resource_op_defaults_set_remove_cmd(
|
|
+ lib: Any, argv: Sequence[str], modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ """
|
|
+ return _defaults_set_remove_cmd(
|
|
+ lib.cib_options.operation_defaults_remove, argv, modifiers
|
|
+ )
|
|
+
|
|
+
|
|
+def _defaults_set_update_cmd(
|
|
+ lib_command: Callable[..., Any],
|
|
+ argv: Sequence[str],
|
|
+ modifiers: InputModifiers,
|
|
+) -> None:
|
|
"""
|
|
Options:
|
|
* -f - CIB file
|
|
"""
|
|
modifiers.ensure_only_supported("-f")
|
|
if not argv:
|
|
- print("\n".join(show_defaults(utils.get_cib_dom(), "op_defaults")))
|
|
- else:
|
|
- lib.cib_options.set_operations_defaults(prepare_options(argv))
|
|
+ raise CmdLineInputError()
|
|
+
|
|
+ set_id = argv[0]
|
|
+ groups = group_by_keywords(
|
|
+ argv[1:], set(["meta"]), keyword_repeat_allowed=False,
|
|
+ )
|
|
+ lib_command(
|
|
+ set_id, prepare_options(groups["meta"]),
|
|
+ )
|
|
+
|
|
+
|
|
+def resource_defaults_set_update_cmd(
|
|
+ lib: Any, argv: Sequence[str], modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ """
|
|
+ return _defaults_set_update_cmd(
|
|
+ lib.cib_options.resource_defaults_update, argv, modifiers
|
|
+ )
|
|
+
|
|
+
|
|
+def resource_op_defaults_set_update_cmd(
|
|
+ lib: Any, argv: Sequence[str], modifiers: InputModifiers,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ """
|
|
+ return _defaults_set_update_cmd(
|
|
+ lib.cib_options.operation_defaults_update, argv, modifiers
|
|
+ )
|
|
+
|
|
+
|
|
+def resource_defaults_legacy_cmd(
|
|
+ lib: Any,
|
|
+ argv: Sequence[str],
|
|
+ modifiers: InputModifiers,
|
|
+ deprecated_syntax_used: bool = False,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ """
|
|
+ del modifiers
|
|
+ if deprecated_syntax_used:
|
|
+ warn(
|
|
+ "This command is deprecated and will be removed. "
|
|
+ "Please use 'pcs resource defaults update' instead."
|
|
+ )
|
|
+ return lib.cib_options.resource_defaults_update(None, prepare_options(argv))
|
|
+
|
|
+
|
|
+def resource_op_defaults_legacy_cmd(
|
|
+ lib: Any,
|
|
+ argv: Sequence[str],
|
|
+ modifiers: InputModifiers,
|
|
+ deprecated_syntax_used: bool = False,
|
|
+) -> None:
|
|
+ """
|
|
+ Options:
|
|
+ * -f - CIB file
|
|
+ """
|
|
+ del modifiers
|
|
+ if deprecated_syntax_used:
|
|
+ warn(
|
|
+ "This command is deprecated and will be removed. "
|
|
+ "Please use 'pcs resource op defaults update' instead."
|
|
+ )
|
|
+ return lib.cib_options.operation_defaults_update(
|
|
+ None, prepare_options(argv)
|
|
+ )
|
|
|
|
|
|
def resource_op_add_cmd(lib, argv, modifiers):
|
|
@@ -741,9 +948,9 @@ def resource_update(lib, args, modifiers, deal_with_guest_change=True):
|
|
process_library_reports(report_list)
|
|
except lib_ra.ResourceAgentError as e:
|
|
severity = (
|
|
- ReportItemSeverity.WARNING
|
|
+ reports.ReportItemSeverity.WARNING
|
|
if modifiers.get("--force")
|
|
- else ReportItemSeverity.ERROR
|
|
+ else reports.ReportItemSeverity.ERROR
|
|
)
|
|
process_library_reports(
|
|
[lib_ra.resource_agent_error_to_report_item(e, severity)]
|
|
@@ -2543,30 +2750,6 @@ def resource_failcount_show(lib, resource, node, operation, interval, full):
|
|
return "\n".join(result_lines)
|
|
|
|
|
|
-def show_defaults(cib_dom, def_type):
|
|
- """
|
|
- Commandline options: no options
|
|
- """
|
|
- defs = cib_dom.getElementsByTagName(def_type)
|
|
- if not defs:
|
|
- return ["No defaults set"]
|
|
- defs = defs[0]
|
|
-
|
|
- # TODO duplicite to _nvpairs_strings
|
|
- key_val = {
|
|
- nvpair.getAttribute("name"): nvpair.getAttribute("value")
|
|
- for nvpair in defs.getElementsByTagName("nvpair")
|
|
- }
|
|
- if not key_val:
|
|
- return ["No defaults set"]
|
|
- strings = []
|
|
- for name, value in sorted(key_val.items()):
|
|
- if " " in value:
|
|
- value = f'"{value}"'
|
|
- strings.append(f"{name}={value}")
|
|
- return strings
|
|
-
|
|
-
|
|
def resource_node_lines(node):
|
|
"""
|
|
Commandline options: no options
|
|
@@ -2677,6 +2860,7 @@ def _nvpairs_strings(node, parent_tag, extra_vars_dict=None):
|
|
"""
|
|
Commandline options: no options
|
|
"""
|
|
+ # In the new architecture, this is implemented in pcs.cli.nvset.
|
|
key_val = {
|
|
nvpair.attrib["name"]: nvpair.attrib["value"]
|
|
for nvpair in node.findall(f"{parent_tag}/nvpair")
|
|
diff --git a/pcs/usage.py b/pcs/usage.py
|
|
index 2cab7a6c..8722bd7b 100644
|
|
--- a/pcs/usage.py
|
|
+++ b/pcs/usage.py
|
|
@@ -442,10 +442,50 @@ Commands:
|
|
op remove <operation id>
|
|
Remove the specified operation id.
|
|
|
|
- op defaults [options]
|
|
- Set default values for operations, if no options are passed, lists
|
|
- currently configured defaults. Defaults do not apply to resources which
|
|
- override them with their own defined operations.
|
|
+ op defaults [config] [--full]
|
|
+ List currently configured default values for operations. If --full is
|
|
+ specified, also list ids.
|
|
+
|
|
+ op defaults <name>=<value>...
|
|
+ Set default values for operations.
|
|
+ NOTE: Defaults do not apply to resources which override them with their
|
|
+ own defined values.
|
|
+
|
|
+ op defaults set create [<set options>] [meta [<name>=<value>]...]
|
|
+ [rule [<expression>]]
|
|
+ Create a new set of default values for resource operations. You may
|
|
+ specify a rule describing resources and / or operations to which the set
|
|
+ applies.
|
|
+ Set options are: id, score
|
|
+ Expression looks like one of the following:
|
|
+ op <operation name> [interval=<interval>]
|
|
+ resource [<standard>]:[<provider>]:[<type>]
|
|
+ <expression> and|or <expression>
|
|
+ ( <expression> )
|
|
+ You may specify all or any of 'standard', 'provider' and 'type' in
|
|
+ a resource expression. For example: 'resource ocf::' matches all
|
|
+ resources of 'ocf' standard, while 'resource ::Dummy' matches all
|
|
+ resources of 'Dummy' type regardless of their standard and provider.
|
|
+ NOTE: Defaults do not apply to resources which override them with their
|
|
+ own defined values.
|
|
+
|
|
+ op defaults set delete [<set id>]...
|
|
+ Delete specified options sets.
|
|
+
|
|
+ op defaults set remove [<set id>]...
|
|
+ Delete specified options sets.
|
|
+
|
|
+ op defaults set update <set id> [meta [<name>=<value>]...]
|
|
+ Add, remove or change values in specified set of default values for
|
|
+ resource operations.
|
|
+ NOTE: Defaults do not apply to resources which override them with their
|
|
+ own defined values.
|
|
+
|
|
+ op defaults update <name>=<value>...
|
|
+ Set default values for operations. This is a simplified command useful
|
|
+ for cases when you only manage one set of default values.
|
|
+ NOTE: Defaults do not apply to resources which override them with their
|
|
+ own defined values.
|
|
|
|
meta <resource id | group id | clone id> <meta options>
|
|
[--wait[=n]]
|
|
@@ -561,10 +601,48 @@ Commands:
|
|
--monitor is specified, disable all monitor operations of the
|
|
resources.
|
|
|
|
- defaults [options]
|
|
- Set default values for resources, if no options are passed, lists
|
|
- currently configured defaults. Defaults do not apply to resources which
|
|
- override them with their own defined values.
|
|
+ defaults [config] [--full]
|
|
+ List currently configured default values for resources. If --full is
|
|
+ specified, also list ids.
|
|
+
|
|
+ defaults <name>=<value>...
|
|
+ Set default values for resources.
|
|
+ NOTE: Defaults do not apply to resources which override them with their
|
|
+ own defined values.
|
|
+
|
|
+ defaults set create [<set options>] [meta [<name>=<value>]...]
|
|
+ [rule [<expression>]]
|
|
+ Create a new set of default values for resources. You may specify a rule
|
|
+ describing resources to which the set applies.
|
|
+ Set options are: id, score
|
|
+ Expression looks like one of the following:
|
|
+ resource [<standard>]:[<provider>]:[<type>]
|
|
+ <expression> and|or <expression>
|
|
+ ( <expression> )
|
|
+ You may specify all or any of 'standard', 'provider' and 'type' in
|
|
+ a resource expression. For example: 'resource ocf::' matches all
|
|
+ resources of 'ocf' standard, while 'resource ::Dummy' matches all
|
|
+ resources of 'Dummy' type regardless of their standard and provider.
|
|
+ NOTE: Defaults do not apply to resources which override them with their
|
|
+ own defined values.
|
|
+
|
|
+ defaults set delete [<set id>]...
|
|
+ Delete specified options sets.
|
|
+
|
|
+ defaults set remove [<set id>]...
|
|
+ Delete specified options sets.
|
|
+
|
|
+ defaults set update <set id> [meta [<name>=<value>]...]
|
|
+ Add, remove or change values in specified set of default values for
|
|
+ resources.
|
|
+ NOTE: Defaults do not apply to resources which override them with their
|
|
+ own defined values.
|
|
+
|
|
+ defaults update <name>=<value>...
|
|
+ Set default values for resources. This is a simplified command useful
|
|
+ for cases when you only manage one set of default values.
|
|
+ NOTE: Defaults do not apply to resources which override them with their
|
|
+ own defined values.
|
|
|
|
cleanup [<resource id>] [node=<node>] [operation=<operation>
|
|
[interval=<interval>]] [--strict]
|
|
diff --git a/pcs_test/resources/cib-empty-3.1.xml b/pcs_test/resources/cib-empty-3.1.xml
|
|
index 75bbb26d..88f5c414 100644
|
|
--- a/pcs_test/resources/cib-empty-3.1.xml
|
|
+++ b/pcs_test/resources/cib-empty-3.1.xml
|
|
@@ -1,4 +1,4 @@
|
|
-<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-3.1" crm_feature_set="3.0.9" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
|
|
+<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-3.1" crm_feature_set="3.1.0" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
|
|
<configuration>
|
|
<crm_config/>
|
|
<nodes>
|
|
diff --git a/pcs_test/resources/cib-empty-3.2.xml b/pcs_test/resources/cib-empty-3.2.xml
|
|
index 0b0b04b8..7ffaccb1 100644
|
|
--- a/pcs_test/resources/cib-empty-3.2.xml
|
|
+++ b/pcs_test/resources/cib-empty-3.2.xml
|
|
@@ -1,4 +1,4 @@
|
|
-<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-3.2" crm_feature_set="3.0.9" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
|
|
+<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-3.2" crm_feature_set="3.1.0" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
|
|
<configuration>
|
|
<crm_config/>
|
|
<nodes>
|
|
diff --git a/pcs_test/resources/cib-empty-3.3.xml b/pcs_test/resources/cib-empty-3.3.xml
|
|
new file mode 100644
|
|
index 00000000..3a44fe08
|
|
--- /dev/null
|
|
+++ b/pcs_test/resources/cib-empty-3.3.xml
|
|
@@ -0,0 +1,10 @@
|
|
+<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-3.3" crm_feature_set="3.1.0" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
|
|
+ <configuration>
|
|
+ <crm_config/>
|
|
+ <nodes>
|
|
+ </nodes>
|
|
+ <resources/>
|
|
+ <constraints/>
|
|
+ </configuration>
|
|
+ <status/>
|
|
+</cib>
|
|
diff --git a/pcs_test/resources/cib-empty-3.4.xml b/pcs_test/resources/cib-empty-3.4.xml
|
|
new file mode 100644
|
|
index 00000000..dcd4ff44
|
|
--- /dev/null
|
|
+++ b/pcs_test/resources/cib-empty-3.4.xml
|
|
@@ -0,0 +1,10 @@
|
|
+<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-3.4" crm_feature_set="3.1.0" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
|
|
+ <configuration>
|
|
+ <crm_config/>
|
|
+ <nodes>
|
|
+ </nodes>
|
|
+ <resources/>
|
|
+ <constraints/>
|
|
+ </configuration>
|
|
+ <status/>
|
|
+</cib>
|
|
diff --git a/pcs_test/resources/cib-empty.xml b/pcs_test/resources/cib-empty.xml
|
|
index 75bbb26d..7ffaccb1 100644
|
|
--- a/pcs_test/resources/cib-empty.xml
|
|
+++ b/pcs_test/resources/cib-empty.xml
|
|
@@ -1,4 +1,4 @@
|
|
-<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-3.1" crm_feature_set="3.0.9" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
|
|
+<cib epoch="557" num_updates="122" admin_epoch="0" validate-with="pacemaker-3.2" crm_feature_set="3.1.0" update-origin="rh7-3" update-client="crmd" cib-last-written="Thu Aug 23 16:49:17 2012" have-quorum="0" dc-uuid="2">
|
|
<configuration>
|
|
<crm_config/>
|
|
<nodes>
|
|
diff --git a/pcs_test/tier0/cli/reports/test_messages.py b/pcs_test/tier0/cli/reports/test_messages.py
|
|
index 06f32e68..47aabd63 100644
|
|
--- a/pcs_test/tier0/cli/reports/test_messages.py
|
|
+++ b/pcs_test/tier0/cli/reports/test_messages.py
|
|
@@ -481,6 +481,35 @@ class TagCannotRemoveReferencesWithoutRemovingTag(CliReportMessageTestBase):
|
|
)
|
|
|
|
|
|
+class RuleExpressionParseError(CliReportMessageTestBase):
|
|
+ def test_success(self):
|
|
+ self.assert_message(
|
|
+ messages.RuleExpressionParseError(
|
|
+ "resource dummy op monitor",
|
|
+ "Expected end of text",
|
|
+ "resource dummy op monitor",
|
|
+ 1,
|
|
+ 16,
|
|
+ 15,
|
|
+ ),
|
|
+ "'resource dummy op monitor' is not a valid rule expression, "
|
|
+ "parse error near or after line 1 column 16\n"
|
|
+ " resource dummy op monitor\n"
|
|
+ " ---------------^",
|
|
+ )
|
|
+
|
|
+
|
|
+class CibNvsetAmbiguousProvideNvsetId(CliReportMessageTestBase):
|
|
+ def test_success(self):
|
|
+ self.assert_message(
|
|
+ messages.CibNvsetAmbiguousProvideNvsetId(
|
|
+ const.PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE
|
|
+ ),
|
|
+ "Several options sets exist, please use the 'pcs resource defaults "
|
|
+ "set update' command and specify an option set ID",
|
|
+ )
|
|
+
|
|
+
|
|
# TODO: create test/check that all subclasses of
|
|
# pcs.cli.reports.messages.CliReportMessageCustom have their test class with
|
|
# the same name in this file
|
|
diff --git a/pcs_test/tier0/cli/resource/test_defaults.py b/pcs_test/tier0/cli/resource/test_defaults.py
|
|
new file mode 100644
|
|
index 00000000..0582c664
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/cli/resource/test_defaults.py
|
|
@@ -0,0 +1,324 @@
|
|
+from textwrap import dedent
|
|
+from unittest import mock, TestCase
|
|
+
|
|
+from pcs_test.tools.misc import dict_to_modifiers
|
|
+
|
|
+from pcs import resource
|
|
+from pcs.cli.common.errors import CmdLineInputError
|
|
+from pcs.common.pacemaker.nvset import (
|
|
+ CibNvpairDto,
|
|
+ CibNvsetDto,
|
|
+)
|
|
+from pcs.common.pacemaker.rule import CibRuleExpressionDto
|
|
+from pcs.common.reports import codes as report_codes
|
|
+from pcs.common.types import (
|
|
+ CibNvsetType,
|
|
+ CibRuleExpressionType,
|
|
+)
|
|
+
|
|
+
|
|
+class DefaultsBaseMixin:
|
|
+ cli_command_name = ""
|
|
+ lib_command_name = ""
|
|
+
|
|
+ def setUp(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.lib = mock.Mock(spec_set=["cib_options"])
|
|
+ self.cib_options = mock.Mock(spec_set=[self.lib_command_name])
|
|
+ self.lib.cib_options = self.cib_options
|
|
+ self.lib_command = getattr(self.cib_options, self.lib_command_name)
|
|
+ self.cli_command = getattr(resource, self.cli_command_name)
|
|
+
|
|
+ def _call_cmd(self, argv, modifiers=None):
|
|
+ modifiers = modifiers or dict()
|
|
+ self.cli_command(self.lib, argv, dict_to_modifiers(modifiers))
|
|
+
|
|
+
|
|
+@mock.patch("pcs.resource.print")
|
|
+class DefaultsConfigMixin(DefaultsBaseMixin):
|
|
+ dto_list = [
|
|
+ CibNvsetDto(
|
|
+ "my-meta_attributes",
|
|
+ CibNvsetType.META,
|
|
+ {},
|
|
+ CibRuleExpressionDto(
|
|
+ "my-meta-rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "and", "score": "INFINITY"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-meta-rule-rsc",
|
|
+ CibRuleExpressionType.RSC_EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "class": "ocf",
|
|
+ "provider": "pacemaker",
|
|
+ "type": "Dummy",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "resource ocf:pacemaker:Dummy",
|
|
+ ),
|
|
+ ],
|
|
+ "resource ocf:pacemaker:Dummy",
|
|
+ ),
|
|
+ [
|
|
+ CibNvpairDto("my-id-pair1", "name1", "value1"),
|
|
+ CibNvpairDto("my-id-pair2", "name2", "value2"),
|
|
+ ],
|
|
+ ),
|
|
+ CibNvsetDto(
|
|
+ "instance",
|
|
+ CibNvsetType.INSTANCE,
|
|
+ {},
|
|
+ None,
|
|
+ [CibNvpairDto("instance-pair", "inst", "ance")],
|
|
+ ),
|
|
+ CibNvsetDto(
|
|
+ "meta-plain",
|
|
+ CibNvsetType.META,
|
|
+ {"score": "123"},
|
|
+ None,
|
|
+ [CibNvpairDto("my-id-pair3", "name 1", "value 1")],
|
|
+ ),
|
|
+ ]
|
|
+
|
|
+ def test_no_args(self, mock_print):
|
|
+ self.lib_command.return_value = []
|
|
+ self._call_cmd([])
|
|
+ self.lib_command.assert_called_once_with()
|
|
+ mock_print.assert_called_once_with("No defaults set")
|
|
+
|
|
+ def test_usage(self, mock_print):
|
|
+ with self.assertRaises(CmdLineInputError) as cm:
|
|
+ self._call_cmd(["arg"])
|
|
+ self.assertIsNone(cm.exception.message)
|
|
+ self.lib_command.assert_not_called()
|
|
+ mock_print.assert_not_called()
|
|
+
|
|
+ def test_full(self, mock_print):
|
|
+ self.lib_command.return_value = []
|
|
+ self._call_cmd([], {"full": True})
|
|
+ self.lib_command.assert_called_once_with()
|
|
+ mock_print.assert_called_once_with("No defaults set")
|
|
+
|
|
+ def test_print(self, mock_print):
|
|
+ self.lib_command.return_value = self.dto_list
|
|
+ self._call_cmd([])
|
|
+ self.lib_command.assert_called_once_with()
|
|
+ mock_print.assert_called_once_with(
|
|
+ dedent(
|
|
+ '''\
|
|
+ Meta Attrs: my-meta_attributes
|
|
+ name1=value1
|
|
+ name2=value2
|
|
+ Rule: boolean-op=and score=INFINITY
|
|
+ Expression: resource ocf:pacemaker:Dummy
|
|
+ Attributes: instance
|
|
+ inst=ance
|
|
+ Meta Attrs: meta-plain score=123
|
|
+ "name 1"="value 1"'''
|
|
+ )
|
|
+ )
|
|
+
|
|
+ def test_print_full(self, mock_print):
|
|
+ self.lib_command.return_value = self.dto_list
|
|
+ self._call_cmd([], {"full": True})
|
|
+ self.lib_command.assert_called_once_with()
|
|
+ mock_print.assert_called_once_with(
|
|
+ dedent(
|
|
+ '''\
|
|
+ Meta Attrs: my-meta_attributes
|
|
+ name1=value1
|
|
+ name2=value2
|
|
+ Rule: boolean-op=and score=INFINITY (id:my-meta-rule)
|
|
+ Expression: resource ocf:pacemaker:Dummy (id:my-meta-rule-rsc)
|
|
+ Attributes: instance
|
|
+ inst=ance
|
|
+ Meta Attrs: meta-plain score=123
|
|
+ "name 1"="value 1"'''
|
|
+ )
|
|
+ )
|
|
+
|
|
+
|
|
+class RscDefaultsConfig(DefaultsConfigMixin, TestCase):
|
|
+ cli_command_name = "resource_defaults_config_cmd"
|
|
+ lib_command_name = "resource_defaults_config"
|
|
+
|
|
+
|
|
+class OpDefaultsConfig(DefaultsConfigMixin, TestCase):
|
|
+ cli_command_name = "resource_op_defaults_config_cmd"
|
|
+ lib_command_name = "operation_defaults_config"
|
|
+
|
|
+
|
|
+class DefaultsSetCreateMixin(DefaultsBaseMixin):
|
|
+ def test_no_args(self):
|
|
+ self._call_cmd([])
|
|
+ self.lib_command.assert_called_once_with(
|
|
+ {}, {}, nvset_rule=None, force_flags=set()
|
|
+ )
|
|
+
|
|
+ def test_no_values(self):
|
|
+ self._call_cmd(["meta", "rule"])
|
|
+ self.lib_command.assert_called_once_with(
|
|
+ {}, {}, nvset_rule=None, force_flags=set()
|
|
+ )
|
|
+
|
|
+ def test_bad_options_or_keyword(self):
|
|
+ with self.assertRaises(CmdLineInputError) as cm:
|
|
+ self._call_cmd(["aaa"])
|
|
+ self.assertEqual(
|
|
+ cm.exception.message, "missing value of 'aaa' option",
|
|
+ )
|
|
+ self.lib_command.assert_not_called()
|
|
+
|
|
+ def test_bad_values(self):
|
|
+ with self.assertRaises(CmdLineInputError) as cm:
|
|
+ self._call_cmd(["meta", "aaa"])
|
|
+ self.assertEqual(
|
|
+ cm.exception.message, "missing value of 'aaa' option",
|
|
+ )
|
|
+ self.lib_command.assert_not_called()
|
|
+
|
|
+ def test_options(self):
|
|
+ self._call_cmd(["id=custom-id", "score=10"])
|
|
+ self.lib_command.assert_called_once_with(
|
|
+ {},
|
|
+ {"id": "custom-id", "score": "10"},
|
|
+ nvset_rule=None,
|
|
+ force_flags=set(),
|
|
+ )
|
|
+
|
|
+ def test_nvpairs(self):
|
|
+ self._call_cmd(["meta", "name1=value1", "name2=value2"])
|
|
+ self.lib_command.assert_called_once_with(
|
|
+ {"name1": "value1", "name2": "value2"},
|
|
+ {},
|
|
+ nvset_rule=None,
|
|
+ force_flags=set(),
|
|
+ )
|
|
+
|
|
+ def test_rule(self):
|
|
+ self._call_cmd(["rule", "resource", "dummy", "or", "op", "monitor"])
|
|
+ self.lib_command.assert_called_once_with(
|
|
+ {},
|
|
+ {},
|
|
+ nvset_rule="resource dummy or op monitor",
|
|
+ force_flags=set(),
|
|
+ )
|
|
+
|
|
+ def test_force(self):
|
|
+ self._call_cmd([], {"force": True})
|
|
+ self.lib_command.assert_called_once_with(
|
|
+ {}, {}, nvset_rule=None, force_flags=set([report_codes.FORCE])
|
|
+ )
|
|
+
|
|
+ def test_all(self):
|
|
+ self._call_cmd(
|
|
+ [
|
|
+ "id=custom-id",
|
|
+ "score=10",
|
|
+ "meta",
|
|
+ "name1=value1",
|
|
+ "name2=value2",
|
|
+ "rule",
|
|
+ "resource",
|
|
+ "dummy",
|
|
+ "or",
|
|
+ "op",
|
|
+ "monitor",
|
|
+ ],
|
|
+ {"force": True},
|
|
+ )
|
|
+ self.lib_command.assert_called_once_with(
|
|
+ {"name1": "value1", "name2": "value2"},
|
|
+ {"id": "custom-id", "score": "10"},
|
|
+ nvset_rule="resource dummy or op monitor",
|
|
+ force_flags=set([report_codes.FORCE]),
|
|
+ )
|
|
+
|
|
+
|
|
+class RscDefaultsSetCreate(DefaultsSetCreateMixin, TestCase):
|
|
+ cli_command_name = "resource_defaults_set_create_cmd"
|
|
+ lib_command_name = "resource_defaults_create"
|
|
+
|
|
+
|
|
+class OpDefaultsSetCreate(DefaultsSetCreateMixin, TestCase):
|
|
+ cli_command_name = "resource_op_defaults_set_create_cmd"
|
|
+ lib_command_name = "operation_defaults_create"
|
|
+
|
|
+
|
|
+class DefaultsSetRemoveMixin(DefaultsBaseMixin):
|
|
+ def test_no_args(self):
|
|
+ self._call_cmd([])
|
|
+ self.lib_command.assert_called_once_with([])
|
|
+
|
|
+ def test_some_args(self):
|
|
+ self._call_cmd(["set1", "set2"])
|
|
+ self.lib_command.assert_called_once_with(["set1", "set2"])
|
|
+
|
|
+
|
|
+class RscDefaultsSetRemove(DefaultsSetRemoveMixin, TestCase):
|
|
+ cli_command_name = "resource_defaults_set_remove_cmd"
|
|
+ lib_command_name = "resource_defaults_remove"
|
|
+
|
|
+
|
|
+class OpDefaultsSetRemove(DefaultsSetRemoveMixin, TestCase):
|
|
+ cli_command_name = "resource_op_defaults_set_remove_cmd"
|
|
+ lib_command_name = "operation_defaults_remove"
|
|
+
|
|
+
|
|
+class DefaultsSetUpdateMixin(DefaultsBaseMixin):
|
|
+ def test_no_args(self):
|
|
+ with self.assertRaises(CmdLineInputError) as cm:
|
|
+ self._call_cmd([])
|
|
+ self.assertIsNone(cm.exception.message)
|
|
+ self.lib_command.assert_not_called()
|
|
+
|
|
+ def test_no_meta(self):
|
|
+ self._call_cmd(["nvset-id"])
|
|
+ self.lib_command.assert_called_once_with("nvset-id", {})
|
|
+
|
|
+ def test_no_meta_values(self):
|
|
+ self._call_cmd(["nvset-id", "meta"])
|
|
+ self.lib_command.assert_called_once_with("nvset-id", {})
|
|
+
|
|
+ def test_meta_values(self):
|
|
+ self._call_cmd(["nvset-id", "meta", "a=b", "c=d"])
|
|
+ self.lib_command.assert_called_once_with(
|
|
+ "nvset-id", {"a": "b", "c": "d"}
|
|
+ )
|
|
+
|
|
+
|
|
+class RscDefaultsSetUpdate(DefaultsSetUpdateMixin, TestCase):
|
|
+ cli_command_name = "resource_defaults_set_update_cmd"
|
|
+ lib_command_name = "resource_defaults_update"
|
|
+
|
|
+
|
|
+class OpDefaultsSetUpdate(DefaultsSetUpdateMixin, TestCase):
|
|
+ cli_command_name = "resource_op_defaults_set_update_cmd"
|
|
+ lib_command_name = "operation_defaults_update"
|
|
+
|
|
+
|
|
+class DefaultsUpdateMixin(DefaultsBaseMixin):
|
|
+ def test_no_args(self):
|
|
+ self._call_cmd([])
|
|
+ self.lib_command.assert_called_once_with(None, {})
|
|
+
|
|
+ def test_args(self):
|
|
+ self._call_cmd(["a=b", "c="])
|
|
+ self.lib_command.assert_called_once_with(None, {"a": "b", "c": ""})
|
|
+
|
|
+
|
|
+class RscDefaultsUpdate(DefaultsUpdateMixin, TestCase):
|
|
+ cli_command_name = "resource_defaults_legacy_cmd"
|
|
+ lib_command_name = "resource_defaults_update"
|
|
+
|
|
+
|
|
+class OpDefaultsUpdate(DefaultsUpdateMixin, TestCase):
|
|
+ cli_command_name = "resource_op_defaults_legacy_cmd"
|
|
+ lib_command_name = "operation_defaults_update"
|
|
diff --git a/pcs_test/tier0/cli/test_nvset.py b/pcs_test/tier0/cli/test_nvset.py
|
|
new file mode 100644
|
|
index 00000000..675d2899
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/cli/test_nvset.py
|
|
@@ -0,0 +1,92 @@
|
|
+import re
|
|
+from textwrap import dedent
|
|
+from unittest import TestCase
|
|
+
|
|
+from pcs.cli import nvset
|
|
+from pcs.common.pacemaker.nvset import (
|
|
+ CibNvpairDto,
|
|
+ CibNvsetDto,
|
|
+)
|
|
+from pcs.common.pacemaker.rule import CibRuleExpressionDto
|
|
+from pcs.common.types import (
|
|
+ CibNvsetType,
|
|
+ CibRuleExpressionType,
|
|
+)
|
|
+
|
|
+
|
|
+class NvsetDtoToLines(TestCase):
|
|
+ type_to_label = (
|
|
+ (CibNvsetType.META, "Meta Attrs"),
|
|
+ (CibNvsetType.INSTANCE, "Attributes"),
|
|
+ )
|
|
+
|
|
+ @staticmethod
|
|
+ def _export(dto, with_ids):
|
|
+ return (
|
|
+ "\n".join(nvset.nvset_dto_to_lines(dto, with_ids=with_ids)) + "\n"
|
|
+ )
|
|
+
|
|
+ def assert_lines(self, dto, lines):
|
|
+ self.assertEqual(
|
|
+ self._export(dto, True), lines,
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ self._export(dto, False), re.sub(r" +\(id:.*\)", "", lines),
|
|
+ )
|
|
+
|
|
+ def test_minimal(self):
|
|
+ for nvtype, label in self.type_to_label:
|
|
+ with self.subTest(nvset_type=nvtype, lanel=label):
|
|
+ dto = CibNvsetDto("my-id", nvtype, {}, None, [])
|
|
+ output = dedent(
|
|
+ f"""\
|
|
+ {label}: my-id
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+ def test_full(self):
|
|
+ for nvtype, label in self.type_to_label:
|
|
+ with self.subTest(nvset_type=nvtype, lanel=label):
|
|
+ dto = CibNvsetDto(
|
|
+ "my-id",
|
|
+ nvtype,
|
|
+ {"score": "150"},
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "or"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-rule-op",
|
|
+ CibRuleExpressionType.OP_EXPRESSION,
|
|
+ False,
|
|
+ {"name": "monitor"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "op monitor",
|
|
+ ),
|
|
+ ],
|
|
+ "op monitor",
|
|
+ ),
|
|
+ [
|
|
+ CibNvpairDto("my-id-pair1", "name1", "value1"),
|
|
+ CibNvpairDto("my-id-pair2", "name 2", "value 2"),
|
|
+ CibNvpairDto("my-id-pair3", "name=3", "value=3"),
|
|
+ ],
|
|
+ )
|
|
+ output = dedent(
|
|
+ f"""\
|
|
+ {label}: my-id score=150
|
|
+ "name 2"="value 2"
|
|
+ name1=value1
|
|
+ "name=3"="value=3"
|
|
+ Rule: boolean-op=or (id:my-id-rule)
|
|
+ Expression: op monitor (id:my-id-rule-op)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
diff --git a/pcs_test/tier0/cli/test_rule.py b/pcs_test/tier0/cli/test_rule.py
|
|
new file mode 100644
|
|
index 00000000..c3f6ddc4
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/cli/test_rule.py
|
|
@@ -0,0 +1,477 @@
|
|
+import re
|
|
+from textwrap import dedent
|
|
+from unittest import TestCase
|
|
+
|
|
+from pcs.cli import rule
|
|
+from pcs.common.pacemaker.rule import (
|
|
+ CibRuleDateCommonDto,
|
|
+ CibRuleExpressionDto,
|
|
+)
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
+
|
|
+
|
|
+class RuleDtoToLinesMixin:
|
|
+ @staticmethod
|
|
+ def _export(dto, with_ids):
|
|
+ return (
|
|
+ "\n".join(rule.rule_expression_dto_to_lines(dto, with_ids=with_ids))
|
|
+ + "\n"
|
|
+ )
|
|
+
|
|
+ def assert_lines(self, dto, lines):
|
|
+ self.assertEqual(
|
|
+ self._export(dto, True), lines,
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ self._export(dto, False), re.sub(r" +\(id:.*\)", "", lines),
|
|
+ )
|
|
+
|
|
+
|
|
+class ExpressionDtoToLines(RuleDtoToLinesMixin, TestCase):
|
|
+ def test_defined(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-expr",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {"attribute": "pingd", "operation": "defined"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "defined pingd",
|
|
+ ),
|
|
+ ],
|
|
+ "defined pingd",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:my-id)
|
|
+ Expression: defined pingd (id:my-id-expr)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+ def test_value_comparison(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-expr",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "my-attr",
|
|
+ "operation": "eq",
|
|
+ "value": "my value",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "my-attr eq 'my value'",
|
|
+ ),
|
|
+ ],
|
|
+ "my-attr eq 'my value'",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:my-id)
|
|
+ Expression: my-attr eq 'my value' (id:my-id-expr)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+ def test_value_comparison_with_type(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-expr",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "foo",
|
|
+ "operation": "gt",
|
|
+ "type": "version",
|
|
+ "value": "1.2.3",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "foo gt version 1.2.3",
|
|
+ ),
|
|
+ ],
|
|
+ "foo gt version 1.2.3",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:my-id)
|
|
+ Expression: foo gt version 1.2.3 (id:my-id-expr)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+
|
|
+class DateExpressionDtoToLines(RuleDtoToLinesMixin, TestCase):
|
|
+ def test_simple(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "gt", "start": "2014-06-26"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "date gt 2014-06-26",
|
|
+ ),
|
|
+ ],
|
|
+ "date gt 2014-06-26",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:rule)
|
|
+ Expression: date gt 2014-06-26 (id:rule-expr)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+ def test_datespec(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "date_spec"},
|
|
+ CibRuleDateCommonDto(
|
|
+ "rule-expr-datespec",
|
|
+ {"hours": "1-14", "monthdays": "20-30", "months": "1"},
|
|
+ ),
|
|
+ None,
|
|
+ [],
|
|
+ "date-spec hours=1-14 monthdays=20-30 months=1",
|
|
+ ),
|
|
+ ],
|
|
+ "date-spec hours=1-14 monthdays=20-30 months=1",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:rule)
|
|
+ Expression: (id:rule-expr)
|
|
+ Date Spec: hours=1-14 monthdays=20-30 months=1 (id:rule-expr-datespec)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+ def test_inrange(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "operation": "in_range",
|
|
+ "start": "2014-06-26",
|
|
+ "end": "2014-07-26",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "date in_range 2014-06-26 to 2014-07-26",
|
|
+ ),
|
|
+ ],
|
|
+ "date in_range 2014-06-26 to 2014-07-26",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:rule)
|
|
+ Expression: date in_range 2014-06-26 to 2014-07-26 (id:rule-expr)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+ def test_inrange_duration(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "in_range", "start": "2014-06-26",},
|
|
+ None,
|
|
+ CibRuleDateCommonDto("rule-expr-duration", {"years": "1"}),
|
|
+ [],
|
|
+ "date in_range 2014-06-26 to duration years=1",
|
|
+ ),
|
|
+ ],
|
|
+ "date in_range 2014-06-26 to duration years=1",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:rule)
|
|
+ Expression: date in_range 2014-06-26 to duration (id:rule-expr)
|
|
+ Duration: years=1 (id:rule-expr-duration)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+
|
|
+class OpExpressionDtoToLines(RuleDtoToLinesMixin, TestCase):
|
|
+ def test_minimal(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-op",
|
|
+ CibRuleExpressionType.OP_EXPRESSION,
|
|
+ False,
|
|
+ {"name": "start"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "op start",
|
|
+ ),
|
|
+ ],
|
|
+ "op start",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:my-id)
|
|
+ Expression: op start (id:my-id-op)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+ def test_interval(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-op",
|
|
+ CibRuleExpressionType.OP_EXPRESSION,
|
|
+ False,
|
|
+ {"name": "start", "interval": "2min"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "op start interval=2min",
|
|
+ ),
|
|
+ ],
|
|
+ "op start interval=2min",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:my-id)
|
|
+ Expression: op start interval=2min (id:my-id-op)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+
|
|
+class ResourceExpressionDtoToLines(RuleDtoToLinesMixin, TestCase):
|
|
+ def test_success(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-expr",
|
|
+ CibRuleExpressionType.RSC_EXPRESSION,
|
|
+ False,
|
|
+ {"class": "ocf", "provider": "pacemaker", "type": "Dummy"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "resource ocf:pacemaker:Dummy",
|
|
+ ),
|
|
+ ],
|
|
+ "resource ocf:pacemaker:Dummy",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: (id:my-id)
|
|
+ Expression: resource ocf:pacemaker:Dummy (id:my-id-expr)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
+
|
|
+
|
|
+class RuleDtoToLines(RuleDtoToLinesMixin, TestCase):
|
|
+ def test_complex_rule(self):
|
|
+ dto = CibRuleExpressionDto(
|
|
+ "complex",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "or", "score": "INFINITY"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-1",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "and", "score": "0"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-1-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "date_spec"},
|
|
+ CibRuleDateCommonDto(
|
|
+ "complex-rule-1-expr-datespec",
|
|
+ {"hours": "12-23", "weekdays": "1-5"},
|
|
+ ),
|
|
+ None,
|
|
+ [],
|
|
+ "date-spec hours=12-23 weekdays=1-5",
|
|
+ ),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-1-expr-1",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "in_range", "start": "2014-07-26",},
|
|
+ None,
|
|
+ CibRuleDateCommonDto(
|
|
+ "complex-rule-1-expr-1-durat", {"months": "1"},
|
|
+ ),
|
|
+ [],
|
|
+ "date in_range 2014-07-26 to duration months=1",
|
|
+ ),
|
|
+ ],
|
|
+ "date-spec hours=12-23 weekdays=1-5 and date in_range "
|
|
+ "2014-07-26 to duration months=1",
|
|
+ ),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "and", "score": "0"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-expr-1",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "foo",
|
|
+ "operation": "gt",
|
|
+ "type": "version",
|
|
+ "value": "1.2",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "foo gt version 1.2",
|
|
+ ),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-expr",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "#uname",
|
|
+ "operation": "eq",
|
|
+ "value": "node3 4",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "#uname eq 'node3 4'",
|
|
+ ),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-expr-2",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "#uname",
|
|
+ "operation": "eq",
|
|
+ "value": "nodeA",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "#uname eq nodeA",
|
|
+ ),
|
|
+ ],
|
|
+ "foo gt version 1.2 and #uname eq 'node3 4' and #uname "
|
|
+ "eq nodeA",
|
|
+ ),
|
|
+ ],
|
|
+ "(date-spec hours=12-23 weekdays=1-5 and date in_range "
|
|
+ "2014-07-26 to duration months=1) or (foo gt version 1.2 and "
|
|
+ "#uname eq 'node3 4' and #uname eq nodeA)",
|
|
+ )
|
|
+ output = dedent(
|
|
+ """\
|
|
+ Rule: boolean-op=or score=INFINITY (id:complex)
|
|
+ Rule: boolean-op=and score=0 (id:complex-rule-1)
|
|
+ Expression: (id:complex-rule-1-expr)
|
|
+ Date Spec: hours=12-23 weekdays=1-5 (id:complex-rule-1-expr-datespec)
|
|
+ Expression: date in_range 2014-07-26 to duration (id:complex-rule-1-expr-1)
|
|
+ Duration: months=1 (id:complex-rule-1-expr-1-durat)
|
|
+ Rule: boolean-op=and score=0 (id:complex-rule)
|
|
+ Expression: foo gt version 1.2 (id:complex-rule-expr-1)
|
|
+ Expression: #uname eq 'node3 4' (id:complex-rule-expr)
|
|
+ Expression: #uname eq nodeA (id:complex-rule-expr-2)
|
|
+ """
|
|
+ )
|
|
+ self.assert_lines(dto, output)
|
|
diff --git a/pcs_test/tier0/common/reports/test_messages.py b/pcs_test/tier0/common/reports/test_messages.py
|
|
index 2592bd40..fd217ffb 100644
|
|
--- a/pcs_test/tier0/common/reports/test_messages.py
|
|
+++ b/pcs_test/tier0/common/reports/test_messages.py
|
|
@@ -1,16 +1,17 @@
|
|
from unittest import TestCase
|
|
|
|
from pcs.common import file_type_codes
|
|
-from pcs.common.file import RawFileError
|
|
-from pcs.common.reports import (
|
|
- const,
|
|
- messages as reports,
|
|
-)
|
|
from pcs.common.fencing_topology import (
|
|
TARGET_TYPE_NODE,
|
|
TARGET_TYPE_REGEXP,
|
|
TARGET_TYPE_ATTRIBUTE,
|
|
)
|
|
+from pcs.common.file import RawFileError
|
|
+from pcs.common.reports import (
|
|
+ const,
|
|
+ messages as reports,
|
|
+)
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
|
|
# pylint: disable=too-many-lines
|
|
|
|
@@ -4653,3 +4654,47 @@ class TagIdsNotInTheTag(NameBuildTest):
|
|
"Tag 'tag-id' does not contain ids: 'a', 'b'",
|
|
reports.TagIdsNotInTheTag("tag-id", ["b", "a"]),
|
|
)
|
|
+
|
|
+
|
|
+class RuleExpressionParseError(NameBuildTest):
|
|
+ def test_success(self):
|
|
+ self.assert_message_from_report(
|
|
+ "'resource dummy op monitor' is not a valid rule expression, "
|
|
+ "parse error near or after line 1 column 16",
|
|
+ reports.RuleExpressionParseError(
|
|
+ "resource dummy op monitor",
|
|
+ "Expected end of text",
|
|
+ "resource dummy op monitor",
|
|
+ 1,
|
|
+ 16,
|
|
+ 15,
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class RuleExpressionNotAllowed(NameBuildTest):
|
|
+ def test_op(self):
|
|
+ self.assert_message_from_report(
|
|
+ "Keyword 'op' cannot be used in a rule in this command",
|
|
+ reports.RuleExpressionNotAllowed(
|
|
+ CibRuleExpressionType.OP_EXPRESSION
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_rsc(self):
|
|
+ self.assert_message_from_report(
|
|
+ "Keyword 'resource' cannot be used in a rule in this command",
|
|
+ reports.RuleExpressionNotAllowed(
|
|
+ CibRuleExpressionType.RSC_EXPRESSION
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class CibNvsetAmbiguousProvideNvsetId(NameBuildTest):
|
|
+ def test_success(self):
|
|
+ self.assert_message_from_report(
|
|
+ "Several options sets exist, please specify an option set ID",
|
|
+ reports.CibNvsetAmbiguousProvideNvsetId(
|
|
+ const.PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE
|
|
+ ),
|
|
+ )
|
|
diff --git a/pcs_test/tier0/common/test_str_tools.py b/pcs_test/tier0/common/test_str_tools.py
|
|
index c4753437..97c1d223 100644
|
|
--- a/pcs_test/tier0/common/test_str_tools.py
|
|
+++ b/pcs_test/tier0/common/test_str_tools.py
|
|
@@ -249,6 +249,39 @@ class FormatListCustomLastSeparatort(TestCase):
|
|
)
|
|
|
|
|
|
+class FormatNameValueList(TestCase):
|
|
+ def test_empty(self):
|
|
+ self.assertEqual([], tools.format_name_value_list([]))
|
|
+
|
|
+ def test_many(self):
|
|
+ self.assertEqual(
|
|
+ ["name1=value1", '"name=2"="value 2"', '"name 3"="value=3"'],
|
|
+ tools.format_name_value_list(
|
|
+ [
|
|
+ ("name1", "value1"),
|
|
+ ("name=2", "value 2"),
|
|
+ ("name 3", "value=3"),
|
|
+ ]
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class Quote(TestCase):
|
|
+ def test_no_quote(self):
|
|
+ self.assertEqual("string", tools.quote("string", " "))
|
|
+ self.assertEqual("string", tools.quote("string", " ="))
|
|
+
|
|
+ def test_quote(self):
|
|
+ self.assertEqual('"str ing"', tools.quote("str ing", " ="))
|
|
+ self.assertEqual('"str=ing"', tools.quote("str=ing", " ="))
|
|
+
|
|
+ def test_alternative_quote(self):
|
|
+ self.assertEqual("""'st"r i"ng'""", tools.quote('st"r i"ng', " "))
|
|
+
|
|
+ def test_escape(self):
|
|
+ self.assertEqual('''"st\\"r i'ng"''', tools.quote("st\"r i'ng", " "))
|
|
+
|
|
+
|
|
class Transform(TestCase):
|
|
def test_transform(self):
|
|
self.assertEqual(
|
|
diff --git a/pcs_test/tier0/lib/commands/cib_options/__init__.py b/pcs_test/tier0/lib/cib/rule/__init__.py
|
|
similarity index 100%
|
|
rename from pcs_test/tier0/lib/commands/cib_options/__init__.py
|
|
rename to pcs_test/tier0/lib/cib/rule/__init__.py
|
|
diff --git a/pcs_test/tier0/lib/cib/rule/test_cib_to_dto.py b/pcs_test/tier0/lib/cib/rule/test_cib_to_dto.py
|
|
new file mode 100644
|
|
index 00000000..ce06c469
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/lib/cib/rule/test_cib_to_dto.py
|
|
@@ -0,0 +1,593 @@
|
|
+from unittest import TestCase
|
|
+
|
|
+from lxml import etree
|
|
+
|
|
+from pcs.common.pacemaker.rule import (
|
|
+ CibRuleDateCommonDto,
|
|
+ CibRuleExpressionDto,
|
|
+)
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
+from pcs.lib.cib.rule import rule_element_to_dto
|
|
+
|
|
+
|
|
+class ExpressionToDto(TestCase):
|
|
+ def test_defined(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="my-id">
|
|
+ <expression id="my-id-expr"
|
|
+ attribute="pingd" operation="defined"
|
|
+ />
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-expr",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {"attribute": "pingd", "operation": "defined"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "defined pingd",
|
|
+ ),
|
|
+ ],
|
|
+ "defined pingd",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_value_comparison(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="my-id">
|
|
+ <expression id="my-id-expr"
|
|
+ attribute="my-attr" operation="eq" value="my value"
|
|
+ />
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-expr",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "my-attr",
|
|
+ "operation": "eq",
|
|
+ "value": "my value",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ 'my-attr eq "my value"',
|
|
+ ),
|
|
+ ],
|
|
+ 'my-attr eq "my value"',
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_value_comparison_with_type(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="my-id">
|
|
+ <expression id="my-id-expr"
|
|
+ attribute="foo" operation="gt" type="version" value="1.2.3"
|
|
+ />
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-expr",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "foo",
|
|
+ "operation": "gt",
|
|
+ "type": "version",
|
|
+ "value": "1.2.3",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "foo gt version 1.2.3",
|
|
+ ),
|
|
+ ],
|
|
+ "foo gt version 1.2.3",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class DateExpressionToDto(TestCase):
|
|
+ def test_gt(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="rule">
|
|
+ <date_expression id="rule-expr"
|
|
+ operation="gt" start="2014-06-26"
|
|
+ />
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "gt", "start": "2014-06-26"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "date gt 2014-06-26",
|
|
+ ),
|
|
+ ],
|
|
+ "date gt 2014-06-26",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_lt(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="rule">
|
|
+ <date_expression id="rule-expr"
|
|
+ operation="lt" end="2014-06-26"
|
|
+ />
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "lt", "end": "2014-06-26"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "date lt 2014-06-26",
|
|
+ ),
|
|
+ ],
|
|
+ "date lt 2014-06-26",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_datespec(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="rule">
|
|
+ <date_expression id="rule-expr" operation="date_spec">
|
|
+ <date_spec id="rule-expr-datespec"
|
|
+ hours="1-14" monthdays="20-30" months="1"
|
|
+ />
|
|
+ </date_expression>
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "date_spec"},
|
|
+ CibRuleDateCommonDto(
|
|
+ "rule-expr-datespec",
|
|
+ {
|
|
+ "hours": "1-14",
|
|
+ "monthdays": "20-30",
|
|
+ "months": "1",
|
|
+ },
|
|
+ ),
|
|
+ None,
|
|
+ [],
|
|
+ "date-spec hours=1-14 monthdays=20-30 months=1",
|
|
+ ),
|
|
+ ],
|
|
+ "date-spec hours=1-14 monthdays=20-30 months=1",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_inrange(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="rule">
|
|
+ <date_expression id="rule-expr"
|
|
+ operation="in_range" start="2014-06-26" end="2014-07-26"
|
|
+ />
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "operation": "in_range",
|
|
+ "start": "2014-06-26",
|
|
+ "end": "2014-07-26",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "date in_range 2014-06-26 to 2014-07-26",
|
|
+ ),
|
|
+ ],
|
|
+ "date in_range 2014-06-26 to 2014-07-26",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_inrange_duration(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="rule">
|
|
+ <date_expression id="rule-expr"
|
|
+ operation="in_range" start="2014-06-26"
|
|
+ >
|
|
+ <duration id="rule-expr-duration" years="1"/>
|
|
+ </date_expression>
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "rule-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "in_range", "start": "2014-06-26",},
|
|
+ None,
|
|
+ CibRuleDateCommonDto(
|
|
+ "rule-expr-duration", {"years": "1"},
|
|
+ ),
|
|
+ [],
|
|
+ "date in_range 2014-06-26 to duration years=1",
|
|
+ ),
|
|
+ ],
|
|
+ "date in_range 2014-06-26 to duration years=1",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class OpExpressionToDto(TestCase):
|
|
+ def test_minimal(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="my-id">
|
|
+ <op_expression id="my-id-op" name="start" />
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-op",
|
|
+ CibRuleExpressionType.OP_EXPRESSION,
|
|
+ False,
|
|
+ {"name": "start"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "op start",
|
|
+ ),
|
|
+ ],
|
|
+ "op start",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_interval(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="my-id">
|
|
+ <op_expression id="my-id-op" name="start" interval="2min" />
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-op",
|
|
+ CibRuleExpressionType.OP_EXPRESSION,
|
|
+ False,
|
|
+ {"name": "start", "interval": "2min"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "op start interval=2min",
|
|
+ ),
|
|
+ ],
|
|
+ "op start interval=2min",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class ResourceExpressionToDto(TestCase):
|
|
+ def test_success(self):
|
|
+ test_data = [
|
|
+ # ((class, provider, type), output)
|
|
+ ((None, None, None), "::"),
|
|
+ (("ocf", None, None), "ocf::"),
|
|
+ ((None, "pacemaker", None), ":pacemaker:"),
|
|
+ ((None, None, "Dummy"), "::Dummy"),
|
|
+ (("ocf", "pacemaker", None), "ocf:pacemaker:"),
|
|
+ (("ocf", None, "Dummy"), "ocf::Dummy"),
|
|
+ ((None, "pacemaker", "Dummy"), ":pacemaker:Dummy"),
|
|
+ (("ocf", "pacemaker", "Dummy"), "ocf:pacemaker:Dummy"),
|
|
+ ]
|
|
+ for in_data, out_data in test_data:
|
|
+ with self.subTest(in_data=in_data):
|
|
+ attrs = {}
|
|
+ if in_data[0] is not None:
|
|
+ attrs["class"] = in_data[0]
|
|
+ if in_data[1] is not None:
|
|
+ attrs["provider"] = in_data[1]
|
|
+ if in_data[2] is not None:
|
|
+ attrs["type"] = in_data[2]
|
|
+ attrs_str = " ".join(
|
|
+ [f"{name}='{value}'" for name, value in attrs.items()]
|
|
+ )
|
|
+ xml = etree.fromstring(
|
|
+ f"""
|
|
+ <rule id="my-id">
|
|
+ <rsc_expression id="my-id-expr" {attrs_str}/>
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-expr",
|
|
+ CibRuleExpressionType.RSC_EXPRESSION,
|
|
+ False,
|
|
+ attrs,
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ f"resource {out_data}",
|
|
+ ),
|
|
+ ],
|
|
+ f"resource {out_data}",
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class RuleToDto(TestCase):
|
|
+ def test_complex_rule(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <rule id="complex" boolean-op="or" score="INFINITY">
|
|
+ <rule id="complex-rule-1" boolean-op="and" score="0">
|
|
+ <date_expression id="complex-rule-1-expr"
|
|
+ operation="date_spec"
|
|
+ >
|
|
+ <date_spec id="complex-rule-1-expr-datespec"
|
|
+ weekdays="1-5" hours="12-23"
|
|
+ />
|
|
+ </date_expression>
|
|
+ <date_expression id="complex-rule-1-expr-1"
|
|
+ operation="in_range" start="2014-07-26"
|
|
+ >
|
|
+ <duration id="complex-rule-1-expr-1-durat" months="1"/>
|
|
+ </date_expression>
|
|
+ </rule>
|
|
+ <rule id="complex-rule" boolean-op="and" score="0">
|
|
+ <expression id="complex-rule-expr-1"
|
|
+ attribute="foo" operation="gt" type="version" value="1.2"
|
|
+ />
|
|
+ <expression id="complex-rule-expr"
|
|
+ attribute="#uname" operation="eq" value="node3 4"
|
|
+ />
|
|
+ <expression id="complex-rule-expr-2"
|
|
+ attribute="#uname" operation="eq" value="nodeA"
|
|
+ />
|
|
+ </rule>
|
|
+ </rule>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ rule_element_to_dto(xml),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "or", "score": "INFINITY"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-1",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "and", "score": "0"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-1-expr",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {"operation": "date_spec"},
|
|
+ CibRuleDateCommonDto(
|
|
+ "complex-rule-1-expr-datespec",
|
|
+ {"hours": "12-23", "weekdays": "1-5"},
|
|
+ ),
|
|
+ None,
|
|
+ [],
|
|
+ "date-spec hours=12-23 weekdays=1-5",
|
|
+ ),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-1-expr-1",
|
|
+ CibRuleExpressionType.DATE_EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "operation": "in_range",
|
|
+ "start": "2014-07-26",
|
|
+ },
|
|
+ None,
|
|
+ CibRuleDateCommonDto(
|
|
+ "complex-rule-1-expr-1-durat",
|
|
+ {"months": "1"},
|
|
+ ),
|
|
+ [],
|
|
+ "date in_range 2014-07-26 to duration months=1",
|
|
+ ),
|
|
+ ],
|
|
+ "date-spec hours=12-23 weekdays=1-5 and date in_range "
|
|
+ "2014-07-26 to duration months=1",
|
|
+ ),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "and", "score": "0"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-expr-1",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "foo",
|
|
+ "operation": "gt",
|
|
+ "type": "version",
|
|
+ "value": "1.2",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "foo gt version 1.2",
|
|
+ ),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-expr",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "#uname",
|
|
+ "operation": "eq",
|
|
+ "value": "node3 4",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ '#uname eq "node3 4"',
|
|
+ ),
|
|
+ CibRuleExpressionDto(
|
|
+ "complex-rule-expr-2",
|
|
+ CibRuleExpressionType.EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "attribute": "#uname",
|
|
+ "operation": "eq",
|
|
+ "value": "nodeA",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "#uname eq nodeA",
|
|
+ ),
|
|
+ ],
|
|
+ 'foo gt version 1.2 and #uname eq "node3 4" and #uname '
|
|
+ "eq nodeA",
|
|
+ ),
|
|
+ ],
|
|
+ "(date-spec hours=12-23 weekdays=1-5 and date in_range "
|
|
+ "2014-07-26 to duration months=1) or (foo gt version 1.2 and "
|
|
+ '#uname eq "node3 4" and #uname eq nodeA)',
|
|
+ ),
|
|
+ )
|
|
diff --git a/pcs_test/tier0/lib/cib/rule/test_parsed_to_cib.py b/pcs_test/tier0/lib/cib/rule/test_parsed_to_cib.py
|
|
new file mode 100644
|
|
index 00000000..f61fce99
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/lib/cib/rule/test_parsed_to_cib.py
|
|
@@ -0,0 +1,214 @@
|
|
+from unittest import TestCase
|
|
+
|
|
+from lxml import etree
|
|
+
|
|
+from pcs_test.tools.assertions import assert_xml_equal
|
|
+from pcs_test.tools.xml import etree_to_str
|
|
+
|
|
+from pcs.lib.cib import rule
|
|
+from pcs.lib.cib.rule.expression_part import (
|
|
+ BOOL_AND,
|
|
+ BOOL_OR,
|
|
+ BoolExpr,
|
|
+ OpExpr,
|
|
+ RscExpr,
|
|
+)
|
|
+from pcs.lib.cib.tools import IdProvider
|
|
+
|
|
+
|
|
+class Base(TestCase):
|
|
+ @staticmethod
|
|
+ def assert_cib(tree, expected_xml):
|
|
+ xml = etree.fromstring('<root id="X"/>')
|
|
+ rule.rule_to_cib(xml, IdProvider(xml), tree)
|
|
+ assert_xml_equal(
|
|
+ '<root id="X">' + expected_xml + "</root>", etree_to_str(xml)
|
|
+ )
|
|
+
|
|
+
|
|
+class SimpleBool(Base):
|
|
+ def test_no_children(self):
|
|
+ self.assert_cib(
|
|
+ BoolExpr(BOOL_AND, []),
|
|
+ """
|
|
+ <rule id="X-rule" boolean-op="and" score="INFINITY" />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def test_one_child(self):
|
|
+ self.assert_cib(
|
|
+ BoolExpr(BOOL_AND, [OpExpr("start", None)]),
|
|
+ """
|
|
+ <rule id="X-rule" boolean-op="and" score="INFINITY">
|
|
+ <op_expression id="X-rule-op-start" name="start" />
|
|
+ </rule>
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def test_two_children(self):
|
|
+ operators = [
|
|
+ (BOOL_OR, "or"),
|
|
+ (BOOL_AND, "and"),
|
|
+ ]
|
|
+ for op_in, op_out in operators:
|
|
+ with self.subTest(op_in=op_in, op_out=op_out):
|
|
+ self.assert_cib(
|
|
+ BoolExpr(
|
|
+ op_in,
|
|
+ [
|
|
+ OpExpr("start", None),
|
|
+ RscExpr("systemd", None, "pcsd"),
|
|
+ ],
|
|
+ ),
|
|
+ f"""
|
|
+ <rule id="X-rule" boolean-op="{op_out}" score="INFINITY">
|
|
+ <op_expression id="X-rule-op-start" name="start" />
|
|
+ <rsc_expression id="X-rule-rsc-systemd-pcsd"
|
|
+ class="systemd" type="pcsd"
|
|
+ />
|
|
+ </rule>
|
|
+ """,
|
|
+ )
|
|
+
|
|
+
|
|
+class SimpleOp(Base):
|
|
+ def test_minimal(self):
|
|
+ self.assert_cib(
|
|
+ OpExpr("start", None),
|
|
+ """
|
|
+ <op_expression id="X-op-start" name="start" />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def test_interval(self):
|
|
+ self.assert_cib(
|
|
+ OpExpr("monitor", "2min"),
|
|
+ """
|
|
+ <op_expression id="X-op-monitor" name="monitor"
|
|
+ interval="2min"
|
|
+ />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+
|
|
+class SimpleRsc(Base):
|
|
+ def test_class(self):
|
|
+ self.assert_cib(
|
|
+ RscExpr("ocf", None, None),
|
|
+ """
|
|
+ <rsc_expression id="X-rsc-ocf" class="ocf" />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def test_provider(self):
|
|
+ self.assert_cib(
|
|
+ RscExpr(None, "pacemaker", None),
|
|
+ """
|
|
+ <rsc_expression id="X-rsc-pacemaker" provider="pacemaker" />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def type(self):
|
|
+ self.assert_cib(
|
|
+ RscExpr(None, None, "Dummy"),
|
|
+ """
|
|
+ <rsc_expression id="X-rsc-Dummy" type="Dummy" />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def test_provider_type(self):
|
|
+ self.assert_cib(
|
|
+ RscExpr(None, "pacemaker", "Dummy"),
|
|
+ """
|
|
+ <rsc_expression id="X-rsc-pacemaker-Dummy"
|
|
+ provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def test_class_provider(self):
|
|
+ self.assert_cib(
|
|
+ RscExpr("ocf", "pacemaker", None),
|
|
+ """
|
|
+ <rsc_expression id="X-rsc-ocf-pacemaker"
|
|
+ class="ocf" provider="pacemaker"
|
|
+ />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def test_class_type(self):
|
|
+ self.assert_cib(
|
|
+ RscExpr("systemd", None, "pcsd"),
|
|
+ """
|
|
+ <rsc_expression id="X-rsc-systemd-pcsd"
|
|
+ class="systemd" type="pcsd"
|
|
+ />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+ def test_class_provider_type(self):
|
|
+ self.assert_cib(
|
|
+ RscExpr("ocf", "pacemaker", "Dummy"),
|
|
+ """
|
|
+ <rsc_expression id="X-rsc-ocf-pacemaker-Dummy"
|
|
+ class="ocf" provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ """,
|
|
+ )
|
|
+
|
|
+
|
|
+class Complex(Base):
|
|
+ def test_expr_1(self):
|
|
+ self.assert_cib(
|
|
+ BoolExpr(
|
|
+ BOOL_AND,
|
|
+ [
|
|
+ BoolExpr(
|
|
+ BOOL_OR,
|
|
+ [
|
|
+ RscExpr("ocf", "pacemaker", "Dummy"),
|
|
+ OpExpr("start", None),
|
|
+ RscExpr("systemd", None, "pcsd"),
|
|
+ RscExpr("ocf", "heartbeat", "Dummy"),
|
|
+ ],
|
|
+ ),
|
|
+ BoolExpr(
|
|
+ BOOL_OR,
|
|
+ [
|
|
+ OpExpr("monitor", "30s"),
|
|
+ RscExpr("ocf", "pacemaker", "Dummy"),
|
|
+ OpExpr("start", None),
|
|
+ OpExpr("monitor", "2min"),
|
|
+ ],
|
|
+ ),
|
|
+ ],
|
|
+ ),
|
|
+ """
|
|
+ <rule id="X-rule" boolean-op="and" score="INFINITY">
|
|
+ <rule id="X-rule-rule" boolean-op="or">
|
|
+ <rsc_expression id="X-rule-rule-rsc-ocf-pacemaker-Dummy"
|
|
+ class="ocf" provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ <op_expression id="X-rule-rule-op-start" name="start" />
|
|
+ <rsc_expression id="X-rule-rule-rsc-systemd-pcsd"
|
|
+ class="systemd" type="pcsd"
|
|
+ />
|
|
+ <rsc_expression id="X-rule-rule-rsc-ocf-heartbeat-Dummy"
|
|
+ class="ocf" provider="heartbeat" type="Dummy"
|
|
+ />
|
|
+ </rule>
|
|
+ <rule id="X-rule-rule-1" boolean-op="or">
|
|
+ <op_expression id="X-rule-rule-1-op-monitor"
|
|
+ name="monitor" interval="30s"
|
|
+ />
|
|
+ <rsc_expression id="X-rule-rule-1-rsc-ocf-pacemaker-Dummy"
|
|
+ class="ocf" provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ <op_expression id="X-rule-rule-1-op-start" name="start" />
|
|
+ <op_expression id="X-rule-rule-1-op-monitor-1"
|
|
+ name="monitor" interval="2min"
|
|
+ />
|
|
+ </rule>
|
|
+ </rule>
|
|
+ """,
|
|
+ )
|
|
diff --git a/pcs_test/tier0/lib/cib/rule/test_parser.py b/pcs_test/tier0/lib/cib/rule/test_parser.py
|
|
new file mode 100644
|
|
index 00000000..110fc739
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/lib/cib/rule/test_parser.py
|
|
@@ -0,0 +1,270 @@
|
|
+from dataclasses import fields
|
|
+from textwrap import dedent
|
|
+from unittest import TestCase
|
|
+
|
|
+from pcs.common.str_tools import indent
|
|
+from pcs.lib.cib import rule
|
|
+from pcs.lib.cib.rule.expression_part import BoolExpr
|
|
+
|
|
+
|
|
+def _parsed_to_str(parsed):
|
|
+ if isinstance(parsed, BoolExpr):
|
|
+ str_args = []
|
|
+ for arg in parsed.children:
|
|
+ str_args.extend(_parsed_to_str(arg).splitlines())
|
|
+ return "\n".join(
|
|
+ [f"{parsed.__class__.__name__} {parsed.operator}"]
|
|
+ + indent(str_args)
|
|
+ )
|
|
+
|
|
+ parts = [parsed.__class__.__name__]
|
|
+ for field in fields(parsed):
|
|
+ value = getattr(parsed, field.name)
|
|
+ if value is not None:
|
|
+ parts.append(f"{field.name}={value}")
|
|
+ return " ".join(parts)
|
|
+
|
|
+
|
|
+class Parser(TestCase):
|
|
+ def test_success_parse_to_tree(self):
|
|
+ test_data = [
|
|
+ ("", "BoolExpr AND"),
|
|
+ (
|
|
+ "resource ::",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ::dummy",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr type=dummy"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ocf::",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr standard=ocf"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource :pacemaker:",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr provider=pacemaker"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource systemd::Dummy",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr standard=systemd type=Dummy"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ocf:pacemaker:",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr standard=ocf provider=pacemaker"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource :pacemaker:Dummy",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr provider=pacemaker type=Dummy"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ocf:pacemaker:Dummy",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr standard=ocf provider=pacemaker type=Dummy"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "op monitor",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ OpExpr name=monitor"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "op monitor interval=10",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ OpExpr name=monitor interval=10"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ::dummy and op monitor",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr type=dummy
|
|
+ OpExpr name=monitor"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ::dummy or op monitor interval=15s",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr OR
|
|
+ RscExpr type=dummy
|
|
+ OpExpr name=monitor interval=15s"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "op monitor and resource ::dummy",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ OpExpr name=monitor
|
|
+ RscExpr type=dummy"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "op monitor interval=5min or resource ::dummy",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr OR
|
|
+ OpExpr name=monitor interval=5min
|
|
+ RscExpr type=dummy"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "(resource ::dummy or resource ::delay) and op monitor",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ BoolExpr OR
|
|
+ RscExpr type=dummy
|
|
+ RscExpr type=delay
|
|
+ OpExpr name=monitor"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "(op start and op stop) or resource ::dummy",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr OR
|
|
+ BoolExpr AND
|
|
+ OpExpr name=start
|
|
+ OpExpr name=stop
|
|
+ RscExpr type=dummy"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "op monitor or (resource ::dummy and resource ::delay)",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr OR
|
|
+ OpExpr name=monitor
|
|
+ BoolExpr AND
|
|
+ RscExpr type=dummy
|
|
+ RscExpr type=delay"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ::dummy and (op start or op stop)",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr type=dummy
|
|
+ BoolExpr OR
|
|
+ OpExpr name=start
|
|
+ OpExpr name=stop"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ::dummy and resource ::delay and op monitor",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ RscExpr type=dummy
|
|
+ RscExpr type=delay
|
|
+ OpExpr name=monitor"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "resource ::rA or resource ::rB or resource ::rC and op monitor",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ BoolExpr OR
|
|
+ RscExpr type=rA
|
|
+ RscExpr type=rB
|
|
+ RscExpr type=rC
|
|
+ OpExpr name=monitor"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "op start and op stop and op monitor or resource ::delay",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr OR
|
|
+ BoolExpr AND
|
|
+ OpExpr name=start
|
|
+ OpExpr name=stop
|
|
+ OpExpr name=monitor
|
|
+ RscExpr type=delay"""
|
|
+ ),
|
|
+ ),
|
|
+ (
|
|
+ "(resource ::rA or resource ::rB or resource ::rC) and (op oX or op oY or op oZ)",
|
|
+ dedent(
|
|
+ """\
|
|
+ BoolExpr AND
|
|
+ BoolExpr OR
|
|
+ RscExpr type=rA
|
|
+ RscExpr type=rB
|
|
+ RscExpr type=rC
|
|
+ BoolExpr OR
|
|
+ OpExpr name=oX
|
|
+ OpExpr name=oY
|
|
+ OpExpr name=oZ"""
|
|
+ ),
|
|
+ ),
|
|
+ ]
|
|
+ for rule_string, rule_tree in test_data:
|
|
+ with self.subTest(rule_string=rule_string):
|
|
+ self.assertEqual(
|
|
+ rule_tree,
|
|
+ _parsed_to_str(
|
|
+ rule.parse_rule(
|
|
+ rule_string, allow_rsc_expr=True, allow_op_expr=True
|
|
+ )
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_not_valid_rule(self):
|
|
+ test_data = [
|
|
+ ("resource", (1, 9, 8, "Expected <resource name>")),
|
|
+ ("op", (1, 3, 2, "Expected <operation name>")),
|
|
+ ("resource ::rA and", (1, 15, 14, "Expected end of text")),
|
|
+ ("resource ::rA and op ", (1, 15, 14, "Expected end of text")),
|
|
+ ("resource ::rA and (", (1, 15, 14, "Expected end of text")),
|
|
+ ]
|
|
+
|
|
+ for rule_string, exception_data in test_data:
|
|
+ with self.subTest(rule_string=rule_string):
|
|
+ with self.assertRaises(rule.RuleParseError) as cm:
|
|
+ rule.parse_rule(
|
|
+ rule_string, allow_rsc_expr=True, allow_op_expr=True
|
|
+ )
|
|
+ e = cm.exception
|
|
+ self.assertEqual(exception_data, (e.lineno, e.colno, e.pos, e.msg))
|
|
+ self.assertEqual(rule_string, e.rule_string)
|
|
diff --git a/pcs_test/tier0/lib/cib/rule/test_validator.py b/pcs_test/tier0/lib/cib/rule/test_validator.py
|
|
new file mode 100644
|
|
index 00000000..95344a4a
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/lib/cib/rule/test_validator.py
|
|
@@ -0,0 +1,68 @@
|
|
+from unittest import TestCase
|
|
+
|
|
+from pcs_test.tools import fixture
|
|
+from pcs_test.tools.assertions import assert_report_item_list_equal
|
|
+
|
|
+from pcs.common import reports
|
|
+from pcs.common.types import CibRuleExpressionType
|
|
+from pcs.lib.cib.rule.expression_part import (
|
|
+ BOOL_AND,
|
|
+ BOOL_OR,
|
|
+ BoolExpr,
|
|
+ OpExpr,
|
|
+ RscExpr,
|
|
+)
|
|
+from pcs.lib.cib.rule.validator import Validator
|
|
+
|
|
+
|
|
+class ValidatorTest(TestCase):
|
|
+ def setUp(self):
|
|
+ self.report_op = fixture.error(
|
|
+ reports.codes.RULE_EXPRESSION_NOT_ALLOWED,
|
|
+ expression_type=CibRuleExpressionType.OP_EXPRESSION,
|
|
+ )
|
|
+ self.report_rsc = fixture.error(
|
|
+ reports.codes.RULE_EXPRESSION_NOT_ALLOWED,
|
|
+ expression_type=CibRuleExpressionType.RSC_EXPRESSION,
|
|
+ )
|
|
+ self.rule_rsc = BoolExpr(
|
|
+ BOOL_OR, [RscExpr(None, None, "a"), RscExpr(None, None, "b")]
|
|
+ )
|
|
+ self.rule_op = BoolExpr(
|
|
+ BOOL_OR, [OpExpr("start", None), OpExpr("stop", None)]
|
|
+ )
|
|
+ self.rule = BoolExpr(BOOL_AND, [self.rule_rsc, self.rule_op])
|
|
+
|
|
+ def test_complex_rule(self):
|
|
+ test_data = (
|
|
+ (True, True, []),
|
|
+ (True, False, [self.report_rsc]),
|
|
+ (False, True, [self.report_op]),
|
|
+ (False, False, [self.report_rsc, self.report_op]),
|
|
+ )
|
|
+ for op_allowed, rsc_allowed, report_list in test_data:
|
|
+ with self.subTest(op_allowed=op_allowed, rsc_allowed=rsc_allowed):
|
|
+ assert_report_item_list_equal(
|
|
+ Validator(
|
|
+ self.rule,
|
|
+ allow_rsc_expr=rsc_allowed,
|
|
+ allow_op_expr=op_allowed,
|
|
+ ).get_reports(),
|
|
+ report_list,
|
|
+ )
|
|
+
|
|
+ def test_disallow_missing_op(self):
|
|
+ assert_report_item_list_equal(
|
|
+ Validator(
|
|
+ self.rule_rsc, allow_rsc_expr=True, allow_op_expr=False
|
|
+ ).get_reports(),
|
|
+ [],
|
|
+ )
|
|
+
|
|
+ def test_disallow_missing_rsc(self):
|
|
+ assert_report_item_list_equal(
|
|
+ Validator(
|
|
+ self.rule_op, allow_rsc_expr=False, allow_op_expr=True
|
|
+ ).get_reports(),
|
|
+ [],
|
|
+ )
|
|
diff --git a/pcs_test/tier0/lib/cib/test_nvpair_multi.py b/pcs_test/tier0/lib/cib/test_nvpair_multi.py
|
|
new file mode 100644
|
|
index 00000000..c68c7233
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/lib/cib/test_nvpair_multi.py
|
|
@@ -0,0 +1,513 @@
|
|
+from unittest import TestCase
|
|
+
|
|
+from lxml import etree
|
|
+
|
|
+from pcs_test.tools import fixture
|
|
+from pcs_test.tools.assertions import (
|
|
+ assert_report_item_list_equal,
|
|
+ assert_xml_equal,
|
|
+)
|
|
+from pcs_test.tools.xml import etree_to_str
|
|
+
|
|
+from pcs.common import reports
|
|
+from pcs.common.pacemaker.nvset import (
|
|
+ CibNvpairDto,
|
|
+ CibNvsetDto,
|
|
+)
|
|
+from pcs.common.pacemaker.rule import CibRuleExpressionDto
|
|
+from pcs.common.types import (
|
|
+ CibNvsetType,
|
|
+ CibRuleExpressionType,
|
|
+)
|
|
+from pcs.lib.cib import nvpair_multi
|
|
+from pcs.lib.cib.rule.expression_part import (
|
|
+ BOOL_AND,
|
|
+ BoolExpr,
|
|
+ OpExpr,
|
|
+ RscExpr,
|
|
+)
|
|
+from pcs.lib.cib.tools import IdProvider
|
|
+
|
|
+
|
|
+class NvpairElementToDto(TestCase):
|
|
+ def test_success(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <nvpair id="my-id" name="my-name" value="my-value" />
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ nvpair_multi.nvpair_element_to_dto(xml),
|
|
+ CibNvpairDto("my-id", "my-name", "my-value"),
|
|
+ )
|
|
+
|
|
+
|
|
+class NvsetElementToDto(TestCase):
|
|
+ tag_type = (
|
|
+ ("meta_attributes", CibNvsetType.META),
|
|
+ ("instance_attributes", CibNvsetType.INSTANCE),
|
|
+ )
|
|
+
|
|
+ def test_minimal(self):
|
|
+ for tag, nvtype in self.tag_type:
|
|
+ with self.subTest(tag=tag, nvset_type=nvtype):
|
|
+ xml = etree.fromstring(f"""<{tag} id="my-id" />""")
|
|
+ self.assertEqual(
|
|
+ nvpair_multi.nvset_element_to_dto(xml),
|
|
+ CibNvsetDto("my-id", nvtype, {}, None, []),
|
|
+ )
|
|
+
|
|
+ def test_full(self):
|
|
+ for tag, nvtype in self.tag_type:
|
|
+ with self.subTest(tag=tag, nvset_type=nvtype):
|
|
+ xml = etree.fromstring(
|
|
+ f"""
|
|
+ <{tag} id="my-id" score="150">
|
|
+ <rule id="my-id-rule" boolean-op="or">
|
|
+ <op_expression id="my-id-rule-op" name="monitor" />
|
|
+ </rule>
|
|
+ <nvpair id="my-id-pair1" name="name1" value="value1" />
|
|
+ <nvpair id="my-id-pair2" name="name2" value="value2" />
|
|
+ </{tag}>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ nvpair_multi.nvset_element_to_dto(xml),
|
|
+ CibNvsetDto(
|
|
+ "my-id",
|
|
+ nvtype,
|
|
+ {"score": "150"},
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "or"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ "my-id-rule-op",
|
|
+ CibRuleExpressionType.OP_EXPRESSION,
|
|
+ False,
|
|
+ {"name": "monitor"},
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "op monitor",
|
|
+ ),
|
|
+ ],
|
|
+ "op monitor",
|
|
+ ),
|
|
+ [
|
|
+ CibNvpairDto("my-id-pair1", "name1", "value1"),
|
|
+ CibNvpairDto("my-id-pair2", "name2", "value2"),
|
|
+ ],
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class FindNvsets(TestCase):
|
|
+ def test_empty(self):
|
|
+ xml = etree.fromstring("<parent />")
|
|
+ self.assertEqual([], nvpair_multi.find_nvsets(xml))
|
|
+
|
|
+ def test_full(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <parent>
|
|
+ <meta_attributes id="set1" />
|
|
+ <instance_attributes id="set2" />
|
|
+ <not_an_nvset id="set3" />
|
|
+ </parent>
|
|
+ """
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ ["set1", "set2"],
|
|
+ [el.get("id") for el in nvpair_multi.find_nvsets(xml)],
|
|
+ )
|
|
+
|
|
+
|
|
+class FindNvsetsByIds(TestCase):
|
|
+ def test_success(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <parent>
|
|
+ <meta_attributes id="set1" />
|
|
+ <instance_attributes id="set2" />
|
|
+ <not_an_nvset id="set3" />
|
|
+ <meta_attributes id="set4" />
|
|
+ </parent>
|
|
+ """
|
|
+ )
|
|
+ element_list, report_list = nvpair_multi.find_nvsets_by_ids(
|
|
+ xml, ["set1", "set2", "set3", "setX"]
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ ["set1", "set2"], [el.get("id") for el in element_list],
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ report_list,
|
|
+ [
|
|
+ fixture.report_unexpected_element(
|
|
+ "set3", "not_an_nvset", ["options set"]
|
|
+ ),
|
|
+ fixture.report_not_found(
|
|
+ "setX",
|
|
+ context_type="parent",
|
|
+ expected_types=["options set"],
|
|
+ ),
|
|
+ ],
|
|
+ )
|
|
+
|
|
+
|
|
+class ValidateNvsetAppendNew(TestCase):
|
|
+ def setUp(self):
|
|
+ self.id_provider = IdProvider(
|
|
+ etree.fromstring("""<cib><tags><tag id="a" /></tags></cib>""")
|
|
+ )
|
|
+
|
|
+ def test_success_minimal(self):
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ self.id_provider, {}, {}
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ validator.validate(force_options=True), []
|
|
+ )
|
|
+ self.assertIsNone(validator.get_parsed_rule())
|
|
+
|
|
+ def test_success_full(self):
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ self.id_provider,
|
|
+ {"name": "value"},
|
|
+ {"id": "some-id", "score": "10"},
|
|
+ nvset_rule="resource ::stateful",
|
|
+ rule_allows_rsc_expr=True,
|
|
+ rule_allows_op_expr=True,
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ validator.validate(), [],
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ repr(validator.get_parsed_rule()),
|
|
+ "BoolExpr(operator='AND', children=["
|
|
+ "RscExpr(standard=None, provider=None, type='stateful')"
|
|
+ "])",
|
|
+ )
|
|
+
|
|
+ def test_id_not_valid(self):
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ self.id_provider, {}, {"id": "123"}
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ validator.validate(force_options=True),
|
|
+ [fixture.report_invalid_id("123", "1")],
|
|
+ )
|
|
+ self.assertIsNone(validator.get_parsed_rule())
|
|
+
|
|
+ def test_id_not_available(self):
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ self.id_provider, {}, {"id": "a"}
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ validator.validate(force_options=True),
|
|
+ [fixture.error(reports.codes.ID_ALREADY_EXISTS, id="a")],
|
|
+ )
|
|
+ self.assertIsNone(validator.get_parsed_rule())
|
|
+
|
|
+ def test_score_not_valid(self):
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ self.id_provider, {}, {"score": "a"}
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ validator.validate(force_options=True),
|
|
+ [fixture.error(reports.codes.INVALID_SCORE, score="a")],
|
|
+ )
|
|
+ self.assertIsNone(validator.get_parsed_rule())
|
|
+
|
|
+ def test_options_names(self):
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ self.id_provider, {}, {"not_valid": "a"}
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ validator.validate(),
|
|
+ [
|
|
+ fixture.error(
|
|
+ reports.codes.INVALID_OPTIONS,
|
|
+ force_code=reports.codes.FORCE_OPTIONS,
|
|
+ option_names=["not_valid"],
|
|
+ allowed=["id", "score"],
|
|
+ option_type=None,
|
|
+ allowed_patterns=[],
|
|
+ ),
|
|
+ ],
|
|
+ )
|
|
+ self.assertIsNone(validator.get_parsed_rule())
|
|
+
|
|
+ def test_options_names_forced(self):
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ self.id_provider, {}, {"not_valid": "a"}
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ validator.validate(force_options=True),
|
|
+ [
|
|
+ fixture.warn(
|
|
+ reports.codes.INVALID_OPTIONS,
|
|
+ option_names=["not_valid"],
|
|
+ allowed=["id", "score"],
|
|
+ option_type=None,
|
|
+ allowed_patterns=[],
|
|
+ ),
|
|
+ ],
|
|
+ )
|
|
+ self.assertIsNone(validator.get_parsed_rule())
|
|
+
|
|
+ def test_rule_not_valid(self):
|
|
+ validator = nvpair_multi.ValidateNvsetAppendNew(
|
|
+ self.id_provider,
|
|
+ {},
|
|
+ {},
|
|
+ "bad rule",
|
|
+ rule_allows_rsc_expr=True,
|
|
+ rule_allows_op_expr=True,
|
|
+ )
|
|
+ assert_report_item_list_equal(
|
|
+ validator.validate(force_options=True),
|
|
+ [
|
|
+ fixture.error(
|
|
+ reports.codes.RULE_EXPRESSION_PARSE_ERROR,
|
|
+ rule_string="bad rule",
|
|
+ reason='Expected "resource"',
|
|
+ rule_line="bad rule",
|
|
+ line_number=1,
|
|
+ column_number=1,
|
|
+ position=0,
|
|
+ ),
|
|
+ ],
|
|
+ )
|
|
+ self.assertIsNone(validator.get_parsed_rule())
|
|
+
|
|
+
|
|
+class NvsetAppendNew(TestCase):
|
|
+ # pylint: disable=no-self-use
|
|
+ def test_minimal(self):
|
|
+ context_element = etree.fromstring("""<context id="a" />""")
|
|
+ id_provider = IdProvider(context_element)
|
|
+ nvpair_multi.nvset_append_new(
|
|
+ context_element, id_provider, nvpair_multi.NVSET_META, {}, {}
|
|
+ )
|
|
+ assert_xml_equal(
|
|
+ """
|
|
+ <context id="a">
|
|
+ <meta_attributes id="a-meta_attributes" />
|
|
+ </context>
|
|
+ """,
|
|
+ etree_to_str(context_element),
|
|
+ )
|
|
+
|
|
+ def test_nvpairs(self):
|
|
+ context_element = etree.fromstring("""<context id="a" />""")
|
|
+ id_provider = IdProvider(context_element)
|
|
+ nvpair_multi.nvset_append_new(
|
|
+ context_element,
|
|
+ id_provider,
|
|
+ nvpair_multi.NVSET_META,
|
|
+ {"attr1": "value1", "attr-empty": "", "attr2": "value2"},
|
|
+ {},
|
|
+ )
|
|
+ assert_xml_equal(
|
|
+ """
|
|
+ <context id="a">
|
|
+ <meta_attributes id="a-meta_attributes">
|
|
+ <nvpair id="a-meta_attributes-attr1"
|
|
+ name="attr1" value="value1"
|
|
+ />
|
|
+ <nvpair id="a-meta_attributes-attr2"
|
|
+ name="attr2" value="value2"
|
|
+ />
|
|
+ </meta_attributes>
|
|
+ </context>
|
|
+ """,
|
|
+ etree_to_str(context_element),
|
|
+ )
|
|
+
|
|
+ def test_rule(self):
|
|
+ context_element = etree.fromstring("""<context id="a" />""")
|
|
+ id_provider = IdProvider(context_element)
|
|
+ nvpair_multi.nvset_append_new(
|
|
+ context_element,
|
|
+ id_provider,
|
|
+ nvpair_multi.NVSET_META,
|
|
+ {},
|
|
+ {},
|
|
+ nvset_rule=BoolExpr(
|
|
+ BOOL_AND,
|
|
+ [RscExpr("ocf", "pacemaker", "Dummy"), OpExpr("start", None)],
|
|
+ ),
|
|
+ )
|
|
+ assert_xml_equal(
|
|
+ """
|
|
+ <context id="a">
|
|
+ <meta_attributes id="a-meta_attributes">
|
|
+ <rule id="a-meta_attributes-rule"
|
|
+ boolean-op="and" score="INFINITY"
|
|
+ >
|
|
+ <rsc_expression
|
|
+ id="a-meta_attributes-rule-rsc-ocf-pacemaker-Dummy"
|
|
+ class="ocf" provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ <op_expression id="a-meta_attributes-rule-op-start"
|
|
+ name="start"
|
|
+ />
|
|
+ </rule>
|
|
+ </meta_attributes>
|
|
+ </context>
|
|
+ """,
|
|
+ etree_to_str(context_element),
|
|
+ )
|
|
+
|
|
+ def test_custom_id(self):
|
|
+ context_element = etree.fromstring("""<context id="a" />""")
|
|
+ id_provider = IdProvider(context_element)
|
|
+ nvpair_multi.nvset_append_new(
|
|
+ context_element,
|
|
+ id_provider,
|
|
+ nvpair_multi.NVSET_META,
|
|
+ {},
|
|
+ {"id": "custom-id"},
|
|
+ )
|
|
+ assert_xml_equal(
|
|
+ """
|
|
+ <context id="a">
|
|
+ <meta_attributes id="custom-id" />
|
|
+ </context>
|
|
+ """,
|
|
+ etree_to_str(context_element),
|
|
+ )
|
|
+
|
|
+ def test_options(self):
|
|
+ context_element = etree.fromstring("""<context id="a" />""")
|
|
+ id_provider = IdProvider(context_element)
|
|
+ nvpair_multi.nvset_append_new(
|
|
+ context_element,
|
|
+ id_provider,
|
|
+ nvpair_multi.NVSET_META,
|
|
+ {},
|
|
+ {"score": "INFINITY", "empty-attr": ""},
|
|
+ )
|
|
+ assert_xml_equal(
|
|
+ """
|
|
+ <context id="a">
|
|
+ <meta_attributes id="a-meta_attributes" score="INFINITY" />
|
|
+ </context>
|
|
+ """,
|
|
+ etree_to_str(context_element),
|
|
+ )
|
|
+
|
|
+ def test_everything(self):
|
|
+ context_element = etree.fromstring("""<context id="a" />""")
|
|
+ id_provider = IdProvider(context_element)
|
|
+ nvpair_multi.nvset_append_new(
|
|
+ context_element,
|
|
+ id_provider,
|
|
+ nvpair_multi.NVSET_META,
|
|
+ {"attr1": "value1", "attr-empty": "", "attr2": "value2"},
|
|
+ {"id": "custom-id", "score": "INFINITY", "empty-attr": ""},
|
|
+ nvset_rule=BoolExpr(
|
|
+ BOOL_AND,
|
|
+ [RscExpr("ocf", "pacemaker", "Dummy"), OpExpr("start", None)],
|
|
+ ),
|
|
+ )
|
|
+ assert_xml_equal(
|
|
+ """
|
|
+ <context id="a">
|
|
+ <meta_attributes id="custom-id" score="INFINITY">
|
|
+ <rule id="custom-id-rule"
|
|
+ boolean-op="and" score="INFINITY"
|
|
+ >
|
|
+ <rsc_expression id="custom-id-rule-rsc-ocf-pacemaker-Dummy"
|
|
+ class="ocf" provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ <op_expression id="custom-id-rule-op-start"
|
|
+ name="start"
|
|
+ />
|
|
+ </rule>
|
|
+ <nvpair id="custom-id-attr1"
|
|
+ name="attr1" value="value1"
|
|
+ />
|
|
+ <nvpair id="custom-id-attr2"
|
|
+ name="attr2" value="value2"
|
|
+ />
|
|
+ </meta_attributes>
|
|
+ </context>
|
|
+ """,
|
|
+ etree_to_str(context_element),
|
|
+ )
|
|
+
|
|
+
|
|
+class NvsetRemove(TestCase):
|
|
+ # pylint: disable=no-self-use
|
|
+ def test_success(self):
|
|
+ xml = etree.fromstring(
|
|
+ """
|
|
+ <parent>
|
|
+ <meta_attributes id="set1" />
|
|
+ <instance_attributes id="set2" />
|
|
+ <not_an_nvset id="set3" />
|
|
+ <meta_attributes id="set4" />
|
|
+ </parent>
|
|
+ """
|
|
+ )
|
|
+ nvpair_multi.nvset_remove(
|
|
+ [xml.find(".//*[@id='set2']"), xml.find(".//*[@id='set4']")]
|
|
+ )
|
|
+ assert_xml_equal(
|
|
+ """
|
|
+ <parent>
|
|
+ <meta_attributes id="set1" />
|
|
+ <not_an_nvset id="set3" />
|
|
+ </parent>
|
|
+ """,
|
|
+ etree_to_str(xml),
|
|
+ )
|
|
+
|
|
+
|
|
+class NvsetUpdate(TestCase):
|
|
+ # pylint: disable=no-self-use
|
|
+ def test_success_nvpair_all_cases(self):
|
|
+ nvset_element = etree.fromstring(
|
|
+ """
|
|
+ <meta_attributes id="set1">
|
|
+ <nvpair id="pair1" name="name1" value="value1" />
|
|
+ <nvpair id="pair2" name="name2" value="value2" />
|
|
+ <nvpair id="pair3" name="name 3" value="value 3" />
|
|
+ <nvpair id="pair4" name="name4" value="value4" />
|
|
+ <nvpair id="pair4A" name="name4" value="value4A" />
|
|
+ <nvpair id="pair4B" name="name4" value="value4B" />
|
|
+ </meta_attributes>
|
|
+ """
|
|
+ )
|
|
+ id_provider = IdProvider(nvset_element)
|
|
+ nvpair_multi.nvset_update(
|
|
+ nvset_element,
|
|
+ id_provider,
|
|
+ {
|
|
+ "name2": "", # delete
|
|
+ "name 3": "value 3 new", # change and escaping spaces
|
|
+ "name4": "value4new", # change and make unique
|
|
+ "name5": "", # do not add empty
|
|
+ "name'6'": 'value"6"', # escaping
|
|
+ },
|
|
+ )
|
|
+ assert_xml_equal(
|
|
+ """
|
|
+ <meta_attributes id="set1">
|
|
+ <nvpair id="pair1" name="name1" value="value1" />
|
|
+ <nvpair id="pair3" name="name 3" value="value 3 new" />
|
|
+ <nvpair id="pair4" name="name4" value="value4new" />
|
|
+ <nvpair id="set1-name6"
|
|
+ name="name'6'" value="value"6""
|
|
+ />
|
|
+ </meta_attributes>
|
|
+ """,
|
|
+ etree_to_str(nvset_element),
|
|
+ )
|
|
diff --git a/pcs_test/tier0/lib/cib/test_tools.py b/pcs_test/tier0/lib/cib/test_tools.py
|
|
index 376012a1..56f29148 100644
|
|
--- a/pcs_test/tier0/lib/cib/test_tools.py
|
|
+++ b/pcs_test/tier0/lib/cib/test_tools.py
|
|
@@ -233,8 +233,8 @@ class FindUniqueIdTest(CibToolsTest):
|
|
)
|
|
|
|
|
|
-class CreateNvsetIdTest(TestCase):
|
|
- def test_create_plain_id_when_no_confilicting_id_there(self):
|
|
+class CreateSubelementId(TestCase):
|
|
+ def test_create_plain_id_when_no_conflicting_id_there(self):
|
|
context = etree.fromstring('<cib><a id="b"/></cib>')
|
|
self.assertEqual(
|
|
"b-name",
|
|
@@ -252,6 +252,15 @@ class CreateNvsetIdTest(TestCase):
|
|
),
|
|
)
|
|
|
|
+ def test_parent_has_no_id(self):
|
|
+ context = etree.fromstring("<cib><a/></cib>")
|
|
+ self.assertEqual(
|
|
+ "a-name",
|
|
+ lib.create_subelement_id(
|
|
+ context.find(".//a"), "name", lib.IdProvider(context)
|
|
+ ),
|
|
+ )
|
|
+
|
|
|
|
class GetConfigurationTest(CibToolsTest):
|
|
def test_success_if_exists(self):
|
|
diff --git a/pcs_test/tier0/lib/commands/cib_options/test_operations_defaults.py b/pcs_test/tier0/lib/commands/cib_options/test_operations_defaults.py
|
|
deleted file mode 100644
|
|
index 2542043a..00000000
|
|
--- a/pcs_test/tier0/lib/commands/cib_options/test_operations_defaults.py
|
|
+++ /dev/null
|
|
@@ -1,120 +0,0 @@
|
|
-from unittest import TestCase
|
|
-
|
|
-from pcs_test.tools import fixture
|
|
-from pcs_test.tools.command_env import get_env_tools
|
|
-
|
|
-from pcs.common.reports import codes as report_codes
|
|
-from pcs.lib.commands import cib_options
|
|
-
|
|
-FIXTURE_INITIAL_DEFAULTS = """
|
|
- <op_defaults>
|
|
- <meta_attributes id="op_defaults-options">
|
|
- <nvpair id="op_defaults-options-a" name="a" value="b"/>
|
|
- <nvpair id="op_defaults-options-b" name="b" value="c"/>
|
|
- </meta_attributes>
|
|
- </op_defaults>
|
|
-"""
|
|
-
|
|
-
|
|
-class SetOperationsDefaults(TestCase):
|
|
- def setUp(self):
|
|
- self.env_assist, self.config = get_env_tools(test_case=self)
|
|
-
|
|
- def tearDown(self):
|
|
- self.env_assist.assert_reports(
|
|
- [fixture.warn(report_codes.DEFAULTS_CAN_BE_OVERRIDEN)]
|
|
- )
|
|
-
|
|
- def test_change(self):
|
|
- self.config.runner.cib.load(optional_in_conf=FIXTURE_INITIAL_DEFAULTS)
|
|
- self.config.env.push_cib(
|
|
- optional_in_conf="""
|
|
- <op_defaults>
|
|
- <meta_attributes id="op_defaults-options">
|
|
- <nvpair id="op_defaults-options-a" name="a" value="B"/>
|
|
- <nvpair id="op_defaults-options-b" name="b" value="C"/>
|
|
- </meta_attributes>
|
|
- </op_defaults>
|
|
- """
|
|
- )
|
|
- cib_options.set_operations_defaults(
|
|
- self.env_assist.get_env(), {"a": "B", "b": "C",}
|
|
- )
|
|
-
|
|
- def test_add(self):
|
|
- self.config.runner.cib.load(optional_in_conf=FIXTURE_INITIAL_DEFAULTS)
|
|
- self.config.env.push_cib(
|
|
- optional_in_conf="""
|
|
- <op_defaults>
|
|
- <meta_attributes id="op_defaults-options">
|
|
- <nvpair id="op_defaults-options-a" name="a" value="b"/>
|
|
- <nvpair id="op_defaults-options-b" name="b" value="c"/>
|
|
- <nvpair id="op_defaults-options-c" name="c" value="d"/>
|
|
- </meta_attributes>
|
|
- </op_defaults>
|
|
- """
|
|
- )
|
|
- cib_options.set_operations_defaults(
|
|
- self.env_assist.get_env(), {"c": "d"},
|
|
- )
|
|
-
|
|
- def test_remove(self):
|
|
- self.config.runner.cib.load(optional_in_conf=FIXTURE_INITIAL_DEFAULTS)
|
|
- self.config.env.push_cib(
|
|
- remove=(
|
|
- "./configuration/op_defaults/meta_attributes/nvpair[@name='a']"
|
|
- )
|
|
- )
|
|
- cib_options.set_operations_defaults(
|
|
- self.env_assist.get_env(), {"a": ""},
|
|
- )
|
|
-
|
|
- def test_add_section_if_missing(self):
|
|
- self.config.runner.cib.load()
|
|
- self.config.env.push_cib(
|
|
- optional_in_conf="""
|
|
- <op_defaults>
|
|
- <meta_attributes id="op_defaults-options">
|
|
- <nvpair id="op_defaults-options-a" name="a" value="A"/>
|
|
- </meta_attributes>
|
|
- </op_defaults>
|
|
- """
|
|
- )
|
|
- cib_options.set_operations_defaults(
|
|
- self.env_assist.get_env(), {"a": "A",}
|
|
- )
|
|
-
|
|
- def test_add_meta_if_missing(self):
|
|
- self.config.runner.cib.load(optional_in_conf="<op_defaults />")
|
|
- self.config.env.push_cib(
|
|
- optional_in_conf="""
|
|
- <op_defaults>
|
|
- <meta_attributes id="op_defaults-options">
|
|
- <nvpair id="op_defaults-options-a" name="a" value="A"/>
|
|
- </meta_attributes>
|
|
- </op_defaults>
|
|
- """
|
|
- )
|
|
- cib_options.set_operations_defaults(
|
|
- self.env_assist.get_env(), {"a": "A",}
|
|
- )
|
|
-
|
|
- def test_dont_add_section_if_only_removing(self):
|
|
- self.config.runner.cib.load()
|
|
- cib_options.set_operations_defaults(
|
|
- self.env_assist.get_env(), {"a": "", "b": "",}
|
|
- )
|
|
-
|
|
- def test_dont_add_meta_if_only_removing(self):
|
|
- self.config.runner.cib.load(optional_in_conf="<op_defaults />")
|
|
- self.config.env.push_cib(optional_in_conf="<op_defaults />")
|
|
- cib_options.set_operations_defaults(
|
|
- self.env_assist.get_env(), {"a": "", "b": "",}
|
|
- )
|
|
-
|
|
- def test_keep_section_when_empty(self):
|
|
- self.config.runner.cib.load(optional_in_conf=FIXTURE_INITIAL_DEFAULTS)
|
|
- self.config.env.push_cib(remove="./configuration/op_defaults//nvpair")
|
|
- cib_options.set_operations_defaults(
|
|
- self.env_assist.get_env(), {"a": "", "b": "",}
|
|
- )
|
|
diff --git a/pcs_test/tier0/lib/commands/cib_options/test_resources_defaults.py b/pcs_test/tier0/lib/commands/cib_options/test_resources_defaults.py
|
|
deleted file mode 100644
|
|
index 51d8abf4..00000000
|
|
--- a/pcs_test/tier0/lib/commands/cib_options/test_resources_defaults.py
|
|
+++ /dev/null
|
|
@@ -1,120 +0,0 @@
|
|
-from unittest import TestCase
|
|
-
|
|
-from pcs_test.tools import fixture
|
|
-from pcs_test.tools.command_env import get_env_tools
|
|
-
|
|
-from pcs.common.reports import codes as report_codes
|
|
-from pcs.lib.commands import cib_options
|
|
-
|
|
-FIXTURE_INITIAL_DEFAULTS = """
|
|
- <rsc_defaults>
|
|
- <meta_attributes id="rsc_defaults-options">
|
|
- <nvpair id="rsc_defaults-options-a" name="a" value="b"/>
|
|
- <nvpair id="rsc_defaults-options-b" name="b" value="c"/>
|
|
- </meta_attributes>
|
|
- </rsc_defaults>
|
|
-"""
|
|
-
|
|
-
|
|
-class SetResourcesDefaults(TestCase):
|
|
- def setUp(self):
|
|
- self.env_assist, self.config = get_env_tools(test_case=self)
|
|
-
|
|
- def tearDown(self):
|
|
- self.env_assist.assert_reports(
|
|
- [fixture.warn(report_codes.DEFAULTS_CAN_BE_OVERRIDEN)]
|
|
- )
|
|
-
|
|
- def test_change(self):
|
|
- self.config.runner.cib.load(optional_in_conf=FIXTURE_INITIAL_DEFAULTS)
|
|
- self.config.env.push_cib(
|
|
- optional_in_conf="""
|
|
- <rsc_defaults>
|
|
- <meta_attributes id="rsc_defaults-options">
|
|
- <nvpair id="rsc_defaults-options-a" name="a" value="B"/>
|
|
- <nvpair id="rsc_defaults-options-b" name="b" value="C"/>
|
|
- </meta_attributes>
|
|
- </rsc_defaults>
|
|
- """
|
|
- )
|
|
- cib_options.set_resources_defaults(
|
|
- self.env_assist.get_env(), {"a": "B", "b": "C",}
|
|
- )
|
|
-
|
|
- def test_add(self):
|
|
- self.config.runner.cib.load(optional_in_conf=FIXTURE_INITIAL_DEFAULTS)
|
|
- self.config.env.push_cib(
|
|
- optional_in_conf="""
|
|
- <rsc_defaults>
|
|
- <meta_attributes id="rsc_defaults-options">
|
|
- <nvpair id="rsc_defaults-options-a" name="a" value="b"/>
|
|
- <nvpair id="rsc_defaults-options-b" name="b" value="c"/>
|
|
- <nvpair id="rsc_defaults-options-c" name="c" value="d"/>
|
|
- </meta_attributes>
|
|
- </rsc_defaults>
|
|
- """
|
|
- )
|
|
- cib_options.set_resources_defaults(
|
|
- self.env_assist.get_env(), {"c": "d"},
|
|
- )
|
|
-
|
|
- def test_remove(self):
|
|
- self.config.runner.cib.load(optional_in_conf=FIXTURE_INITIAL_DEFAULTS)
|
|
- self.config.env.push_cib(
|
|
- remove=(
|
|
- "./configuration/rsc_defaults/meta_attributes/nvpair[@name='a']"
|
|
- )
|
|
- )
|
|
- cib_options.set_resources_defaults(
|
|
- self.env_assist.get_env(), {"a": ""},
|
|
- )
|
|
-
|
|
- def test_add_section_if_missing(self):
|
|
- self.config.runner.cib.load()
|
|
- self.config.env.push_cib(
|
|
- optional_in_conf="""
|
|
- <rsc_defaults>
|
|
- <meta_attributes id="rsc_defaults-options">
|
|
- <nvpair id="rsc_defaults-options-a" name="a" value="A"/>
|
|
- </meta_attributes>
|
|
- </rsc_defaults>
|
|
- """
|
|
- )
|
|
- cib_options.set_resources_defaults(
|
|
- self.env_assist.get_env(), {"a": "A",}
|
|
- )
|
|
-
|
|
- def test_add_meta_if_missing(self):
|
|
- self.config.runner.cib.load(optional_in_conf="<rsc_defaults />")
|
|
- self.config.env.push_cib(
|
|
- optional_in_conf="""
|
|
- <rsc_defaults>
|
|
- <meta_attributes id="rsc_defaults-options">
|
|
- <nvpair id="rsc_defaults-options-a" name="a" value="A"/>
|
|
- </meta_attributes>
|
|
- </rsc_defaults>
|
|
- """
|
|
- )
|
|
- cib_options.set_resources_defaults(
|
|
- self.env_assist.get_env(), {"a": "A",}
|
|
- )
|
|
-
|
|
- def test_dont_add_section_if_only_removing(self):
|
|
- self.config.runner.cib.load()
|
|
- cib_options.set_resources_defaults(
|
|
- self.env_assist.get_env(), {"a": "", "b": "",}
|
|
- )
|
|
-
|
|
- def test_dont_add_meta_if_only_removing(self):
|
|
- self.config.runner.cib.load(optional_in_conf="<rsc_defaults />")
|
|
- self.config.env.push_cib(optional_in_conf="<rsc_defaults />")
|
|
- cib_options.set_resources_defaults(
|
|
- self.env_assist.get_env(), {"a": "", "b": "",}
|
|
- )
|
|
-
|
|
- def test_keep_section_when_empty(self):
|
|
- self.config.runner.cib.load(optional_in_conf=FIXTURE_INITIAL_DEFAULTS)
|
|
- self.config.env.push_cib(remove="./configuration/rsc_defaults//nvpair")
|
|
- cib_options.set_resources_defaults(
|
|
- self.env_assist.get_env(), {"a": "", "b": "",}
|
|
- )
|
|
diff --git a/pcs_test/tier0/lib/commands/test_cib_options.py b/pcs_test/tier0/lib/commands/test_cib_options.py
|
|
new file mode 100644
|
|
index 00000000..c7c8cb1f
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier0/lib/commands/test_cib_options.py
|
|
@@ -0,0 +1,669 @@
|
|
+from unittest import TestCase
|
|
+
|
|
+from pcs_test.tools import fixture
|
|
+from pcs_test.tools.command_env import get_env_tools
|
|
+
|
|
+from pcs.common import reports
|
|
+from pcs.common.pacemaker.nvset import (
|
|
+ CibNvpairDto,
|
|
+ CibNvsetDto,
|
|
+)
|
|
+from pcs.common.pacemaker.rule import CibRuleExpressionDto
|
|
+from pcs.common.types import (
|
|
+ CibNvsetType,
|
|
+ CibRuleExpressionType,
|
|
+)
|
|
+from pcs.lib.commands import cib_options
|
|
+
|
|
+
|
|
+class DefaultsCreateMixin:
|
|
+ command = lambda *args, **kwargs: None
|
|
+ tag = ""
|
|
+
|
|
+ def setUp(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.env_assist, self.config = get_env_tools(self)
|
|
+ self.config.runner.cib.load(filename="cib-empty-1.2.xml")
|
|
+
|
|
+ def test_success_minimal(self):
|
|
+ defaults_xml = f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-meta_attributes" />
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ self.config.env.push_cib(optional_in_conf=defaults_xml)
|
|
+
|
|
+ self.command(self.env_assist.get_env(), {}, {})
|
|
+
|
|
+ self.env_assist.assert_reports(
|
|
+ [fixture.warn(reports.codes.DEFAULTS_CAN_BE_OVERRIDEN)]
|
|
+ )
|
|
+
|
|
+ def test_success_one_set_already_there(self):
|
|
+ defaults_xml_1 = f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-meta_attributes" />
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ defaults_xml_2 = f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-meta_attributes" />
|
|
+ <meta_attributes id="{self.tag}-meta_attributes-1" />
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ self.config.runner.cib.load(
|
|
+ instead="runner.cib.load", optional_in_conf=defaults_xml_1
|
|
+ )
|
|
+ self.config.env.push_cib(optional_in_conf=defaults_xml_2)
|
|
+
|
|
+ self.command(self.env_assist.get_env(), {}, {})
|
|
+
|
|
+ self.env_assist.assert_reports(
|
|
+ [fixture.warn(reports.codes.DEFAULTS_CAN_BE_OVERRIDEN)]
|
|
+ )
|
|
+
|
|
+ def test_success_cib_upgrade(self):
|
|
+ defaults_xml = f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-meta_attributes">
|
|
+ <rule id="{self.tag}-meta_attributes-rule"
|
|
+ boolean-op="and" score="INFINITY"
|
|
+ >
|
|
+ <rsc_expression
|
|
+ id="{self.tag}-meta_attributes-rule-rsc-ocf-pacemaker-Dummy"
|
|
+ class="ocf" provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ </rule>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ self.config.runner.cib.load(
|
|
+ name="load_cib_old_version",
|
|
+ filename="cib-empty-3.3.xml",
|
|
+ before="runner.cib.load",
|
|
+ )
|
|
+ self.config.runner.cib.upgrade(before="runner.cib.load")
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-3.4.xml", instead="runner.cib.load"
|
|
+ )
|
|
+ self.config.env.push_cib(optional_in_conf=defaults_xml)
|
|
+
|
|
+ self.command(
|
|
+ self.env_assist.get_env(),
|
|
+ {},
|
|
+ {},
|
|
+ nvset_rule="resource ocf:pacemaker:Dummy",
|
|
+ )
|
|
+
|
|
+ self.env_assist.assert_reports(
|
|
+ [
|
|
+ fixture.info(reports.codes.CIB_UPGRADE_SUCCESSFUL),
|
|
+ fixture.warn(reports.codes.DEFAULTS_CAN_BE_OVERRIDEN),
|
|
+ ]
|
|
+ )
|
|
+
|
|
+ def test_success_full(self):
|
|
+ defaults_xml = f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="my-id" score="10">
|
|
+ <rule id="my-id-rule" boolean-op="and" score="INFINITY">
|
|
+ <rsc_expression id="my-id-rule-rsc-ocf-pacemaker-Dummy"
|
|
+ class="ocf" provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ </rule>
|
|
+ <nvpair id="my-id-name1" name="name1" value="value1" />
|
|
+ <nvpair id="my-id-2name" name="2na#me" value="value2" />
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-3.4.xml", instead="runner.cib.load"
|
|
+ )
|
|
+ self.config.env.push_cib(optional_in_conf=defaults_xml)
|
|
+
|
|
+ self.command(
|
|
+ self.env_assist.get_env(),
|
|
+ {"name1": "value1", "2na#me": "value2"},
|
|
+ {"id": "my-id", "score": "10"},
|
|
+ nvset_rule="resource ocf:pacemaker:Dummy",
|
|
+ )
|
|
+
|
|
+ self.env_assist.assert_reports(
|
|
+ [fixture.warn(reports.codes.DEFAULTS_CAN_BE_OVERRIDEN)]
|
|
+ )
|
|
+
|
|
+ def test_validation(self):
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-3.4.xml", instead="runner.cib.load"
|
|
+ )
|
|
+ self.env_assist.assert_raise_library_error(
|
|
+ lambda: self.command(
|
|
+ self.env_assist.get_env(),
|
|
+ {},
|
|
+ {"unknown-option": "value"},
|
|
+ "bad rule",
|
|
+ )
|
|
+ )
|
|
+ self.env_assist.assert_reports(
|
|
+ [
|
|
+ fixture.error(
|
|
+ reports.codes.INVALID_OPTIONS,
|
|
+ force_code=reports.codes.FORCE_OPTIONS,
|
|
+ option_names=["unknown-option"],
|
|
+ allowed=["id", "score"],
|
|
+ option_type=None,
|
|
+ allowed_patterns=[],
|
|
+ ),
|
|
+ fixture.error(
|
|
+ reports.codes.RULE_EXPRESSION_PARSE_ERROR,
|
|
+ rule_string="bad rule",
|
|
+ reason='Expected "resource"',
|
|
+ rule_line="bad rule",
|
|
+ line_number=1,
|
|
+ column_number=1,
|
|
+ position=0,
|
|
+ ),
|
|
+ ]
|
|
+ )
|
|
+
|
|
+ def test_validation_forced(self):
|
|
+ defaults_xml = f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-meta_attributes"
|
|
+ unknown-option="value"
|
|
+ />
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ self.config.env.push_cib(optional_in_conf=defaults_xml)
|
|
+
|
|
+ self.command(
|
|
+ self.env_assist.get_env(),
|
|
+ {},
|
|
+ {"unknown-option": "value"},
|
|
+ force_flags={reports.codes.FORCE_OPTIONS},
|
|
+ )
|
|
+
|
|
+ self.env_assist.assert_reports(
|
|
+ [
|
|
+ fixture.warn(
|
|
+ reports.codes.INVALID_OPTIONS,
|
|
+ option_names=["unknown-option"],
|
|
+ allowed=["id", "score"],
|
|
+ option_type=None,
|
|
+ allowed_patterns=[],
|
|
+ ),
|
|
+ fixture.warn(reports.codes.DEFAULTS_CAN_BE_OVERRIDEN),
|
|
+ ]
|
|
+ )
|
|
+
|
|
+
|
|
+class ResourceDefaultsCreate(DefaultsCreateMixin, TestCase):
|
|
+ command = staticmethod(cib_options.resource_defaults_create)
|
|
+ tag = "rsc_defaults"
|
|
+
|
|
+ def test_rule_op_expression_not_allowed(self):
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-3.4.xml", instead="runner.cib.load"
|
|
+ )
|
|
+ self.env_assist.assert_raise_library_error(
|
|
+ lambda: self.command(
|
|
+ self.env_assist.get_env(), {}, {}, "op monitor"
|
|
+ )
|
|
+ )
|
|
+ self.env_assist.assert_reports(
|
|
+ [
|
|
+ fixture.error(
|
|
+ reports.codes.RULE_EXPRESSION_NOT_ALLOWED,
|
|
+ expression_type=CibRuleExpressionType.OP_EXPRESSION,
|
|
+ ),
|
|
+ ]
|
|
+ )
|
|
+
|
|
+
|
|
+class OperationDefaultsCreate(DefaultsCreateMixin, TestCase):
|
|
+ command = staticmethod(cib_options.operation_defaults_create)
|
|
+ tag = "op_defaults"
|
|
+
|
|
+
|
|
+class DefaultsConfigMixin:
|
|
+ command = lambda *args, **kwargs: None
|
|
+ tag = ""
|
|
+
|
|
+ def setUp(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.env_assist, self.config = get_env_tools(self)
|
|
+
|
|
+ def test_empty(self):
|
|
+ defaults_xml = f"""<{self.tag} />"""
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-3.4.xml", optional_in_conf=defaults_xml
|
|
+ )
|
|
+ self.assertEqual([], self.command(self.env_assist.get_env()))
|
|
+
|
|
+ def test_full(self):
|
|
+ defaults_xml = f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-meta_attributes">
|
|
+ <rule id="{self.tag}-meta_attributes-rule"
|
|
+ boolean-op="and" score="INFINITY"
|
|
+ >
|
|
+ <rsc_expression
|
|
+ id="{self.tag}-meta_attributes-rule-rsc-Dummy"
|
|
+ class="ocf" provider="pacemaker" type="Dummy"
|
|
+ />
|
|
+ </rule>
|
|
+ <nvpair id="my-id-pair1" name="name1" value="value1" />
|
|
+ <nvpair id="my-id-pair2" name="name2" value="value2" />
|
|
+ </meta_attributes>
|
|
+ <instance_attributes id="instance">
|
|
+ <nvpair id="instance-pair" name="inst" value="ance" />
|
|
+ </instance_attributes>
|
|
+ <meta_attributes id="meta-plain" score="123">
|
|
+ <nvpair id="my-id-pair3" name="name1" value="value1" />
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-3.4.xml", optional_in_conf=defaults_xml
|
|
+ )
|
|
+ self.assertEqual(
|
|
+ [
|
|
+ CibNvsetDto(
|
|
+ f"{self.tag}-meta_attributes",
|
|
+ CibNvsetType.META,
|
|
+ {},
|
|
+ CibRuleExpressionDto(
|
|
+ f"{self.tag}-meta_attributes-rule",
|
|
+ CibRuleExpressionType.RULE,
|
|
+ False,
|
|
+ {"boolean-op": "and", "score": "INFINITY"},
|
|
+ None,
|
|
+ None,
|
|
+ [
|
|
+ CibRuleExpressionDto(
|
|
+ f"{self.tag}-meta_attributes-rule-rsc-Dummy",
|
|
+ CibRuleExpressionType.RSC_EXPRESSION,
|
|
+ False,
|
|
+ {
|
|
+ "class": "ocf",
|
|
+ "provider": "pacemaker",
|
|
+ "type": "Dummy",
|
|
+ },
|
|
+ None,
|
|
+ None,
|
|
+ [],
|
|
+ "resource ocf:pacemaker:Dummy",
|
|
+ ),
|
|
+ ],
|
|
+ "resource ocf:pacemaker:Dummy",
|
|
+ ),
|
|
+ [
|
|
+ CibNvpairDto("my-id-pair1", "name1", "value1"),
|
|
+ CibNvpairDto("my-id-pair2", "name2", "value2"),
|
|
+ ],
|
|
+ ),
|
|
+ CibNvsetDto(
|
|
+ "instance",
|
|
+ CibNvsetType.INSTANCE,
|
|
+ {},
|
|
+ None,
|
|
+ [CibNvpairDto("instance-pair", "inst", "ance")],
|
|
+ ),
|
|
+ CibNvsetDto(
|
|
+ "meta-plain",
|
|
+ CibNvsetType.META,
|
|
+ {"score": "123"},
|
|
+ None,
|
|
+ [CibNvpairDto("my-id-pair3", "name1", "value1")],
|
|
+ ),
|
|
+ ],
|
|
+ self.command(self.env_assist.get_env()),
|
|
+ )
|
|
+
|
|
+
|
|
+class ResourceDefaultsConfig(DefaultsConfigMixin, TestCase):
|
|
+ command = staticmethod(cib_options.resource_defaults_config)
|
|
+ tag = "rsc_defaults"
|
|
+
|
|
+
|
|
+class OperationDefaultsConfig(DefaultsConfigMixin, TestCase):
|
|
+ command = staticmethod(cib_options.operation_defaults_config)
|
|
+ tag = "op_defaults"
|
|
+
|
|
+
|
|
+class DefaultsRemoveMixin:
|
|
+ command = lambda *args, **kwargs: None
|
|
+ tag = ""
|
|
+
|
|
+ def setUp(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.env_assist, self.config = get_env_tools(self)
|
|
+
|
|
+ def test_nothing_to_delete(self):
|
|
+ self.command(self.env_assist.get_env(), [])
|
|
+
|
|
+ def test_defaults_section_missing(self):
|
|
+ self.config.runner.cib.load(filename="cib-empty-1.2.xml")
|
|
+ self.env_assist.assert_raise_library_error(
|
|
+ lambda: self.command(self.env_assist.get_env(), ["set1"])
|
|
+ )
|
|
+ self.env_assist.assert_reports(
|
|
+ [
|
|
+ fixture.report_not_found(
|
|
+ "set1",
|
|
+ context_type=self.tag,
|
|
+ expected_types=["options set"],
|
|
+ ),
|
|
+ ]
|
|
+ )
|
|
+
|
|
+ def test_success(self):
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-1.2.xml",
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="set1" />
|
|
+ <instance_attributes id="set2" />
|
|
+ <not_an_nvset id="set3" />
|
|
+ <meta_attributes id="set4" />
|
|
+ <instance_attributes id="set5" />
|
|
+ </{self.tag}>
|
|
+ """,
|
|
+ )
|
|
+ self.config.env.push_cib(
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="set1" />
|
|
+ <not_an_nvset id="set3" />
|
|
+ <meta_attributes id="set4" />
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ )
|
|
+ self.command(self.env_assist.get_env(), ["set2", "set5"])
|
|
+
|
|
+ def test_delete_all_keep_the_section(self):
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-1.2.xml",
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="set1" />
|
|
+ </{self.tag}>
|
|
+ """,
|
|
+ )
|
|
+ self.config.env.push_cib(optional_in_conf=f"<{self.tag} />")
|
|
+ self.command(self.env_assist.get_env(), ["set1"])
|
|
+
|
|
+ def test_nvset_not_found(self):
|
|
+ self.config.runner.cib.load(
|
|
+ filename="cib-empty-1.2.xml",
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="set1" />
|
|
+ <instance_attributes id="set2" />
|
|
+ <not_an_nvset id="set3" />
|
|
+ <meta_attributes id="set4" />
|
|
+ <instance_attributes id="set5" />
|
|
+ </{self.tag}>
|
|
+ """,
|
|
+ )
|
|
+ self.env_assist.assert_raise_library_error(
|
|
+ lambda: self.command(
|
|
+ self.env_assist.get_env(), ["set2", "set3", "setX"]
|
|
+ )
|
|
+ )
|
|
+ self.env_assist.assert_reports(
|
|
+ [
|
|
+ fixture.report_unexpected_element(
|
|
+ "set3", "not_an_nvset", ["options set"]
|
|
+ ),
|
|
+ fixture.report_not_found(
|
|
+ "setX",
|
|
+ context_type=self.tag,
|
|
+ expected_types=["options set"],
|
|
+ ),
|
|
+ ]
|
|
+ )
|
|
+
|
|
+
|
|
+class ResourceDefaultsRemove(DefaultsRemoveMixin, TestCase):
|
|
+ command = staticmethod(cib_options.resource_defaults_remove)
|
|
+ tag = "rsc_defaults"
|
|
+
|
|
+
|
|
+class OperationDefaultsRemove(DefaultsRemoveMixin, TestCase):
|
|
+ command = staticmethod(cib_options.operation_defaults_remove)
|
|
+ tag = "op_defaults"
|
|
+
|
|
+
|
|
+class DefaultsUpdateLegacyMixin:
|
|
+ # This class tests legacy use cases of not providing an nvset ID
|
|
+ command = lambda *args, **kwargs: None
|
|
+ tag = ""
|
|
+ command_for_report = None
|
|
+
|
|
+ def setUp(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.env_assist, self.config = get_env_tools(self)
|
|
+ self.reports = [fixture.warn(reports.codes.DEFAULTS_CAN_BE_OVERRIDEN)]
|
|
+
|
|
+ def tearDown(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.env_assist.assert_reports(self.reports)
|
|
+
|
|
+ def fixture_initial_defaults(self):
|
|
+ return f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-options">
|
|
+ <nvpair id="{self.tag}-options-a" name="a" value="b"/>
|
|
+ <nvpair id="{self.tag}-options-b" name="b" value="c"/>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+
|
|
+ def test_change(self):
|
|
+ self.config.runner.cib.load(
|
|
+ optional_in_conf=self.fixture_initial_defaults()
|
|
+ )
|
|
+ self.config.env.push_cib(
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-options">
|
|
+ <nvpair id="{self.tag}-options-a" name="a" value="B"/>
|
|
+ <nvpair id="{self.tag}-options-b" name="b" value="C"/>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ )
|
|
+ self.command(self.env_assist.get_env(), None, {"a": "B", "b": "C"})
|
|
+
|
|
+ def test_add(self):
|
|
+ self.config.runner.cib.load(
|
|
+ optional_in_conf=self.fixture_initial_defaults()
|
|
+ )
|
|
+ self.config.env.push_cib(
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-options">
|
|
+ <nvpair id="{self.tag}-options-a" name="a" value="b"/>
|
|
+ <nvpair id="{self.tag}-options-b" name="b" value="c"/>
|
|
+ <nvpair id="{self.tag}-options-c" name="c" value="d"/>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ )
|
|
+ self.command(self.env_assist.get_env(), None, {"c": "d"})
|
|
+
|
|
+ def test_remove(self):
|
|
+ self.config.runner.cib.load(
|
|
+ optional_in_conf=self.fixture_initial_defaults()
|
|
+ )
|
|
+ self.config.env.push_cib(
|
|
+ remove=(
|
|
+ f"./configuration/{self.tag}/meta_attributes/nvpair[@name='a']"
|
|
+ )
|
|
+ )
|
|
+ self.command(self.env_assist.get_env(), None, {"a": ""})
|
|
+
|
|
+ def test_add_section_if_missing(self):
|
|
+ self.config.runner.cib.load()
|
|
+ self.config.env.push_cib(
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-meta_attributes">
|
|
+ <nvpair id="{self.tag}-meta_attributes-a" name="a" value="A"/>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ )
|
|
+ self.command(self.env_assist.get_env(), None, {"a": "A"})
|
|
+
|
|
+ def test_add_meta_if_missing(self):
|
|
+ self.config.runner.cib.load(optional_in_conf=f"<{self.tag} />")
|
|
+ self.config.env.push_cib(
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-meta_attributes">
|
|
+ <nvpair id="{self.tag}-meta_attributes-a" name="a" value="A"/>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ )
|
|
+ self.command(self.env_assist.get_env(), None, {"a": "A"})
|
|
+
|
|
+ def test_dont_add_section_if_only_removing(self):
|
|
+ self.config.runner.cib.load()
|
|
+ self.command(self.env_assist.get_env(), None, {"a": "", "b": ""})
|
|
+
|
|
+ def test_dont_add_meta_if_only_removing(self):
|
|
+ self.config.runner.cib.load(optional_in_conf=f"<{self.tag} />")
|
|
+ self.command(self.env_assist.get_env(), None, {"a": "", "b": ""})
|
|
+
|
|
+ def test_keep_section_when_empty(self):
|
|
+ self.config.runner.cib.load(
|
|
+ optional_in_conf=self.fixture_initial_defaults()
|
|
+ )
|
|
+ self.config.env.push_cib(remove=f"./configuration/{self.tag}//nvpair")
|
|
+ self.command(self.env_assist.get_env(), None, {"a": "", "b": ""})
|
|
+
|
|
+ def test_ambiguous(self):
|
|
+ self.config.runner.cib.load(
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-options">
|
|
+ <nvpair id="{self.tag}-options-a" name="a" value="b"/>
|
|
+ <nvpair id="{self.tag}-options-b" name="b" value="c"/>
|
|
+ </meta_attributes>
|
|
+ <meta_attributes id="{self.tag}-options-1">
|
|
+ <nvpair id="{self.tag}-options-c" name="c" value="d"/>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ )
|
|
+ self.env_assist.assert_raise_library_error(
|
|
+ lambda: self.command(self.env_assist.get_env(), None, {"x": "y"})
|
|
+ )
|
|
+ self.reports = [
|
|
+ fixture.error(
|
|
+ reports.codes.CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID,
|
|
+ pcs_command=self.command_for_report,
|
|
+ )
|
|
+ ]
|
|
+
|
|
+
|
|
+class DefaultsUpdateMixin:
|
|
+ command = lambda *args, **kwargs: None
|
|
+ tag = ""
|
|
+
|
|
+ def setUp(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.env_assist, self.config = get_env_tools(self)
|
|
+
|
|
+ def fixture_initial_defaults(self):
|
|
+ return f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-options">
|
|
+ <nvpair id="{self.tag}-options-a" name="a" value="b"/>
|
|
+ <nvpair id="{self.tag}-options-b" name="b" value="c"/>
|
|
+ <nvpair id="{self.tag}-options-c" name="c" value="d"/>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+
|
|
+ def test_success(self):
|
|
+ self.config.runner.cib.load(
|
|
+ optional_in_conf=self.fixture_initial_defaults()
|
|
+ )
|
|
+ self.config.env.push_cib(
|
|
+ optional_in_conf=f"""
|
|
+ <{self.tag}>
|
|
+ <meta_attributes id="{self.tag}-options">
|
|
+ <nvpair id="{self.tag}-options-a" name="a" value="B"/>
|
|
+ <nvpair id="{self.tag}-options-b" name="b" value="c"/>
|
|
+ <nvpair id="{self.tag}-options-d" name="d" value="e"/>
|
|
+ </meta_attributes>
|
|
+ </{self.tag}>
|
|
+ """
|
|
+ )
|
|
+ self.command(
|
|
+ self.env_assist.get_env(),
|
|
+ f"{self.tag}-options",
|
|
+ {"a": "B", "c": "", "d": "e"},
|
|
+ )
|
|
+ self.env_assist.assert_reports(
|
|
+ [fixture.warn(reports.codes.DEFAULTS_CAN_BE_OVERRIDEN)]
|
|
+ )
|
|
+
|
|
+ def test_nvset_doesnt_exist(self):
|
|
+ self.config.runner.cib.load(
|
|
+ optional_in_conf=self.fixture_initial_defaults()
|
|
+ )
|
|
+ self.env_assist.assert_raise_library_error(
|
|
+ lambda: self.command(
|
|
+ self.env_assist.get_env(), "wrong-nvset-id", {},
|
|
+ )
|
|
+ )
|
|
+ self.env_assist.assert_reports(
|
|
+ [
|
|
+ fixture.report_not_found(
|
|
+ "wrong-nvset-id",
|
|
+ context_type=self.tag,
|
|
+ expected_types=["options set"],
|
|
+ ),
|
|
+ ]
|
|
+ )
|
|
+
|
|
+ def test_keep_elements_when_empty(self):
|
|
+ self.config.runner.cib.load(
|
|
+ optional_in_conf=self.fixture_initial_defaults()
|
|
+ )
|
|
+ self.config.env.push_cib(remove=f"./configuration/{self.tag}//nvpair")
|
|
+ self.command(
|
|
+ self.env_assist.get_env(),
|
|
+ f"{self.tag}-options",
|
|
+ {"a": "", "b": "", "c": ""},
|
|
+ )
|
|
+ self.env_assist.assert_reports(
|
|
+ [fixture.warn(reports.codes.DEFAULTS_CAN_BE_OVERRIDEN)]
|
|
+ )
|
|
+
|
|
+
|
|
+class ResourceDefaultsUpdateLegacy(DefaultsUpdateLegacyMixin, TestCase):
|
|
+ command = staticmethod(cib_options.resource_defaults_update)
|
|
+ tag = "rsc_defaults"
|
|
+ command_for_report = reports.const.PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE
|
|
+
|
|
+
|
|
+class OperationDefaultsUpdateLegacy(DefaultsUpdateLegacyMixin, TestCase):
|
|
+ command = staticmethod(cib_options.operation_defaults_update)
|
|
+ tag = "op_defaults"
|
|
+ command_for_report = reports.const.PCS_COMMAND_OPERATION_DEFAULTS_UPDATE
|
|
+
|
|
+
|
|
+class ResourceDefaultsUpdate(DefaultsUpdateMixin, TestCase):
|
|
+ command = staticmethod(cib_options.resource_defaults_update)
|
|
+ tag = "rsc_defaults"
|
|
+
|
|
+
|
|
+class OperationDefaultsUpdate(DefaultsUpdateMixin, TestCase):
|
|
+ command = staticmethod(cib_options.operation_defaults_update)
|
|
+ tag = "op_defaults"
|
|
diff --git a/pcs_test/tier0/lib/test_validate.py b/pcs_test/tier0/lib/test_validate.py
|
|
index 002fd8ed..8c0e0261 100644
|
|
--- a/pcs_test/tier0/lib/test_validate.py
|
|
+++ b/pcs_test/tier0/lib/test_validate.py
|
|
@@ -1238,6 +1238,33 @@ class ValuePositiveInteger(TestCase):
|
|
)
|
|
|
|
|
|
+class ValueScore(TestCase):
|
|
+ def test_valid_score(self):
|
|
+ for score in [
|
|
+ "1",
|
|
+ "-1",
|
|
+ "+1",
|
|
+ "123",
|
|
+ "-123",
|
|
+ "+123",
|
|
+ "INFINITY",
|
|
+ "-INFINITY",
|
|
+ "+INFINITY",
|
|
+ ]:
|
|
+ with self.subTest(score=score):
|
|
+ assert_report_item_list_equal(
|
|
+ validate.ValueScore("a").validate({"a": score}), [],
|
|
+ )
|
|
+
|
|
+ def test_not_valid_score(self):
|
|
+ for score in ["something", "++1", "--1", "++INFINITY"]:
|
|
+ with self.subTest(score=score):
|
|
+ assert_report_item_list_equal(
|
|
+ validate.ValueScore("a").validate({"a": score}),
|
|
+ [fixture.error(report_codes.INVALID_SCORE, score=score,),],
|
|
+ )
|
|
+
|
|
+
|
|
class ValueTimeInterval(TestCase):
|
|
def test_no_reports_for_valid_time_interval(self):
|
|
for interval in ["0", "1s", "2sec", "3m", "4min", "5h", "6hr"]:
|
|
diff --git a/pcs_test/tier1/legacy/test_resource.py b/pcs_test/tier1/legacy/test_resource.py
|
|
index 5770d81a..7ffcc83b 100644
|
|
--- a/pcs_test/tier1/legacy/test_resource.py
|
|
+++ b/pcs_test/tier1/legacy/test_resource.py
|
|
@@ -1421,9 +1421,9 @@ monitor interval=20 (A-monitor-interval-20)
|
|
No alerts defined
|
|
|
|
Resources Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
Operations Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
|
|
Cluster Properties:
|
|
|
|
@@ -1657,9 +1657,9 @@ monitor interval=20 (A-monitor-interval-20)
|
|
No alerts defined
|
|
|
|
Resources Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
Operations Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
|
|
Cluster Properties:
|
|
|
|
diff --git a/pcs_test/tier1/legacy/test_stonith.py b/pcs_test/tier1/legacy/test_stonith.py
|
|
index 3fc2c4d5..c51a02b5 100644
|
|
--- a/pcs_test/tier1/legacy/test_stonith.py
|
|
+++ b/pcs_test/tier1/legacy/test_stonith.py
|
|
@@ -293,9 +293,9 @@ class StonithTest(TestCase, AssertPcsMixin):
|
|
No alerts defined
|
|
|
|
Resources Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
Operations Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
|
|
Cluster Properties:
|
|
|
|
@@ -1305,9 +1305,9 @@ class LevelConfig(LevelTestsBase):
|
|
No alerts defined
|
|
|
|
Resources Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
Operations Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
|
|
Cluster Properties:
|
|
|
|
diff --git a/pcs_test/tier1/test_cib_options.py b/pcs_test/tier1/test_cib_options.py
|
|
new file mode 100644
|
|
index 00000000..ba8f3515
|
|
--- /dev/null
|
|
+++ b/pcs_test/tier1/test_cib_options.py
|
|
@@ -0,0 +1,571 @@
|
|
+from textwrap import dedent
|
|
+from unittest import TestCase
|
|
+
|
|
+from lxml import etree
|
|
+
|
|
+from pcs_test.tools.assertions import AssertPcsMixin
|
|
+from pcs_test.tools.cib import get_assert_pcs_effect_mixin
|
|
+from pcs_test.tools.misc import (
|
|
+ get_test_resource as rc,
|
|
+ get_tmp_file,
|
|
+ skip_unless_pacemaker_supports_rsc_and_op_rules,
|
|
+ write_data_to_tmpfile,
|
|
+ write_file_to_tmpfile,
|
|
+)
|
|
+from pcs_test.tools.pcs_runner import PcsRunner
|
|
+from pcs_test.tools.xml import XmlManipulation
|
|
+
|
|
+
|
|
+empty_cib = rc("cib-empty-2.0.xml")
|
|
+empty_cib_rules = rc("cib-empty-3.4.xml")
|
|
+
|
|
+
|
|
+class TestDefaultsMixin:
|
|
+ def setUp(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.temp_cib = get_tmp_file("tier1_cib_options")
|
|
+ self.pcs_runner = PcsRunner(self.temp_cib.name)
|
|
+
|
|
+ def tearDown(self):
|
|
+ # pylint: disable=invalid-name
|
|
+ self.temp_cib.close()
|
|
+
|
|
+
|
|
+class DefaultsConfigMixin(TestDefaultsMixin, AssertPcsMixin):
|
|
+ cli_command = ""
|
|
+ prefix = ""
|
|
+
|
|
+ def test_success(self):
|
|
+ xml_rsc = """
|
|
+ <rsc_defaults>
|
|
+ <meta_attributes id="rsc-set1" score="10">
|
|
+ <nvpair id="rsc-set1-nv1" name="name1" value="rsc1"/>
|
|
+ <nvpair id="rsc-set1-nv2" name="name2" value="rsc2"/>
|
|
+ </meta_attributes>
|
|
+ <meta_attributes id="rsc-setA">
|
|
+ <nvpair id="rsc-setA-nv1" name="name1" value="rscA"/>
|
|
+ <nvpair id="rsc-setA-nv2" name="name2" value="rscB"/>
|
|
+ </meta_attributes>
|
|
+ </rsc_defaults>
|
|
+ """
|
|
+ xml_op = """
|
|
+ <op_defaults>
|
|
+ <meta_attributes id="op-set1" score="10">
|
|
+ <nvpair id="op-set1-nv1" name="name1" value="op1"/>
|
|
+ <nvpair id="op-set1-nv2" name="name2" value="op2"/>
|
|
+ </meta_attributes>
|
|
+ <meta_attributes id="op-setA">
|
|
+ <nvpair id="op-setA-nv1" name="name1" value="opA"/>
|
|
+ <nvpair id="op-setA-nv2" name="name2" value="opB"/>
|
|
+ </meta_attributes>
|
|
+ </op_defaults>
|
|
+ """
|
|
+ xml_manip = XmlManipulation.from_file(empty_cib)
|
|
+ xml_manip.append_to_first_tag_name("configuration", xml_rsc, xml_op)
|
|
+ write_data_to_tmpfile(str(xml_manip), self.temp_cib)
|
|
+
|
|
+ self.assert_pcs_success(
|
|
+ self.cli_command,
|
|
+ stdout_full=dedent(
|
|
+ f"""\
|
|
+ Meta Attrs: {self.prefix}-set1 score=10
|
|
+ name1={self.prefix}1
|
|
+ name2={self.prefix}2
|
|
+ Meta Attrs: {self.prefix}-setA
|
|
+ name1={self.prefix}A
|
|
+ name2={self.prefix}B
|
|
+ """
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class RscDefaultsConfig(
|
|
+ DefaultsConfigMixin, TestCase,
|
|
+):
|
|
+ cli_command = "resource defaults"
|
|
+ prefix = "rsc"
|
|
+
|
|
+ @skip_unless_pacemaker_supports_rsc_and_op_rules()
|
|
+ def test_success_rules(self):
|
|
+ xml = """
|
|
+ <rsc_defaults>
|
|
+ <meta_attributes id="X">
|
|
+ <rule id="X-rule" boolean-op="and" score="INFINITY">
|
|
+ <rsc_expression id="X-rule-rsc-Dummy" type="Dummy"/>
|
|
+ </rule>
|
|
+ <nvpair id="X-nam1" name="nam1" value="val1"/>
|
|
+ </meta_attributes>
|
|
+ </rsc_defaults>
|
|
+ """
|
|
+ xml_manip = XmlManipulation.from_file(empty_cib_rules)
|
|
+ xml_manip.append_to_first_tag_name("configuration", xml)
|
|
+ write_data_to_tmpfile(str(xml_manip), self.temp_cib)
|
|
+
|
|
+ self.assert_pcs_success(
|
|
+ self.cli_command,
|
|
+ stdout_full=dedent(
|
|
+ """\
|
|
+ Meta Attrs: X
|
|
+ nam1=val1
|
|
+ Rule: boolean-op=and score=INFINITY
|
|
+ Expression: resource ::Dummy
|
|
+ """
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class OpDefaultsConfig(
|
|
+ DefaultsConfigMixin, TestCase,
|
|
+):
|
|
+ cli_command = "resource op defaults"
|
|
+ prefix = "op"
|
|
+
|
|
+ @skip_unless_pacemaker_supports_rsc_and_op_rules()
|
|
+ def test_success_rules(self):
|
|
+ xml = """
|
|
+ <op_defaults>
|
|
+ <meta_attributes id="X">
|
|
+ <rule id="X-rule" boolean-op="and" score="INFINITY">
|
|
+ <rsc_expression id="X-rule-rsc-Dummy" type="Dummy"/>
|
|
+ <op_expression id="X-rule-op-monitor" name="monitor"/>
|
|
+ </rule>
|
|
+ <nvpair id="X-nam1" name="nam1" value="val1"/>
|
|
+ </meta_attributes>
|
|
+ </op_defaults>
|
|
+ """
|
|
+ xml_manip = XmlManipulation.from_file(empty_cib_rules)
|
|
+ xml_manip.append_to_first_tag_name("configuration", xml)
|
|
+ write_data_to_tmpfile(str(xml_manip), self.temp_cib)
|
|
+
|
|
+ self.assert_pcs_success(
|
|
+ self.cli_command,
|
|
+ stdout_full=dedent(
|
|
+ """\
|
|
+ Meta Attrs: X
|
|
+ nam1=val1
|
|
+ Rule: boolean-op=and score=INFINITY
|
|
+ Expression: resource ::Dummy
|
|
+ Expression: op monitor
|
|
+ """
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class DefaultsSetCreateMixin(TestDefaultsMixin):
|
|
+ cli_command = ""
|
|
+ cib_tag = ""
|
|
+
|
|
+ def setUp(self):
|
|
+ super().setUp()
|
|
+ write_file_to_tmpfile(empty_cib, self.temp_cib)
|
|
+
|
|
+ def test_no_args(self):
|
|
+ self.assert_effect(
|
|
+ f"{self.cli_command} set create",
|
|
+ dedent(
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="{self.cib_tag}-meta_attributes"/>
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ ),
|
|
+ output=(
|
|
+ "Warning: Defaults do not apply to resources which override "
|
|
+ "them with their own defined values\n"
|
|
+ ),
|
|
+ )
|
|
+
|
|
+ def test_success(self):
|
|
+ self.assert_effect(
|
|
+ (
|
|
+ f"{self.cli_command} set create id=mine score=10 "
|
|
+ "meta nam1=val1 nam2=val2 --force"
|
|
+ ),
|
|
+ dedent(
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="mine" score="10">
|
|
+ <nvpair id="mine-nam1" name="nam1" value="val1"/>
|
|
+ <nvpair id="mine-nam2" name="nam2" value="val2"/>
|
|
+ </meta_attributes>
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ ),
|
|
+ output=(
|
|
+ "Warning: Defaults do not apply to resources which override "
|
|
+ "them with their own defined values\n"
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class RscDefaultsSetCreate(
|
|
+ get_assert_pcs_effect_mixin(
|
|
+ lambda cib: etree.tostring(
|
|
+ # pylint:disable=undefined-variable
|
|
+ etree.parse(cib).findall(".//rsc_defaults")[0]
|
|
+ )
|
|
+ ),
|
|
+ DefaultsSetCreateMixin,
|
|
+ TestCase,
|
|
+):
|
|
+ cli_command = "resource defaults"
|
|
+ cib_tag = "rsc_defaults"
|
|
+
|
|
+ @skip_unless_pacemaker_supports_rsc_and_op_rules()
|
|
+ def test_success_rules(self):
|
|
+ self.assert_effect(
|
|
+ (
|
|
+ f"{self.cli_command} set create id=X meta nam1=val1 "
|
|
+ "rule resource ::Dummy"
|
|
+ ),
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="X">
|
|
+ <rule id="X-rule" boolean-op="and" score="INFINITY">
|
|
+ <rsc_expression id="X-rule-rsc-Dummy" type="Dummy"/>
|
|
+ </rule>
|
|
+ <nvpair id="X-nam1" name="nam1" value="val1"/>
|
|
+ </meta_attributes>
|
|
+ </{self.cib_tag}>
|
|
+ """,
|
|
+ output=(
|
|
+ "CIB has been upgraded to the latest schema version.\n"
|
|
+ "Warning: Defaults do not apply to resources which override "
|
|
+ "them with their own defined values\n"
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class OpDefaultsSetCreate(
|
|
+ get_assert_pcs_effect_mixin(
|
|
+ lambda cib: etree.tostring(
|
|
+ # pylint:disable=undefined-variable
|
|
+ etree.parse(cib).findall(".//op_defaults")[0]
|
|
+ )
|
|
+ ),
|
|
+ DefaultsSetCreateMixin,
|
|
+ TestCase,
|
|
+):
|
|
+ cli_command = "resource op defaults"
|
|
+ cib_tag = "op_defaults"
|
|
+
|
|
+ @skip_unless_pacemaker_supports_rsc_and_op_rules()
|
|
+ def test_success_rules(self):
|
|
+ self.assert_effect(
|
|
+ (
|
|
+ f"{self.cli_command} set create id=X meta nam1=val1 "
|
|
+ "rule resource ::Dummy and op monitor"
|
|
+ ),
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="X">
|
|
+ <rule id="X-rule" boolean-op="and" score="INFINITY">
|
|
+ <rsc_expression id="X-rule-rsc-Dummy" type="Dummy"/>
|
|
+ <op_expression id="X-rule-op-monitor" name="monitor"/>
|
|
+ </rule>
|
|
+ <nvpair id="X-nam1" name="nam1" value="val1"/>
|
|
+ </meta_attributes>
|
|
+ </{self.cib_tag}>
|
|
+ """,
|
|
+ output=(
|
|
+ "CIB has been upgraded to the latest schema version.\n"
|
|
+ "Warning: Defaults do not apply to resources which override "
|
|
+ "them with their own defined values\n"
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class DefaultsSetDeleteMixin(TestDefaultsMixin, AssertPcsMixin):
|
|
+ cli_command = ""
|
|
+ prefix = ""
|
|
+ cib_tag = ""
|
|
+
|
|
+ def setUp(self):
|
|
+ super().setUp()
|
|
+ xml_rsc = """
|
|
+ <rsc_defaults>
|
|
+ <meta_attributes id="rsc-set1" />
|
|
+ <meta_attributes id="rsc-set2" />
|
|
+ <meta_attributes id="rsc-set3" />
|
|
+ <meta_attributes id="rsc-set4" />
|
|
+ </rsc_defaults>
|
|
+ """
|
|
+ xml_op = """
|
|
+ <op_defaults>
|
|
+ <meta_attributes id="op-set1" />
|
|
+ <meta_attributes id="op-set2" />
|
|
+ <meta_attributes id="op-set3" />
|
|
+ <meta_attributes id="op-set4" />
|
|
+ </op_defaults>
|
|
+ """
|
|
+ xml_manip = XmlManipulation.from_file(empty_cib)
|
|
+ xml_manip.append_to_first_tag_name("configuration", xml_rsc, xml_op)
|
|
+ write_data_to_tmpfile(str(xml_manip), self.temp_cib)
|
|
+
|
|
+ def test_success(self):
|
|
+ self.assert_effect(
|
|
+ [
|
|
+ f"{self.cli_command} set delete {self.prefix}-set1 "
|
|
+ f"{self.prefix}-set3",
|
|
+ f"{self.cli_command} set remove {self.prefix}-set1 "
|
|
+ f"{self.prefix}-set3",
|
|
+ ],
|
|
+ dedent(
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="{self.prefix}-set2" />
|
|
+ <meta_attributes id="{self.prefix}-set4" />
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+class RscDefaultsSetDelete(
|
|
+ get_assert_pcs_effect_mixin(
|
|
+ lambda cib: etree.tostring(
|
|
+ # pylint:disable=undefined-variable
|
|
+ etree.parse(cib).findall(".//rsc_defaults")[0]
|
|
+ )
|
|
+ ),
|
|
+ DefaultsSetDeleteMixin,
|
|
+ TestCase,
|
|
+):
|
|
+ cli_command = "resource defaults"
|
|
+ prefix = "rsc"
|
|
+ cib_tag = "rsc_defaults"
|
|
+
|
|
+
|
|
+class OpDefaultsSetDelete(
|
|
+ get_assert_pcs_effect_mixin(
|
|
+ lambda cib: etree.tostring(
|
|
+ # pylint:disable=undefined-variable
|
|
+ etree.parse(cib).findall(".//op_defaults")[0]
|
|
+ )
|
|
+ ),
|
|
+ DefaultsSetDeleteMixin,
|
|
+ TestCase,
|
|
+):
|
|
+ cli_command = "resource op defaults"
|
|
+ prefix = "op"
|
|
+ cib_tag = "op_defaults"
|
|
+
|
|
+
|
|
+class DefaultsSetUpdateMixin(TestDefaultsMixin, AssertPcsMixin):
|
|
+ cli_command = ""
|
|
+ prefix = ""
|
|
+ cib_tag = ""
|
|
+
|
|
+ def test_success(self):
|
|
+ xml = f"""
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="my-set">
|
|
+ <nvpair id="my-set-name1" name="name1" value="value1" />
|
|
+ <nvpair id="my-set-name2" name="name2" value="value2" />
|
|
+ <nvpair id="my-set-name3" name="name3" value="value3" />
|
|
+ </meta_attributes>
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ xml_manip = XmlManipulation.from_file(empty_cib)
|
|
+ xml_manip.append_to_first_tag_name("configuration", xml)
|
|
+ write_data_to_tmpfile(str(xml_manip), self.temp_cib)
|
|
+ warnings = (
|
|
+ "Warning: Defaults do not apply to resources which override "
|
|
+ "them with their own defined values\n"
|
|
+ )
|
|
+
|
|
+ self.assert_effect(
|
|
+ f"{self.cli_command} set update my-set meta name2=value2A name3=",
|
|
+ dedent(
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="my-set">
|
|
+ <nvpair id="my-set-name1" name="name1" value="value1" />
|
|
+ <nvpair id="my-set-name2" name="name2" value="value2A" />
|
|
+ </meta_attributes>
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ ),
|
|
+ output=warnings,
|
|
+ )
|
|
+
|
|
+ self.assert_effect(
|
|
+ f"{self.cli_command} set update my-set meta name1= name2=",
|
|
+ dedent(
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="my-set" />
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ ),
|
|
+ output=warnings,
|
|
+ )
|
|
+
|
|
+
|
|
+class RscDefaultsSetUpdate(
|
|
+ get_assert_pcs_effect_mixin(
|
|
+ lambda cib: etree.tostring(
|
|
+ # pylint:disable=undefined-variable
|
|
+ etree.parse(cib).findall(".//rsc_defaults")[0]
|
|
+ )
|
|
+ ),
|
|
+ DefaultsSetUpdateMixin,
|
|
+ TestCase,
|
|
+):
|
|
+ cli_command = "resource defaults"
|
|
+ prefix = "rsc"
|
|
+ cib_tag = "rsc_defaults"
|
|
+
|
|
+
|
|
+class OpDefaultsSetUpdate(
|
|
+ get_assert_pcs_effect_mixin(
|
|
+ lambda cib: etree.tostring(
|
|
+ # pylint:disable=undefined-variable
|
|
+ etree.parse(cib).findall(".//op_defaults")[0]
|
|
+ )
|
|
+ ),
|
|
+ DefaultsSetUpdateMixin,
|
|
+ TestCase,
|
|
+):
|
|
+ cli_command = "resource op defaults"
|
|
+ prefix = "op"
|
|
+ cib_tag = "op_defaults"
|
|
+
|
|
+
|
|
+class DefaultsSetUsageMixin(TestDefaultsMixin, AssertPcsMixin):
|
|
+ cli_command = ""
|
|
+
|
|
+ def test_no_args(self):
|
|
+ self.assert_pcs_fail(
|
|
+ f"{self.cli_command} set",
|
|
+ stdout_start=f"\nUsage: pcs {self.cli_command} set...\n",
|
|
+ )
|
|
+
|
|
+ def test_bad_command(self):
|
|
+ self.assert_pcs_fail(
|
|
+ f"{self.cli_command} set bad-command",
|
|
+ stdout_start=f"\nUsage: pcs {self.cli_command} set ...\n",
|
|
+ )
|
|
+
|
|
+
|
|
+class RscDefaultsSetUsage(
|
|
+ DefaultsSetUsageMixin, TestCase,
|
|
+):
|
|
+ cli_command = "resource defaults"
|
|
+
|
|
+
|
|
+class OpDefaultsSetUsage(
|
|
+ DefaultsSetUsageMixin, TestCase,
|
|
+):
|
|
+ cli_command = "resource op defaults"
|
|
+
|
|
+
|
|
+class DefaultsUpdateMixin(TestDefaultsMixin, AssertPcsMixin):
|
|
+ cli_command = ""
|
|
+ prefix = ""
|
|
+ cib_tag = ""
|
|
+
|
|
+ def assert_success_legacy(self, update_keyword):
|
|
+ write_file_to_tmpfile(empty_cib, self.temp_cib)
|
|
+ warning_lines = []
|
|
+ if not update_keyword:
|
|
+ warning_lines.append(
|
|
+ "Warning: This command is deprecated and will be removed. "
|
|
+ f"Please use 'pcs {self.cli_command} update' instead.\n"
|
|
+ )
|
|
+ warning_lines.append(
|
|
+ "Warning: Defaults do not apply to resources which override "
|
|
+ "them with their own defined values\n"
|
|
+ )
|
|
+ warnings = "".join(warning_lines)
|
|
+
|
|
+ update = "update" if update_keyword else ""
|
|
+
|
|
+ self.assert_effect(
|
|
+ f"{self.cli_command} {update} name1=value1 name2=value2 name3=value3",
|
|
+ dedent(
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="{self.cib_tag}-meta_attributes">
|
|
+ <nvpair id="{self.cib_tag}-meta_attributes-name1"
|
|
+ name="name1" value="value1"
|
|
+ />
|
|
+ <nvpair id="{self.cib_tag}-meta_attributes-name2"
|
|
+ name="name2" value="value2"
|
|
+ />
|
|
+ <nvpair id="{self.cib_tag}-meta_attributes-name3"
|
|
+ name="name3" value="value3"
|
|
+ />
|
|
+ </meta_attributes>
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ ),
|
|
+ output=warnings,
|
|
+ )
|
|
+
|
|
+ self.assert_effect(
|
|
+ f"{self.cli_command} {update} name2=value2A name3=",
|
|
+ dedent(
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="{self.cib_tag}-meta_attributes">
|
|
+ <nvpair id="{self.cib_tag}-meta_attributes-name1"
|
|
+ name="name1" value="value1"
|
|
+ />
|
|
+ <nvpair id="{self.cib_tag}-meta_attributes-name2"
|
|
+ name="name2" value="value2A"
|
|
+ />
|
|
+ </meta_attributes>
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ ),
|
|
+ output=warnings,
|
|
+ )
|
|
+
|
|
+ self.assert_effect(
|
|
+ f"{self.cli_command} {update} name1= name2=",
|
|
+ dedent(
|
|
+ f"""\
|
|
+ <{self.cib_tag}>
|
|
+ <meta_attributes id="{self.cib_tag}-meta_attributes" />
|
|
+ </{self.cib_tag}>
|
|
+ """
|
|
+ ),
|
|
+ output=warnings,
|
|
+ )
|
|
+
|
|
+ def test_deprecated(self):
|
|
+ self.assert_success_legacy(False)
|
|
+
|
|
+ def test_legacy(self):
|
|
+ self.assert_success_legacy(True)
|
|
+
|
|
+
|
|
+class RscDefaultsUpdate(
|
|
+ get_assert_pcs_effect_mixin(
|
|
+ lambda cib: etree.tostring(
|
|
+ # pylint:disable=undefined-variable
|
|
+ etree.parse(cib).findall(".//rsc_defaults")[0]
|
|
+ )
|
|
+ ),
|
|
+ DefaultsUpdateMixin,
|
|
+ TestCase,
|
|
+):
|
|
+ cli_command = "resource defaults"
|
|
+ prefix = "rsc"
|
|
+ cib_tag = "rsc_defaults"
|
|
+
|
|
+
|
|
+class OpDefaultsUpdate(
|
|
+ get_assert_pcs_effect_mixin(
|
|
+ lambda cib: etree.tostring(
|
|
+ # pylint:disable=undefined-variable
|
|
+ etree.parse(cib).findall(".//op_defaults")[0]
|
|
+ )
|
|
+ ),
|
|
+ DefaultsUpdateMixin,
|
|
+ TestCase,
|
|
+):
|
|
+ cli_command = "resource op defaults"
|
|
+ prefix = "op"
|
|
+ cib_tag = "op_defaults"
|
|
diff --git a/pcs_test/tier1/test_tag.py b/pcs_test/tier1/test_tag.py
|
|
index d28d3ae5..8057476a 100644
|
|
--- a/pcs_test/tier1/test_tag.py
|
|
+++ b/pcs_test/tier1/test_tag.py
|
|
@@ -246,9 +246,9 @@ class PcsConfigTagsTest(TestTagMixin, TestCase):
|
|
No alerts defined
|
|
|
|
Resources Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
Operations Defaults:
|
|
- No defaults set
|
|
+ No defaults set
|
|
|
|
Cluster Properties:
|
|
{tags}
|
|
diff --git a/pcs_test/tools/fixture.py b/pcs_test/tools/fixture.py
|
|
index a460acc7..6480617e 100644
|
|
--- a/pcs_test/tools/fixture.py
|
|
+++ b/pcs_test/tools/fixture.py
|
|
@@ -245,14 +245,14 @@ def report_resource_running(resource, roles, severity=severities.INFO):
|
|
)
|
|
|
|
|
|
-def report_unexpected_element(element_id, elemet_type, expected_types):
|
|
+def report_unexpected_element(element_id, element_type, expected_types):
|
|
return (
|
|
severities.ERROR,
|
|
report_codes.ID_BELONGS_TO_UNEXPECTED_TYPE,
|
|
{
|
|
"id": element_id,
|
|
"expected_types": expected_types,
|
|
- "current_type": elemet_type,
|
|
+ "current_type": element_type,
|
|
},
|
|
None,
|
|
)
|
|
diff --git a/pcs_test/tools/misc.py b/pcs_test/tools/misc.py
|
|
index f481a267..33d78002 100644
|
|
--- a/pcs_test/tools/misc.py
|
|
+++ b/pcs_test/tools/misc.py
|
|
@@ -5,6 +5,8 @@ import re
|
|
import tempfile
|
|
from unittest import mock, skipUnless
|
|
|
|
+from lxml import etree
|
|
+
|
|
from pcs_test.tools.custom_mock import MockLibraryReportProcessor
|
|
|
|
from pcs import settings
|
|
@@ -128,12 +130,12 @@ def compare_version(a, b):
|
|
|
|
def is_minimum_pacemaker_version(major, minor, rev):
|
|
return is_version_sufficient(
|
|
- get_current_pacemaker_version(), (major, minor, rev)
|
|
+ _get_current_pacemaker_version(), (major, minor, rev)
|
|
)
|
|
|
|
|
|
@lru_cache()
|
|
-def get_current_pacemaker_version():
|
|
+def _get_current_pacemaker_version():
|
|
output, dummy_stderr, dummy_retval = runner.run(
|
|
[os.path.join(settings.pacemaker_binaries, "crm_mon"), "--version",]
|
|
)
|
|
@@ -146,6 +148,26 @@ def get_current_pacemaker_version():
|
|
return major, minor, rev
|
|
|
|
|
|
+@lru_cache()
|
|
+def _get_current_cib_schema_version():
|
|
+ regexp = re.compile(r"pacemaker-((\d+)\.(\d+))")
|
|
+ all_versions = set()
|
|
+ xml = etree.parse("/usr/share/pacemaker/versions.rng").getroot()
|
|
+ for value_el in xml.xpath(
|
|
+ ".//x:attribute[@name='validate-with']//x:value",
|
|
+ namespaces={"x": "http://relaxng.org/ns/structure/1.0"},
|
|
+ ):
|
|
+ match = re.match(regexp, value_el.text)
|
|
+ if match:
|
|
+ all_versions.add((int(match.group(2)), int(match.group(3))))
|
|
+ return sorted(all_versions)[-1]
|
|
+
|
|
+
|
|
+def _is_minimum_cib_schema_version(cmajor, cminor, crev):
|
|
+ major, minor = _get_current_cib_schema_version()
|
|
+ return compare_version((major, minor, 0), (cmajor, cminor, crev)) > -1
|
|
+
|
|
+
|
|
def is_version_sufficient(current_version, minimal_version):
|
|
return compare_version(current_version, minimal_version) > -1
|
|
|
|
@@ -174,7 +196,7 @@ def _get_current_pacemaker_features():
|
|
|
|
|
|
def skip_unless_pacemaker_version(version_tuple, feature):
|
|
- current_version = get_current_pacemaker_version()
|
|
+ current_version = _get_current_pacemaker_version()
|
|
return skipUnless(
|
|
is_version_sufficient(current_version, version_tuple),
|
|
(
|
|
@@ -188,12 +210,6 @@ def skip_unless_pacemaker_version(version_tuple, feature):
|
|
)
|
|
|
|
|
|
-def skip_unless_crm_rule():
|
|
- return skip_unless_pacemaker_version(
|
|
- (2, 0, 2), "listing of constraints that might be expired"
|
|
- )
|
|
-
|
|
-
|
|
def skip_unless_pacemaker_features(version_tuple, feature):
|
|
return skipUnless(
|
|
is_minimum_pacemaker_features(*version_tuple),
|
|
@@ -204,12 +220,39 @@ def skip_unless_pacemaker_features(version_tuple, feature):
|
|
)
|
|
|
|
|
|
+def skip_unless_cib_schema_version(version_tuple, feature):
|
|
+ current_version = _get_current_cib_schema_version()
|
|
+ return skipUnless(
|
|
+ _is_minimum_cib_schema_version(*version_tuple),
|
|
+ (
|
|
+ "Pacemaker supported CIB schema version is too low (current: "
|
|
+ "{current_version}, must be >= {minimal_version}) to test {feature}"
|
|
+ ).format(
|
|
+ current_version=format_version(current_version),
|
|
+ minimal_version=format_version(version_tuple),
|
|
+ feature=feature,
|
|
+ ),
|
|
+ )
|
|
+
|
|
+
|
|
+def skip_unless_crm_rule():
|
|
+ return skip_unless_pacemaker_version(
|
|
+ (2, 0, 2), "listing of constraints that might be expired"
|
|
+ )
|
|
+
|
|
+
|
|
def skip_unless_pacemaker_supports_bundle():
|
|
return skip_unless_pacemaker_features(
|
|
(3, 1, 0), "bundle resources with promoted-max attribute"
|
|
)
|
|
|
|
|
|
+def skip_unless_pacemaker_supports_rsc_and_op_rules():
|
|
+ return skip_unless_cib_schema_version(
|
|
+ (3, 4, 0), "rsc_expression and op_expression elements in rule elements"
|
|
+ )
|
|
+
|
|
+
|
|
def skip_if_service_enabled(service_name):
|
|
return skipUnless(
|
|
not is_service_enabled(runner, service_name),
|
|
diff --git a/pcsd/capabilities.xml b/pcsd/capabilities.xml
|
|
index daf23e5a..6e1886cb 100644
|
|
--- a/pcsd/capabilities.xml
|
|
+++ b/pcsd/capabilities.xml
|
|
@@ -964,6 +964,21 @@
|
|
pcs commands: resource op defaults
|
|
</description>
|
|
</capability>
|
|
+ <capability id="pcmk.properties.operation-defaults.multiple" in-pcs="1" in-pcsd="0">
|
|
+ <description>
|
|
+ Support for managing multiple sets of resource operations defaults.
|
|
+
|
|
+ pcs commands: resource op defaults set create | delete | remove | update
|
|
+ </description>
|
|
+ </capability>
|
|
+ <capability id="pcmk.properties.operation-defaults.rule-rsc-op" in-pcs="1" in-pcsd="0">
|
|
+ <description>
|
|
+ Support for rules with 'resource' and 'op' expressions in sets of
|
|
+ resource operations defaults.
|
|
+
|
|
+ pcs commands: resource op defaults set create
|
|
+ </description>
|
|
+ </capability>
|
|
<capability id="pcmk.properties.resource-defaults" in-pcs="1" in-pcsd="0">
|
|
<description>
|
|
Show and set resources defaults, can set multiple defaults at once.
|
|
@@ -971,6 +986,21 @@
|
|
pcs commands: resource defaults
|
|
</description>
|
|
</capability>
|
|
+ <capability id="pcmk.properties.resource-defaults.multiple" in-pcs="1" in-pcsd="0">
|
|
+ <description>
|
|
+ Support for managing multiple sets of resources defaults.
|
|
+
|
|
+ pcs commands: resource defaults set create | delete | remove | update
|
|
+ </description>
|
|
+ </capability>
|
|
+ <capability id="pcmk.properties.resource-defaults.rule-rsc-op" in-pcs="1" in-pcsd="0">
|
|
+ <description>
|
|
+ Support for rules with 'resource' and 'op' expressions in sets of
|
|
+ resources defaults.
|
|
+
|
|
+ pcs commands: resource defaults set create
|
|
+ </description>
|
|
+ </capability>
|
|
|
|
|
|
|
|
diff --git a/test/centos8/Dockerfile b/test/centos8/Dockerfile
|
|
index bcdfadef..753f0ca7 100644
|
|
--- a/test/centos8/Dockerfile
|
|
+++ b/test/centos8/Dockerfile
|
|
@@ -12,6 +12,7 @@ RUN dnf install -y \
|
|
python3-pip \
|
|
python3-pycurl \
|
|
python3-pyOpenSSL \
|
|
+ python3-pyparsing \
|
|
# ruby
|
|
ruby \
|
|
ruby-devel \
|
|
diff --git a/test/fedora30/Dockerfile b/test/fedora30/Dockerfile
|
|
index 60aad892..7edbfe5b 100644
|
|
--- a/test/fedora30/Dockerfile
|
|
+++ b/test/fedora30/Dockerfile
|
|
@@ -9,6 +9,7 @@ RUN dnf install -y \
|
|
python3-mock \
|
|
python3-pycurl \
|
|
python3-pyOpenSSL \
|
|
+ python3-pyparsing \
|
|
# ruby
|
|
ruby \
|
|
ruby-devel \
|
|
diff --git a/test/fedora31/Dockerfile b/test/fedora31/Dockerfile
|
|
index eb24bb1c..6750e222 100644
|
|
--- a/test/fedora31/Dockerfile
|
|
+++ b/test/fedora31/Dockerfile
|
|
@@ -10,6 +10,7 @@ RUN dnf install -y \
|
|
python3-pip \
|
|
python3-pycurl \
|
|
python3-pyOpenSSL \
|
|
+ python3-pyparsing \
|
|
# ruby
|
|
ruby \
|
|
ruby-devel \
|
|
diff --git a/test/fedora32/Dockerfile b/test/fedora32/Dockerfile
|
|
index 61a0a439..c6cc2146 100644
|
|
--- a/test/fedora32/Dockerfile
|
|
+++ b/test/fedora32/Dockerfile
|
|
@@ -11,6 +11,7 @@ RUN dnf install -y \
|
|
python3-pip \
|
|
python3-pycurl \
|
|
python3-pyOpenSSL \
|
|
+ python3-pyparsing \
|
|
# ruby
|
|
ruby \
|
|
ruby-devel \
|
|
--
|
|
2.25.4
|
|
|