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