From cb31390fb04623250db814d08e84a0a1981916c7 Mon Sep 17 00:00:00 2001 From: Tomas Jelinek Date: Mon, 10 Jul 2023 14:40:07 +0200 Subject: [PATCH 1/3] fix exporting location constraints with rules --- pcs/cli/constraint/output/all.py | 62 ++++---- pcs/cli/constraint/output/colocation.py | 46 ++++-- pcs/cli/constraint/output/location.py | 20 ++- pcs/cli/constraint/output/order.py | 15 +- pcs/cli/constraint/output/set.py | 12 +- pcs/cli/constraint/output/ticket.py | 9 +- pcs/lib/cib/rule/cib_to_str.py | 21 ++- pcs/rule.py | 14 ++ pcs_test/Makefile.am | 2 + pcs_test/resources/cib-all.xml | 8 +- .../cib-rule-with-spaces-in-date.xml | 44 ++++++ .../cib-unexportable-constraints.xml | 109 +++++++++++++ pcs_test/resources/constraint-commands | 7 +- pcs_test/resources/resource-commands | 7 +- .../tier0/lib/cib/rule/test_cib_to_str.py | 93 +++++++++++ pcs_test/tier0/lib/cib/rule/test_parser.py | 58 +++++++ pcs_test/tier1/constraint/test_config.py | 147 ++++++++++++++++-- pcs_test/tier1/legacy/test_constraints.py | 111 +++++++++++++ pcs_test/tier1/legacy/test_rule.py | 24 +++ pcs_test/tools/constraints_dto.py | 44 +++++- 20 files changed, 755 insertions(+), 98 deletions(-) create mode 100644 pcs_test/resources/cib-rule-with-spaces-in-date.xml create mode 100644 pcs_test/resources/cib-unexportable-constraints.xml diff --git a/pcs/cli/constraint/output/all.py b/pcs/cli/constraint/output/all.py index 829584bb..a173507c 100644 --- a/pcs/cli/constraint/output/all.py +++ b/pcs/cli/constraint/output/all.py @@ -47,38 +47,44 @@ def constraints_to_cmd(constraints_dto: CibConstraintsDto) -> list[list[str]]: for location_set_dto in constraints_dto.location_set: warn( "Location set constraint with id " - f"'{location_set_dto.attributes.constraint_id}'configured but it's " - "not supported by this command" + f"'{location_set_dto.attributes.constraint_id}' configured but it's " + "not supported by this command." + " Command for creating the constraint is omitted." ) location_cmds = [] for location_dto in constraints_dto.location: location_cmds.extend(location.plain_constraint_to_cmd(location_dto)) - return ( - location_cmds - + [ - colocation.plain_constraint_to_cmd(colocation_dto) - for colocation_dto in constraints_dto.colocation - ] - + [ - colocation.set_constraint_to_cmd(colocation_set_dto) - for colocation_set_dto in constraints_dto.colocation_set - ] - + [ - order.plain_constraint_to_cmd(order_dto) - for order_dto in constraints_dto.order - ] - + [ - order.set_constraint_to_cmd(order_set_dto) - for order_set_dto in constraints_dto.order_set - ] - + [ - ticket.plain_constraint_to_cmd(ticket_dto) - for ticket_dto in constraints_dto.ticket - ] - + [ - ticket.set_constraint_to_cmd(ticket_set_dto) - for ticket_set_dto in constraints_dto.ticket_set - ] + return list( + filter( + None, + ( + location_cmds + + [ + colocation.plain_constraint_to_cmd(colocation_dto) + for colocation_dto in constraints_dto.colocation + ] + + [ + colocation.set_constraint_to_cmd(colocation_set_dto) + for colocation_set_dto in constraints_dto.colocation_set + ] + + [ + order.plain_constraint_to_cmd(order_dto) + for order_dto in constraints_dto.order + ] + + [ + order.set_constraint_to_cmd(order_set_dto) + for order_set_dto in constraints_dto.order_set + ] + + [ + ticket.plain_constraint_to_cmd(ticket_dto) + for ticket_dto in constraints_dto.ticket + ] + + [ + ticket.set_constraint_to_cmd(ticket_set_dto) + for ticket_set_dto in constraints_dto.ticket_set + ] + ), + ) ) diff --git a/pcs/cli/constraint/output/colocation.py b/pcs/cli/constraint/output/colocation.py index 9c8db33b..7a13ed27 100644 --- a/pcs/cli/constraint/output/colocation.py +++ b/pcs/cli/constraint/output/colocation.py @@ -1,5 +1,8 @@ from shlex import quote -from typing import Iterable +from typing import ( + Iterable, + Optional, +) from pcs.cli.common.output import ( INDENT_STEP, @@ -120,13 +123,15 @@ def constraints_to_text( def _attributes_to_cmd_pairs( attributes_dto: CibConstraintColocationAttributesDto, filter_out: StringCollection = tuple(), -) -> list[tuple[str, str]]: +) -> Optional[list[tuple[str, str]]]: if attributes_dto.lifetime: warn( "Lifetime configuration detected in constraint " f"'{attributes_dto.constraint_id}' but not supported by this " "command." + " Command for creating the constraint is omitted." ) + return None unsupported_options = {"influence"} result = [] for pair in [("id", attributes_dto.constraint_id)] + _attributes_to_pairs( @@ -136,8 +141,10 @@ def _attributes_to_cmd_pairs( warn( f"Option '{pair[0]}' detected in constraint " f"'{attributes_dto.constraint_id}' but not supported by this " - "command" + "command." + " Command for creating the constraint is omitted." ) + return None if pair[0] in filter_out: continue result.append(pair) @@ -155,7 +162,17 @@ def plain_constraint_to_cmd( "Resource instance(s) detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." + " Command for creating the constraint is omitted." ) + return [] + if constraint_dto.node_attribute is not None: + warn( + "Option 'node_attribute' detected in constraint " + f"'{constraint_dto.attributes.constraint_id}' but not supported by " + "this command." + " Command for creating the constraint is omitted." + ) + return [] result = [ "pcs -- constraint colocation add {resource_role}{resource_id} with {with_resource_role}{with_resource_id}{score}".format( resource_role=format_optional(constraint_dto.resource_role), @@ -169,11 +186,12 @@ def plain_constraint_to_cmd( ), ) ] - params = pairs_to_cmd( - _attributes_to_cmd_pairs( - constraint_dto.attributes, filter_out=("score",) - ) + pairs = _attributes_to_cmd_pairs( + constraint_dto.attributes, filter_out=("score",) ) + if pairs is None: + return [] + params = pairs_to_cmd(pairs) if params: result.extend(indent([params], indent_step=INDENT_STEP)) return result @@ -184,12 +202,14 @@ def set_constraint_to_cmd( ) -> list[str]: result = ["pcs -- constraint colocation"] for resource_set in constraint_dto.resource_sets: - result.extend( - indent( - _set.resource_set_to_cmd(resource_set), indent_step=INDENT_STEP - ) - ) - params = pairs_to_cmd(_attributes_to_cmd_pairs(constraint_dto.attributes)) + set_cmd_part = _set.resource_set_to_cmd(resource_set) + if not set_cmd_part: + return [] + result.extend(indent(set_cmd_part, indent_step=INDENT_STEP)) + pairs = _attributes_to_cmd_pairs(constraint_dto.attributes) + if pairs is None: + return [] + params = pairs_to_cmd(pairs) if params: result.extend(indent([f"setoptions {params}"], indent_step=INDENT_STEP)) return result diff --git a/pcs/cli/constraint/output/location.py b/pcs/cli/constraint/output/location.py index 2713b7d0..25ac646a 100644 --- a/pcs/cli/constraint/output/location.py +++ b/pcs/cli/constraint/output/location.py @@ -1,5 +1,5 @@ +import shlex from collections import defaultdict -from shlex import quote from typing import ( Callable, Iterable, @@ -149,7 +149,7 @@ def _plain_constraint_get_resource_for_cmd( resource = f"resource%{constraint_dto.resource_id}" else: resource = f"regexp%{constraint_dto.resource_pattern}" - return quote(resource) + return shlex.quote(resource) def _plain_constraint_to_cmd( @@ -157,9 +157,9 @@ def _plain_constraint_to_cmd( ) -> list[str]: result = [ "pcs -- constraint location add {id} {resource} {node} {score}".format( - id=quote(constraint_dto.attributes.constraint_id), + id=shlex.quote(constraint_dto.attributes.constraint_id), resource=_plain_constraint_get_resource_for_cmd(constraint_dto), - node=quote(str(constraint_dto.attributes.node)), + node=shlex.quote(str(constraint_dto.attributes.node)), score=constraint_dto.attributes.score, ) ] @@ -185,12 +185,12 @@ def _rule_to_cmd_pairs(rule: CibRuleExpressionDto) -> list[tuple[str, str]]: def _add_rule_cmd(constraint_id: str, rule: CibRuleExpressionDto) -> list[str]: - result = [f"pcs -- constraint rule add {quote(constraint_id)}"] + result = [f"pcs -- constraint rule add {shlex.quote(constraint_id)}"] result.extend( indent( [ pairs_to_cmd([("id", rule.id)] + _rule_to_cmd_pairs(rule)), - rule.as_string, + shlex.join(shlex.split(rule.as_string)), ], indent_step=INDENT_STEP, ) @@ -221,7 +221,7 @@ def _plain_constraint_rule_to_cmd( + _attributes_to_pairs(constraint_dto.attributes) + _rule_to_cmd_pairs(first_rule) ), - first_rule.as_string, + shlex.join(shlex.split(first_rule.as_string)), ], indent_step=INDENT_STEP, ) @@ -240,13 +240,17 @@ def plain_constraint_to_cmd( "Lifetime configuration detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." + " Command for creating the constraint is omitted." ) + return [] if constraint_dto.role: warn( - f"Resource role '{constraint_dto.role}' detected in constraint " + f"Resource role detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." + " Command for creating the constraint is omitted." ) + return [] if constraint_dto.attributes.rules: return _plain_constraint_rule_to_cmd(constraint_dto) return [_plain_constraint_to_cmd(constraint_dto)] diff --git a/pcs/cli/constraint/output/order.py b/pcs/cli/constraint/output/order.py index f407270d..53fe546a 100644 --- a/pcs/cli/constraint/output/order.py +++ b/pcs/cli/constraint/output/order.py @@ -127,7 +127,9 @@ def plain_constraint_to_cmd( "Resource instance(s) detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." + " Command for creating the constraint is omitted." ) + return [] result = [ "pcs -- constraint order {first_action}{first_resource_id} then {then_action}{then_resource_id}".format( first_action=format_optional(constraint_dto.first_action), @@ -147,11 +149,10 @@ def set_constraint_to_cmd( ) -> list[str]: result = ["pcs -- constraint order"] for resource_set in constraint_dto.resource_sets: - result.extend( - indent( - _set.resource_set_to_cmd(resource_set), indent_step=INDENT_STEP - ) - ) + set_cmd_part = _set.resource_set_to_cmd(resource_set) + if not set_cmd_part: + return [] + result.extend(indent(set_cmd_part, indent_step=INDENT_STEP)) pairs = [] for pair in _attributes_to_cmd_pairs(constraint_dto.attributes): # this list is based on pcs.lib.cib.constraint.order.ATTRIB @@ -159,8 +160,10 @@ def set_constraint_to_cmd( warn( f"Option '{pair[0]}' detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not " - "supported by this command" + "supported by this command." + " Command for creating the constraint is omitted." ) + return [] pairs.append(pair) if pairs: result.extend( diff --git a/pcs/cli/constraint/output/set.py b/pcs/cli/constraint/output/set.py index 3b1fa31a..5395ebf7 100644 --- a/pcs/cli/constraint/output/set.py +++ b/pcs/cli/constraint/output/set.py @@ -1,4 +1,7 @@ -from typing import Sequence +from typing import ( + Optional, + Sequence, +) from pcs.cli.common.output import ( INDENT_STEP, @@ -81,7 +84,7 @@ def set_constraint_to_text( return result -def resource_set_to_cmd(resource_set: CibResourceSetDto) -> list[str]: +def resource_set_to_cmd(resource_set: CibResourceSetDto) -> Optional[list[str]]: filtered_pairs = [] for pair in _resource_set_options_to_pairs(resource_set): # this list is based on pcs.lib.cib.constraint.resource_set._ATTRIBUTES @@ -89,9 +92,10 @@ def resource_set_to_cmd(resource_set: CibResourceSetDto) -> list[str]: warn( f"Option '{pair[0]}' detected in resource set " f"'{resource_set.set_id}' but not " - "supported by this command" + "supported by this command." + " Command for creating the constraint is omitted." ) - continue + return None filtered_pairs.append(pair) return [ diff --git a/pcs/cli/constraint/output/ticket.py b/pcs/cli/constraint/output/ticket.py index e047226c..d83e65b8 100644 --- a/pcs/cli/constraint/output/ticket.py +++ b/pcs/cli/constraint/output/ticket.py @@ -121,11 +121,10 @@ def set_constraint_to_cmd( ) -> list[str]: result = ["pcs -- constraint ticket"] for resource_set in constraint_dto.resource_sets: - result.extend( - indent( - _set.resource_set_to_cmd(resource_set), indent_step=INDENT_STEP - ) - ) + set_cmd_part = _set.resource_set_to_cmd(resource_set) + if not set_cmd_part: + return [] + result.extend(indent(set_cmd_part, indent_step=INDENT_STEP)) params = pairs_to_cmd( _attributes_to_cmd_pairs(constraint_dto.attributes) + [("ticket", constraint_dto.attributes.ticket)] diff --git a/pcs/lib/cib/rule/cib_to_str.py b/pcs/lib/cib/rule/cib_to_str.py index b196d8f6..29b67a8a 100644 --- a/pcs/lib/cib/rule/cib_to_str.py +++ b/pcs/lib/cib/rule/cib_to_str.py @@ -1,3 +1,4 @@ +import re from typing import ( Dict, cast, @@ -17,6 +18,8 @@ class RuleToStr: Export a rule XML element to a string which creates the same element """ + _date_separators_re = re.compile(r"\s*([TZ:.+-])\s*") + def __init__(self) -> None: # The cache prevents evaluating subtrees repeatedly. self._cache: Dict[str, str] = {} @@ -43,6 +46,16 @@ class RuleToStr: ) ) + @staticmethod + def _date_to_str(date: str) -> str: + # remove spaces around separators + result = re.sub(RuleToStr._date_separators_re, r"\1", date) + # if there are any spaces left, replace the first one with T + result = re.sub(r"\s+", "T", result, count=1) + # keep all other spaces in place + # the date wouldn't be valid, but there is nothing more we can do + return result + def _rule_to_str(self, rule_el: _Element) -> str: # "and" is a documented pacemaker default # https://clusterlabs.org/pacemaker/doc/en-US/Pacemaker/2.0/html-single/Pacemaker_Explained/index.html#_rule_properties @@ -95,10 +108,10 @@ class RuleToStr: string_parts.extend(["date", "in_range"]) # CIB schema allows "start" + "duration" or optional "start" + "end" if "start" in expr_el.attrib: - string_parts.append(str(expr_el.get("start", ""))) + string_parts.append(self._date_to_str(expr_el.get("start", ""))) string_parts.append("to") if "end" in expr_el.attrib: - string_parts.append(str(expr_el.get("end", ""))) + string_parts.append(self._date_to_str(expr_el.get("end", ""))) if duration is not None: string_parts.append("duration") string_parts.append(self._attrs_to_str(duration)) @@ -107,9 +120,9 @@ class RuleToStr: # operation=="lt" + "end" string_parts.extend(["date", str(expr_el.get("operation", ""))]) if "start" in expr_el.attrib: - string_parts.append(str(expr_el.get("start", ""))) + string_parts.append(self._date_to_str(expr_el.get("start", ""))) if "end" in expr_el.attrib: - string_parts.append(str(expr_el.get("end", ""))) + string_parts.append(self._date_to_str(expr_el.get("end", ""))) return " ".join(string_parts) def _op_expr_to_str(self, expr_el: _Element) -> str: diff --git a/pcs/rule.py b/pcs/rule.py index 7c0e1400..172f1e1e 100644 --- a/pcs/rule.py +++ b/pcs/rule.py @@ -7,6 +7,7 @@ from typing import ( ) from pcs import utils +from pcs.cli.reports.output import deprecation_warning from pcs.common import ( const, pacemaker, @@ -893,6 +894,17 @@ class RuleParser(Parser): class CibBuilder: def __init__(self, cib_schema_version): self.cib_schema_version = cib_schema_version + self.space_deprecation_printed = False + + # deprecated since pcs-0.11.7 + def date_space_deprecation(self, date): + if self.space_deprecation_printed or " " not in date: + return + self.space_deprecation_printed = True + deprecation_warning( + "Using spaces in date values is deprecated and will be removed. " + "Use 'T' as a delimiter between date and time." + ) def build(self, dom_element, syntactic_tree, rule_id=None): dom_rule = self.add_element( @@ -978,6 +990,7 @@ class CibBuilder: "'%s' is not an ISO 8601 date" % syntactic_tree.children[1].value ) + self.date_space_deprecation(syntactic_tree.children[1].value) dom_expression.setAttribute("operation", syntactic_tree.symbol_id) if syntactic_tree.symbol_id == "gt": dom_expression.setAttribute( @@ -1008,6 +1021,7 @@ class CibBuilder: "'%s' is not an ISO 8601 date" % syntactic_tree.children[2].value ) + self.date_space_deprecation(syntactic_tree.children[2].value) dom_expression.setAttribute( "end", syntactic_tree.children[2].value ) diff --git a/pcs_test/Makefile.am b/pcs_test/Makefile.am index 738f6622..64ef1d9e 100644 --- a/pcs_test/Makefile.am +++ b/pcs_test/Makefile.am @@ -20,7 +20,9 @@ EXTRA_DIST = \ resources/cib-property.xml \ resources/cib-resources.xml \ resources/cib-all.xml \ + resources/cib-rule-with-spaces-in-date.xml \ resources/cib-tags.xml \ + resources/cib-unexportable-constraints.xml \ resources/controld_metadata.xml \ resources/corosync-3nodes.conf \ resources/corosync-3nodes-qdevice.conf \ diff --git a/pcs_test/resources/cib-all.xml b/pcs_test/resources/cib-all.xml index a44a546b..b738d7e6 100644 --- a/pcs_test/resources/cib-all.xml +++ b/pcs_test/resources/cib-all.xml @@ -135,11 +135,13 @@ - - + + + - + + diff --git a/pcs_test/resources/cib-rule-with-spaces-in-date.xml b/pcs_test/resources/cib-rule-with-spaces-in-date.xml new file mode 100644 index 00000000..a68a1287 --- /dev/null +++ b/pcs_test/resources/cib-rule-with-spaces-in-date.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pcs_test/resources/cib-unexportable-constraints.xml b/pcs_test/resources/cib-unexportable-constraints.xml new file mode 100644 index 00000000..642bad96 --- /dev/null +++ b/pcs_test/resources/cib-unexportable-constraints.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pcs_test/resources/constraint-commands b/pcs_test/resources/constraint-commands index 096bdec0..759e7146 100644 --- a/pcs_test/resources/constraint-commands +++ b/pcs_test/resources/constraint-commands @@ -5,15 +5,12 @@ pcs -- constraint location add location-R7-localhost-INFINITY resource%R7 localh resource-discovery=always; pcs -- constraint location add location-G2-localhost-INFINITY resource%G2 localhost INFINITY; pcs -- constraint location add location-R-localhost-INFINITY 'regexp%R*' localhost INFINITY; -pcs -- constraint location resource%B2 rule \ - id=loc_constr_with_expired_rule-rule constraint-id=loc_constr_with_expired_rule score=500 \ - date lt 2000-01-01; pcs -- constraint location resource%R6-clone rule \ id=loc_constr_with_not_expired_rule-rule constraint-id=loc_constr_with_not_expired_rule role=Unpromoted score=500 \ - date gt 2000-01-01; + '#uname' eq node1 and date gt 2000-01-01; pcs -- constraint rule add loc_constr_with_not_expired_rule \ id=loc_constr_with_not_expired_rule-rule-1 role=Promoted score-attribute=test-attr \ - date gt 2010-12-31; + date gt 2010-12-31 and '#uname' eq node1; pcs -- constraint colocation add Promoted G1-clone with Stopped R6-clone -100 \ id=colocation-G1-clone-R6-clone--100; pcs -- constraint colocation \ diff --git a/pcs_test/resources/resource-commands b/pcs_test/resources/resource-commands index 80775e7e..296d279c 100644 --- a/pcs_test/resources/resource-commands +++ b/pcs_test/resources/resource-commands @@ -15,14 +15,15 @@ pcs resource bundle create B2 \ container docker \ image=pcs:test; pcs resource create R1 ocf:pacemaker:Dummy --no-default-ops bundle B2 --force; -pcs resource create R2 ocf:pacemaker:Dummy --no-default-ops; -pcs resource create R3 ocf:pacemaker:Dummy --no-default-ops; -pcs resource create R4 ocf:pacemaker:Dummy --no-default-ops; +pcs resource create R2 ocf:pacemaker:Stateful --no-default-ops; +pcs resource create R3 ocf:pacemaker:Stateful --no-default-ops; +pcs resource create R4 ocf:pacemaker:Stateful --no-default-ops; pcs resource create R5 ocf:pacemaker:Dummy --no-default-ops; pcs resource create R6 ocf:pacemaker:Dummy; pcs resource create R7 ocf:pacemaker:Dummy --force \ fake=looool envfile=/dev/null \ op custom_action interval=10s OCF_CHECK_LEVEL=2 \ + migrate_to interval=0s id=R7-migrate_to-interval-0s timeout=20s enabled=0 record-pending=0 \ meta m1=value1 meta2=valueofmeta2isthisverylongstring "anotherone=something'\"special" m10=value1 meta20=valueofmeta2isthisverylongstring "another one0=a + b = c"; pcs stonith create S1 fence_kdump nodename=testnodename; pcs stonith create S2 fence_kdump; diff --git a/pcs_test/tier0/lib/cib/rule/test_cib_to_str.py b/pcs_test/tier0/lib/cib/rule/test_cib_to_str.py index eaef7d4b..4d93628a 100644 --- a/pcs_test/tier0/lib/cib/rule/test_cib_to_str.py +++ b/pcs_test/tier0/lib/cib/rule/test_cib_to_str.py @@ -5,3 +5,96 @@ # pcs_test/tier0/lib/commands/test_cib_options.py. # Therefore we don't duplicate those here. However, if there's a need to write # specific tests here, feel free to do so. + + +from unittest import TestCase + +from pcs.lib.cib.rule.cib_to_str import RuleToStr + + +class IsoToStr(TestCase): + # pylint: disable=protected-access + def test_no_change(self): + self.assertEqual(RuleToStr._date_to_str("2023-06"), "2023-06") + self.assertEqual(RuleToStr._date_to_str("202306"), "202306") + self.assertEqual(RuleToStr._date_to_str("2023-06-30"), "2023-06-30") + self.assertEqual(RuleToStr._date_to_str("20230630"), "20230630") + self.assertEqual( + RuleToStr._date_to_str("2023-06-30T16:30"), "2023-06-30T16:30" + ) + self.assertEqual( + RuleToStr._date_to_str("20230630T1630"), "20230630T1630" + ) + self.assertEqual( + RuleToStr._date_to_str("2023-06-30T16:30Z"), "2023-06-30T16:30Z" + ) + self.assertEqual( + RuleToStr._date_to_str("20230630T1630+2"), "20230630T1630+2" + ) + self.assertEqual( + RuleToStr._date_to_str("2023-06-30T16:30:40+2:00"), + "2023-06-30T16:30:40+2:00", + ) + self.assertEqual( + RuleToStr._date_to_str("20230630T1630+02:00"), "20230630T1630+02:00" + ) + + def test_remove_spaces(self): + self.assertEqual(RuleToStr._date_to_str("- 2023"), "-2023") + self.assertEqual(RuleToStr._date_to_str("+ 2023"), "+2023") + self.assertEqual(RuleToStr._date_to_str("2023- 06"), "2023-06") + self.assertEqual(RuleToStr._date_to_str("2023 -06- 30"), "2023-06-30") + self.assertEqual( + RuleToStr._date_to_str("2023-06-30 T16:30"), "2023-06-30T16:30" + ) + self.assertEqual( + RuleToStr._date_to_str("20230630T 1630"), "20230630T1630" + ) + self.assertEqual( + RuleToStr._date_to_str("2023-06-30 T 16:30 Z"), "2023-06-30T16:30Z" + ) + self.assertEqual( + RuleToStr._date_to_str("20230630 T 1630 + 2"), "20230630T1630+2" + ) + self.assertEqual( + RuleToStr._date_to_str( + "2023 - 06 - 30 T 16 : 30 : 40 + 2: 00" + ), + "2023-06-30T16:30:40+2:00", + ) + self.assertEqual( + RuleToStr._date_to_str("20230630 T 1630+ 02:00"), + "20230630T1630+02:00", + ) + + def test_add_time_separator(self): + self.assertEqual( + RuleToStr._date_to_str("2023-06-30 16:30"), "2023-06-30T16:30" + ) + self.assertEqual( + RuleToStr._date_to_str("20230630 1630"), "20230630T1630" + ) + self.assertEqual( + RuleToStr._date_to_str("2023-06-30 16:30 Z"), "2023-06-30T16:30Z" + ) + self.assertEqual( + RuleToStr._date_to_str("20230630 1630 + 2"), "20230630T1630+2" + ) + self.assertEqual( + RuleToStr._date_to_str("2023 - 06 - 30 16 : 30 : 40 + 2: 00"), + "2023-06-30T16:30:40+2:00", + ) + self.assertEqual( + RuleToStr._date_to_str("20230630 1630+ 02:00"), + "20230630T1630+02:00", + ) + + def test_extra_spaces(self): + self.assertEqual( + RuleToStr._date_to_str("2023-06-30 16:30:40 +2 00"), + "2023-06-30T16:30:40+2 00", + ) + self.assertEqual( + RuleToStr._date_to_str("2023 06 30 16 30 +02"), + "2023T06 30 16 30+02", + ) diff --git a/pcs_test/tier0/lib/cib/rule/test_parser.py b/pcs_test/tier0/lib/cib/rule/test_parser.py index fd089ec8..37ae52f1 100644 --- a/pcs_test/tier0/lib/cib/rule/test_parser.py +++ b/pcs_test/tier0/lib/cib/rule/test_parser.py @@ -512,6 +512,14 @@ class Parser(TestCase): DateUnaryExpr operator=GT date=2014-06-26""" ), ), + ( + "date gt 2014-06-26T12:00:00", + dedent( + """\ + BoolExpr AND + DateUnaryExpr operator=GT date=2014-06-26T12:00:00""" + ), + ), ( "date lt 2014-06-26", dedent( @@ -520,6 +528,14 @@ class Parser(TestCase): DateUnaryExpr operator=LT date=2014-06-26""" ), ), + ( + "date lt 2014-06-26T12:00:00", + dedent( + """\ + BoolExpr AND + DateUnaryExpr operator=LT date=2014-06-26T12:00:00""" + ), + ), ( "date in_range 2014-06-26 to 2014-07-26", dedent( @@ -528,6 +544,14 @@ class Parser(TestCase): DateInRangeExpr date_start=2014-06-26 date_end=2014-07-26""" ), ), + ( + "date in_range 2014-06-26T12:00:00 to 2014-07-26T13:00:00", + dedent( + """\ + BoolExpr AND + DateInRangeExpr date_start=2014-06-26T12:00:00 date_end=2014-07-26T13:00:00""" + ), + ), ( "date in_range to 2014-07-26", dedent( @@ -536,6 +560,14 @@ class Parser(TestCase): DateInRangeExpr date_end=2014-07-26""" ), ), + ( + "date in_range to 2014-07-26T12:00:00", + dedent( + """\ + BoolExpr AND + DateInRangeExpr date_end=2014-07-26T12:00:00""" + ), + ), ( "date in_range 2014-06-26 to duration years=1", dedent( @@ -546,6 +578,16 @@ class Parser(TestCase): )""" ), ), + ( + "date in_range 2014-06-26T12:00:00 to duration years=1", + dedent( + """\ + BoolExpr AND + DateInRangeExpr date_start=2014-06-26T12:00:00 duration_parts=( + years=1 + )""" + ), + ), ( "date in_range 2014-06-26 to duration a=1 b=2 a=3", dedent( @@ -876,6 +918,22 @@ class Parser(TestCase): "#uname in_range 2014-06-26 to 2014-07-26", (1, 8, 7, "Expected 'eq'"), ), + ( + "date gt 2014-06-24 12:00:00", + (1, 20, 19, "Expected end of text"), + ), + ( + "date lt 2014-06-24 12:00:00", + (1, 20, 19, "Expected end of text"), + ), + ( + "date in_range 2014-06-26 12:00:00 to 2014-07-26", + (1, 15, 14, "Expected 'to'"), + ), + ( + "date in_range 2014-06-26 to 2014-07-26 12:00:00", + (1, 40, 39, "Expected end of text"), + ), # braces ("(#uname)", (1, 8, 7, "Expected 'eq'")), ("(", (1, 2, 1, "Expected 'date'")), diff --git a/pcs_test/tier1/constraint/test_config.py b/pcs_test/tier1/constraint/test_config.py index 27aed9c0..525aac25 100644 --- a/pcs_test/tier1/constraint/test_config.py +++ b/pcs_test/tier1/constraint/test_config.py @@ -72,18 +72,19 @@ class ConstraintConfigJson(TestCase): class ConstraintConfigCmdMixin: - # pylint: disable=invalid-name + orig_cib_file_path = get_test_resource("cib-all.xml") + def setUp(self): - orig_cib_file_path = get_test_resource("cib-all.xml") + # pylint: disable=invalid-name self.new_cib_file = get_tmp_file(self._get_tmp_file_name()) - self.pcs_runner_orig = PcsRunner(cib_file=orig_cib_file_path) + self.pcs_runner_orig = PcsRunner(cib_file=self.orig_cib_file_path) self.pcs_runner_new = PcsRunner(cib_file=self.new_cib_file.name) write_data_to_tmpfile( fixture_cib.modify_cib_file( get_test_resource("cib-empty.xml"), resources=etree_to_str( get_resources( - XmlManipulation.from_file(orig_cib_file_path).tree + XmlManipulation.from_file(self.orig_cib_file_path).tree ) ), ), @@ -92,6 +93,7 @@ class ConstraintConfigCmdMixin: self.maxDiff = None def tearDown(self): + # pylint: disable=invalid-name self.new_cib_file.close() def _get_as_json(self, runner, use_all): @@ -141,6 +143,115 @@ class ConstraintConfigCmd(ConstraintConfigCmdMixin, TestCase): return "tier1_constraint_test_config_cib.xml" +class ConstraintConfigCmdSpaceInDate(ConstraintConfigCmdMixin, TestCase): + # This class tests that pcs exports dates from location rules constraint + # with spaces replaced by T in pcs commands, so that they can be run and + # processed by pcs correctly. + orig_cib_file_path = get_test_resource("cib-rule-with-spaces-in-date.xml") + + @staticmethod + def _get_tmp_file_name(): + return "tier1_constraint_test_config_cib_date_space.xml" + + @staticmethod + def _replace(struct, search_replace): + if isinstance(struct, dict): + for key, val in struct.items(): + struct[key] = ConstraintConfigCmdSpaceInDate._replace( + val, search_replace + ) + return struct + if isinstance(struct, list): + return [ + ConstraintConfigCmdSpaceInDate._replace(val, search_replace) + for val in struct + ] + for search, replace in search_replace: + if struct == search: + return replace + return struct + + def _get_as_json(self, runner, use_all): + data = super()._get_as_json(runner, use_all) + data = self._replace( + data, + [ + ("2023-01-01 12:00", "2023-01-01T12:00"), + ("2023-12-31 12:00", "2023-12-31T12:00"), + ], + ) + return data + + def test_commands(self): + stdout, stderr, retval = self.pcs_runner_orig.run( + ["constraint", "config", "--output-format=cmd"] + ) + self.assertEqual(retval, 0) + self.assertEqual(stderr, "") + self.assertEqual( + stdout, + ( + "pcs -- constraint location resource%R1 rule \\\n" + " id=location-R1-rule constraint-id=location-R1 score=INFINITY \\\n" + " '#uname' eq node1 and date gt 2023-01-01T12:00 and " + "date lt 2023-12-31T12:00 and date in_range 2023-01-01T12:00 " + "to 2023-12-31T12:00;\n" + "pcs -- constraint rule add location-R1 \\\n" + " id=location-R1-rule-1 score=INFINITY \\\n" + " '#uname' eq node1 and date gt 2023-01-01T12:00 and " + "date lt 2023-12-31T12:00 and date in_range 2023-01-01T12:00 " + "to 2023-12-31T12:00\n" + ), + ) + + +class ConstraintConfigCmdUnsupported(TestCase): + def setUp(self): + self.maxDiff = None + self.pcs_runner = PcsRunner( + cib_file=get_test_resource("cib-unexportable-constraints.xml"), + ) + + def test_dont_export_unsupported_constraints(self): + stdout, stderr, retval = self.pcs_runner.run( + ["constraint", "config", "--output-format=cmd"] + ) + self.assertEqual(retval, 0) + sufix = "not supported by this command. Command for creating the constraint is omitted.\n" + self.assertEqual( + stderr, + ( + f"Warning: Location set constraint with id 'location-set' configured but it's {sufix}" + f"Warning: Resource role detected in constraint 'location-role' but {sufix}" + f"Warning: Lifetime configuration detected in constraint 'location-lifetime' but {sufix}" + f"Warning: Option 'influence' detected in constraint 'colocation-influence' but {sufix}" + f"Warning: Lifetime configuration detected in constraint 'colocation-lifetime' but {sufix}" + f"Warning: Option 'node_attribute' detected in constraint 'colocation-node-attribute' but {sufix}" + f"Warning: Option 'ordering' detected in resource set 'colocation-set-ordering-set' but {sufix}" + f"Warning: Option 'require-all' detected in constraint 'order-set-require-all' but {sufix}" + f"Warning: Option 'ordering' detected in resource set 'order-set-ordering-set' but {sufix}" + ), + ) + self.assertEqual( + stdout, + ( + "pcs -- constraint location add location-OK resource%R1 node1 INFINITY;\n" + "pcs -- constraint colocation add R1 with R3 INFINITY \\\n" + " id=colocation-OK;\n" + "pcs -- constraint colocation \\\n" + " set R1 R3 \\\n" + " setoptions id=colocation-set-OK;\n" + "pcs -- constraint order start R1 then start R3 \\\n" + " id=order-OK;\n" + "pcs -- constraint order start R1 then start R3 \\\n" + " id=order-lifetime;\n" + "pcs -- constraint order \\\n" + " set R1 R3 \\\n" + " setoptions id=order-set-OK\n" + ), + ) + + class ConstraintConfigText(TestCase): def setUp(self): self.maxDiff = None @@ -161,10 +272,12 @@ class ConstraintConfigText(TestCase): resource pattern 'R*' prefers node 'localhost' with score INFINITY resource 'R6-clone' Rules: - Rule: role=Unpromoted score=500 + Rule: boolean-op=and role=Unpromoted score=500 + Expression: #uname eq node1 Expression: date gt 2000-01-01 - Rule: role=Promoted score-attribute=test-attr + Rule: boolean-op=and role=Promoted score-attribute=test-attr Expression: date gt 2010-12-31 + Expression: #uname eq node1 Colocation Constraints: Promoted resource 'G1-clone' with Stopped resource 'R6-clone' score=-100 @@ -225,10 +338,12 @@ class ConstraintConfigText(TestCase): Expression: date lt 2000-01-01 resource 'R6-clone' Rules: - Rule: role=Unpromoted score=500 + Rule: boolean-op=and role=Unpromoted score=500 + Expression: #uname eq node1 Expression: date gt 2000-01-01 - Rule: role=Promoted score-attribute=test-attr + Rule: boolean-op=and role=Promoted score-attribute=test-attr Expression: date gt 2010-12-31 + Expression: #uname eq node1 Colocation Constraints: Promoted resource 'G1-clone' with Stopped resource 'R6-clone' score=-100 @@ -285,10 +400,12 @@ class ConstraintConfigText(TestCase): resource pattern 'R*' prefers node 'localhost' with score INFINITY (id: location-R-localhost-INFINITY) resource 'R6-clone' (id: loc_constr_with_not_expired_rule) Rules: - Rule: role=Unpromoted score=500 (id: loc_constr_with_not_expired_rule-rule) - Expression: date gt 2000-01-01 (id: loc_constr_with_not_expired_rule-rule-expr) - Rule: role=Promoted score-attribute=test-attr (id: loc_constr_with_not_expired_rule-rule-1) + Rule: boolean-op=and role=Unpromoted score=500 (id: loc_constr_with_not_expired_rule-rule) + Expression: #uname eq node1 (id: loc_constr_with_not_expired_rule-rule-expr) + Expression: date gt 2000-01-01 (id: loc_constr_with_not_expired_rule-rule-expr-1) + Rule: boolean-op=and role=Promoted score-attribute=test-attr (id: loc_constr_with_not_expired_rule-rule-1) Expression: date gt 2010-12-31 (id: loc_constr_with_not_expired_rule-rule-1-expr) + Expression: #uname eq node1 (id: loc_constr_with_not_expired_rule-rule-1-expr-1) Colocation Constraints: Promoted resource 'G1-clone' with Stopped resource 'R6-clone' (id: colocation-G1-clone-R6-clone--100) score=-100 @@ -349,10 +466,12 @@ class ConstraintConfigText(TestCase): Expression: date lt 2000-01-01 (id: loc_constr_with_expired_rule-rule-expr) resource 'R6-clone' (id: loc_constr_with_not_expired_rule) Rules: - Rule: role=Unpromoted score=500 (id: loc_constr_with_not_expired_rule-rule) - Expression: date gt 2000-01-01 (id: loc_constr_with_not_expired_rule-rule-expr) - Rule: role=Promoted score-attribute=test-attr (id: loc_constr_with_not_expired_rule-rule-1) + Rule: boolean-op=and role=Unpromoted score=500 (id: loc_constr_with_not_expired_rule-rule) + Expression: #uname eq node1 (id: loc_constr_with_not_expired_rule-rule-expr) + Expression: date gt 2000-01-01 (id: loc_constr_with_not_expired_rule-rule-expr-1) + Rule: boolean-op=and role=Promoted score-attribute=test-attr (id: loc_constr_with_not_expired_rule-rule-1) Expression: date gt 2010-12-31 (id: loc_constr_with_not_expired_rule-rule-1-expr) + Expression: #uname eq node1 (id: loc_constr_with_not_expired_rule-rule-1-expr-1) Colocation Constraints: Promoted resource 'G1-clone' with Stopped resource 'R6-clone' (id: colocation-G1-clone-R6-clone--100) score=-100 diff --git a/pcs_test/tier1/legacy/test_constraints.py b/pcs_test/tier1/legacy/test_constraints.py index e5e8bd27..7e629a89 100644 --- a/pcs_test/tier1/legacy/test_constraints.py +++ b/pcs_test/tier1/legacy/test_constraints.py @@ -202,6 +202,117 @@ class ConstraintTest(unittest.TestCase, AssertPcsMixin): ), ) + def test_constraint_rules_space_deprecated(self): + self.fixture_resources() + message = ( + "Deprecation Warning: Using spaces in date values is deprecated and " + "will be removed. Use 'T' as a delimiter between date and time.\n" + ) + self.assert_pcs_success( + "constraint location D1 rule".split() + + [ + "date", + "gt", + "2023-01-01 12:00 +3:00", + "and", + "date", + "lt", + "2023-12-31 12:00 -10:30", + "and", + "date", + "in_range", + "2023-01-01 12:00", + "to", + "2023-12-31 12:00", + ], + stderr_full=message, + ) + self.assert_pcs_success( + "constraint location D1 rule".split() + + ["date", "gt", "2023-01-01 12:00"], + stderr_full=message, + ) + self.assert_pcs_success( + "constraint location D1 rule".split() + + ["date", "lt", "2023-12-31 12:00"], + stderr_full=message, + ) + self.assert_pcs_success( + "constraint location D1 rule".split() + + [ + "date", + "in_range", + "2023-01-01 12:00", + "to", + "2023-12-31T12:00", + ], + stderr_full=message, + ) + self.assert_pcs_success( + "constraint location D1 rule".split() + + [ + "date", + "in_range", + "2023-01-01T12:00", + "to", + "2023-12-31 12:00", + ], + stderr_full=message, + ) + # when exporting the rules, spaces are replaced by T + self.assert_pcs_success( + "constraint config".split(), + dedent( + """\ + Location Constraints: + resource 'D1' + Rules: + Rule: boolean-op=and score=INFINITY + Expression: date gt 2023-01-01T12:00+3:00 + Expression: date lt 2023-12-31T12:00-10:30 + Expression: date in_range 2023-01-01T12:00 to 2023-12-31T12:00 + resource 'D1' + Rules: + Rule: score=INFINITY + Expression: date gt 2023-01-01T12:00 + resource 'D1' + Rules: + Rule: score=INFINITY + Expression: date lt 2023-12-31T12:00 + resource 'D1' + Rules: + Rule: score=INFINITY + Expression: date in_range 2023-01-01T12:00 to 2023-12-31T12:00 + resource 'D1' + Rules: + Rule: score=INFINITY + Expression: date in_range 2023-01-01T12:00 to 2023-12-31T12:00 + """ + ), + ) + self.assert_pcs_success( + "constraint config --output-format=cmd".split(), + dedent( + """\ + pcs -- constraint location resource%D1 rule \\ + id=location-D1-rule constraint-id=location-D1 score=INFINITY \\ + date gt 2023-01-01T12:00+3:00 and date lt 2023-12-31T12:00-10:30 and date in_range 2023-01-01T12:00 to 2023-12-31T12:00; + pcs -- constraint location resource%D1 rule \\ + id=location-D1-1-rule constraint-id=location-D1-1 score=INFINITY \\ + date gt 2023-01-01T12:00; + pcs -- constraint location resource%D1 rule \\ + id=location-D1-2-rule constraint-id=location-D1-2 score=INFINITY \\ + date lt 2023-12-31T12:00; + pcs -- constraint location resource%D1 rule \\ + id=location-D1-3-rule constraint-id=location-D1-3 score=INFINITY \\ + date in_range 2023-01-01T12:00 to 2023-12-31T12:00; + pcs -- constraint location resource%D1 rule \\ + id=location-D1-4-rule constraint-id=location-D1-4 score=INFINITY \\ + date in_range 2023-01-01T12:00 to 2023-12-31T12:00 + """ + ), + ) + def testAdvancedConstraintRule(self): self.fixture_resources() stdout, stderr, retval = pcs( diff --git a/pcs_test/tier1/legacy/test_rule.py b/pcs_test/tier1/legacy/test_rule.py index eff3f878..b8f37e5d 100644 --- a/pcs_test/tier1/legacy/test_rule.py +++ b/pcs_test/tier1/legacy/test_rule.py @@ -453,10 +453,18 @@ class ParserTest(TestCase): "(gt (literal date) (literal 2014-06-26))", str(self.parser.parse(["date", "gt", "2014-06-26"])), ) + self.assertEqual( + "(gt (literal date) (literal 2014-06-26 12:00:00))", + str(self.parser.parse(["date", "gt", "2014-06-26 12:00:00"])), + ) self.assertEqual( "(lt (literal date) (literal 2014-06-26))", str(self.parser.parse(["date", "lt", "2014-06-26"])), ) + self.assertEqual( + "(lt (literal date) (literal 2014-06-26 12:00:00))", + str(self.parser.parse(["date", "lt", "2014-06-26 12:00:00"])), + ) self.assertEqual( "(in_range " "(literal date) (literal 2014-06-26) (literal 2014-07-26)" @@ -467,6 +475,22 @@ class ParserTest(TestCase): ) ), ) + self.assertEqual( + "(in_range " + "(literal date) (literal 2014-06-26 12:00) (literal 2014-07-26 13:00)" + ")", + str( + self.parser.parse( + [ + "date", + "in_range", + "2014-06-26 12:00", + "to", + "2014-07-26 13:00", + ] + ) + ), + ) self.assertEqual( "(in_range " "(literal date) " diff --git a/pcs_test/tools/constraints_dto.py b/pcs_test/tools/constraints_dto.py index f9c91510..c1ac7454 100644 --- a/pcs_test/tools/constraints_dto.py +++ b/pcs_test/tools/constraints_dto.py @@ -160,6 +160,7 @@ def get_all_constraints( "loc_constr_with_not_expired_rule-rule" ), options={ + "boolean-op": "and", "role": "Unpromoted", "score": "500", }, @@ -168,10 +169,26 @@ def get_all_constraints( expressions=[ CibRuleExpressionDto( id="loc_constr_with_not_expired_rule-rule-expr", - type=CibRuleExpressionType.DATE_EXPRESSION, + type=CibRuleExpressionType.EXPRESSION, in_effect=rule_eval.get_rule_status( "loc_constr_with_not_expired_rule-rule-expr" ), + options={ + "operation": "eq", + "attribute": "#uname", + "value": "node1", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="#uname eq node1", + ), + CibRuleExpressionDto( + id="loc_constr_with_not_expired_rule-rule-expr-1", + type=CibRuleExpressionType.DATE_EXPRESSION, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_not_expired_rule-rule-expr-1" + ), options={ "operation": "gt", "start": "2000-01-01", @@ -180,9 +197,9 @@ def get_all_constraints( duration=None, expressions=[], as_string="date gt 2000-01-01", - ) + ), ], - as_string="date gt 2000-01-01", + as_string="#uname eq node1 and date gt 2000-01-01", ), CibRuleExpressionDto( id="loc_constr_with_not_expired_rule-rule-1", @@ -191,6 +208,7 @@ def get_all_constraints( "loc_constr_with_not_expired_rule-rule-1" ), options={ + "boolean-op": "and", "role": "Promoted", "score-attribute": "test-attr", }, @@ -211,9 +229,25 @@ def get_all_constraints( duration=None, expressions=[], as_string="date gt 2010-12-31", - ) + ), + CibRuleExpressionDto( + id="loc_constr_with_not_expired_rule-rule-1-expr-1", + type=CibRuleExpressionType.EXPRESSION, + in_effect=rule_eval.get_rule_status( + "loc_constr_with_not_expired_rule-rule-1-expr-1" + ), + options={ + "operation": "eq", + "attribute": "#uname", + "value": "node1", + }, + date_spec=None, + duration=None, + expressions=[], + as_string="#uname eq node1", + ), ], - as_string="date gt 2010-12-31", + as_string="date gt 2010-12-31 and #uname eq node1", ), ], lifetime=[], -- 2.41.0