1    	import logging
2    	import os
3    	from unittest import mock
4    	
5    	from lxml import etree
6    	
7    	from pcs import settings
8    	from pcs.lib.external import CommandRunner
9    	
10   	from pcs_test.tools.assertions import AssertPcsMixin
11   	from pcs_test.tools.bin_mock import get_mock_settings
12   	from pcs_test.tools.custom_mock import MockLibraryReportProcessor
13   	from pcs_test.tools.misc import (
14   	    get_test_resource,
15   	    get_tmp_file,
16   	    write_file_to_tmpfile,
17   	)
18   	from pcs_test.tools.pcs_runner import PcsRunner
19   	from pcs_test.tools.xml import etree_to_str
20   	
21   	
22   	class CachedCibFixture(AssertPcsMixin):
23   	    def __init__(self, cache_name, empty_cib_path):
24   	        self._empty_cib_path = empty_cib_path
25   	        self._cache_name = cache_name
26   	        self._cache_path = None
27   	        self._pcs_runner = None
28   	
29   	    def _setup_cib(self):
30   	        raise NotImplementedError()
31   	
32   	    def set_up(self):
33   	        fixture_dir = get_test_resource("temp_fixtures")
34   	        os.makedirs(fixture_dir, exist_ok=True)
35   	        self._cache_path = os.path.join(fixture_dir, self._cache_name)
36   	        self._pcs_runner = PcsRunner(self._cache_path)
37   	        self._pcs_runner.mock_settings = get_mock_settings()
38   	
39   	        with (
40   	            open(self._empty_cib_path, "r") as template_file,
41   	            open(self.cache_path, "w") as cache_file,
42   	        ):
43   	            cache_file.write(template_file.read())
44   	        self._setup_cib()
45   	
46   	    def clean_up(self):
47   	        if os.path.isfile(self.cache_path):
48   	            os.unlink(self.cache_path)
49   	
50   	    @property
51   	    def cache_path(self):
52   	        if self._cache_path is None:
53   	            raise AssertionError("Cache has not been initialized")
54   	        return self._cache_path
55   	
56   	    # methods for supporting assert_pcs_success
57   	    @property
58   	    def pcs_runner(self):
59   	        if self._pcs_runner is None:
60   	            raise AssertionError("Cache has not been initialized")
61   	        return self._pcs_runner
62   	
63   	    def assertEqual(self, first, second, msg=None):
64   	        if first != second:
65   	            raise AssertionError(
66   	                f"{msg}\n{first} != {second}" if msg else f"{first} != {second}"
67   	            )
68   	
69   	
70   	def wrap_element_by_master(cib_file, resource_id, master_id=None):
71   	    cib_file.seek(0)
CID (unavailable; MK=78f0d48bc4ae31466f05d1dd939e338c) (#1 of 1): XML entity processing enabled (SIGMA.xml_entity_enabled):
(1) Event Sigma main event: The Python application enables entity expansion by setting the `lxml.etree.XMLParser` value `resolve_entities` to `true` or `internal` (the default value). If untrusted XML is parsed with entity expansion enabled, a malicious attacker could submit a document that contains very deeply nested entity definitions (known as a Billion Laughs Attack), causing the parser to use large amounts of memory and processing power resulting in a denial of service (DoS) condition.
(2) Event remediation: Explicitly set `resolve_entities` argument to `False`.
72   	    cib_tree = etree.parse(cib_file, etree.XMLParser(huge_tree=True)).getroot()
73   	    element = cib_tree.find(f'.//*[@id="{resource_id}"]')
74   	    final_master_id = (
75   	        master_id if master_id is not None else f"{resource_id}-master"
76   	    )
77   	    master_element = _xml_to_element(
78   	        f"""
79   	        <master id="{final_master_id}">
80   	        </master>
81   	    """
82   	    )
83   	    element.getparent().append(master_element)
84   	    master_element.append(element)
85   	    final_xml = etree_to_str(cib_tree)
86   	
87   	    environ = dict(os.environ)
88   	    environ["CIB_file"] = cib_file.name
89   	    runner = CommandRunner(
90   	        mock.MagicMock(logging.Logger), MockLibraryReportProcessor(), environ
91   	    )
92   	    stdout, stderr, retval = runner.run(
93   	        [
94   	            settings.cibadmin_exec,
95   	            "--replace",
96   	            "--scope",
97   	            "resources",
98   	            "--xml-pipe",
99   	        ],
100  	        stdin_string=final_xml,
101  	    )
102  	    assert retval == 0, (
103  	        "Error running wrap_element_by_master:\n" + stderr + "\n" + stdout
104  	    )
105  	
106  	
107  	def wrap_element_by_master_file(filepath, resource_id, master_id=None):
108  	    cib_tmp = get_tmp_file("wrap_by_master")
109  	    write_file_to_tmpfile(filepath, cib_tmp)
110  	    wrap_element_by_master(cib_tmp, resource_id, master_id=master_id)
111  	    cib_tmp.seek(0)
112  	    with open(filepath, "w") as target:
113  	        target.write(cib_tmp.read())
114  	    cib_tmp.close()
115  	
116  	
117  	def fixture_master_xml(name, all_ops=True, meta_dict=None):
118  	    default_ops = f"""
119  	            <op id="{name}-notify-interval-0s" interval="0s" name="notify"
120  	                timeout="5"
121  	            />
122  	            <op id="{name}-start-interval-0s" interval="0s" name="start"
123  	                timeout="20"
124  	            />
125  	            <op id="{name}-stop-interval-0s" interval="0s" name="stop"
126  	                timeout="20"
127  	            />
128  	    """
129  	    meta_xml = ""
130  	    if meta_dict:
131  	        meta_lines = (
132  	            [f'<meta_attributes id="{name}-master-meta_attributes">']
133  	            + [
134  	                f'<nvpair id="{name}-master-meta_attributes-{key}" name="{key}" value="{val}"/>'
135  	                for key, val in meta_dict.items()
136  	            ]
137  	            + ["</meta_attributes>"]
138  	        )
139  	        meta_xml = "\n".join(meta_lines)
140  	    master = f"""
141  	      <master id="{name}-master">
142  	        <primitive class="ocf" id="{name}" provider="pcsmock" type="stateful">
143  	          <operations>
144  	            <op id="{name}-monitor-interval-10" interval="10" name="monitor"
145  	                role="Master" timeout="20"
146  	            />
147  	            <op id="{name}-monitor-interval-11" interval="11" name="monitor"
148  	                role="Slave" timeout="20"
149  	            />
150  	    """
151  	    if all_ops:
152  	        master += default_ops
153  	    master += f"""
154  	          </operations>
155  	        </primitive>
156  	        {meta_xml}
157  	      </master>
158  	    """
159  	    return master
160  	
161  	
162  	def fixture_to_cib(cib_file, xml):
163  	    environ = dict(os.environ)
164  	    environ["CIB_file"] = cib_file
165  	    runner = CommandRunner(
166  	        mock.MagicMock(logging.Logger), MockLibraryReportProcessor(), environ
167  	    )
168  	    stdout, stderr, retval = runner.run(
169  	        [
170  	            settings.cibadmin_exec,
171  	            "--create",
172  	            "--scope",
173  	            "resources",
174  	            "--xml-text",
175  	            xml,
176  	        ]
177  	    )
178  	    assert retval == 0, (
179  	        "Error running fixture_to_cib:\n" + stderr + "\n" + stdout
180  	    )
181  	
182  	
183  	def _replace(element_to_replace, new_element):
184  	    parent = element_to_replace.getparent()
185  	    for child in parent:
186  	        if element_to_replace == child:
187  	            index = list(parent).index(child)
188  	            parent.remove(child)
189  	            parent.insert(index, new_element)
190  	            return
191  	
192  	
193  	def _xml_to_element(xml):
194  	    try:
195  	        new_element = etree.fromstring(xml)
196  	    except etree.XMLSyntaxError as e:
197  	        raise AssertionError(
198  	            "Cannot put to the cib a non-xml fragment:\n'{0}'".format(xml)
199  	        ) from e
200  	    return new_element
201  	
202  	
203  	def _find_all_in(cib_tree, element_xpath):
204  	    element_list = cib_tree.xpath(element_xpath)
205  	    if not element_list:
206  	        raise AssertionError(
207  	            "Cannot find '{0}' in given cib:\n{1}".format(
208  	                element_xpath, etree_to_str(cib_tree)
209  	            )
210  	        )
211  	    return element_list
212  	
213  	
214  	def _find_in(cib_tree, element_xpath):
215  	    element_list = _find_all_in(cib_tree, element_xpath)
216  	    if len(element_list) > 1:
217  	        raise AssertionError(
218  	            "Found more than one '{0}' in given cib:\n{1}".format(
219  	                element_xpath, etree_to_str(cib_tree)
220  	            )
221  	        )
222  	    return element_list[0]
223  	
224  	
225  	def remove(element_xpath):
226  	    def _remove(cib_tree):
227  	        xpath_list = (
228  	            [element_xpath] if isinstance(element_xpath, str) else element_xpath
229  	        )
230  	        for xpath in xpath_list:
231  	            for element_to_remove in _find_all_in(cib_tree, xpath):
232  	                element_to_remove.getparent().remove(element_to_remove)
233  	
234  	    return _remove
235  	
236  	
237  	def put_or_replace(parent_xpath, new_content):
238  	    # This transformation makes sense in "configuration" section only. In this
239  	    # section there are sub-tags (optional or mandatory) that can occur max 1x.
240  	    #
241  	    # In other sections it is possible to have more occurrences of sub-tags. For
242  	    # such cases it is better to use `replace_all` - the difference is that in
243  	    # `replace_all` the element to be replaced is specified by full xpath
244  	    # whilst in `put_or_replace` the xpath to the parent element is specified.
245  	    def replace_optional(cib_tree):
246  	        element = _xml_to_element(new_content)
247  	        parent = _find_in(cib_tree, parent_xpath)
248  	        current_elements = parent.findall(element.tag)
249  	
250  	        if len(current_elements) > 1:
251  	            raise _cannot_multireplace(element.tag, parent_xpath, cib_tree)
252  	
253  	        if current_elements:
254  	            _replace(current_elements[0], element)
255  	        else:
256  	            parent.append(element)
257  	
258  	    return replace_optional
259  	
260  	
261  	def replace_all(replacements):
262  	    """
263  	    Return a function that replace more elements (defined by replacement_dict)
264  	    in the cib_tree with new_content.
265  	
266  	    dict replacemens -- contains more replacements:
267  	        key is xpath - its destination must be one element: replacement is
268  	        applied only on the first occurrence
269  	        value is new content -contains a content that have to be placed instead
270  	        of an element found by element_xpath
271  	    """
272  	
273  	    def replace(cib_tree):
274  	        for xpath, new_content in replacements.items():
275  	            _replace(_find_in(cib_tree, xpath), _xml_to_element(new_content))
276  	
277  	    return replace
278  	
279  	
280  	def append_all(append_map):
281  	    """
282  	    Return a function that appends more elements after specified (xpath) element
283  	    dict append_map -- a key is an xpath pointing to a target element (for
284  	        appending), value is appended content
285  	    """
286  	
287  	    def append(cib_tree):
288  	        for xpath, new_content in append_map.items():
289  	            _find_in(cib_tree, xpath).append(_xml_to_element(new_content))
290  	
291  	    return append
292  	
293  	
294  	# Possible modifier shortcuts are defined here.
295  	# Keep in mind that every key will be named parameter in config function
296  	# (see modifier_shortcuts param in some of pcs_test.tools.command_env.config_*
297  	# modules)
298  	#
299  	# DO NOT USE CONFLICTING KEYS HERE!
300  	# 1) args of pcs_test.tools.command_env.calls#CallListBuilder.place:
301  	#  name, before, instead
302  	# 2) args of pcs_test.tools.command_env.mock_runner#Call.__init__
303  	#  command, stdout, stderr, returncode, check_stdin
304  	# 3) special args of pcs_test.tools.command_env.config_*
305  	#  modifiers, filename, load_key, wait, exception
306  	# It would be not applied. Not even mention that the majority of these names do
307  	# not make sense for a cib modifying ;)
308  	MODIFIER_GENERATORS = {
309  	    "remove": remove,
310  	    "replace": replace_all,
311  	    "append": append_all,
312  	    "resources": lambda xml: replace_all({"./configuration/resources": xml}),
313  	    "nodes": lambda xml: replace_all({"./configuration/nodes": xml}),
314  	    "constraints": lambda xml: replace_all(
315  	        {"./configuration/constraints": xml}
316  	    ),
317  	    "crm_config": lambda xml: replace_all({"./configuration/crm_config": xml}),
318  	    "fencing_topology": lambda xml: put_or_replace("./configuration", xml),
319  	    "status": lambda xml: put_or_replace(".", xml),
320  	    "tags": lambda xml: put_or_replace("./configuration", xml),
321  	    "acls": lambda xml: put_or_replace("./configuration", xml),
322  	    "optional_in_conf": lambda xml: put_or_replace("./configuration", xml),
323  	    # common modifier `put_or_replace` makes not sense - see explanation inside
324  	    # this function - all occurrences should be satisfied by `optional_in_conf`
325  	}
326  	
327  	
328  	def create_modifiers(**modifier_shortcuts):
329  	    """
330  	    Return list of modifiers: list of functions that transform cib
331  	
332  	    dict modifier_shortcuts -- a new modifier is generated from each modifier
333  	        shortcut.
334  	        As key there can be keys of MODIFIER_GENERATORS.
335  	        Value is passed into appropriate generator from MODIFIER_GENERATORS.
336  	
337  	    """
338  	    unknown_shortcuts = set(modifier_shortcuts.keys()) - set(
339  	        MODIFIER_GENERATORS.keys()
340  	    )
341  	    if unknown_shortcuts:
342  	        raise AssertionError(
343  	            "Unknown modifier shortcuts '{0}', available are: '{1}'".format(
344  	                "', '".join(list(unknown_shortcuts)),
345  	                "', '".join(MODIFIER_GENERATORS.keys()),
346  	            )
347  	        )
348  	
349  	    return [
350  	        MODIFIER_GENERATORS[name](param)
351  	        for name, param in modifier_shortcuts.items()
352  	    ]
353  	
354  	
355  	def modify_cib(cib_xml, modifiers=None, **modifier_shortcuts):
356  	    """
357  	    Apply modifiers to cib_xml and return the result cib_xml
358  	
359  	    string cib_xml -- initial cib
360  	    list of callable modifiers -- each takes cib (etree.Element)
361  	    dict modifier_shortcuts -- a new modifier is generated from each modifier
362  	        shortcut.
363  	        As key there can be keys of MODIFIER_GENERATORS.
364  	        Value is passed into appropriate generator from MODIFIER_GENERATORS.
365  	    """
366  	    modifiers = modifiers if modifiers else []
367  	    all_modifiers = modifiers + create_modifiers(**modifier_shortcuts)
368  	
369  	    if not all_modifiers:
370  	        return cib_xml
371  	
372  	    cib_tree = etree.fromstring(cib_xml)
373  	    for modify in all_modifiers:
374  	        modify(cib_tree)
375  	
376  	    return etree_to_str(cib_tree)
377  	
378  	
379  	def modify_cib_file(file_path, **modifiers_shortcuts):
380  	    with open(file_path, "r") as file:
381  	        return modify_cib(file.read(), **modifiers_shortcuts)
382  	
383  	
384  	def _cannot_multireplace(tag, parent_xpath, cib_tree):
385  	    return AssertionError(
386  	        (
387  	            "Cannot replace '{element}' in '{parent}' because '{parent}'"
388  	            " contains more than one '{element}' in given cib:\n{cib}"
389  	        ).format(element=tag, parent=parent_xpath, cib=etree_to_str(cib_tree))
390  	    )
391