pcs/SOURCES/bz2050274-02-relax-OCF-1.0-parser.patch
2022-03-30 15:55:14 +00:00

588 lines
22 KiB
Diff

From 65b30a04a234449cb4aa65606d47bf1d673592a4 Mon Sep 17 00:00:00 2001
From: Tomas Jelinek <tojeline@redhat.com>
Date: Wed, 9 Feb 2022 11:16:49 +0100
Subject: [PATCH 2/3] relax OCF 1.0 parser
---
pcs/lib/resource_agent/facade.py | 50 ++++--
pcs/lib/resource_agent/ocf_transform.py | 51 +++++-
pcs/lib/resource_agent/xml.py | 8 +-
.../tier0/lib/resource_agent/test_facade.py | 44 +++++
.../lib/resource_agent/test_ocf_transform.py | 48 +++++-
pcs_test/tier0/lib/resource_agent/test_xml.py | 155 ++++++++++--------
6 files changed, 256 insertions(+), 100 deletions(-)
diff --git a/pcs/lib/resource_agent/facade.py b/pcs/lib/resource_agent/facade.py
index dea59a1a..8a65eb1c 100644
--- a/pcs/lib/resource_agent/facade.py
+++ b/pcs/lib/resource_agent/facade.py
@@ -2,12 +2,19 @@ from collections import defaultdict
from dataclasses import replace as dc_replace
from typing import Dict, Iterable, List, Optional, Set
+from lxml import etree
+
+from pcs import settings
from pcs.common import reports
from pcs.lib import validate
from pcs.lib.external import CommandRunner
from . import const
-from .error import ResourceAgentError, resource_agent_error_to_report_item
+from .error import (
+ ResourceAgentError,
+ resource_agent_error_to_report_item,
+ UnableToGetAgentMetadata,
+)
from .name import name_to_void_metadata
from .ocf_transform import ocf_version_to_ocf_unified
from .pcs_transform import get_additional_trace_parameters, ocf_unified_to_pcs
@@ -195,24 +202,33 @@ class ResourceAgentFacadeFactory:
name -- agent name to get a facade for
"""
- metadata, raw_ocf_version = parse_metadata(
- name,
- load_metadata(self._runner, name),
- )
- if (
- report_warnings
- and raw_ocf_version not in const.SUPPORTED_OCF_VERSIONS
- ):
- self._report_processor.report(
- reports.ReportItem.warning(
- reports.messages.AgentImplementsUnsupportedOcfVersionAssumedVersion(
- name.full_name,
- raw_ocf_version,
- sorted(const.SUPPORTED_OCF_VERSIONS),
- const.OCF_1_0,
+ dom_metadata = load_metadata(self._runner, name)
+ metadata, raw_ocf_version = parse_metadata(name, dom_metadata)
+ if report_warnings:
+ if raw_ocf_version not in const.SUPPORTED_OCF_VERSIONS:
+ self._report_processor.report(
+ reports.ReportItem.warning(
+ reports.messages.AgentImplementsUnsupportedOcfVersionAssumedVersion(
+ name.full_name,
+ raw_ocf_version,
+ sorted(const.SUPPORTED_OCF_VERSIONS),
+ const.OCF_1_0,
+ )
)
)
- )
+ if raw_ocf_version != const.OCF_1_1:
+ try:
+ etree.RelaxNG(
+ file=settings.path.ocf_1_0_schema
+ ).assertValid(dom_metadata)
+ except etree.DocumentInvalid as e:
+ self._report_processor.report(
+ resource_agent_error_to_report_item(
+ UnableToGetAgentMetadata(name.full_name, str(e)),
+ severity=reports.ReportItemSeverity.warning(),
+ is_stonith=name.is_stonith,
+ )
+ )
return self._facade_from_metadata(ocf_version_to_ocf_unified(metadata))
def void_facade_from_parsed_name(
diff --git a/pcs/lib/resource_agent/ocf_transform.py b/pcs/lib/resource_agent/ocf_transform.py
index e841b55e..7e6a14ad 100644
--- a/pcs/lib/resource_agent/ocf_transform.py
+++ b/pcs/lib/resource_agent/ocf_transform.py
@@ -67,20 +67,42 @@ def _ocf_1_1_to_ocf_unified(
longdesc=metadata.longdesc,
parameters=_ocf_1_1_parameter_list_to_ocf_unified(metadata.parameters),
# OCF 1.1 actions are the same as in OCF 1.0
- actions=_ocf_1_0_action_list_to_ocf_unified(metadata.actions),
+ actions=_ocf_1_1_action_list_to_ocf_unified(metadata.actions),
)
def _ocf_1_0_action_list_to_ocf_unified(
- action_list: Iterable[
- Union[ResourceAgentActionOcf1_0, ResourceAgentActionOcf1_1]
- ],
+ action_list: Iterable[ResourceAgentActionOcf1_0],
) -> List[ResourceAgentAction]:
"""
Transform OCF 1.0 actions to a universal format
action_list -- actions according OCF 1.0
"""
+ return [
+ ResourceAgentAction(
+ name=action.name,
+ timeout=action.timeout,
+ interval=action.interval,
+ role=action.role,
+ start_delay=action.start_delay,
+ depth=action.depth,
+ automatic=_bool_value_legacy(action.automatic),
+ on_target=_bool_value_legacy(action.on_target),
+ )
+ for action in action_list
+ if action.name
+ ]
+
+
+def _ocf_1_1_action_list_to_ocf_unified(
+ action_list: Iterable[ResourceAgentActionOcf1_1],
+) -> List[ResourceAgentAction]:
+ """
+ Transform OCF 1.1 actions to a universal format
+
+ action_list -- actions according OCF 1.1
+ """
return [
ResourceAgentAction(
name=action.name,
@@ -111,6 +133,8 @@ def _ocf_1_0_parameter_list_to_ocf_unified(
result = []
for parameter in parameter_list:
+ if not parameter.name:
+ continue
result.append(
ResourceAgentParameter(
name=parameter.name,
@@ -119,17 +143,17 @@ def _ocf_1_0_parameter_list_to_ocf_unified(
type=parameter.type,
default=parameter.default,
enum_values=parameter.enum_values,
- required=_bool_value(parameter.required),
+ required=_bool_value_legacy(parameter.required),
advanced=False,
- deprecated=_bool_value(parameter.deprecated),
+ deprecated=_bool_value_legacy(parameter.deprecated),
deprecated_by=sorted(deprecated_by_dict[parameter.name]),
deprecated_desc=None,
unique_group=(
f"{const.DEFAULT_UNIQUE_GROUP_PREFIX}{parameter.name}"
- if _bool_value(parameter.unique)
+ if _bool_value_legacy(parameter.unique)
else None
),
- reloadable=_bool_value(parameter.unique),
+ reloadable=_bool_value_legacy(parameter.unique),
)
)
return result
@@ -170,3 +194,14 @@ def _bool_value(value: Optional[str]) -> bool:
value -- raw bool value
"""
return value == "1"
+
+
+def _bool_value_legacy(value: Optional[str]) -> bool:
+ """
+ Transform raw bool value from metadata to bool type in backward compatible way
+
+ value -- raw bool value
+ """
+ return (
+ False if not value else value.lower() in {"true", "on", "yes", "y", "1"}
+ )
diff --git a/pcs/lib/resource_agent/xml.py b/pcs/lib/resource_agent/xml.py
index 1ba97216..0fc70527 100644
--- a/pcs/lib/resource_agent/xml.py
+++ b/pcs/lib/resource_agent/xml.py
@@ -94,9 +94,7 @@ def _metadata_xml_to_dom(metadata: str) -> _Element:
"""
dom = xml_fromstring(metadata)
ocf_version = _get_ocf_version(dom)
- if ocf_version == const.OCF_1_0:
- etree.RelaxNG(file=settings.path.ocf_1_0_schema).assertValid(dom)
- elif ocf_version == const.OCF_1_1:
+ if ocf_version == const.OCF_1_1:
etree.RelaxNG(file=settings.path.ocf_1_1_schema).assertValid(dom)
return dom
@@ -230,7 +228,7 @@ def _parse_parameters_1_0(
)
result.append(
ResourceAgentParameterOcf1_0(
- name=str(parameter_el.attrib["name"]),
+ name=str(parameter_el.get("name", "")),
shortdesc=_get_shortdesc(parameter_el),
longdesc=_get_longdesc(parameter_el),
type=value_type,
@@ -286,7 +284,7 @@ def _parse_parameters_1_1(
def _parse_actions_1_0(element: _Element) -> List[ResourceAgentActionOcf1_0]:
return [
ResourceAgentActionOcf1_0(
- name=str(action.attrib["name"]),
+ name=str(action.get("name", "")),
timeout=action.get("timeout"),
interval=action.get("interval"),
role=action.get("role"),
diff --git a/pcs_test/tier0/lib/resource_agent/test_facade.py b/pcs_test/tier0/lib/resource_agent/test_facade.py
index f6a9899c..313dfa2b 100644
--- a/pcs_test/tier0/lib/resource_agent/test_facade.py
+++ b/pcs_test/tier0/lib/resource_agent/test_facade.py
@@ -100,6 +100,13 @@ class ResourceAgentFacadeFactory(TestCase):
</parameters>
</resource-agent>
"""
+ _fixture_agent_not_valid_xml = """
+ <resource-agent name="agent">
+ <parameters>
+ <parameter label="something wrong"/>
+ </parameters>
+ </resource-agent>
+ """
_fixture_fenced_xml = """
<resource-agent name="pacemaker-fenced">
<parameters>
@@ -172,6 +179,43 @@ class ResourceAgentFacadeFactory(TestCase):
self.assertEqual(facade.metadata.name, name)
self.assertTrue(facade.metadata.agent_exists)
+ def test_facade_ocf_1_0_not_valid(self):
+ name = ra.ResourceAgentName("service", None, "daemon")
+ self.config.runner.pcmk.load_agent(
+ agent_name="service:daemon",
+ stdout=self._fixture_agent_not_valid_xml,
+ )
+
+ env = self.env_assist.get_env()
+ facade = ra.ResourceAgentFacadeFactory(
+ env.cmd_runner(), env.report_processor
+ ).facade_from_parsed_name(name)
+ self.assertEqual(facade.metadata.name, name)
+ self.assertTrue(facade.metadata.agent_exists)
+ self.env_assist.assert_reports(
+ [
+ fixture.warn(
+ reports.codes.UNABLE_TO_GET_AGENT_METADATA,
+ agent=name.full_name,
+ reason="Element parameter failed to validate attributes, line 3",
+ )
+ ]
+ )
+
+ def test_facade_ocf_1_0_not_valid_disabled_warning(self):
+ name = ra.ResourceAgentName("service", None, "daemon")
+ self.config.runner.pcmk.load_agent(
+ agent_name="service:daemon",
+ stdout=self._fixture_agent_not_valid_xml,
+ )
+
+ env = self.env_assist.get_env()
+ facade = ra.ResourceAgentFacadeFactory(
+ env.cmd_runner(), env.report_processor
+ ).facade_from_parsed_name(name, report_warnings=False)
+ self.assertEqual(facade.metadata.name, name)
+ self.assertTrue(facade.metadata.agent_exists)
+
def test_facade_missing_agent(self):
name = ra.ResourceAgentName("service", None, "daemon")
self.config.runner.pcmk.load_agent(
diff --git a/pcs_test/tier0/lib/resource_agent/test_ocf_transform.py b/pcs_test/tier0/lib/resource_agent/test_ocf_transform.py
index 9e41b6af..d0de86e5 100644
--- a/pcs_test/tier0/lib/resource_agent/test_ocf_transform.py
+++ b/pcs_test/tier0/lib/resource_agent/test_ocf_transform.py
@@ -66,6 +66,18 @@ class OcfVersionToOcfUnified(TestCase):
obsoletes=None,
unique=None,
),
+ ra.types.ResourceAgentParameterOcf1_0(
+ name="",
+ shortdesc="Parameters with no name are ignored",
+ longdesc=None,
+ type="string",
+ default=None,
+ enum_values=None,
+ required=None,
+ deprecated=None,
+ obsoletes=None,
+ unique=None,
+ ),
ra.types.ResourceAgentParameterOcf1_0(
name="param_2",
shortdesc="param_2 shortdesc",
@@ -109,10 +121,10 @@ class OcfVersionToOcfUnified(TestCase):
type="string",
default=None,
enum_values=None,
- required="1",
- deprecated="1",
+ required="yeS",
+ deprecated="True",
obsoletes="param_4",
- unique="1",
+ unique="on",
),
ra.types.ResourceAgentParameterOcf1_0(
name="param_6",
@@ -138,6 +150,16 @@ class OcfVersionToOcfUnified(TestCase):
automatic=None,
on_target=None,
),
+ ra.types.ResourceAgentActionOcf1_0(
+ name="",
+ timeout=None,
+ interval=None,
+ role=None,
+ start_delay=None,
+ depth=None,
+ automatic=None,
+ on_target=None,
+ ),
ra.types.ResourceAgentActionOcf1_0(
name="action_2",
timeout="12",
@@ -158,6 +180,16 @@ class OcfVersionToOcfUnified(TestCase):
automatic="1",
on_target="0",
),
+ ra.types.ResourceAgentActionOcf1_0(
+ name="action_4",
+ timeout=None,
+ interval=None,
+ role=None,
+ start_delay=None,
+ depth=None,
+ automatic="yes",
+ on_target="True",
+ ),
],
)
metadata_out = ra.ResourceAgentMetadata(
@@ -289,6 +321,16 @@ class OcfVersionToOcfUnified(TestCase):
automatic=True,
on_target=False,
),
+ ra.ResourceAgentAction(
+ name="action_4",
+ timeout=None,
+ interval=None,
+ role=None,
+ start_delay=None,
+ depth=None,
+ automatic=True,
+ on_target=True,
+ ),
],
)
self.assertEqual(
diff --git a/pcs_test/tier0/lib/resource_agent/test_xml.py b/pcs_test/tier0/lib/resource_agent/test_xml.py
index 26bbbb7d..ea055ee2 100644
--- a/pcs_test/tier0/lib/resource_agent/test_xml.py
+++ b/pcs_test/tier0/lib/resource_agent/test_xml.py
@@ -164,8 +164,13 @@ class MetadataXmlToDom(TestCase):
ra.xml._metadata_xml_to_dom("not an xml")
def test_no_version_not_valid(self):
- with self.assertRaises(etree.DocumentInvalid):
- ra.xml._metadata_xml_to_dom("<resource-agent/>")
+ # pylint: disable=no-self-use
+ metadata = """
+ <resource-agent/>
+ """
+ assert_xml_equal(
+ metadata, etree_to_str(ra.xml._metadata_xml_to_dom(metadata))
+ )
def test_no_version_valid(self):
# pylint: disable=no-self-use
@@ -178,14 +183,15 @@ class MetadataXmlToDom(TestCase):
)
def test_ocf_1_0_not_valid(self):
- with self.assertRaises(etree.DocumentInvalid):
- ra.xml._metadata_xml_to_dom(
- """
- <resource-agent>
- <version>1.0</version>
- </resource-agent>
- """
- )
+ # pylint: disable=no-self-use
+ metadata = """
+ <resource-agent>
+ <version>1.0</version>
+ </resource-agent>
+ """
+ assert_xml_equal(
+ metadata, etree_to_str(ra.xml._metadata_xml_to_dom(metadata))
+ )
def test_ocf_1_0_valid(self):
# pylint: disable=no-self-use
@@ -273,19 +279,16 @@ class LoadMetadata(TestCase):
def test_not_valid_xml(self):
agent_name = ra.ResourceAgentName("ocf", "pacemaker", "Dummy")
+ metadata = "<resource-agent/>"
self.config.runner.pcmk.load_agent(
agent_name="ocf:pacemaker:Dummy",
- stdout="<resource-agent/>",
+ stdout=metadata,
)
env = self.env_assist.get_env()
- with self.assertRaises(ra.UnableToGetAgentMetadata) as cm:
- ra.xml.load_metadata(env.cmd_runner(), agent_name)
- self.assertEqual(cm.exception.agent_name, "ocf:pacemaker:Dummy")
- self.assertTrue(
- cm.exception.message.startswith(
- "Element resource-agent failed to validate"
- )
+ assert_xml_equal(
+ metadata,
+ etree_to_str(ra.xml.load_metadata(env.cmd_runner(), agent_name)),
)
@@ -335,16 +338,15 @@ class LoadFakeAgentMetadata(TestCase):
def test_not_valid_xml(self):
agent_name = ra.const.PACEMAKER_FENCED
- self.config.runner.pcmk.load_fenced_metadata(stdout="<resource-agent/>")
+ metadata = "<resource-agent/>"
+ self.config.runner.pcmk.load_fenced_metadata(stdout=metadata)
env = self.env_assist.get_env()
- with self.assertRaises(ra.UnableToGetAgentMetadata) as cm:
- ra.xml.load_fake_agent_metadata(env.cmd_runner(), agent_name)
- self.assertEqual(cm.exception.agent_name, "pacemaker-fenced")
- self.assertTrue(
- cm.exception.message.startswith(
- "Element resource-agent failed to validate"
- )
+ assert_xml_equal(
+ metadata,
+ etree_to_str(
+ ra.xml.load_fake_agent_metadata(env.cmd_runner(), agent_name)
+ ),
)
@@ -549,19 +551,37 @@ class ParseOcf10BaseMixin(ParseOcfToolsMixin):
)
def test_parameters_empty_parameter(self):
- # parameters must have at least 'name' attribute
- with self.assertRaises(ra.UnableToGetAgentMetadata):
- self.parse(
- self.xml(
- """
- <resource-agent>
- <parameters>
- <parameter/>
- </parameters>
- </resource-agent>
- """
- )
- )
+ self.assert_parse_result(
+ self.xml(
+ """
+ <resource-agent>
+ <parameters>
+ <parameter/>
+ </parameters>
+ </resource-agent>
+ """
+ ),
+ ResourceAgentMetadataOcf1_0(
+ self.agent_name,
+ shortdesc=None,
+ longdesc=None,
+ parameters=[
+ ResourceAgentParameterOcf1_0(
+ name="",
+ shortdesc=None,
+ longdesc=None,
+ type="string",
+ default=None,
+ enum_values=None,
+ required=None,
+ deprecated=None,
+ obsoletes=None,
+ unique=None,
+ )
+ ],
+ actions=[],
+ ),
+ )
def test_parameters_minimal(self):
self.assert_parse_result(
@@ -708,19 +728,35 @@ class ParseOcf10BaseMixin(ParseOcfToolsMixin):
)
def test_actions_empty_action(self):
- # actions must have at least 'name' attribute
- with self.assertRaises(ra.UnableToGetAgentMetadata):
- self.parse(
- self.xml(
- """
- <resource-agent>
- <actions>
- <action/>
- </actions>
- </resource-agent>
- """
- )
- )
+ self.assert_parse_result(
+ self.xml(
+ """
+ <resource-agent>
+ <actions>
+ <action/>
+ </actions>
+ </resource-agent>
+ """
+ ),
+ ResourceAgentMetadataOcf1_0(
+ self.agent_name,
+ shortdesc=None,
+ longdesc=None,
+ parameters=[],
+ actions=[
+ ResourceAgentActionOcf1_0(
+ name="",
+ timeout=None,
+ interval=None,
+ role=None,
+ start_delay=None,
+ depth=None,
+ automatic=None,
+ on_target=None,
+ ),
+ ],
+ ),
+ )
def test_actions_multiple(self):
self.assert_parse_result(
@@ -787,21 +823,6 @@ class ParseOcf10NoVersion(ParseOcf10BaseMixin, TestCase):
class ParseOcf10UnsupportedVersion(ParseOcf10BaseMixin, TestCase):
ocf_version = "0.1.2"
- # These tests test that pcs raises an error if an agent doesn't conform to
- # OCF schema. There is, however, no validation against OCF schema for
- # agents with unsupported OCF version. That means no error message, pcs
- # tries to process the agent and crashes. However bad that sounds, it's
- # indended as that's how pcs behaved before OCF 1.1 was implemented.
- # There's therefore no point in running these tests.
-
- def test_parameters_empty_parameter(self):
- # parameters must have at least 'name' attribute
- pass
-
- def test_actions_empty_action(self):
- # actions must have at least 'name' attribute
- pass
-
class ParseOcf10ExplicitVersion(ParseOcf10BaseMixin, TestCase):
ocf_version = "1.0"
--
2.34.1