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