1    	import base64
2    	import getpass
3    	import json
4    	import logging
5    	import os
6    	import re
7    	import signal
8    	import subprocess
9    	import sys
10   	import tarfile
11   	import tempfile
12   	import threading
13   	import time
14   	import xml.dom.minidom
15   	from functools import lru_cache
16   	from io import BytesIO
17   	from textwrap import dedent
18   	from typing import TYPE_CHECKING, Any, cast
19   	from urllib.parse import urlencode
20   	from xml.dom.minidom import Document as DomDocument
21   	from xml.dom.minidom import parseString
22   	
23   	import pcs.cli.booth.env
24   	import pcs.lib.corosync.config_parser as corosync_conf_parser
25   	from pcs import settings, usage
26   	from pcs.cli.cluster_property.output import PropertyConfigurationFacade
27   	from pcs.cli.common import middleware
28   	from pcs.cli.common.env_cli import Env
29   	from pcs.cli.common.errors import CmdLineInputError
30   	from pcs.cli.common.lib_wrapper import Library
31   	from pcs.cli.common.parse_args import InputModifiers
32   	from pcs.cli.common.tools import print_to_stderr, timeout_to_seconds_legacy
33   	from pcs.cli.file import metadata as cli_file_metadata
34   	from pcs.cli.reports import ReportProcessorToConsole, process_library_reports
35   	from pcs.cli.reports import output as reports_output
36   	from pcs.common import const, file_type_codes
37   	from pcs.common import file as pcs_file
38   	from pcs.common import pacemaker as common_pacemaker
39   	from pcs.common import pcs_pycurl as pycurl
40   	from pcs.common.host import PcsKnownHost
41   	from pcs.common.pacemaker.resource.operations import (
42   	    OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME,
43   	)
44   	from pcs.common.reports import ReportProcessor
45   	from pcs.common.reports.messages import CibUpgradeFailedToMinimalRequiredVersion
46   	from pcs.common.services.errors import ManageServiceError
47   	from pcs.common.services.interfaces import ServiceManagerInterface
48   	from pcs.common.str_tools import format_list
49   	from pcs.common.tools import Version, timeout_to_seconds
50   	from pcs.common.types import StringSequence
51   	from pcs.lib.corosync.config_facade import ConfigFacade as corosync_conf_facade
52   	from pcs.lib.env import LibraryEnvironment
53   	from pcs.lib.errors import LibraryError
54   	from pcs.lib.external import CommandRunner, is_proxy_set
55   	from pcs.lib.file.instance import FileInstance as LibFileInstance
56   	from pcs.lib.host.config.facade import Facade as KnownHostsFacade
57   	from pcs.lib.interface.config import ParserErrorException
58   	from pcs.lib.pacemaker.live import get_cluster_status_dom
59   	from pcs.lib.pacemaker.state import ClusterState
60   	from pcs.lib.pacemaker.values import is_score as is_score_value
61   	from pcs.lib.pacemaker.values import validate_id
62   	from pcs.lib.services import get_service_manager as _get_service_manager
63   	from pcs.lib.services import service_exception_to_report
64   	
65   	if TYPE_CHECKING:
66   	    from pcs.common.reports.item import ReportItemList
67   	
68   	
69   	# usefile & filename variables are set in pcs module
70   	usefile = False
71   	filename = ""
72   	# Note: not properly typed
73   	pcs_options: dict[Any, Any] = {}
74   	
75   	
76   	def _getValidateWithVersion(dom) -> Version:
77   	    """
78   	    Commandline options: no options
79   	    """
80   	    cib = dom.getElementsByTagName("cib")
81   	    if len(cib) != 1:
82   	        err("Bad cib")
83   	
84   	    cib = cib[0]
85   	
86   	    version = cib.getAttribute("validate-with")
87   	    r = re.compile(r"pacemaker-(\d+)\.(\d+)\.?(\d+)?")
88   	    m = r.match(version)
89   	    if m is None:
90   	        raise AssertionError()
91   	    major = int(m.group(1))
92   	    minor = int(m.group(2))
93   	    rev = int(m.group(3) or 0)
94   	    return Version(major, minor, rev)
95   	
96   	
97   	def isCibVersionSatisfied(cib_dom, required_version: Version) -> bool:
98   	    if not isinstance(cib_dom, DomDocument):
99   	        cib_dom = cib_dom.ownerDocument
100  	    return _getValidateWithVersion(cib_dom) >= required_version
101  	
102  	
103  	# Check the current pacemaker version in cib and upgrade it if necessary
104  	# Returns False if not upgraded and True if upgraded
105  	def _checkAndUpgradeCIB(required_version: Version) -> bool:
106  	    """
107  	    Commandline options:
108  	      * -f - CIB file
109  	    """
110  	    if isCibVersionSatisfied(get_cib_dom(), required_version):
111  	        return False
112  	    cluster_upgrade()
113  	    return True
114  	
115  	
116  	def cluster_upgrade():
117  	    """
118  	    Commandline options:
119  	      * -f - CIB file
120  	    """
121  	    output, retval = run(["cibadmin", "--upgrade", "--force"])
122  	    if retval != 0:
123  	        err("unable to upgrade cluster: %s" % output)
124  	    if (
125  	        output.strip()
126  	        == "Upgrade unnecessary: Schema is already the latest available"
127  	    ):
128  	        return
129  	    print_to_stderr("Cluster CIB has been upgraded to latest version")
130  	
131  	
132  	def cluster_upgrade_to_version(required_version: Version) -> Any:
133  	    """
134  	    Commandline options:
135  	      * -f - CIB file
136  	    """
137  	    _checkAndUpgradeCIB(required_version)
138  	    dom = get_cib_dom()
139  	    current_version = _getValidateWithVersion(dom)
140  	    if current_version < required_version:
141  	        err(
142  	            CibUpgradeFailedToMinimalRequiredVersion(
143  	                str(current_version),
144  	                str(required_version),
145  	            ).message
146  	        )
147  	    return dom
148  	
149  	
150  	# Check status of node
151  	def checkStatus(node):
152  	    """
153  	    Commandline options:
154  	      * --request-timeout - timeout for HTTP requests
155  	    """
156  	    return sendHTTPRequest(
157  	        node, "remote/status", urlencode({"version": "2"}), False, False
158  	    )
159  	
160  	
161  	# Check and see if we're authorized (faster than a status check)
162  	def checkAuthorization(node):
163  	    """
164  	    Commandline options:
165  	      * --request-timeout - timeout for HTTP requests
166  	    """
167  	    return sendHTTPRequest(node, "remote/check_auth", None, False, False)
168  	
169  	
170  	def get_uid_gid_file_name(uid, gid):
171  	    """
172  	    Commandline options: no options
173  	    """
174  	    return "pcs-uidgid-%s-%s" % (uid, gid)
175  	
176  	
177  	# Reads in uid file and returns dict of values {'uid':'theuid', 'gid':'thegid'}
178  	def read_uid_gid_file(uidgid_filename):
179  	    """
180  	    Commandline options: no options
181  	    """
182  	    uidgid = {}
183  	    with open(
184  	        os.path.join(settings.corosync_uidgid_dir, uidgid_filename), "r"
185  	    ) as myfile:
186  	        data = myfile.read().split("\n")
187  	    in_uidgid = False
188  	    for data_line in data:
189  	        line = re.sub(r"#.*", "", data_line)
190  	        if not in_uidgid:
191  	            if re.search(r"uidgid.*{", line):
192  	                in_uidgid = True
193  	            else:
194  	                continue
195  	        matches = re.search(r"uid:\s*(\S+)", line)
196  	        if matches:
197  	            uidgid["uid"] = matches.group(1)
198  	
199  	        matches = re.search(r"gid:\s*(\S+)", line)
200  	        if matches:
201  	            uidgid["gid"] = matches.group(1)
202  	
203  	    return uidgid
204  	
205  	
206  	def get_uidgid_file_content(
207  	    uid: str | None = None, gid: str | None = None
208  	) -> str | None:
209  	    if not uid and not gid:
210  	        return None
211  	    uid_gid_lines = []
212  	    if uid:
213  	        uid_gid_lines.append(f"  uid: {uid}")
214  	    if gid:
215  	        uid_gid_lines.append(f"  gid: {gid}")
216  	    return dedent(
217  	        """\
218  	        uidgid {{
219  	        {uid_gid_keys}
220  	        }}
221  	        """
222  	    ).format(uid_gid_keys="\n".join(uid_gid_lines))
223  	
224  	
225  	def write_uid_gid_file(uid, gid):
226  	    """
227  	    Commandline options: no options
228  	    """
229  	    orig_filename = get_uid_gid_file_name(uid, gid)
230  	    uidgid_filename = orig_filename
231  	    counter = 0
232  	    if find_uid_gid_files(uid, gid):
233  	        err("uidgid file with uid=%s and gid=%s already exists" % (uid, gid))
234  	
235  	    while os.path.exists(
236  	        os.path.join(settings.corosync_uidgid_dir, uidgid_filename)
237  	    ):
238  	        counter = counter + 1
239  	        uidgid_filename = orig_filename + "-" + str(counter)
240  	
241  	    data = get_uidgid_file_content(uid, gid)
242  	    if data:
243  	        with open(
244  	            os.path.join(settings.corosync_uidgid_dir, uidgid_filename), "w"
245  	        ) as uidgid_file:
246  	            uidgid_file.write(data)
247  	
248  	
249  	def find_uid_gid_files(uid, gid):
250  	    """
251  	    Commandline options: no options
252  	    """
253  	    if uid == "" and gid == "":
254  	        return []
255  	
256  	    found_files = []
257  	    uid_gid_files = os.listdir(settings.corosync_uidgid_dir)
258  	    for uidgid_file in uid_gid_files:
259  	        uid_gid_dict = read_uid_gid_file(uidgid_file)
260  	        if ("uid" in uid_gid_dict and uid == "") or (
261  	            "uid" not in uid_gid_dict and uid != ""
262  	        ):
263  	            continue
264  	        if ("gid" in uid_gid_dict and gid == "") or (
265  	            "gid" not in uid_gid_dict and gid != ""
266  	        ):
267  	            continue
268  	        if "uid" in uid_gid_dict and uid != uid_gid_dict["uid"]:
269  	            continue
270  	        if "gid" in uid_gid_dict and gid != uid_gid_dict["gid"]:
271  	            continue
272  	
273  	        found_files.append(uidgid_file)
274  	
275  	    return found_files
276  	
277  	
278  	# Removes all uid/gid files with the specified uid/gid, returns false if we
279  	# couldn't find one
280  	def remove_uid_gid_file(uid, gid):
281  	    """
282  	    Commandline options: no options
283  	    """
284  	    if uid == "" and gid == "":
285  	        return False
286  	
287  	    file_removed = False
288  	    for uidgid_file in find_uid_gid_files(uid, gid):
289  	        os.remove(os.path.join(settings.corosync_uidgid_dir, uidgid_file))
290  	        file_removed = True
291  	
292  	    return file_removed
293  	
294  	
295  	@lru_cache
296  	def read_known_hosts_file() -> dict[str, PcsKnownHost]:
297  	    return read_known_hosts_file_not_cached()
298  	
299  	
300  	def read_known_hosts_file_not_cached() -> dict[str, PcsKnownHost]:
301  	    """
302  	    Commandline options: no options
303  	    """
304  	    try:
305  	        if os.getuid() != 0:
306  	            known_hosts_raw_file = pcs_file.RawFile(
307  	                cli_file_metadata.for_file_type(file_type_codes.PCS_KNOWN_HOSTS)
308  	            )
309  	            # json.loads handles bytes, it expects utf-8, 16 or 32 encoding
310  	            known_hosts_struct = json.loads(known_hosts_raw_file.read())
311  	            # TODO use known hosts facade for getting info from json struct once the
312  	            # facade exists
313  	            return {
314  	                name: PcsKnownHost.from_known_host_file_dict(name, host)
315  	                for name, host in known_hosts_struct["known_hosts"].items()
316  	            }
317  	        # TODO remove
318  	        # This is here to provide known-hosts to functions not yet
319  	        # overhauled to pcs.lib. Cli should never read known hosts from
320  	        # /var/lib/pcsd/.
321  	        known_hosts_instance = LibFileInstance.for_known_hosts()
322  	        known_hosts_facade = cast(
323  	            KnownHostsFacade, known_hosts_instance.read_to_facade()
324  	        )
325  	        return known_hosts_facade.known_hosts
326  	
327  	    except LibraryError as e:
328  	        # TODO remove
329  	        # This is here to provide known-hosts to functions not yet
330  	        # overhauled to pcs.lib. Cli should never read known hosts from
331  	        # /var/lib/pcsd/.
332  	        process_library_reports(list(e.args))
333  	    except ParserErrorException as e:
334  	        # TODO remove
335  	        # This is here to provide known-hosts to functions not yet
336  	        # overhauled to pcs.lib. Cli should never read known hosts from
337  	        # /var/lib/pcsd/.
338  	        process_library_reports(
339  	            known_hosts_instance.parser_exception_to_report_list(e)
340  	        )
341  	    except pcs_file.RawFileError as e:
342  	        reports_output.warn("Unable to read the known-hosts file: " + e.reason)
343  	    except json.JSONDecodeError as e:
344  	        reports_output.warn(f"Unable to parse the known-hosts file: {e}")
345  	    except (TypeError, KeyError):
346  	        reports_output.warn("Warning: Unable to parse the known-hosts file.")
347  	    return {}
348  	
349  	
350  	def repeat_if_timeout(send_http_request_function, repeat_count=15):
351  	    """
352  	    Commandline options: no options
353  	    NOTE: callback send_http_request_function may use --request-timeout
354  	    """
355  	
356  	    def repeater(node, *args, **kwargs):
357  	        repeats_left = repeat_count
358  	        while True:
359  	            retval, output = send_http_request_function(node, *args, **kwargs)
360  	            if (
361  	                retval != 2
362  	                or "Operation timed out" not in output
363  	                or repeats_left < 1
364  	            ):
365  	                # did not timed out OR repeat limit exceeded
366  	                return retval, output
367  	            repeats_left = repeats_left - 1
368  	            if "--debug" in pcs_options:
369  	                print_to_stderr(f"{node}: {output}, trying again...")
370  	
371  	    return repeater
372  	
373  	
374  	def setCorosyncConfig(node, config):
375  	    """
376  	    Commandline options:
377  	      * --request-timeout - timeout for HTTP requests
378  	    """
379  	    data = urlencode({"corosync_conf": config})
380  	    (status, data) = sendHTTPRequest(node, "remote/set_corosync_conf", data)
381  	    if status != 0:
382  	        err("Unable to set corosync config: {0}".format(data))
383  	
384  	
385  	def getPacemakerNodeStatus(node):
386  	    """
387  	    Commandline options:
388  	      * --request-timeout - timeout for HTTP requests
389  	    """
390  	    return sendHTTPRequest(
391  	        node, "remote/pacemaker_node_status", None, False, False
392  	    )
393  	
394  	
395  	def startCluster(node, quiet=False, timeout=None):
396  	    """
397  	    Commandline options:
398  	      * --request-timeout - timeout for HTTP requests
399  	    """
400  	    return sendHTTPRequest(
401  	        node,
402  	        "remote/cluster_start",
403  	        printResult=False,
404  	        printSuccess=not quiet,
405  	        timeout=timeout,
406  	    )
407  	
408  	
409  	def stopPacemaker(node, quiet=False, force=True):
410  	    """
411  	    Commandline options:
412  	      * --request-timeout - timeout for HTTP requests
413  	    """
414  	    return stopCluster(
415  	        node, pacemaker=True, corosync=False, quiet=quiet, force=force
416  	    )
417  	
418  	
419  	def stopCorosync(node, quiet=False, force=True):
420  	    """
421  	    Commandline options:
422  	      * --request-timeout - timeout for HTTP requests
423  	    """
424  	    return stopCluster(
425  	        node, pacemaker=False, corosync=True, quiet=quiet, force=force
426  	    )
427  	
428  	
429  	def stopCluster(node, quiet=False, pacemaker=True, corosync=True, force=True):
430  	    """
431  	    Commandline options:
432  	      * --request-timeout - timeout for HTTP requests
433  	    """
434  	    data = {}
435  	    timeout = None
436  	    if pacemaker and not corosync:
437  	        data["component"] = "pacemaker"
438  	        timeout = 2 * 60
439  	    elif corosync and not pacemaker:
440  	        data["component"] = "corosync"
441  	    if force:
442  	        data["force"] = 1
443  	    data = urlencode(data)
444  	    return sendHTTPRequest(
445  	        node,
446  	        "remote/cluster_stop",
447  	        data,
448  	        printResult=False,
449  	        printSuccess=not quiet,
450  	        timeout=timeout,
451  	    )
452  	
453  	
454  	def enableCluster(node):
455  	    """
456  	    Commandline options:
457  	      * --request-timeout - timeout for HTTP requests
458  	    """
459  	    return sendHTTPRequest(node, "remote/cluster_enable", None, False, True)
460  	
461  	
462  	def disableCluster(node):
463  	    """
464  	    Commandline options:
465  	      * --request-timeout - timeout for HTTP requests
466  	    """
467  	    return sendHTTPRequest(node, "remote/cluster_disable", None, False, True)
468  	
469  	
470  	def destroyCluster(node, quiet=False):
471  	    """
472  	    Commandline options:
473  	      * --request-timeout - timeout for HTTP requests
474  	    """
475  	    return sendHTTPRequest(
476  	        node, "remote/cluster_destroy", None, not quiet, not quiet
477  	    )
478  	
479  	
480  	def restoreConfig(node, tarball_data):
481  	    """
482  	    Commandline options:
483  	      * --request-timeout - timeout for HTTP requests
484  	    """
485  	    data = urlencode({"tarball": tarball_data})
486  	    return sendHTTPRequest(node, "remote/config_restore", data, False, True)
487  	
488  	
489  	def pauseConfigSyncing(node, delay_seconds=300):
490  	    """
491  	    Commandline options:
492  	      * --request-timeout - timeout for HTTP requests
493  	    """
494  	    data = urlencode({"sync_thread_pause": delay_seconds})
495  	    return sendHTTPRequest(node, "remote/set_sync_options", data, False, False)
496  	
497  	
498  	# Send an HTTP request to a node return a tuple with status, data
499  	# If status is 0 then data contains server response
500  	# Otherwise if non-zero then data contains error message
501  	# Returns a tuple (error, error message)
502  	# 0 = Success,
503  	# 1 = HTTP Error
504  	# 2 = No response,
505  	# 3 = Auth Error
506  	# 4 = Permission denied
507  	def sendHTTPRequest(  # noqa: PLR0912, PLR0915
508  	    host, request, data=None, printResult=True, printSuccess=True, timeout=None
509  	):
510  	    """
511  	    Commandline options:
512  	      * --request-timeout - timeout for HTTP requests
513  	      * --debug
514  	    """
515  	    port = None
516  	    addr = host
517  	    token = None
518  	    known_host = read_known_hosts_file().get(host, None)
519  	    # TODO: do not allow communication with unknown host
520  	    if known_host:
521  	        port = known_host.dest.port
522  	        addr = known_host.dest.addr
523  	        token = known_host.token
524  	    if port is None:
525  	        port = settings.pcsd_default_port
526  	    url = "https://{host}:{port}/{request}".format(
527  	        host="[{0}]".format(addr) if ":" in addr else addr,
528  	        request=request,
529  	        port=port,
530  	    )
531  	    if "--debug" in pcs_options:
532  	        print_to_stderr(f"Sending HTTP Request to: {url}\nData: {data}")
533  	
534  	    def __debug_callback(data_type, debug_data):
535  	        prefixes = {
536  	            pycurl.DEBUG_TEXT: b"* ",
537  	            pycurl.DEBUG_HEADER_IN: b"< ",
538  	            pycurl.DEBUG_HEADER_OUT: b"> ",
539  	            pycurl.DEBUG_DATA_IN: b"<< ",
540  	            pycurl.DEBUG_DATA_OUT: b">> ",
541  	        }
542  	        if data_type in prefixes:
543  	            debug_output.write(prefixes[data_type])
544  	            debug_output.write(debug_data)
545  	            if not debug_data.endswith(b"\n"):
546  	                debug_output.write(b"\n")
547  	
548  	    output = BytesIO()
549  	    debug_output = BytesIO()
550  	    cookies = __get_cookie_list(token)
551  	    if not timeout:
552  	        timeout = settings.default_request_timeout
553  	    timeout = pcs_options.get("--request-timeout", timeout)
554  	
555  	    handler = pycurl.Curl()
556  	    handler.setopt(pycurl.PROTOCOLS, pycurl.PROTO_HTTPS)
557  	    handler.setopt(pycurl.URL, url.encode("utf-8"))
558  	    handler.setopt(pycurl.WRITEFUNCTION, output.write)
559  	    handler.setopt(pycurl.VERBOSE, 1)
560  	    handler.setopt(pycurl.NOSIGNAL, 1)  # required for multi-threading
561  	    handler.setopt(pycurl.DEBUGFUNCTION, __debug_callback)
562  	    handler.setopt(pycurl.TIMEOUT_MS, int(timeout * 1000))
563  	    handler.setopt(pycurl.SSL_VERIFYHOST, 0)
564  	    handler.setopt(pycurl.SSL_VERIFYPEER, 0)
565  	    handler.setopt(pycurl.HTTPHEADER, ["Expect: "])
566  	    if cookies:
567  	        handler.setopt(pycurl.COOKIE, ";".join(cookies).encode("utf-8"))
568  	    if data:
569  	        handler.setopt(pycurl.COPYPOSTFIELDS, data.encode("utf-8"))
570  	    try:
571  	        handler.perform()
572  	        response_data = output.getvalue().decode("utf-8")
573  	        response_code = handler.getinfo(pycurl.RESPONSE_CODE)
574  	        if printResult or printSuccess:
575  	            print_to_stderr(host + ": " + response_data.strip())
576  	        if "--debug" in pcs_options:
577  	            print_to_stderr(
578  	                "Response Code: {response_code}\n"
579  	                "--Debug Response Start--\n"
580  	                "{response_data}\n"
581  	                "--Debug Response End--\n"
582  	                "Communication debug info for calling: {url}\n"
583  	                "--Debug Communication Output Start--\n"
584  	                "{debug_comm_output}\n"
585  	                "--Debug Communication Output End--".format(
586  	                    response_code=response_code,
587  	                    response_data=response_data,
588  	                    url=url,
589  	                    debug_comm_output=debug_output.getvalue().decode(
590  	                        "utf-8", "ignore"
591  	                    ),
592  	                )
593  	            )
594  	
595  	        if response_code == 401:
596  	            output = (
597  	                3,
598  	                (
599  	                    "Unable to authenticate to {node} - (HTTP error: {code}), "
600  	                    "try running 'pcs host auth {node}'"
601  	                ).format(node=host, code=response_code),
602  	            )
603  	        elif response_code == 403:
604  	            output = (
605  	                4,
606  	                "{node}: Permission denied - (HTTP error: {code})".format(
607  	                    node=host, code=response_code
608  	                ),
609  	            )
610  	        elif response_code >= 400:
611  	            output = (
612  	                1,
613  	                "Error connecting to {node} - (HTTP error: {code})".format(
614  	                    node=host, code=response_code
615  	                ),
616  	            )
617  	        else:
618  	            output = (0, response_data)
619  	
620  	        if printResult and output[0] != 0:
621  	            print_to_stderr(output[1])
622  	
623  	        return output
624  	    except pycurl.error as e:
625  	        if is_proxy_set(os.environ):
626  	            reports_output.warn(
627  	                "Proxy is set in environment variables, try disabling it"
628  	            )
629  	        dummy_errno, reason = e.args
630  	        if "--debug" in pcs_options:
631  	            print_to_stderr(f"Response Reason: {reason}")
632  	        msg = (
633  	            "Unable to connect to {host}, check if pcsd is running there or try "
634  	            "setting higher timeout with --request-timeout option ({reason})"
635  	        ).format(host=host, reason=reason)
636  	        if printResult:
637  	            print_to_stderr(msg)
638  	        return (2, msg)
639  	
640  	
641  	def __get_cookie_list(token):
642  	    """
643  	    Commandline options: no options
644  	    """
645  	    cookies = []
646  	    if token:
647  	        cookies.append("token=" + token)
648  	    if os.geteuid() == 0:
649  	        for name in ("CIB_user", "CIB_user_groups"):
650  	            if name in os.environ and os.environ[name].strip():
651  	                value = os.environ[name].strip()
652  	                # Let's be safe about characters in env variables and do base64.
653  	                # We cannot do it for CIB_user however to be backward compatible
654  	                # so we at least remove disallowed characters.
655  	                if name == "CIB_user":
656  	                    value = re.sub(r"[^!-~]", "", value).replace(";", "")
657  	                else:
658  	                    # python3 requires the value to be bytes not str
659  	                    value = base64.b64encode(value.encode("utf8")).decode(
660  	                        "utf-8"
661  	                    )
662  	                cookies.append("{0}={1}".format(name, value))
663  	    return cookies
664  	
665  	
666  	def get_corosync_conf_facade(conf_text=None):
667  	    """
668  	    Commandline options:
669  	      * --corosync_conf - path to a mocked corosync.conf is set directly to
670  	        settings
671  	    """
672  	    try:
673  	        return corosync_conf_facade(
674  	            corosync_conf_parser.Parser.parse(
675  	                (getCorosyncConf() if conf_text is None else conf_text).encode(
676  	                    "utf-8"
677  	                )
678  	            )
679  	        )
680  	    except corosync_conf_parser.CorosyncConfParserException as e:
681  	        return err("Unable to parse corosync.conf: %s" % e)
682  	
683  	
684  	def getNodeAttributesFromPacemaker():
685  	    """
686  	    Commandline options: no options
687  	    """
688  	    try:
689  	        return [
690  	            node.attrs
691  	            for node in ClusterState(
692  	                get_cluster_status_dom(cmd_runner())
693  	            ).node_section.nodes
694  	        ]
695  	    except LibraryError as e:
696  	        return process_library_reports(e.args)
697  	
698  	
699  	def hasCorosyncConf():
700  	    """
701  	    Commandline options:
702  	      * --corosync_conf - path to a mocked corosync.conf is set directly to
703  	        settings
704  	    """
705  	    return os.path.isfile(settings.corosync_conf_file)
706  	
707  	
708  	def getCorosyncConf():
709  	    """
710  	    Commandline options:
711  	      * --corosync_conf - path to a mocked corosync.conf is set directly to
712  	        settings
713  	    """
714  	    corosync_conf_content = None
715  	    try:
716  	        with open(
717  	            settings.corosync_conf_file, "r", encoding="utf-8"
718  	        ) as corosync_conf_file:
719  	            corosync_conf_content = corosync_conf_file.read()
720  	    except OSError as e:
721  	        err("Unable to read %s: %s" % (settings.corosync_conf_file, e.strerror))
722  	    return corosync_conf_content
723  	
724  	
725  	def getCorosyncActiveNodes():
726  	    """
727  	    Commandline options: no options
728  	    """
729  	    output, retval = run(["corosync-cmapctl"])
730  	    if retval != 0:
731  	        return []
732  	
733  	    nodename_re = re.compile(r"^nodelist\.node\.(\d+)\.name .*= (.*)", re.M)
734  	    nodestatus_re = re.compile(
735  	        r"^runtime\.members\.(\d+).status .*= (.*)", re.M
736  	    )
737  	    nodenameid_mapping_re = re.compile(
738  	        r"nodelist\.node\.(\d+)\.nodeid .*= (\d+)", re.M
739  	    )
740  	
741  	    node_names = nodename_re.findall(output)
742  	
743  	    index_to_id = dict(nodenameid_mapping_re.findall(output))
744  	    id_to_status = dict(nodestatus_re.findall(output))
745  	
746  	    node_status = {}
747  	    for index, node_name in node_names:
748  	        if index in index_to_id:
749  	            nodeid = index_to_id[index]
750  	            if nodeid in id_to_status:
751  	                node_status[node_name] = id_to_status[nodeid]
752  	        else:
753  	            print_to_stderr(f"Error mapping {node_name}")
754  	
755  	    nodes_active = []
756  	    for node, status in node_status.items():
757  	        if status == "joined":
758  	            nodes_active.append(node)
759  	
760  	    return nodes_active
761  	
762  	
763  	# is it needed to handle corosync-qdevice service when managing cluster services
764  	def need_to_handle_qdevice_service():
765  	    """
766  	    Commandline options: no options
767  	      * --corosync_conf - path to a mocked corosync.conf is set directly to
768  	        settings but it doesn't make sense for contexts in which this function
769  	        is used
770  	    """
771  	    try:
772  	        with open(settings.corosync_conf_file, "rb") as corosync_conf_file:
773  	            return (
774  	                corosync_conf_facade(
775  	                    corosync_conf_parser.Parser.parse(corosync_conf_file.read())
776  	                ).get_quorum_device_model()
777  	                is not None
778  	            )
779  	    except (OSError, corosync_conf_parser.CorosyncConfParserException):
780  	        # corosync.conf not present or not valid => no qdevice specified
781  	        return False
782  	
783  	
784  	# Restore default behavior before starting subprocesses
785  	def subprocess_setup():
786  	    signal.signal(signal.SIGPIPE, signal.SIG_DFL)
787  	
788  	
789  	def touch_cib_file(cib_filename):
790  	    if not os.path.isfile(cib_filename):
791  	        try:
792  	            write_empty_cib(cib_filename)
793  	        except OSError as e:
794  	            err(
795  	                "Unable to write to file: '{0}': '{1}'".format(
796  	                    cib_filename, str(e)
797  	                )
798  	            )
799  	
800  	
801  	# Run command, with environment and return (output, retval)
802  	# DEPRECATED, please use lib.external.CommandRunner via utils.cmd_runner()
803  	def run(
804  	    args,
805  	    ignore_stderr=False,
806  	    string_for_stdin=None,
807  	    env_extend=None,
808  	    binary_output=False,
809  	):
810  	    """
811  	    Commandline options:
812  	      * -f - CIB file (effective only for some pacemaker tools)
813  	      * --debug
814  	    """
815  	    if not env_extend:
816  	        env_extend = {}
817  	    env_var = env_extend
818  	    env_var.update(dict(os.environ))
819  	    env_var["LC_ALL"] = "C"
820  	    if usefile:
821  	        env_var["CIB_file"] = filename
822  	        touch_cib_file(filename)
823  	
824  	    command = args[0]
825  	    if command[0:3] == "crm" or command in [
826  	        "cibadmin",
827  	        "iso8601",
828  	        "stonith_admin",
829  	    ]:
830  	        args[0] = os.path.join(settings.pacemaker_execs, command)
831  	    elif command[0:8] == "corosync":
832  	        args[0] = os.path.join(settings.corosync_execs, command)
833  	
834  	    try:
835  	        if "--debug" in pcs_options:
836  	            print_to_stderr("Running: " + " ".join(args))
837  	            if string_for_stdin:
838  	                print_to_stderr(
839  	                    f"--Debug Input Start--\n"
840  	                    f"{string_for_stdin}\n"
841  	                    f"--Debug Input End--"
842  	                )
843  	
844  	        # Some commands react differently if you give them anything via stdin
845  	        if string_for_stdin is not None:
846  	            stdin_pipe = subprocess.PIPE
847  	        else:
848  	            stdin_pipe = subprocess.DEVNULL
849  	
850  	        p = subprocess.Popen(
851  	            args,
852  	            stdin=stdin_pipe,
853  	            stdout=subprocess.PIPE,
854  	            stderr=(subprocess.PIPE if ignore_stderr else subprocess.STDOUT),
855  	            preexec_fn=subprocess_setup,  # noqa: PLW1509
856  	            close_fds=True,
857  	            env=env_var,
858  	            # decodes newlines and in python3 also converts bytes to str
859  	            universal_newlines=(not binary_output),
860  	        )
861  	        output, dummy_stderror = p.communicate(string_for_stdin)
862  	        retval = p.returncode
863  	        if "--debug" in pcs_options:
864  	            print_to_stderr(
865  	                "Return Value: {retval}\n"
866  	                "--Debug Output Start--\n"
867  	                "{debug_output}\n"
868  	                "--Debug Output End--".format(
869  	                    retval=retval,
870  	                    debug_output=output.rstrip(),
871  	                )
872  	            )
873  	    except OSError as e:
874  	        print_to_stderr(e.strerror)
875  	        err("unable to locate command: " + args[0])
876  	
877  	    return output, retval
878  	
879  	
880  	def cmd_runner(cib_file_override=None):
881  	    """
882  	    Commandline options:
883  	      * -f - CIB file
884  	    """
885  	    env_vars = {}
886  	    if usefile:
887  	        env_vars["CIB_file"] = filename
888  	    if cib_file_override:
889  	        env_vars["CIB_file"] = cib_file_override
890  	    env_vars.update(os.environ)
891  	    env_vars["LC_ALL"] = "C"
892  	    return CommandRunner(
893  	        logging.getLogger("pcs"), get_report_processor(), env_vars
894  	    )
895  	
896  	
897  	def run_pcsdcli(command, data=None):
898  	    """
899  	    Commandline options:
900  	      * --request-timeout - timeout for HTTP request, applicable for commands:
901  	        * remove_known_hosts - only when running on cluster node (sync will
902  	            be initiated)
903  	        * auth
904  	        * send_local_configs
905  	    """
906  	    if not data:
907  	        data = {}
908  	    env_var = {}
909  	    if "--debug" in pcs_options:
910  	        env_var["PCSD_DEBUG"] = "true"
911  	    if "--request-timeout" in pcs_options:
912  	        env_var["PCSD_NETWORK_TIMEOUT"] = str(pcs_options["--request-timeout"])
913  	    else:
914  	        env_var["PCSD_NETWORK_TIMEOUT"] = str(settings.default_request_timeout)
915  	    pcsd_dir_path = settings.pcsd_exec_location
916  	    pcsdcli_path = os.path.join(pcsd_dir_path, "pcsd-cli.rb")
917  	    if settings.pcsd_gem_path is not None:
918  	        env_var["GEM_HOME"] = settings.pcsd_gem_path
919  	    stdout, dummy_stderr, retval = cmd_runner().run(
920  	        [settings.ruby_exec, "-I" + pcsd_dir_path, pcsdcli_path, command],
921  	        json.dumps(data),
922  	        env_var,
923  	    )
924  	    try:
925  	        output_json = json.loads(stdout)
926  	        for key in ["status", "text", "data"]:
927  	            if key not in output_json:
928  	                output_json[key] = None
929  	
930  	        output = "".join(output_json["log"])
931  	        # check if some requests timed out, if so print message about it
932  	        if "error: operation_timedout" in output:
933  	            print_to_stderr("Error: Operation timed out")
934  	        # check if there are any connection failures due to proxy in pcsd and
935  	        # print warning if so
936  	        proxy_msg = "Proxy is set in environment variables, try disabling it"
937  	        if proxy_msg in output:
938  	            reports_output.warn(proxy_msg)
939  	
940  	    except ValueError:
941  	        output_json = {
942  	            "status": "bad_json_output",
943  	            "text": stdout,
944  	            "data": None,
945  	        }
946  	    return output_json, retval
947  	
948  	
949  	def call_local_pcsd(argv, options, std_in=None):  # noqa: PLR0911
950  	    """
951  	    Commandline options:
952  	      * --request-timeout - timeout of call to local pcsd
953  	    """
954  	    # some commands cannot be run under a non-root account
955  	    # so we pass those commands to locally running pcsd to execute them
956  	    # returns [list_of_errors, exit_code, stdout, stderr]
957  	    data = {
958  	        "command": json.dumps(argv),
959  	        "options": json.dumps(options),
960  	    }
961  	    if std_in:
962  	        data["stdin"] = std_in
963  	    data_send = urlencode(data)
964  	    code, output = sendHTTPRequest(
965  	        "localhost", "run_pcs", data_send, False, False
966  	    )
967  	
968  	    if code == 3:  # not authenticated
969  	        return [
970  	            [
971  	                "Unable to authenticate against the local pcsd. Run the same "
972  	                "command as root or authenticate yourself to the local pcsd "
973  	                "using command 'pcs client local-auth'"
974  	            ],
975  	            1,
976  	            "",
977  	            "",
978  	        ]
979  	    if code != 0:  # http error connecting to localhost
980  	        return [[output], 1, "", ""]
981  	
982  	    try:
983  	        output_json = json.loads(output)
984  	        for key in ["status", "data"]:
985  	            if key not in output_json:
986  	                output_json[key] = None
987  	    except ValueError:
988  	        return [["Unable to communicate with pcsd"], 1, "", ""]
989  	    if output_json["status"] == "bad_command":
990  	        return [["Command not allowed"], 1, "", ""]
991  	    if output_json["status"] == "access_denied":
992  	        return [["Access denied"], 1, "", ""]
993  	    if output_json["status"] != "ok" or not output_json["data"]:
994  	        return [["Unable to communicate with pcsd"], 1, "", ""]
995  	    try:
996  	        exitcode = output_json["data"]["code"]
997  	        std_out = output_json["data"]["stdout"]
998  	        std_err = output_json["data"]["stderr"]
999  	        return [[], exitcode, std_out, std_err]
1000 	    except KeyError:
1001 	        return [["Unable to communicate with pcsd"], 1, "", ""]
1002 	
1003 	
1004 	def map_for_error_list(callab, iterab):
1005 	    """
1006 	    Commandline options: no options
1007 	    NOTE: callback 'callab' may use some options
1008 	    """
1009 	    error_list = []
1010 	    for item in iterab:
1011 	        retval, error = callab(item)
1012 	        if retval != 0:
1013 	            error_list.append(error)
1014 	    return error_list
1015 	
1016 	
1017 	def run_parallel(worker_list, wait_seconds=1):
1018 	    """
1019 	    Commandline options: no options
1020 	    """
1021 	    thread_list = set()
1022 	    for worker in worker_list:
1023 	        thread = threading.Thread(target=worker)
1024 	        thread.daemon = True
1025 	        thread.start()
1026 	        thread_list.add(thread)
1027 	
1028 	    while thread_list:
1029 	        thread = thread_list.pop()
1030 	        thread.join(wait_seconds)
1031 	        if thread.is_alive():
1032 	            thread_list.add(thread)
1033 	
1034 	
1035 	def create_task(report, action, node, *args, **kwargs):
1036 	    """
1037 	    Commandline options: no options
1038 	    """
1039 	
1040 	    def worker():
1041 	        returncode, output = action(node, *args, **kwargs)
1042 	        report(node, returncode, output)
1043 	
1044 	    return worker
1045 	
1046 	
1047 	def create_task_list(report, action, node_list, *args, **kwargs):
1048 	    """
1049 	    Commandline options: no options
1050 	    """
1051 	    return [
1052 	        create_task(report, action, node, *args, **kwargs) for node in node_list
1053 	    ]
1054 	
1055 	
1056 	def parallel_for_nodes(action, node_list, *args, **kwargs):
1057 	    """
1058 	    Commandline options: no options
1059 	    NOTE: callback 'action' may use some cmd options
1060 	    """
1061 	    node_errors = {}
1062 	
1063 	    def report(node, returncode, output):
1064 	        message = "{0}: {1}".format(node, output.strip())
1065 	        print_to_stderr(message)
1066 	        if returncode != 0:
1067 	            node_errors[node] = message
1068 	
1069 	    run_parallel(create_task_list(report, action, node_list, *args, **kwargs))
1070 	    return node_errors
1071 	
1072 	
1073 	def get_group_children(group_id):
1074 	    """
1075 	    Commandline options: no options
1076 	    """
1077 	    return dom_get_group_children(get_cib_dom(), group_id)
1078 	
1079 	
1080 	def dom_get_group_children(dom, group_id):
1081 	    groups = dom.getElementsByTagName("group")
1082 	    for g in groups:
1083 	        if g.getAttribute("id") == group_id:
1084 	            return [
1085 	                child_el.getAttribute("id")
1086 	                for child_el in get_group_children_el_from_el(g)
1087 	            ]
1088 	    return []
1089 	
1090 	
1091 	def get_group_children_el_from_el(group_el):
1092 	    child_resources = []
1093 	    for child in group_el.childNodes:
1094 	        if child.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
1095 	            continue
1096 	        if child.tagName == "primitive":
1097 	            child_resources.append(child)
1098 	    return child_resources
1099 	
1100 	
1101 	def dom_get_clone_ms_resource(dom, clone_ms_id):
1102 	    """
1103 	    Commandline options: no options
1104 	    """
1105 	    clone_ms = dom_get_clone(dom, clone_ms_id) or dom_get_master(
1106 	        dom, clone_ms_id
1107 	    )
1108 	    if clone_ms:
1109 	        return dom_elem_get_clone_ms_resource(clone_ms)
1110 	    return None
1111 	
1112 	
1113 	def dom_elem_get_clone_ms_resource(clone_ms):
1114 	    """
1115 	    Commandline options: no options
1116 	    """
1117 	    for child in clone_ms.childNodes:
1118 	        if (
1119 	            child.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1120 	            and child.tagName in ["group", "primitive"]
1121 	        ):
1122 	            return child
1123 	    return None
1124 	
1125 	
1126 	def dom_get_resource_clone_ms_parent(dom, resource_id):
1127 	    """
1128 	    Commandline options: no options
1129 	    """
1130 	    resource = dom_get_resource(dom, resource_id) or dom_get_group(
1131 	        dom, resource_id
1132 	    )
1133 	    if resource:
1134 	        return dom_get_parent_by_tag_names(resource, ["clone", "master"])
1135 	    return None
1136 	
1137 	
1138 	def dom_get_resource_bundle_parent(dom, resource_id):
1139 	    """
1140 	    Commandline options: no options
1141 	    """
1142 	    resource = dom_get_resource(dom, resource_id)
1143 	    if resource:
1144 	        return dom_get_parent_by_tag_names(resource, ["bundle"])
1145 	    return None
1146 	
1147 	
1148 	def dom_get_master(dom, master_id):
1149 	    """
1150 	    Commandline options: no options
1151 	    """
1152 	    for master in dom.getElementsByTagName("master"):
1153 	        if master.getAttribute("id") == master_id:
1154 	            return master
1155 	    return None
1156 	
1157 	
1158 	def dom_get_clone(dom, clone_id):
1159 	    """
1160 	    Commandline options: no options
1161 	    """
1162 	    for clone in dom.getElementsByTagName("clone"):
1163 	        if clone.getAttribute("id") == clone_id:
1164 	            return clone
1165 	    return None
1166 	
1167 	
1168 	def dom_get_group(dom, group_id):
1169 	    """
1170 	    Commandline options: no options
1171 	    """
1172 	    for group in dom.getElementsByTagName("group"):
1173 	        if group.getAttribute("id") == group_id:
1174 	            return group
1175 	    return None
1176 	
1177 	
1178 	def dom_get_bundle(dom, bundle_id):
1179 	    """
1180 	    Commandline options: no options
1181 	    """
1182 	    for bundle in dom.getElementsByTagName("bundle"):
1183 	        if bundle.getAttribute("id") == bundle_id:
1184 	            return bundle
1185 	    return None
1186 	
1187 	
1188 	def dom_get_resource_bundle(bundle_el):
1189 	    """
1190 	    Commandline options: no options
1191 	    """
1192 	    for child in bundle_el.childNodes:
1193 	        if (
1194 	            child.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1195 	            and child.tagName == "primitive"
1196 	        ):
1197 	            return child
1198 	    return None
1199 	
1200 	
1201 	def dom_get_group_clone(dom, group_id):
1202 	    """
1203 	    Commandline options: no options
1204 	    """
1205 	    for clone in dom.getElementsByTagName("clone"):
1206 	        group = dom_get_group(clone, group_id)
1207 	        if group:
1208 	            return group
1209 	    return None
1210 	
1211 	
1212 	def dom_get_group_masterslave(dom, group_id):
1213 	    """
1214 	    Commandline options: no options
1215 	    """
1216 	    for master in dom.getElementsByTagName("master"):
1217 	        group = dom_get_group(master, group_id)
1218 	        if group:
1219 	            return group
1220 	    return None
1221 	
1222 	
1223 	def dom_get_resource(dom, resource_id):
1224 	    """
1225 	    Commandline options: no options
1226 	    """
1227 	    for primitive in dom.getElementsByTagName("primitive"):
1228 	        if primitive.getAttribute("id") == resource_id:
1229 	            return primitive
1230 	    return None
1231 	
1232 	
1233 	def dom_get_any_resource(dom, resource_id):
1234 	    """
1235 	    Commandline options: no options
1236 	    """
1237 	    return (
1238 	        dom_get_resource(dom, resource_id)
1239 	        or dom_get_group(dom, resource_id)
1240 	        or dom_get_clone(dom, resource_id)
1241 	        or dom_get_master(dom, resource_id)
1242 	    )
1243 	
1244 	
1245 	def dom_get_resource_clone(dom, resource_id):
1246 	    """
1247 	    Commandline options: no options
1248 	    """
1249 	    for clone in dom.getElementsByTagName("clone"):
1250 	        resource = dom_get_resource(clone, resource_id)
1251 	        if resource:
1252 	            return resource
1253 	    return None
1254 	
1255 	
1256 	def dom_get_resource_masterslave(dom, resource_id):
1257 	    """
1258 	    Commandline options: no options
1259 	    """
1260 	    for master in dom.getElementsByTagName("master"):
1261 	        resource = dom_get_resource(master, resource_id)
1262 	        if resource:
1263 	            return resource
1264 	    return None
1265 	
1266 	
1267 	# returns tuple (is_valid, error_message, correct_resource_id_if_exists)
1268 	# there is a duplicate code in pcs/lib/cib/constraint/constraint.py
1269 	# please use function in pcs/lib/cib/constraint/constraint.py
1270 	def validate_constraint_resource(dom, resource_id):  # noqa: PLR0911
1271 	    """
1272 	    Commandline options:
1273 	      * --force - allow constraint on any resource
1274 	    """
1275 	    resource_el = (
1276 	        dom_get_clone(dom, resource_id)
1277 	        or dom_get_master(dom, resource_id)
1278 	        or dom_get_bundle(dom, resource_id)
1279 	    )
1280 	    if resource_el:
1281 	        # clones, masters and bundles are always valid
1282 	        return True, "", resource_id
1283 	
1284 	    resource_el = dom_get_resource(dom, resource_id) or dom_get_group(
1285 	        dom, resource_id
1286 	    )
1287 	    if not resource_el:
1288 	        return False, "Resource '%s' does not exist" % resource_id, None
1289 	
1290 	    clone_el = dom_get_resource_clone_ms_parent(
1291 	        dom, resource_id
1292 	    ) or dom_get_resource_bundle_parent(dom, resource_id)
1293 	    if not clone_el:
1294 	        # a primitive and a group is valid if not in a clone nor a master nor a
1295 	        # bundle
1296 	        return True, "", resource_id
1297 	
1298 	    if "--force" in pcs_options:
1299 	        return True, "", clone_el.getAttribute("id")
1300 	
1301 	    if clone_el.tagName in ["clone", "master"]:
1302 	        return (
1303 	            False,
1304 	            "%s is a clone resource, you should use the clone id: %s "
1305 	            "when adding constraints. Use --force to override."
1306 	            % (resource_id, clone_el.getAttribute("id")),
1307 	            clone_el.getAttribute("id"),
1308 	        )
1309 	    if clone_el.tagName == "bundle":
1310 	        return (
1311 	            False,
1312 	            "%s is a bundle resource, you should use the bundle id: %s "
1313 	            "when adding constraints. Use --force to override."
1314 	            % (resource_id, clone_el.getAttribute("id")),
1315 	            clone_el.getAttribute("id"),
1316 	        )
1317 	    return True, "", resource_id
1318 	
1319 	
1320 	def validate_resources_not_in_same_group(dom, resource_id1, resource_id2):
1321 	    resource_el1 = dom_get_resource(dom, resource_id1)
1322 	    resource_el2 = dom_get_resource(dom, resource_id2)
1323 	    if not resource_el1 or not resource_el2:
1324 	        # Only primitive resources can be in a group. If at least one of the
1325 	        # resources is not a primitive (resource_el is None), then the
1326 	        # resources are not in the same group.
1327 	        return True
1328 	    group1 = dom_get_parent_by_tag_names(resource_el1, ["group"])
1329 	    group2 = dom_get_parent_by_tag_names(resource_el2, ["group"])
1330 	    if not group1 or not group2:
1331 	        return True
1332 	    return group1 != group2
1333 	
1334 	
1335 	def dom_get_resource_remote_node_name(dom_resource):
1336 	    """
1337 	    Commandline options: no options
1338 	    """
1339 	    if dom_resource.tagName != "primitive":
1340 	        return None
1341 	    if (
1342 	        dom_resource.getAttribute("class").lower() == "ocf"
1343 	        and dom_resource.getAttribute("provider").lower() == "pacemaker"
1344 	        and dom_resource.getAttribute("type").lower() == "remote"
1345 	    ):
1346 	        return dom_resource.getAttribute("id")
1347 	    return dom_get_meta_attr_value(dom_resource, "remote-node")
1348 	
1349 	
1350 	def dom_get_meta_attr_value(dom_resource, meta_name):
1351 	    """
1352 	    Commandline options: no options
1353 	    """
1354 	    for meta in dom_resource.getElementsByTagName("meta_attributes"):
1355 	        for nvpair in meta.getElementsByTagName("nvpair"):
1356 	            if nvpair.getAttribute("name") == meta_name:
1357 	                return nvpair.getAttribute("value")
1358 	    return None
1359 	
1360 	
1361 	def dom_get_node(dom, node_name):
1362 	    """
1363 	    Commandline options: no options
1364 	    """
1365 	    for e in dom.getElementsByTagName("node"):
1366 	        if e.hasAttribute("uname") and e.getAttribute("uname") == node_name:
1367 	            return e
1368 	    return None
1369 	
1370 	
1371 	def _dom_get_children_by_tag_name(dom_el, tag_name):
1372 	    """
1373 	    Commandline options: no options
1374 	    """
1375 	    return [
1376 	        node
1377 	        for node in dom_el.childNodes
1378 	        if node.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1379 	        and node.tagName == tag_name
1380 	    ]
1381 	
1382 	
1383 	def dom_get_parent_by_tag_names(dom_el, tag_names):
1384 	    """
1385 	    Commandline options: no options
1386 	    """
1387 	    parent = dom_el.parentNode
1388 	    while parent:
1389 	        if not isinstance(parent, xml.dom.minidom.Element):
1390 	            return None
1391 	        if parent.tagName in tag_names:
1392 	            return parent
1393 	        parent = parent.parentNode
1394 	    return None
1395 	
1396 	
1397 	# moved to pcs.lib.pacemaker.state
1398 	def get_resource_for_running_check(cluster_state, resource_id, stopped=False):
1399 	    """
1400 	    Commandline options: no options
1401 	    """
1402 	
1403 	    def _isnum(value):
1404 	        return all(char in list("0123456789") for char in value)
1405 	
1406 	    for clone in cluster_state.getElementsByTagName("clone"):
1407 	        if clone.getAttribute("id") == resource_id:
1408 	            for child in clone.childNodes:
1409 	                if child.nodeType == child.ELEMENT_NODE and child.tagName in [
1410 	                    "resource",
1411 	                    "group",
1412 	                ]:
1413 	                    resource_id = child.getAttribute("id")
1414 	                    # in a clone, a resource can have an id of '<name>:N'
1415 	                    if ":" in resource_id:
1416 	                        parts = resource_id.rsplit(":", 1)
1417 	                        if _isnum(parts[1]):
1418 	                            resource_id = parts[0]
1419 	                    break
1420 	    for group in cluster_state.getElementsByTagName("group"):
1421 	        # If resource is a clone it can have an id of '<resource name>:N'
1422 	        if group.getAttribute("id") == resource_id or group.getAttribute(
1423 	            "id"
1424 	        ).startswith(resource_id + ":"):
1425 	            if stopped:
1426 	                elem = group.getElementsByTagName("resource")[0]
1427 	            else:
1428 	                elem = group.getElementsByTagName("resource")[-1]
1429 	            resource_id = elem.getAttribute("id")
1430 	    return resource_id
1431 	
1432 	
1433 	# moved to pcs.lib.pacemaker.state
1434 	# see pcs.lib.commands.resource for usage
1435 	def resource_running_on(resource, passed_state=None, stopped=False):
1436 	    """
1437 	    Commandline options:
1438 	      * -f - has effect but doesn't make sense to check state of resource
1439 	    """
1440 	    nodes_started = []
1441 	    nodes_promoted = []
1442 	    nodes_unpromoted = []
1443 	    state = passed_state if passed_state else getClusterState()
1444 	    resource_original = resource
1445 	    resource = get_resource_for_running_check(state, resource, stopped)
1446 	    resources = state.getElementsByTagName("resource")
1447 	    for res in resources:
1448 	        # If resource is a clone it can have an id of '<resource name>:N'
1449 	        # If resource is a clone it will be found more than once - cannot break
1450 	        if (
1451 	            res.getAttribute("id") == resource
1452 	            or res.getAttribute("id").startswith(resource + ":")
1453 	        ) and res.getAttribute("failed") != "true":
1454 	            for node in res.getElementsByTagName("node"):
1455 	                node_name = node.getAttribute("name")
1456 	                role = res.getAttribute("role")
1457 	                if role == const.PCMK_ROLE_STARTED:
1458 	                    nodes_started.append(node_name)
1459 	                elif role in (
1460 	                    const.PCMK_ROLE_PROMOTED,
1461 	                    const.PCMK_ROLE_PROMOTED_LEGACY,
1462 	                ):
1463 	                    nodes_promoted.append(node_name)
1464 	                elif role in (
1465 	                    const.PCMK_ROLE_UNPROMOTED,
1466 	                    const.PCMK_ROLE_UNPROMOTED_LEGACY,
1467 	                ):
1468 	                    nodes_unpromoted.append(node_name)
1469 	    if not nodes_started and not nodes_promoted and not nodes_unpromoted:
1470 	        message = "Resource '%s' is not running on any node" % resource_original
1471 	    else:
1472 	        message_parts = []
1473 	        for alist, label in (
1474 	            (nodes_started, "running"),
1475 	            (nodes_promoted, str(const.PCMK_ROLE_PROMOTED).lower()),
1476 	            (nodes_unpromoted, str(const.PCMK_ROLE_UNPROMOTED).lower()),
1477 	        ):
1478 	            if alist:
1479 	                alist.sort()
1480 	                message_parts.append(
1481 	                    "%s on node%s %s"
1482 	                    % (label, "s" if len(alist) > 1 else "", ", ".join(alist))
1483 	                )
1484 	        message = "Resource '%s' is %s." % (
1485 	            resource_original,
1486 	            "; ".join(message_parts),
1487 	        )
1488 	    return {
1489 	        "message": message,
1490 	        "is_running": bool(nodes_started or nodes_promoted or nodes_unpromoted),
1491 	    }
1492 	
1493 	
1494 	def validate_wait_get_timeout(need_cib_support=True):
1495 	    """
1496 	    Commandline options:
1497 	      * --wait
1498 	      * -f - to check if -f and --wait are not used simultaneously
1499 	    """
1500 	    if need_cib_support and usefile:
1501 	        err("Cannot use '-f' together with '--wait'")
1502 	    wait_timeout = pcs_options["--wait"]
1503 	    if wait_timeout is None:
1504 	        return wait_timeout
1505 	    wait_timeout = timeout_to_seconds(wait_timeout)
1506 	    if wait_timeout is None:
1507 	        err(
1508 	            "%s is not a valid number of seconds to wait"
1509 	            % pcs_options["--wait"]
1510 	        )
1511 	    return wait_timeout
1512 	
1513 	
1514 	# Return matches from the CIB with the xpath_query
1515 	def get_cib_xpath(xpath_query):
1516 	    """
1517 	    Commandline options:
1518 	      * -f - CIB file
1519 	    """
1520 	    args = ["cibadmin", "-Q", "--xpath", xpath_query]
1521 	    output, retval = run(args)
1522 	    if retval != 0:
1523 	        return ""
1524 	    return output
1525 	
1526 	
1527 	def get_cib(scope=None):
1528 	    """
1529 	    Commandline options:
1530 	      * -f - CIB file
1531 	    """
1532 	    command = ["cibadmin", "-l", "-Q"]
1533 	    if scope:
1534 	        command.append("--scope=%s" % scope)
1535 	    output, retval = run(command)
1536 	    if retval != 0:
1537 	        if retval == 105 and scope:
1538 	            err("unable to get cib, scope '%s' not present in cib" % scope)
1539 	        else:
1540 	            err("unable to get cib")
1541 	    return output
1542 	
1543 	
1544 	def get_cib_dom(cib_xml=None):
1545 	    """
1546 	    Commandline options:
1547 	      * -f - CIB file
1548 	    """
1549 	    if cib_xml is None:
1550 	        cib_xml = get_cib()
1551 	    try:
1552 	        return parseString(cib_xml)
1553 	    except xml.parsers.expat.ExpatError:
1554 	        return err("unable to get cib")
1555 	
1556 	
1557 	# Replace only configuration section of cib with dom passed
1558 	def replace_cib_configuration(dom):
1559 	    """
1560 	    Commandline options:
1561 	      * -f - CIB file
1562 	    """
1563 	    new_dom = dom.toxml() if hasattr(dom, "toxml") else dom
1564 	    cmd = ["cibadmin", "--replace", "-V", "--xml-pipe", "-o", "configuration"]
1565 	    output, retval = run(cmd, False, new_dom)
1566 	    if retval != 0:
1567 	        err("Unable to update cib\n" + output)
1568 	
1569 	
1570 	def is_valid_cib_scope(scope):
1571 	    """
1572 	    Commandline options: no options
1573 	    """
1574 	    return scope in [
1575 	        "acls",
1576 	        "alerts",
1577 	        "configuration",
1578 	        "constraints",
1579 	        "crm_config",
1580 	        "fencing-topology",
1581 	        "nodes",
1582 	        "op_defaults",
1583 	        "resources",
1584 	        "rsc_defaults",
1585 	        "tags",
1586 	    ]
1587 	
1588 	
1589 	# Checks to see if id exists in the xml dom passed
1590 	# DEPRECATED use lxml version available in pcs.lib.cib.tools
1591 	def does_id_exist(dom, check_id):
1592 	    """
1593 	    Commandline options: no options
1594 	    """
1595 	    # do not search in /cib/status, it may contain references to previously
1596 	    # existing and deleted resources and thus preventing creating them again
1597 	    document = (
1598 	        dom if isinstance(dom, xml.dom.minidom.Document) else dom.ownerDocument
1599 	    )
1600 	    cib_found = False
1601 	    for cib in _dom_get_children_by_tag_name(document, "cib"):
1602 	        cib_found = True
1603 	        for section in cib.childNodes:
1604 	            if section.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
1605 	                continue
1606 	            if section.tagName == "status":
1607 	                continue
1608 	            for elem in section.getElementsByTagName("*"):
1609 	                if elem.getAttribute("id") == check_id:
1610 	                    return True
1611 	    if not cib_found:
1612 	        for elem in document.getElementsByTagName("*"):
1613 	            if elem.getAttribute("id") == check_id:
1614 	                return True
1615 	    return False
1616 	
1617 	
1618 	# Returns check_id if it doesn't exist in the dom, otherwise it adds an integer
1619 	# to the end of the id and increments it until a unique id is found
1620 	# DEPRECATED use lxml version available in pcs.lib.cib.tools
1621 	def find_unique_id(dom, check_id):
1622 	    """
1623 	    Commandline options: no options
1624 	    """
1625 	    counter = 1
1626 	    temp_id = check_id
1627 	    while does_id_exist(dom, temp_id):
1628 	        temp_id = check_id + "-" + str(counter)
1629 	        counter += 1
1630 	    return temp_id
1631 	
1632 	
1633 	# Checks to see if the specified operation already exists in passed set of
1634 	# operations
1635 	# pacemaker differentiates between operations only by name and interval
1636 	def operation_exists(operations_el, op_el):
1637 	    """
1638 	    Commandline options: no options
1639 	    """
1640 	    op_name = op_el.getAttribute("name")
1641 	    op_interval = timeout_to_seconds_legacy(op_el.getAttribute("interval"))
1642 	    return [
1643 	        op
1644 	        for op in operations_el.getElementsByTagName("op")
1645 	        if (
1646 	            op.getAttribute("name") == op_name
1647 	            and timeout_to_seconds_legacy(op.getAttribute("interval"))
1648 	            == op_interval
1649 	        )
1650 	    ]
1651 	
1652 	
1653 	def operation_exists_by_name(operations_el, op_el):
1654 	    """
1655 	    Commandline options: no options
1656 	    """
1657 	
1658 	    def get_role(_el, new_roles_supported):
1659 	        return common_pacemaker.role.get_value_for_cib(
1660 	            _el.getAttribute("role") or const.PCMK_ROLE_STARTED,
1661 	            new_roles_supported,
1662 	        )
1663 	
1664 	    new_roles_supported = isCibVersionSatisfied(
1665 	        operations_el, const.PCMK_NEW_ROLES_CIB_VERSION
1666 	    )
1667 	    existing = []
1668 	    op_name = op_el.getAttribute("name")
1669 	    op_role = get_role(op_el, new_roles_supported)
1670 	    ocf_check_level = None
1671 	    if op_name == "monitor":
1672 	        ocf_check_level = get_operation_ocf_check_level(op_el)
1673 	
1674 	    for op in operations_el.getElementsByTagName("op"):
1675 	        if op.getAttribute("name") == op_name:
1676 	            if (
1677 	                op_name != "monitor"
1678 	                or get_role(op, new_roles_supported) == op_role
1679 	                and ocf_check_level == get_operation_ocf_check_level(op)
1680 	            ):
1681 	                existing.append(op)
1682 	    return existing
1683 	
1684 	
1685 	def get_operation_ocf_check_level(operation_el):
1686 	    """
1687 	    Commandline options: no options
1688 	    """
1689 	    for attr_el in operation_el.getElementsByTagName("instance_attributes"):
1690 	        for nvpair_el in attr_el.getElementsByTagName("nvpair"):
1691 	            if (
1692 	                nvpair_el.getAttribute("name")
1693 	                == OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME
1694 	            ):
1695 	                return nvpair_el.getAttribute("value")
1696 	    return None
1697 	
1698 	
1699 	def set_node_attribute(prop, value, node):
1700 	    """
1701 	    Commandline options:
1702 	      * -f - CIB file
1703 	      * --force - no error if attribute to delete doesn't exist
1704 	    """
1705 	    if value == "":
1706 	        o, r = run(
1707 	            [
1708 	                "crm_attribute",
1709 	                "-t",
1710 	                "nodes",
1711 	                "--node",
1712 	                node,
1713 	                "--name",
1714 	                prop,
1715 	                "--query",
1716 	            ]
1717 	        )
1718 	        if r != 0 and "--force" not in pcs_options:
1719 	            err(
1720 	                "attribute: '%s' doesn't exist for node: '%s'" % (prop, node),
1721 	                False,
1722 	            )
1723 	            # This return code is used by pcsd
1724 	            sys.exit(2)
1725 	        o, r = run(
1726 	            [
1727 	                "crm_attribute",
1728 	                "-t",
1729 	                "nodes",
1730 	                "--node",
1731 	                node,
1732 	                "--name",
1733 	                prop,
1734 	                "--delete",
1735 	            ]
1736 	        )
1737 	    else:
1738 	        o, r = run(
1739 	            [
1740 	                "crm_attribute",
1741 	                "-t",
1742 	                "nodes",
1743 	                "--node",
1744 	                node,
1745 	                "--name",
1746 	                prop,
1747 	                "--update",
1748 	                value,
1749 	            ]
1750 	        )
1751 	
1752 	    if r != 0:
1753 	        err("unable to set attribute %s\n%s" % (prop, o))
1754 	
1755 	
1756 	def get_terminal_input(message=None):
1757 	    """
1758 	    Commandline options: no options
1759 	    """
1760 	    if message:
1761 	        sys.stdout.write(message)
1762 	        sys.stdout.flush()
1763 	    try:
1764 	        return input("")
1765 	    except EOFError:
1766 	        return ""
1767 	    except KeyboardInterrupt:
1768 	        print("Interrupted")
1769 	        sys.exit(1)
1770 	
1771 	
1772 	def _get_continue_confirmation_interactive(warning_text: str) -> bool:
1773 	    """
1774 	    Warns user and asks for permission to continue. Returns True if user wishes
1775 	    to continue, False otherwise.
1776 	
1777 	    This function is mostly intended for prompting user to confirm destructive
1778 	    operations - that's why WARNING is in all caps here and user is asked to
1779 	    explicitly type 'yes' or 'y' to continue.
1780 	
1781 	    warning_text -- describes action that we want the user to confirm
1782 	    """
1783 	    print(f"WARNING: {warning_text}")
1784 	    print("Type 'yes' or 'y' to proceed, anything else to cancel: ", end="")
1785 	    response = get_terminal_input()
1786 	    if response in ["yes", "y"]:
1787 	        return True
1788 	    print("Canceled")
1789 	    return False
1790 	
1791 	
1792 	def is_run_interactive() -> bool:
1793 	    """
1794 	    Return True if pcs is running in an interactive environment, False otherwise
1795 	    """
1796 	    return (
1797 	        sys.stdin is not None
1798 	        and sys.stdout is not None
1799 	        and sys.stdin.isatty()
1800 	        and sys.stdout.isatty()
1801 	    )
1802 	
1803 	
1804 	def get_continue_confirmation(
1805 	    warning_text: str, yes: bool, force: bool
1806 	) -> bool:
1807 	    """
1808 	    Either asks user to confirm continuation interactively or use --yes to
1809 	    override when running from a script. Returns True if user wants to continue.
1810 	    Returns False if user cancels the action. If a non-interactive environment
1811 	    is detected, pcs exits with an error formed from warning_text.
1812 	
1813 	    warning_text -- describes action that we want the user to confirm
1814 	    yes -- was --yes flag provided?
1815 	    force -- was --force flag provided? (deprecated)
1816 	    """
1817 	    if force and not yes:
1818 	        # Force may be specified for overriding library errors. We don't want
1819 	        # to report an issue in that case.
1820 	        # deprecated in the first pcs-0.12 version
1821 	        reports_output.deprecation_warning(
1822 	            "Using --force to confirm this action is deprecated and might be "
1823 	            "removed in a future release, use --yes instead"
1824 	        )
1825 	    if yes or force:
1826 	        reports_output.warn(warning_text)
1827 	        return True
1828 	    if not is_run_interactive():
1829 	        err(f"{warning_text}, use --yes to override")
1830 	        return False
1831 	    return _get_continue_confirmation_interactive(warning_text)
1832 	
1833 	
1834 	def get_terminal_password(message="Password: "):
1835 	    """
1836 	    Commandline options: no options
1837 	    """
1838 	    if sys.stdin is not None and sys.stdin.isatty():
1839 	        try:
1840 	            return getpass.getpass(message)
1841 	        except KeyboardInterrupt:
1842 	            print("Interrupted")
1843 	            sys.exit(1)
1844 	    else:
1845 	        return get_terminal_input(message)
1846 	
1847 	
1848 	# Returns an xml dom containing the current status of the cluster
1849 	# DEPRECATED, please use
1850 	# ClusterState(lib.pacemaker.live.get_cluster_status_dom()) instead
1851 	def getClusterState():
1852 	    """
1853 	    Commandline options:
1854 	      * -f - CIB file
1855 	    """
1856 	    xml_string, returncode = run(
1857 	        ["crm_mon", "--one-shot", "--output-as=xml", "--inactive"],
1858 	        ignore_stderr=True,
1859 	    )
1860 	    if returncode != 0:
1861 	        err("error running crm_mon, is pacemaker running?")
1862 	    return parseString(xml_string)
1863 	
1864 	
1865 	def write_empty_cib(cibfile):
1866 	    """
1867 	    Commandline options: no options
1868 	    """
1869 	    empty_xml = """
1870 	        <cib admin_epoch="0" epoch="1" num_updates="1" validate-with="pacemaker-3.1">
1871 	          <configuration>
1872 	            <crm_config/>
1873 	            <nodes/>
1874 	            <resources/>
1875 	            <constraints/>
1876 	          </configuration>
1877 	          <status/>
1878 	        </cib>
1879 	    """
1880 	    with open(cibfile, "w") as f:
1881 	        f.write(empty_xml)
1882 	
1883 	
1884 	# Test if 'var' is a score or option (contains an '=')
1885 	def is_score_or_opt(var):
1886 	    """
1887 	    Commandline options: no options
1888 	    """
1889 	    if is_score(var):
1890 	        return True
1891 	    return var.find("=") != -1
1892 	
1893 	
1894 	def is_score(var):
1895 	    """
1896 	    Commandline options: no options
1897 	    """
1898 	    return is_score_value(var)
1899 	
1900 	
1901 	def validate_xml_id(var: str, description: str = "id") -> tuple[bool, str]:
1902 	    """
1903 	    Commandline options: no options
1904 	    """
1905 	    report_list: ReportItemList = []
1906 	    validate_id(var, description, report_list)
1907 	    if report_list:
1908 	        return False, report_list[0].message.message
1909 	    return True, ""
1910 	
1911 	
1912 	def err(errorText: str, exit_after_error: bool = True) -> None:
1913 	    retval = reports_output.error(errorText)
1914 	    if exit_after_error:
1915 	        raise retval
1916 	
1917 	
1918 	@lru_cache(typed=True)
1919 	def get_service_manager() -> ServiceManagerInterface:
1920 	    return _get_service_manager(cmd_runner(), get_report_processor())
1921 	
1922 	
1923 	def enableServices():
1924 	    """
1925 	    Commandline options: no options
1926 	    """
1927 	    # do NOT handle SBD in here, it is started by pacemaker not systemd or init
1928 	    service_list = ["corosync", "pacemaker"]
1929 	    if need_to_handle_qdevice_service():
1930 	        service_list.append("corosync-qdevice")
1931 	    service_manager = get_service_manager()
1932 	
1933 	    report_item_list = []
1934 	    for service in service_list:
1935 	        try:
1936 	            service_manager.enable(service)
1937 	        except ManageServiceError as e:
1938 	            report_item_list.append(service_exception_to_report(e))
1939 	    if report_item_list:
1940 	        raise LibraryError(*report_item_list)
1941 	
1942 	
1943 	def disableServices():
1944 	    """
1945 	    Commandline options: no options
1946 	    """
1947 	    # do NOT handle SBD in here, it is started by pacemaker not systemd or init
1948 	    service_list = ["corosync", "pacemaker"]
1949 	    if need_to_handle_qdevice_service():
1950 	        service_list.append("corosync-qdevice")
1951 	    service_manager = get_service_manager()
1952 	
1953 	    report_item_list = []
1954 	    for service in service_list:
1955 	        try:
1956 	            service_manager.disable(service)
1957 	        except ManageServiceError as e:
1958 	            report_item_list.append(service_exception_to_report(e))
1959 	    if report_item_list:
1960 	        raise LibraryError(*report_item_list)
1961 	
1962 	
1963 	def start_service(service):
1964 	    """
1965 	    Commandline options: no options
1966 	    """
1967 	    service_manager = get_service_manager()
1968 	
1969 	    try:
1970 	        service_manager.start(service)
1971 	    except ManageServiceError as e:
1972 	        raise LibraryError(service_exception_to_report(e)) from e
1973 	
1974 	
1975 	def stop_service(service):
1976 	    """
1977 	    Commandline options: no options
1978 	    """
1979 	    service_manager = get_service_manager()
1980 	
1981 	    try:
1982 	        service_manager.stop(service)
1983 	    except ManageServiceError as e:
1984 	        raise LibraryError(service_exception_to_report(e)) from e
1985 	
1986 	
1987 	def write_file(path, data, permissions=0o644, binary=False):
1988 	    """
1989 	    Commandline options:
1990 	      * --force - overwrite a file if it already exists
1991 	    """
1992 	    if os.path.exists(path):
1993 	        if "--force" not in pcs_options:
1994 	            return False, "'%s' already exists, use --force to overwrite" % path
1995 	        try:
1996 	            os.remove(path)
1997 	        except OSError as e:
1998 	            return False, "unable to remove '%s': %s" % (path, e)
1999 	    mode = "wb" if binary else "w"
2000 	    try:
2001 	        with os.fdopen(
2002 	            os.open(path, os.O_WRONLY | os.O_CREAT, permissions), mode
2003 	        ) as outfile:
2004 	            outfile.write(data)
2005 	    except OSError as e:
2006 	        return False, "unable to write to '%s': %s" % (path, e)
2007 	    return True, ""
2008 	
2009 	
2010 	def tar_add_file_data(  # noqa: PLR0913
2011 	    tarball,
2012 	    data,
2013 	    name,
2014 	    *,
2015 	    mode=None,
2016 	    uid=None,
2017 	    gid=None,
2018 	    uname=None,
2019 	    gname=None,
2020 	    mtime=None,
2021 	):
2022 	    """
2023 	    Commandline options: no options
2024 	    """
2025 	    info = tarfile.TarInfo(name)
2026 	    info.size = len(data)
2027 	    info.type = tarfile.REGTYPE
2028 	    info.mtime = int(time.time()) if mtime is None else mtime
2029 	    if mode is not None:
2030 	        info.mode = mode
2031 	    if uid is not None:
2032 	        info.uid = uid
2033 	    if gid is not None:
2034 	        info.gid = gid
2035 	    if uname is not None:
2036 	        info.uname = uname
2037 	    if gname is not None:
2038 	        info.gname = gname
2039 	    data_io = BytesIO(data)
2040 	    tarball.addfile(info, data_io)
2041 	    data_io.close()
2042 	
2043 	
2044 	# DEPRECATED, please use pcs.lib.pacemaker.live.simulate_cib
2045 	def simulate_cib(cib_dom):
2046 	    """
2047 	    Commandline options: no options
2048 	    """
2049 	    try:
2050 	        with (
2051 	            tempfile.NamedTemporaryFile(
2052 	                mode="w+", suffix=".pcs"
2053 	            ) as new_cib_file,
2054 	            tempfile.NamedTemporaryFile(
2055 	                mode="w+", suffix=".pcs"
2056 	            ) as transitions_file,
2057 	        ):
2058 	            output, retval = run(
2059 	                [
2060 	                    "crm_simulate",
2061 	                    "--simulate",
2062 	                    "--save-output",
2063 	                    new_cib_file.name,
2064 	                    "--save-graph",
2065 	                    transitions_file.name,
2066 	                    "--xml-pipe",
2067 	                ],
2068 	                string_for_stdin=cib_dom.toxml(),
2069 	            )
2070 	            if retval != 0:
2071 	                return err("Unable to run crm_simulate:\n%s" % output)
2072 	            new_cib_file.seek(0)
2073 	            transitions_file.seek(0)
2074 	            return (
2075 	                output,
2076 	                parseString(transitions_file.read()),
CID (unavailable; MK=c30004330dc7a1580afdb00f63baac8c) (#2 of 2): XML external entity processing enabled (SIGMA.xml_external_entity_enabled):
(1) Event Sigma main event: The application uses Python's built in `xml` module which does not properly handle erroneous or maliciously constructed data, making the application vulnerable to one or more types of XML attacks.
(2) Event remediation: Avoid using the `xml` module. Consider using the `defusedxml` module or similar which safely prevents all XML entity attacks.
2077 	                parseString(new_cib_file.read()),
2078 	            )
2079 	    except (OSError, xml.parsers.expat.ExpatError) as e:
2080 	        return err("Unable to run crm_simulate:\n%s" % e)
2081 	    except xml.etree.ElementTree.ParseError as e:
2082 	        return err("Unable to run crm_simulate:\n%s" % e)
2083 	
2084 	
2085 	# DEPRECATED
2086 	# please use pcs.lib.pacemaker.simulate.get_operations_from_transitions
2087 	def get_operations_from_transitions(transitions_dom):
2088 	    """
2089 	    Commandline options: no options
2090 	    """
2091 	    operation_list = []
2092 	    watched_operations = (
2093 	        "start",
2094 	        "stop",
2095 	        "promote",
2096 	        "demote",
2097 	        "migrate_from",
2098 	        "migrate_to",
2099 	    )
2100 	    for rsc_op in transitions_dom.getElementsByTagName("rsc_op"):
2101 	        primitives = rsc_op.getElementsByTagName("primitive")
2102 	        if not primitives:
2103 	            continue
2104 	        if rsc_op.getAttribute("operation").lower() not in watched_operations:
2105 	            continue
2106 	        for prim in primitives:
2107 	            prim_id = prim.getAttribute("id")
2108 	            operation_list.append(
2109 	                (
2110 	                    int(rsc_op.getAttribute("id")),
2111 	                    {
2112 	                        "id": prim_id,
2113 	                        "long_id": prim.getAttribute("long-id") or prim_id,
2114 	                        "operation": rsc_op.getAttribute("operation").lower(),
2115 	                        "on_node": rsc_op.getAttribute("on_node"),
2116 	                    },
2117 	                )
2118 	            )
2119 	    operation_list.sort(key=lambda x: x[0])
2120 	    return [op[1] for op in operation_list]
2121 	
2122 	
2123 	def get_resources_location_from_operations(cib_dom, resources_operations):
2124 	    """
2125 	    Commandline options:
2126 	      * --force - allow constraints on any resource, may not have any effect as
2127 	        an invalid constraint is ignored anyway
2128 	    """
2129 	    locations = {}
2130 	    for res_op in resources_operations:
2131 	        operation = res_op["operation"]
2132 	        if operation not in ("start", "promote", "migrate_from"):
2133 	            continue
2134 	        long_id = res_op["long_id"]
2135 	        if long_id not in locations:
2136 	            # Move clone instances as if they were non-cloned resources, it
2137 	            # really works with current pacemaker (1.1.13-6). Otherwise there
2138 	            # is probably no way to move them other then setting their
2139 	            # stickiness to 0.
2140 	            res_id = res_op["id"]
2141 	            if ":" in res_id:
2142 	                res_id = res_id.split(":")[0]
2143 	            id_for_constraint = validate_constraint_resource(cib_dom, res_id)[2]
2144 	            if not id_for_constraint:
2145 	                continue
2146 	            locations[long_id] = {
2147 	                "id": res_op["id"],
2148 	                "long_id": long_id,
2149 	                "id_for_constraint": id_for_constraint,
2150 	            }
2151 	        if operation in ("start", "migrate_from"):
2152 	            locations[long_id]["start_on_node"] = res_op["on_node"]
2153 	        if operation == "promote":
2154 	            locations[long_id]["promote_on_node"] = res_op["on_node"]
2155 	    return {
2156 	        key: val
2157 	        for key, val in locations.items()
2158 	        if "start_on_node" in val or "promote_on_node" in val
2159 	    }
2160 	
2161 	
2162 	def get_remote_quorumtool_output(node):
2163 	    """
2164 	    Commandline options:
2165 	      * --request-timeout - timeout for HTTP requests
2166 	    """
2167 	    return sendHTTPRequest(node, "remote/get_quorum_info", None, False, False)
2168 	
2169 	
2170 	# return True if quorumtool_output is a string returned when the node is off
2171 	def is_node_offline_by_quorumtool_output(quorum_info):
2172 	    """
2173 	    Commandline options: no options
2174 	    """
2175 	    return quorum_info.strip() == "Cannot initialize CMAP service"
2176 	
2177 	
2178 	def dom_prepare_child_element(dom_element, tag_name, id_candidate):
2179 	    """
2180 	    Commandline options: no options
2181 	    """
2182 	    child_elements = [
2183 	        child
2184 	        for child in dom_element.childNodes
2185 	        if child.nodeType == child.ELEMENT_NODE and child.tagName == tag_name
2186 	    ]
2187 	
2188 	    if not child_elements:
2189 	        dom = dom_element.ownerDocument
2190 	        child_element = dom.createElement(tag_name)
2191 	        child_element.setAttribute("id", find_unique_id(dom, id_candidate))
2192 	        dom_element.appendChild(child_element)
2193 	    else:
2194 	        child_element = child_elements[0]
2195 	    return child_element
2196 	
2197 	
2198 	def dom_update_nvset(dom_element, nvpair_tuples, tag_name, id_candidate):
2199 	    """
2200 	    Commandline options: no options
2201 	    """
2202 	    # Already ported to pcs.libcib.nvpair
2203 	
2204 	    # Do not ever remove the nvset element, even if it is empty. There may be
2205 	    # ACLs set in pacemaker which allow "write" for nvpairs (adding, changing
2206 	    # and removing) but not nvsets. In such a case, removing the nvset would
2207 	    # cause the whole change to be rejected by pacemaker with a "permission
2208 	    # denied" message.
2209 	    # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
2210 	    if not nvpair_tuples:
2211 	        return
2212 	
2213 	    only_removing = True
2214 	    for _, value in nvpair_tuples:
2215 	        if value != "":
2216 	            only_removing = False
2217 	            break
2218 	
2219 	    # Do not use dom.getElementsByTagName, that would get elements we do not
2220 	    # want to. For example if dom_element is a clone, we would get the clones's
2221 	    # as well as clone's primitive's attributes.
2222 	    nvset_element_list = _dom_get_children_by_tag_name(dom_element, tag_name)
2223 	
2224 	    # Do not create new nvset if we are only removing values from it.
2225 	    if not nvset_element_list and only_removing:
2226 	        return
2227 	
2228 	    if not nvset_element_list:
2229 	        dom = dom_element.ownerDocument
2230 	        nvset_element = dom.createElement(tag_name)
2231 	        nvset_element.setAttribute("id", find_unique_id(dom, id_candidate))
2232 	        dom_element.appendChild(nvset_element)
2233 	    else:
2234 	        nvset_element = nvset_element_list[0]
2235 	
2236 	    for name, value in nvpair_tuples:
2237 	        dom_update_nv_pair(
2238 	            nvset_element, name, value, nvset_element.getAttribute("id") + "-"
2239 	        )
2240 	
2241 	
2242 	def dom_update_nv_pair(dom_element, name, value, id_prefix=""):
2243 	    """
2244 	    Commandline options: no options
2245 	    """
2246 	    # Do not ever remove the nvset element, even if it is empty. There may be
2247 	    # ACLs set in pacemaker which allow "write" for nvpairs (adding, changing
2248 	    # and removing) but not nvsets. In such a case, removing the nvset would
2249 	    # cause the whole change to be rejected by pacemaker with a "permission
2250 	    # denied" message.
2251 	    # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
2252 	
2253 	    dom = dom_element.ownerDocument
2254 	    element_found = False
2255 	    for el in dom_element.getElementsByTagName("nvpair"):
2256 	        if el.getAttribute("name") == name:
2257 	            element_found = True
2258 	            if value == "":
2259 	                dom_element.removeChild(el)
2260 	            else:
2261 	                el.setAttribute("value", value)
2262 	            break
2263 	    if not element_found and value != "":
2264 	        el = dom.createElement("nvpair")
2265 	        el.setAttribute("id", id_prefix + name)
2266 	        el.setAttribute("name", name)
2267 	        el.setAttribute("value", value)
2268 	        dom_element.appendChild(el)
2269 	    return dom_element
2270 	
2271 	
2272 	# Passed an array of strings ["a=b","c=d"], return array of tuples
2273 	# [("a","b"),("c","d")]
2274 	def convert_args_to_tuples(ra_values):
2275 	    """
2276 	    Commandline options: no options
2277 	    """
2278 	    ret = []
2279 	    for ra_val in ra_values:
2280 	        if ra_val.count("=") != 0:
2281 	            split_val = ra_val.split("=", 1)
2282 	            ret.append((split_val[0], split_val[1]))
2283 	    return ret
2284 	
2285 	
2286 	def is_int(val):
2287 	    try:
2288 	        int(val)
2289 	        return True
2290 	    except ValueError:
2291 	        return False
2292 	
2293 	
2294 	def dom_update_utilization(dom_element, attributes, id_prefix=""):
2295 	    """
2296 	    Commandline options: no options
2297 	    """
2298 	    attr_tuples = []
2299 	    for name, value in sorted(attributes.items()):
2300 	        if value != "" and not is_int(value):
2301 	            err(
2302 	                "Value of utilization attribute must be integer: "
2303 	                "'{0}={1}'".format(name, value)
2304 	            )
2305 	        attr_tuples.append((name, value))
2306 	    dom_update_nvset(
2307 	        dom_element,
2308 	        attr_tuples,
2309 	        "utilization",
2310 	        id_prefix + dom_element.getAttribute("id") + "-utilization",
2311 	    )
2312 	
2313 	
2314 	def dom_update_meta_attr(dom_element, attributes):
2315 	    """
2316 	    Commandline options: no options
2317 	    """
2318 	    dom_update_nvset(
2319 	        dom_element,
2320 	        attributes,
2321 	        "meta_attributes",
2322 	        dom_element.getAttribute("id") + "-meta_attributes",
2323 	    )
2324 	
2325 	
2326 	def dom_update_instance_attr(dom_element, attributes):
2327 	    """
2328 	    Commandline options: no options
2329 	    """
2330 	    dom_update_nvset(
2331 	        dom_element,
2332 	        attributes,
2333 	        "instance_attributes",
2334 	        dom_element.getAttribute("id") + "-instance_attributes",
2335 	    )
2336 	
2337 	
2338 	def get_utilization(element, filter_name=None):
2339 	    """
2340 	    Commandline options: no options
2341 	    """
2342 	    utilization = {}
2343 	    for e in element.getElementsByTagName("utilization"):
2344 	        for u in e.getElementsByTagName("nvpair"):
2345 	            name = u.getAttribute("name")
2346 	            if filter_name is not None and name != filter_name:
2347 	                continue
2348 	            utilization[name] = u.getAttribute("value")
2349 	        # Use just first element of utilization attributes. We don't support
2350 	        # utilization with rules just yet.
2351 	        break
2352 	    return utilization
2353 	
2354 	
2355 	def get_utilization_str(element, filter_name=None):
2356 	    """
2357 	    Commandline options: no options
2358 	    """
2359 	    output = []
2360 	    for name, value in sorted(get_utilization(element, filter_name).items()):
2361 	        output.append(name + "=" + value)
2362 	    return " ".join(output)
2363 	
2364 	
2365 	def get_lib_env() -> LibraryEnvironment:
2366 	    """
2367 	    Commandline options:
2368 	      * -f - CIB file
2369 	      * --corosync_conf - corosync.conf file
2370 	      * --request-timeout - timeout of HTTP requests
2371 	    """
2372 	    user = None
2373 	    groups = None
2374 	    if os.geteuid() == 0:
2375 	        for name in ("CIB_user", "CIB_user_groups"):
2376 	            if name in os.environ and os.environ[name].strip():
2377 	                value = os.environ[name].strip()
2378 	                if name == "CIB_user":
2379 	                    user = value
2380 	                else:
2381 	                    groups = value.split(" ")
2382 	
2383 	    cib_data = None
2384 	    if usefile:
2385 	        cib_data = get_cib()
2386 	
2387 	    corosync_conf_data = None
2388 	    if "--corosync_conf" in pcs_options:
2389 	        conf = pcs_options["--corosync_conf"]
2390 	        try:
2391 	            with open(conf) as corosync_conf_file:
2392 	                corosync_conf_data = corosync_conf_file.read()
2393 	        except OSError as e:
2394 	            err("Unable to read %s: %s" % (conf, e.strerror))
2395 	
2396 	    return LibraryEnvironment(
2397 	        logging.getLogger("pcs"),
2398 	        get_report_processor(),
2399 	        user,
2400 	        groups,
2401 	        cib_data,
2402 	        corosync_conf_data,
2403 	        known_hosts_getter=read_known_hosts_file,
2404 	        request_timeout=pcs_options.get("--request-timeout"),
2405 	    )
2406 	
2407 	
2408 	def get_cib_user_groups():
2409 	    """
2410 	    Commandline options: no options
2411 	    """
2412 	    user = None
2413 	    groups = None
2414 	    if os.geteuid() == 0:
2415 	        for name in ("CIB_user", "CIB_user_groups"):
2416 	            if name in os.environ and os.environ[name].strip():
2417 	                value = os.environ[name].strip()
2418 	                if name == "CIB_user":
2419 	                    user = value
2420 	                else:
2421 	                    groups = value.split(" ")
2422 	    return user, groups
2423 	
2424 	
2425 	def get_cli_env():
2426 	    """
2427 	    Commandline options:
2428 	      * --debug
2429 	      * --request-timeout
2430 	    """
2431 	    env = Env()
2432 	    env.user, env.groups = get_cib_user_groups()
2433 	    env.known_hosts_getter = read_known_hosts_file
2434 	    env.report_processor = get_report_processor()
2435 	    env.request_timeout = pcs_options.get("--request-timeout")
2436 	    return env
2437 	
2438 	
2439 	def get_middleware_factory():
2440 	    """
2441 	    Commandline options:
2442 	      * --corosync_conf
2443 	      * --name
2444 	      * --booth-conf
2445 	      * --booth-key
2446 	      * -f
2447 	    """
2448 	    return middleware.create_middleware_factory(
2449 	        cib=middleware.cib(filename if usefile else None, touch_cib_file),
2450 	        corosync_conf_existing=middleware.corosync_conf_existing(
2451 	            pcs_options.get("--corosync_conf")
2452 	        ),
2453 	        booth_conf=pcs.cli.booth.env.middleware_config(
2454 	            pcs_options.get("--booth-conf"),
2455 	            pcs_options.get("--booth-key"),
2456 	        ),
2457 	    )
2458 	
2459 	
2460 	def get_library_wrapper():
2461 	    """
2462 	    Commandline options:
2463 	      * --debug
2464 	      * --request-timeout
2465 	      * --corosync_conf
2466 	      * --name
2467 	      * --booth-conf
2468 	      * --booth-key
2469 	      * -f
2470 	    NOTE: usage of options may depend on used middleware for particular command
2471 	    """
2472 	    return Library(get_cli_env(), get_middleware_factory())
2473 	
2474 	
2475 	def exit_on_cmdline_input_error(
2476 	    error: CmdLineInputError, main_name: str, usage_name: StringSequence
2477 	) -> None:
2478 	    if error and error.message:
2479 	        reports_output.error(error.message)
2480 	    if error and error.hint:
2481 	        print_to_stderr(f"Hint: {error.hint}")
2482 	    if not error or (not error.message or error.show_both_usage_and_message):
2483 	        usage.show(main_name, list(usage_name))
2484 	    sys.exit(1)
2485 	
2486 	
2487 	def get_report_processor() -> ReportProcessor:
2488 	    return ReportProcessorToConsole(debug="--debug" in pcs_options)
2489 	
2490 	
2491 	def get_user_and_pass() -> tuple[str, str]:
2492 	    """
2493 	    Commandline options:
2494 	      * -u - username
2495 	      * -p - password
2496 	    """
2497 	    username = (
2498 	        pcs_options["-u"]
2499 	        if "-u" in pcs_options
2500 	        else get_terminal_input("Username: ")
2501 	    )
2502 	    password = (
2503 	        pcs_options["-p"] if "-p" in pcs_options else get_terminal_password()
2504 	    )
2505 	    return username, password
2506 	
2507 	
2508 	def get_input_modifiers() -> InputModifiers:
2509 	    return InputModifiers(pcs_options)
2510 	
2511 	
2512 	def get_token_from_file(file_name: str) -> str:
2513 	    try:
2514 	        with open(file_name, "rb") as file:
2515 	            # 256 to stay backwards compatible
2516 	            max_size = 256
2517 	            value_bytes = file.read(max_size + 1)
2518 	            if len(value_bytes) > max_size:
2519 	                err(f"Maximal token size of {max_size} bytes exceeded")
2520 	            if not value_bytes:
2521 	                err(f"File '{file_name}' is empty")
2522 	            return base64.b64encode(value_bytes).decode("utf-8")
2523 	    except OSError as e:
2524 	        err(f"Unable to read file '{file_name}': {e}", exit_after_error=False)
2525 	        raise SystemExit(1) from e
2526 	
2527 	
2528 	def print_warning_if_utilization_attrs_has_no_effect(
2529 	    properties_facade: PropertyConfigurationFacade,
2530 	) -> None:
2531 	    PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS = [
2532 	        "balanced",
2533 	        "minimal",
2534 	        "utilization",
2535 	    ]
2536 	    value = properties_facade.get_property_value_or_default(
2537 	        "placement-strategy"
2538 	    )
2539 	    if value not in PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS:
2540 	        reports_output.warn(
2541 	            "Utilization attributes configuration has no effect until cluster "
2542 	            "property option 'placement-strategy' is set to one of the "
2543 	            "values: "
2544 	            f"{format_list(PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS)}"
2545 	        )
2546