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  	        "stonith_admin",
827  	    ]:
828  	        args[0] = os.path.join(settings.pacemaker_execs, command)
829  	    elif command[0:8] == "corosync":
830  	        args[0] = os.path.join(settings.corosync_execs, command)
831  	
832  	    try:
833  	        if "--debug" in pcs_options:
834  	            print_to_stderr("Running: " + " ".join(args))
835  	            if string_for_stdin:
836  	                print_to_stderr(
837  	                    f"--Debug Input Start--\n"
838  	                    f"{string_for_stdin}\n"
839  	                    f"--Debug Input End--"
840  	                )
841  	
842  	        # Some commands react differently if you give them anything via stdin
843  	        if string_for_stdin is not None:
844  	            stdin_pipe = subprocess.PIPE
845  	        else:
846  	            stdin_pipe = subprocess.DEVNULL
847  	
848  	        p = subprocess.Popen(
849  	            args,
850  	            stdin=stdin_pipe,
851  	            stdout=subprocess.PIPE,
852  	            stderr=(subprocess.PIPE if ignore_stderr else subprocess.STDOUT),
853  	            preexec_fn=subprocess_setup,  # noqa: PLW1509
854  	            close_fds=True,
855  	            env=env_var,
856  	            # decodes newlines and in python3 also converts bytes to str
857  	            universal_newlines=(not binary_output),
858  	        )
859  	        output, dummy_stderror = p.communicate(string_for_stdin)
860  	        retval = p.returncode
861  	        if "--debug" in pcs_options:
862  	            print_to_stderr(
863  	                "Return Value: {retval}\n"
864  	                "--Debug Output Start--\n"
865  	                "{debug_output}\n"
866  	                "--Debug Output End--".format(
867  	                    retval=retval,
868  	                    debug_output=output.rstrip(),
869  	                )
870  	            )
871  	    except OSError as e:
872  	        print_to_stderr(e.strerror)
873  	        err("unable to locate command: " + args[0])
874  	
875  	    return output, retval
876  	
877  	
878  	def cmd_runner(cib_file_override=None):
879  	    """
880  	    Commandline options:
881  	      * -f - CIB file
882  	    """
883  	    env_vars = {}
884  	    if usefile:
885  	        env_vars["CIB_file"] = filename
886  	    if cib_file_override:
887  	        env_vars["CIB_file"] = cib_file_override
888  	    env_vars.update(os.environ)
889  	    env_vars["LC_ALL"] = "C"
890  	    return CommandRunner(
891  	        logging.getLogger("pcs"), get_report_processor(), env_vars
892  	    )
893  	
894  	
895  	def run_pcsdcli(command, data=None):
896  	    """
897  	    Commandline options:
898  	      * --request-timeout - timeout for HTTP request, applicable for commands:
899  	        * remove_known_hosts - only when running on cluster node (sync will
900  	            be initiated)
901  	        * auth
902  	        * send_local_configs
903  	    """
904  	    if not data:
905  	        data = {}
906  	    env_var = {}
907  	    if "--debug" in pcs_options:
908  	        env_var["PCSD_DEBUG"] = "true"
909  	    if "--request-timeout" in pcs_options:
910  	        env_var["PCSD_NETWORK_TIMEOUT"] = str(pcs_options["--request-timeout"])
911  	    else:
912  	        env_var["PCSD_NETWORK_TIMEOUT"] = str(settings.default_request_timeout)
913  	    pcsd_dir_path = settings.pcsd_exec_location
914  	    pcsdcli_path = os.path.join(pcsd_dir_path, "pcsd-cli.rb")
915  	    if settings.pcsd_gem_path is not None:
916  	        env_var["GEM_HOME"] = settings.pcsd_gem_path
917  	    stdout, dummy_stderr, retval = cmd_runner().run(
918  	        [settings.ruby_exec, "-I" + pcsd_dir_path, pcsdcli_path, command],
919  	        json.dumps(data),
920  	        env_var,
921  	    )
922  	    try:
923  	        output_json = json.loads(stdout)
924  	        for key in ["status", "text", "data"]:
925  	            if key not in output_json:
926  	                output_json[key] = None
927  	
928  	        output = "".join(output_json["log"])
929  	        # check if some requests timed out, if so print message about it
930  	        if "error: operation_timedout" in output:
931  	            print_to_stderr("Error: Operation timed out")
932  	        # check if there are any connection failures due to proxy in pcsd and
933  	        # print warning if so
934  	        proxy_msg = "Proxy is set in environment variables, try disabling it"
935  	        if proxy_msg in output:
936  	            reports_output.warn(proxy_msg)
937  	
938  	    except ValueError:
939  	        output_json = {
940  	            "status": "bad_json_output",
941  	            "text": stdout,
942  	            "data": None,
943  	        }
944  	    return output_json, retval
945  	
946  	
947  	def call_local_pcsd(argv, options, std_in=None):  # noqa: PLR0911
948  	    """
949  	    Commandline options:
950  	      * --request-timeout - timeout of call to local pcsd
951  	    """
952  	    # some commands cannot be run under a non-root account
953  	    # so we pass those commands to locally running pcsd to execute them
954  	    # returns [list_of_errors, exit_code, stdout, stderr]
955  	    data = {
956  	        "command": json.dumps(argv),
957  	        "options": json.dumps(options),
958  	    }
959  	    if std_in:
960  	        data["stdin"] = std_in
961  	    data_send = urlencode(data)
962  	    code, output = sendHTTPRequest(
963  	        "localhost", "run_pcs", data_send, False, False
964  	    )
965  	
966  	    if code == 3:  # not authenticated
967  	        return [
968  	            [
969  	                "Unable to authenticate against the local pcsd. Run the same "
970  	                "command as root or authenticate yourself to the local pcsd "
971  	                "using command 'pcs client local-auth'"
972  	            ],
973  	            1,
974  	            "",
975  	            "",
976  	        ]
977  	    if code != 0:  # http error connecting to localhost
978  	        return [[output], 1, "", ""]
979  	
980  	    try:
981  	        output_json = json.loads(output)
982  	        for key in ["status", "data"]:
983  	            if key not in output_json:
984  	                output_json[key] = None
985  	    except ValueError:
986  	        return [["Unable to communicate with pcsd"], 1, "", ""]
987  	    if output_json["status"] == "bad_command":
988  	        return [["Command not allowed"], 1, "", ""]
989  	    if output_json["status"] == "access_denied":
990  	        return [["Access denied"], 1, "", ""]
991  	    if output_json["status"] != "ok" or not output_json["data"]:
992  	        return [["Unable to communicate with pcsd"], 1, "", ""]
993  	    try:
994  	        exitcode = output_json["data"]["code"]
995  	        std_out = output_json["data"]["stdout"]
996  	        std_err = output_json["data"]["stderr"]
997  	        return [[], exitcode, std_out, std_err]
998  	    except KeyError:
999  	        return [["Unable to communicate with pcsd"], 1, "", ""]
1000 	
1001 	
1002 	def map_for_error_list(callab, iterab):
1003 	    """
1004 	    Commandline options: no options
1005 	    NOTE: callback 'callab' may use some options
1006 	    """
1007 	    error_list = []
1008 	    for item in iterab:
1009 	        retval, error = callab(item)
1010 	        if retval != 0:
1011 	            error_list.append(error)
1012 	    return error_list
1013 	
1014 	
1015 	def run_parallel(worker_list, wait_seconds=1):
1016 	    """
1017 	    Commandline options: no options
1018 	    """
1019 	    thread_list = set()
1020 	    for worker in worker_list:
1021 	        thread = threading.Thread(target=worker)
1022 	        thread.daemon = True
1023 	        thread.start()
1024 	        thread_list.add(thread)
1025 	
1026 	    while thread_list:
1027 	        thread = thread_list.pop()
1028 	        thread.join(wait_seconds)
1029 	        if thread.is_alive():
1030 	            thread_list.add(thread)
1031 	
1032 	
1033 	def create_task(report, action, node, *args, **kwargs):
1034 	    """
1035 	    Commandline options: no options
1036 	    """
1037 	
1038 	    def worker():
1039 	        returncode, output = action(node, *args, **kwargs)
1040 	        report(node, returncode, output)
1041 	
1042 	    return worker
1043 	
1044 	
1045 	def create_task_list(report, action, node_list, *args, **kwargs):
1046 	    """
1047 	    Commandline options: no options
1048 	    """
1049 	    return [
1050 	        create_task(report, action, node, *args, **kwargs) for node in node_list
1051 	    ]
1052 	
1053 	
1054 	def parallel_for_nodes(action, node_list, *args, **kwargs):
1055 	    """
1056 	    Commandline options: no options
1057 	    NOTE: callback 'action' may use some cmd options
1058 	    """
1059 	    node_errors = {}
1060 	
1061 	    def report(node, returncode, output):
1062 	        message = "{0}: {1}".format(node, output.strip())
1063 	        print_to_stderr(message)
1064 	        if returncode != 0:
1065 	            node_errors[node] = message
1066 	
1067 	    run_parallel(create_task_list(report, action, node_list, *args, **kwargs))
1068 	    return node_errors
1069 	
1070 	
1071 	def get_group_children(group_id):
1072 	    """
1073 	    Commandline options: no options
1074 	    """
1075 	    return dom_get_group_children(get_cib_dom(), group_id)
1076 	
1077 	
1078 	def dom_get_group_children(dom, group_id):
1079 	    groups = dom.getElementsByTagName("group")
1080 	    for g in groups:
1081 	        if g.getAttribute("id") == group_id:
1082 	            return [
1083 	                child_el.getAttribute("id")
1084 	                for child_el in get_group_children_el_from_el(g)
1085 	            ]
1086 	    return []
1087 	
1088 	
1089 	def get_group_children_el_from_el(group_el):
1090 	    child_resources = []
1091 	    for child in group_el.childNodes:
1092 	        if child.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
1093 	            continue
1094 	        if child.tagName == "primitive":
1095 	            child_resources.append(child)
1096 	    return child_resources
1097 	
1098 	
1099 	def dom_get_clone_ms_resource(dom, clone_ms_id):
1100 	    """
1101 	    Commandline options: no options
1102 	    """
1103 	    clone_ms = dom_get_clone(dom, clone_ms_id) or dom_get_master(
1104 	        dom, clone_ms_id
1105 	    )
1106 	    if clone_ms:
1107 	        return dom_elem_get_clone_ms_resource(clone_ms)
1108 	    return None
1109 	
1110 	
1111 	def dom_elem_get_clone_ms_resource(clone_ms):
1112 	    """
1113 	    Commandline options: no options
1114 	    """
1115 	    for child in clone_ms.childNodes:
1116 	        if (
1117 	            child.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1118 	            and child.tagName in ["group", "primitive"]
1119 	        ):
1120 	            return child
1121 	    return None
1122 	
1123 	
1124 	def dom_get_resource_clone_ms_parent(dom, resource_id):
1125 	    """
1126 	    Commandline options: no options
1127 	    """
1128 	    resource = dom_get_resource(dom, resource_id) or dom_get_group(
1129 	        dom, resource_id
1130 	    )
1131 	    if resource:
1132 	        return dom_get_parent_by_tag_names(resource, ["clone", "master"])
1133 	    return None
1134 	
1135 	
1136 	def dom_get_resource_bundle_parent(dom, resource_id):
1137 	    """
1138 	    Commandline options: no options
1139 	    """
1140 	    resource = dom_get_resource(dom, resource_id)
1141 	    if resource:
1142 	        return dom_get_parent_by_tag_names(resource, ["bundle"])
1143 	    return None
1144 	
1145 	
1146 	def dom_get_master(dom, master_id):
1147 	    """
1148 	    Commandline options: no options
1149 	    """
1150 	    for master in dom.getElementsByTagName("master"):
1151 	        if master.getAttribute("id") == master_id:
1152 	            return master
1153 	    return None
1154 	
1155 	
1156 	def dom_get_clone(dom, clone_id):
1157 	    """
1158 	    Commandline options: no options
1159 	    """
1160 	    for clone in dom.getElementsByTagName("clone"):
1161 	        if clone.getAttribute("id") == clone_id:
1162 	            return clone
1163 	    return None
1164 	
1165 	
1166 	def dom_get_group(dom, group_id):
1167 	    """
1168 	    Commandline options: no options
1169 	    """
1170 	    for group in dom.getElementsByTagName("group"):
1171 	        if group.getAttribute("id") == group_id:
1172 	            return group
1173 	    return None
1174 	
1175 	
1176 	def dom_get_bundle(dom, bundle_id):
1177 	    """
1178 	    Commandline options: no options
1179 	    """
1180 	    for bundle in dom.getElementsByTagName("bundle"):
1181 	        if bundle.getAttribute("id") == bundle_id:
1182 	            return bundle
1183 	    return None
1184 	
1185 	
1186 	def dom_get_resource_bundle(bundle_el):
1187 	    """
1188 	    Commandline options: no options
1189 	    """
1190 	    for child in bundle_el.childNodes:
1191 	        if (
1192 	            child.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1193 	            and child.tagName == "primitive"
1194 	        ):
1195 	            return child
1196 	    return None
1197 	
1198 	
1199 	def dom_get_group_clone(dom, group_id):
1200 	    """
1201 	    Commandline options: no options
1202 	    """
1203 	    for clone in dom.getElementsByTagName("clone"):
1204 	        group = dom_get_group(clone, group_id)
1205 	        if group:
1206 	            return group
1207 	    return None
1208 	
1209 	
1210 	def dom_get_group_masterslave(dom, group_id):
1211 	    """
1212 	    Commandline options: no options
1213 	    """
1214 	    for master in dom.getElementsByTagName("master"):
1215 	        group = dom_get_group(master, group_id)
1216 	        if group:
1217 	            return group
1218 	    return None
1219 	
1220 	
1221 	def dom_get_resource(dom, resource_id):
1222 	    """
1223 	    Commandline options: no options
1224 	    """
1225 	    for primitive in dom.getElementsByTagName("primitive"):
1226 	        if primitive.getAttribute("id") == resource_id:
1227 	            return primitive
1228 	    return None
1229 	
1230 	
1231 	def dom_get_any_resource(dom, resource_id):
1232 	    """
1233 	    Commandline options: no options
1234 	    """
1235 	    return (
1236 	        dom_get_resource(dom, resource_id)
1237 	        or dom_get_group(dom, resource_id)
1238 	        or dom_get_clone(dom, resource_id)
1239 	        or dom_get_master(dom, resource_id)
1240 	    )
1241 	
1242 	
1243 	def dom_get_resource_clone(dom, resource_id):
1244 	    """
1245 	    Commandline options: no options
1246 	    """
1247 	    for clone in dom.getElementsByTagName("clone"):
1248 	        resource = dom_get_resource(clone, resource_id)
1249 	        if resource:
1250 	            return resource
1251 	    return None
1252 	
1253 	
1254 	def dom_get_resource_masterslave(dom, resource_id):
1255 	    """
1256 	    Commandline options: no options
1257 	    """
1258 	    for master in dom.getElementsByTagName("master"):
1259 	        resource = dom_get_resource(master, resource_id)
1260 	        if resource:
1261 	            return resource
1262 	    return None
1263 	
1264 	
1265 	# returns tuple (is_valid, error_message, correct_resource_id_if_exists)
1266 	# there is a duplicate code in pcs/lib/cib/constraint/constraint.py
1267 	# please use function in pcs/lib/cib/constraint/constraint.py
1268 	def validate_constraint_resource(dom, resource_id):  # noqa: PLR0911
1269 	    """
1270 	    Commandline options:
1271 	      * --force - allow constraint on any resource
1272 	    """
1273 	    resource_el = (
1274 	        dom_get_clone(dom, resource_id)
1275 	        or dom_get_master(dom, resource_id)
1276 	        or dom_get_bundle(dom, resource_id)
1277 	    )
1278 	    if resource_el:
1279 	        # clones, masters and bundles are always valid
1280 	        return True, "", resource_id
1281 	
1282 	    resource_el = dom_get_resource(dom, resource_id) or dom_get_group(
1283 	        dom, resource_id
1284 	    )
1285 	    if not resource_el:
1286 	        return False, "Resource '%s' does not exist" % resource_id, None
1287 	
1288 	    clone_el = dom_get_resource_clone_ms_parent(
1289 	        dom, resource_id
1290 	    ) or dom_get_resource_bundle_parent(dom, resource_id)
1291 	    if not clone_el:
1292 	        # a primitive and a group is valid if not in a clone nor a master nor a
1293 	        # bundle
1294 	        return True, "", resource_id
1295 	
1296 	    if "--force" in pcs_options:
1297 	        return True, "", clone_el.getAttribute("id")
1298 	
1299 	    if clone_el.tagName in ["clone", "master"]:
1300 	        return (
1301 	            False,
1302 	            "%s is a clone resource, you should use the clone id: %s "
1303 	            "when adding constraints. Use --force to override."
1304 	            % (resource_id, clone_el.getAttribute("id")),
1305 	            clone_el.getAttribute("id"),
1306 	        )
1307 	    if clone_el.tagName == "bundle":
1308 	        return (
1309 	            False,
1310 	            "%s is a bundle resource, you should use the bundle id: %s "
1311 	            "when adding constraints. Use --force to override."
1312 	            % (resource_id, clone_el.getAttribute("id")),
1313 	            clone_el.getAttribute("id"),
1314 	        )
1315 	    return True, "", resource_id
1316 	
1317 	
1318 	def validate_resources_not_in_same_group(dom, resource_id1, resource_id2):
1319 	    resource_el1 = dom_get_resource(dom, resource_id1)
1320 	    resource_el2 = dom_get_resource(dom, resource_id2)
1321 	    if not resource_el1 or not resource_el2:
1322 	        # Only primitive resources can be in a group. If at least one of the
1323 	        # resources is not a primitive (resource_el is None), then the
1324 	        # resources are not in the same group.
1325 	        return True
1326 	    group1 = dom_get_parent_by_tag_names(resource_el1, ["group"])
1327 	    group2 = dom_get_parent_by_tag_names(resource_el2, ["group"])
1328 	    if not group1 or not group2:
1329 	        return True
1330 	    return group1 != group2
1331 	
1332 	
1333 	def dom_get_resource_remote_node_name(dom_resource):
1334 	    """
1335 	    Commandline options: no options
1336 	    """
1337 	    if dom_resource.tagName != "primitive":
1338 	        return None
1339 	    if (
1340 	        dom_resource.getAttribute("class").lower() == "ocf"
1341 	        and dom_resource.getAttribute("provider").lower() == "pacemaker"
1342 	        and dom_resource.getAttribute("type").lower() == "remote"
1343 	    ):
1344 	        return dom_resource.getAttribute("id")
1345 	    return dom_get_meta_attr_value(dom_resource, "remote-node")
1346 	
1347 	
1348 	def dom_get_meta_attr_value(dom_resource, meta_name):
1349 	    """
1350 	    Commandline options: no options
1351 	    """
1352 	    for meta in dom_resource.getElementsByTagName("meta_attributes"):
1353 	        for nvpair in meta.getElementsByTagName("nvpair"):
1354 	            if nvpair.getAttribute("name") == meta_name:
1355 	                return nvpair.getAttribute("value")
1356 	    return None
1357 	
1358 	
1359 	def dom_get_node(dom, node_name):
1360 	    """
1361 	    Commandline options: no options
1362 	    """
1363 	    for e in dom.getElementsByTagName("node"):
1364 	        if e.hasAttribute("uname") and e.getAttribute("uname") == node_name:
1365 	            return e
1366 	    return None
1367 	
1368 	
1369 	def _dom_get_children_by_tag_name(dom_el, tag_name):
1370 	    """
1371 	    Commandline options: no options
1372 	    """
1373 	    return [
1374 	        node
1375 	        for node in dom_el.childNodes
1376 	        if node.nodeType == xml.dom.minidom.Node.ELEMENT_NODE
1377 	        and node.tagName == tag_name
1378 	    ]
1379 	
1380 	
1381 	def dom_get_parent_by_tag_names(dom_el, tag_names):
1382 	    """
1383 	    Commandline options: no options
1384 	    """
1385 	    parent = dom_el.parentNode
1386 	    while parent:
1387 	        if not isinstance(parent, xml.dom.minidom.Element):
1388 	            return None
1389 	        if parent.tagName in tag_names:
1390 	            return parent
1391 	        parent = parent.parentNode
1392 	    return None
1393 	
1394 	
1395 	# moved to pcs.lib.pacemaker.state
1396 	def get_resource_for_running_check(cluster_state, resource_id, stopped=False):
1397 	    """
1398 	    Commandline options: no options
1399 	    """
1400 	
1401 	    def _isnum(value):
1402 	        return all(char in list("0123456789") for char in value)
1403 	
1404 	    for clone in cluster_state.getElementsByTagName("clone"):
1405 	        if clone.getAttribute("id") == resource_id:
1406 	            for child in clone.childNodes:
1407 	                if child.nodeType == child.ELEMENT_NODE and child.tagName in [
1408 	                    "resource",
1409 	                    "group",
1410 	                ]:
1411 	                    resource_id = child.getAttribute("id")
1412 	                    # in a clone, a resource can have an id of '<name>:N'
1413 	                    if ":" in resource_id:
1414 	                        parts = resource_id.rsplit(":", 1)
1415 	                        if _isnum(parts[1]):
1416 	                            resource_id = parts[0]
1417 	                    break
1418 	    for group in cluster_state.getElementsByTagName("group"):
1419 	        # If resource is a clone it can have an id of '<resource name>:N'
1420 	        if group.getAttribute("id") == resource_id or group.getAttribute(
1421 	            "id"
1422 	        ).startswith(resource_id + ":"):
1423 	            if stopped:
1424 	                elem = group.getElementsByTagName("resource")[0]
1425 	            else:
1426 	                elem = group.getElementsByTagName("resource")[-1]
1427 	            resource_id = elem.getAttribute("id")
1428 	    return resource_id
1429 	
1430 	
1431 	# moved to pcs.lib.pacemaker.state
1432 	# see pcs.lib.commands.resource for usage
1433 	def resource_running_on(resource, passed_state=None, stopped=False):
1434 	    """
1435 	    Commandline options:
1436 	      * -f - has effect but doesn't make sense to check state of resource
1437 	    """
1438 	    nodes_started = []
1439 	    nodes_promoted = []
1440 	    nodes_unpromoted = []
1441 	    state = passed_state if passed_state else getClusterState()
1442 	    resource_original = resource
1443 	    resource = get_resource_for_running_check(state, resource, stopped)
1444 	    resources = state.getElementsByTagName("resource")
1445 	    for res in resources:
1446 	        # If resource is a clone it can have an id of '<resource name>:N'
1447 	        # If resource is a clone it will be found more than once - cannot break
1448 	        if (
1449 	            res.getAttribute("id") == resource
1450 	            or res.getAttribute("id").startswith(resource + ":")
1451 	        ) and res.getAttribute("failed") != "true":
1452 	            for node in res.getElementsByTagName("node"):
1453 	                node_name = node.getAttribute("name")
1454 	                role = res.getAttribute("role")
1455 	                if role == const.PCMK_ROLE_STARTED:
1456 	                    nodes_started.append(node_name)
1457 	                elif role in (
1458 	                    const.PCMK_ROLE_PROMOTED,
1459 	                    const.PCMK_ROLE_PROMOTED_LEGACY,
1460 	                ):
1461 	                    nodes_promoted.append(node_name)
1462 	                elif role in (
1463 	                    const.PCMK_ROLE_UNPROMOTED,
1464 	                    const.PCMK_ROLE_UNPROMOTED_LEGACY,
1465 	                ):
1466 	                    nodes_unpromoted.append(node_name)
1467 	    if not nodes_started and not nodes_promoted and not nodes_unpromoted:
1468 	        message = "Resource '%s' is not running on any node" % resource_original
1469 	    else:
1470 	        message_parts = []
1471 	        for alist, label in (
1472 	            (nodes_started, "running"),
1473 	            (nodes_promoted, str(const.PCMK_ROLE_PROMOTED).lower()),
1474 	            (nodes_unpromoted, str(const.PCMK_ROLE_UNPROMOTED).lower()),
1475 	        ):
1476 	            if alist:
1477 	                alist.sort()
1478 	                message_parts.append(
1479 	                    "%s on node%s %s"
1480 	                    % (label, "s" if len(alist) > 1 else "", ", ".join(alist))
1481 	                )
1482 	        message = "Resource '%s' is %s." % (
1483 	            resource_original,
1484 	            "; ".join(message_parts),
1485 	        )
1486 	    return {
1487 	        "message": message,
1488 	        "is_running": bool(nodes_started or nodes_promoted or nodes_unpromoted),
1489 	    }
1490 	
1491 	
1492 	def validate_wait_get_timeout(need_cib_support=True):
1493 	    """
1494 	    Commandline options:
1495 	      * --wait
1496 	      * -f - to check if -f and --wait are not used simultaneously
1497 	    """
1498 	    if need_cib_support and usefile:
1499 	        err("Cannot use '-f' together with '--wait'")
1500 	    wait_timeout = pcs_options["--wait"]
1501 	    if wait_timeout is None:
1502 	        return wait_timeout
1503 	    wait_timeout = timeout_to_seconds(wait_timeout)
1504 	    if wait_timeout is None:
1505 	        err(
1506 	            "%s is not a valid number of seconds to wait"
1507 	            % pcs_options["--wait"]
1508 	        )
1509 	    return wait_timeout
1510 	
1511 	
1512 	# Return matches from the CIB with the xpath_query
1513 	def get_cib_xpath(xpath_query):
1514 	    """
1515 	    Commandline options:
1516 	      * -f - CIB file
1517 	    """
1518 	    args = ["cibadmin", "-Q", "--xpath", xpath_query]
1519 	    output, retval = run(args)
1520 	    if retval != 0:
1521 	        return ""
1522 	    return output
1523 	
1524 	
1525 	def get_cib(scope=None):
1526 	    """
1527 	    Commandline options:
1528 	      * -f - CIB file
1529 	    """
1530 	    command = ["cibadmin", "-l", "-Q"]
1531 	    if scope:
1532 	        command.append("--scope=%s" % scope)
1533 	    output, retval = run(command)
1534 	    if retval != 0:
1535 	        if retval == 105 and scope:
1536 	            err("unable to get cib, scope '%s' not present in cib" % scope)
1537 	        else:
1538 	            err("unable to get cib")
1539 	    return output
1540 	
1541 	
1542 	def get_cib_dom(cib_xml=None):
1543 	    """
1544 	    Commandline options:
1545 	      * -f - CIB file
1546 	    """
1547 	    if cib_xml is None:
1548 	        cib_xml = get_cib()
1549 	    try:
1550 	        return parseString(cib_xml)
1551 	    except xml.parsers.expat.ExpatError:
1552 	        return err("unable to get cib")
1553 	
1554 	
1555 	# Replace only configuration section of cib with dom passed
1556 	def replace_cib_configuration(dom):
1557 	    """
1558 	    Commandline options:
1559 	      * -f - CIB file
1560 	    """
1561 	    new_dom = dom.toxml() if hasattr(dom, "toxml") else dom
1562 	    cmd = ["cibadmin", "--replace", "-V", "--xml-pipe", "-o", "configuration"]
1563 	    output, retval = run(cmd, False, new_dom)
1564 	    if retval != 0:
1565 	        err("Unable to update cib\n" + output)
1566 	
1567 	
1568 	def is_valid_cib_scope(scope):
1569 	    """
1570 	    Commandline options: no options
1571 	    """
1572 	    return scope in [
1573 	        "acls",
1574 	        "alerts",
1575 	        "configuration",
1576 	        "constraints",
1577 	        "crm_config",
1578 	        "fencing-topology",
1579 	        "nodes",
1580 	        "op_defaults",
1581 	        "resources",
1582 	        "rsc_defaults",
1583 	        "tags",
1584 	    ]
1585 	
1586 	
1587 	# Checks to see if id exists in the xml dom passed
1588 	# DEPRECATED use lxml version available in pcs.lib.cib.tools
1589 	def does_id_exist(dom, check_id):
1590 	    """
1591 	    Commandline options: no options
1592 	    """
1593 	    # do not search in /cib/status, it may contain references to previously
1594 	    # existing and deleted resources and thus preventing creating them again
1595 	    document = (
1596 	        dom if isinstance(dom, xml.dom.minidom.Document) else dom.ownerDocument
1597 	    )
1598 	    cib_found = False
1599 	    for cib in _dom_get_children_by_tag_name(document, "cib"):
1600 	        cib_found = True
1601 	        for section in cib.childNodes:
1602 	            if section.nodeType != xml.dom.minidom.Node.ELEMENT_NODE:
1603 	                continue
1604 	            if section.tagName == "status":
1605 	                continue
1606 	            for elem in section.getElementsByTagName("*"):
1607 	                if elem.getAttribute("id") == check_id:
1608 	                    return True
1609 	    if not cib_found:
1610 	        for elem in document.getElementsByTagName("*"):
1611 	            if elem.getAttribute("id") == check_id:
1612 	                return True
1613 	    return False
1614 	
1615 	
1616 	# Returns check_id if it doesn't exist in the dom, otherwise it adds an integer
1617 	# to the end of the id and increments it until a unique id is found
1618 	# DEPRECATED use lxml version available in pcs.lib.cib.tools
1619 	def find_unique_id(dom, check_id):
1620 	    """
1621 	    Commandline options: no options
1622 	    """
1623 	    counter = 1
1624 	    temp_id = check_id
1625 	    while does_id_exist(dom, temp_id):
1626 	        temp_id = check_id + "-" + str(counter)
1627 	        counter += 1
1628 	    return temp_id
1629 	
1630 	
1631 	# Checks to see if the specified operation already exists in passed set of
1632 	# operations
1633 	# pacemaker differentiates between operations only by name and interval
1634 	def operation_exists(operations_el, op_el):
1635 	    """
1636 	    Commandline options: no options
1637 	    """
1638 	    op_name = op_el.getAttribute("name")
1639 	    op_interval = timeout_to_seconds_legacy(op_el.getAttribute("interval"))
1640 	    return [
1641 	        op
1642 	        for op in operations_el.getElementsByTagName("op")
1643 	        if (
1644 	            op.getAttribute("name") == op_name
1645 	            and timeout_to_seconds_legacy(op.getAttribute("interval"))
1646 	            == op_interval
1647 	        )
1648 	    ]
1649 	
1650 	
1651 	def operation_exists_by_name(operations_el, op_el):
1652 	    """
1653 	    Commandline options: no options
1654 	    """
1655 	
1656 	    def get_role(_el, new_roles_supported):
1657 	        return common_pacemaker.role.get_value_for_cib(
1658 	            _el.getAttribute("role") or const.PCMK_ROLE_STARTED,
1659 	            new_roles_supported,
1660 	        )
1661 	
1662 	    new_roles_supported = isCibVersionSatisfied(
1663 	        operations_el, const.PCMK_NEW_ROLES_CIB_VERSION
1664 	    )
1665 	    existing = []
1666 	    op_name = op_el.getAttribute("name")
1667 	    op_role = get_role(op_el, new_roles_supported)
1668 	    ocf_check_level = None
1669 	    if op_name == "monitor":
1670 	        ocf_check_level = get_operation_ocf_check_level(op_el)
1671 	
1672 	    for op in operations_el.getElementsByTagName("op"):
1673 	        if op.getAttribute("name") == op_name:
1674 	            if (
1675 	                op_name != "monitor"
1676 	                or get_role(op, new_roles_supported) == op_role
1677 	                and ocf_check_level == get_operation_ocf_check_level(op)
1678 	            ):
1679 	                existing.append(op)
1680 	    return existing
1681 	
1682 	
1683 	def get_operation_ocf_check_level(operation_el):
1684 	    """
1685 	    Commandline options: no options
1686 	    """
1687 	    for attr_el in operation_el.getElementsByTagName("instance_attributes"):
1688 	        for nvpair_el in attr_el.getElementsByTagName("nvpair"):
1689 	            if (
1690 	                nvpair_el.getAttribute("name")
1691 	                == OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME
1692 	            ):
1693 	                return nvpair_el.getAttribute("value")
1694 	    return None
1695 	
1696 	
1697 	def set_node_attribute(prop, value, node):
1698 	    """
1699 	    Commandline options:
1700 	      * -f - CIB file
1701 	      * --force - no error if attribute to delete doesn't exist
1702 	    """
1703 	    if value == "":
1704 	        o, r = run(
1705 	            [
1706 	                "crm_attribute",
1707 	                "-t",
1708 	                "nodes",
1709 	                "--node",
1710 	                node,
1711 	                "--name",
1712 	                prop,
1713 	                "--query",
1714 	            ]
1715 	        )
1716 	        if r != 0 and "--force" not in pcs_options:
1717 	            err(
1718 	                "attribute: '%s' doesn't exist for node: '%s'" % (prop, node),
1719 	                False,
1720 	            )
1721 	            # This return code is used by pcsd
1722 	            sys.exit(2)
1723 	        o, r = run(
1724 	            [
1725 	                "crm_attribute",
1726 	                "-t",
1727 	                "nodes",
1728 	                "--node",
1729 	                node,
1730 	                "--name",
1731 	                prop,
1732 	                "--delete",
1733 	            ]
1734 	        )
1735 	    else:
1736 	        o, r = run(
1737 	            [
1738 	                "crm_attribute",
1739 	                "-t",
1740 	                "nodes",
1741 	                "--node",
1742 	                node,
1743 	                "--name",
1744 	                prop,
1745 	                "--update",
1746 	                value,
1747 	            ]
1748 	        )
1749 	
1750 	    if r != 0:
1751 	        err("unable to set attribute %s\n%s" % (prop, o))
1752 	
1753 	
1754 	def get_terminal_input(message=None):
1755 	    """
1756 	    Commandline options: no options
1757 	    """
1758 	    if message:
1759 	        sys.stdout.write(message)
1760 	        sys.stdout.flush()
1761 	    try:
1762 	        return input("")
1763 	    except EOFError:
1764 	        return ""
1765 	    except KeyboardInterrupt:
1766 	        print("Interrupted")
1767 	        sys.exit(1)
1768 	
1769 	
1770 	def _get_continue_confirmation_interactive(warning_text: str) -> bool:
1771 	    """
1772 	    Warns user and asks for permission to continue. Returns True if user wishes
1773 	    to continue, False otherwise.
1774 	
1775 	    This function is mostly intended for prompting user to confirm destructive
1776 	    operations - that's why WARNING is in all caps here and user is asked to
1777 	    explicitly type 'yes' or 'y' to continue.
1778 	
1779 	    warning_text -- describes action that we want the user to confirm
1780 	    """
1781 	    print(f"WARNING: {warning_text}")
1782 	    print("Type 'yes' or 'y' to proceed, anything else to cancel: ", end="")
1783 	    response = get_terminal_input()
1784 	    if response in ["yes", "y"]:
1785 	        return True
1786 	    print("Canceled")
1787 	    return False
1788 	
1789 	
1790 	def is_run_interactive() -> bool:
1791 	    """
1792 	    Return True if pcs is running in an interactive environment, False otherwise
1793 	    """
1794 	    return (
1795 	        sys.stdin is not None
1796 	        and sys.stdout is not None
1797 	        and sys.stdin.isatty()
1798 	        and sys.stdout.isatty()
1799 	    )
1800 	
1801 	
1802 	def get_continue_confirmation(warning_text: str, yes: bool) -> bool:
1803 	    """
1804 	    Either asks user to confirm continuation interactively or use --yes to
1805 	    override when running from a script. Returns True if user wants to continue.
1806 	    Returns False if user cancels the action. If a non-interactive environment
1807 	    is detected, pcs exits with an error formed from warning_text.
1808 	
1809 	    warning_text -- describes action that we want the user to confirm
1810 	    yes -- was --yes flag provided?
1811 	    """
1812 	    if yes:
1813 	        reports_output.warn(warning_text)
1814 	        return True
1815 	    if not is_run_interactive():
1816 	        err(f"{warning_text}, use --yes to override")
1817 	        return False
1818 	    return _get_continue_confirmation_interactive(warning_text)
1819 	
1820 	
1821 	def get_terminal_password(message="Password: "):
1822 	    """
1823 	    Commandline options: no options
1824 	    """
1825 	    if sys.stdin is not None and sys.stdin.isatty():
1826 	        try:
1827 	            return getpass.getpass(message)
1828 	        except KeyboardInterrupt:
1829 	            print("Interrupted")
1830 	            sys.exit(1)
1831 	    else:
1832 	        return get_terminal_input(message)
1833 	
1834 	
1835 	# Returns an xml dom containing the current status of the cluster
1836 	# DEPRECATED, please use
1837 	# ClusterState(lib.pacemaker.live.get_cluster_status_dom()) instead
1838 	def getClusterState():
1839 	    """
1840 	    Commandline options:
1841 	      * -f - CIB file
1842 	    """
1843 	    xml_string, returncode = run(
1844 	        ["crm_mon", "--one-shot", "--output-as=xml", "--inactive"],
1845 	        ignore_stderr=True,
1846 	    )
1847 	    if returncode != 0:
1848 	        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.
1849 	    return parseString(xml_string)
1850 	
1851 	
1852 	def write_empty_cib(cibfile):
1853 	    """
1854 	    Commandline options: no options
1855 	    """
1856 	    empty_xml = """
1857 	        <cib admin_epoch="0" epoch="1" num_updates="1" validate-with="pacemaker-3.1">
1858 	          <configuration>
1859 	            <crm_config/>
1860 	            <nodes/>
1861 	            <resources/>
1862 	            <constraints/>
1863 	          </configuration>
1864 	          <status/>
1865 	        </cib>
1866 	    """
1867 	    with open(cibfile, "w") as f:
1868 	        f.write(empty_xml)
1869 	
1870 	
1871 	def validate_xml_id(var: str, description: str = "id") -> tuple[bool, str]:
1872 	    """
1873 	    Commandline options: no options
1874 	    """
1875 	    report_list: ReportItemList = []
1876 	    validate_id(var, description, report_list)
1877 	    if report_list:
1878 	        return False, report_list[0].message.message
1879 	    return True, ""
1880 	
1881 	
1882 	def err(errorText: str, exit_after_error: bool = True) -> None:
1883 	    retval = reports_output.error(errorText)
1884 	    if exit_after_error:
1885 	        raise retval
1886 	
1887 	
1888 	@lru_cache(typed=True)
1889 	def get_service_manager() -> ServiceManagerInterface:
1890 	    return _get_service_manager(cmd_runner(), get_report_processor())
1891 	
1892 	
1893 	def enableServices():
1894 	    """
1895 	    Commandline options: no options
1896 	    """
1897 	    # do NOT handle SBD in here, it is started by pacemaker not systemd or init
1898 	    service_list = ["corosync", "pacemaker"]
1899 	    if need_to_handle_qdevice_service():
1900 	        service_list.append("corosync-qdevice")
1901 	    service_manager = get_service_manager()
1902 	
1903 	    report_item_list = []
1904 	    for service in service_list:
1905 	        try:
1906 	            service_manager.enable(service)
1907 	        except ManageServiceError as e:
1908 	            report_item_list.append(service_exception_to_report(e))
1909 	    if report_item_list:
1910 	        raise LibraryError(*report_item_list)
1911 	
1912 	
1913 	def disableServices():
1914 	    """
1915 	    Commandline options: no options
1916 	    """
1917 	    # do NOT handle SBD in here, it is started by pacemaker not systemd or init
1918 	    service_list = ["corosync", "pacemaker"]
1919 	    if need_to_handle_qdevice_service():
1920 	        service_list.append("corosync-qdevice")
1921 	    service_manager = get_service_manager()
1922 	
1923 	    report_item_list = []
1924 	    for service in service_list:
1925 	        try:
1926 	            service_manager.disable(service)
1927 	        except ManageServiceError as e:
1928 	            report_item_list.append(service_exception_to_report(e))
1929 	    if report_item_list:
1930 	        raise LibraryError(*report_item_list)
1931 	
1932 	
1933 	def start_service(service):
1934 	    """
1935 	    Commandline options: no options
1936 	    """
1937 	    service_manager = get_service_manager()
1938 	
1939 	    try:
1940 	        service_manager.start(service)
1941 	    except ManageServiceError as e:
1942 	        raise LibraryError(service_exception_to_report(e)) from e
1943 	
1944 	
1945 	def stop_service(service):
1946 	    """
1947 	    Commandline options: no options
1948 	    """
1949 	    service_manager = get_service_manager()
1950 	
1951 	    try:
1952 	        service_manager.stop(service)
1953 	    except ManageServiceError as e:
1954 	        raise LibraryError(service_exception_to_report(e)) from e
1955 	
1956 	
1957 	def write_file(path, data, permissions=0o644, binary=False):
1958 	    """
1959 	    Commandline options:
1960 	      * --force - overwrite a file if it already exists
1961 	    """
1962 	    if os.path.exists(path):
1963 	        if "--force" not in pcs_options:
1964 	            return False, "'%s' already exists, use --force to overwrite" % path
1965 	        try:
1966 	            os.remove(path)
1967 	        except OSError as e:
1968 	            return False, "unable to remove '%s': %s" % (path, e)
1969 	    mode = "wb" if binary else "w"
1970 	    try:
1971 	        with os.fdopen(
1972 	            os.open(path, os.O_WRONLY | os.O_CREAT, permissions), mode
1973 	        ) as outfile:
1974 	            outfile.write(data)
1975 	    except OSError as e:
1976 	        return False, "unable to write to '%s': %s" % (path, e)
1977 	    return True, ""
1978 	
1979 	
1980 	def tar_add_file_data(  # noqa: PLR0913
1981 	    tarball,
1982 	    data,
1983 	    name,
1984 	    *,
1985 	    mode=None,
1986 	    uid=None,
1987 	    gid=None,
1988 	    uname=None,
1989 	    gname=None,
1990 	    mtime=None,
1991 	):
1992 	    """
1993 	    Commandline options: no options
1994 	    """
1995 	    info = tarfile.TarInfo(name)
1996 	    info.size = len(data)
1997 	    info.type = tarfile.REGTYPE
1998 	    info.mtime = int(time.time()) if mtime is None else mtime
1999 	    if mode is not None:
2000 	        info.mode = mode
2001 	    if uid is not None:
2002 	        info.uid = uid
2003 	    if gid is not None:
2004 	        info.gid = gid
2005 	    if uname is not None:
2006 	        info.uname = uname
2007 	    if gname is not None:
2008 	        info.gname = gname
2009 	    data_io = BytesIO(data)
2010 	    tarball.addfile(info, data_io)
2011 	    data_io.close()
2012 	
2013 	
2014 	# DEPRECATED, please use pcs.lib.pacemaker.live.simulate_cib
2015 	def simulate_cib(cib_dom):
2016 	    """
2017 	    Commandline options: no options
2018 	    """
2019 	    try:
2020 	        with (
2021 	            tempfile.NamedTemporaryFile(
2022 	                mode="w+", suffix=".pcs"
2023 	            ) as new_cib_file,
2024 	            tempfile.NamedTemporaryFile(
2025 	                mode="w+", suffix=".pcs"
2026 	            ) as transitions_file,
2027 	        ):
2028 	            output, retval = run(
2029 	                [
2030 	                    "crm_simulate",
2031 	                    "--simulate",
2032 	                    "--save-output",
2033 	                    new_cib_file.name,
2034 	                    "--save-graph",
2035 	                    transitions_file.name,
2036 	                    "--xml-pipe",
2037 	                ],
2038 	                string_for_stdin=cib_dom.toxml(),
2039 	            )
2040 	            if retval != 0:
2041 	                return err("Unable to run crm_simulate:\n%s" % output)
2042 	            new_cib_file.seek(0)
2043 	            transitions_file.seek(0)
2044 	            return (
2045 	                output,
2046 	                parseString(transitions_file.read()),
2047 	                parseString(new_cib_file.read()),
2048 	            )
2049 	    except (OSError, xml.parsers.expat.ExpatError) as e:
2050 	        return err("Unable to run crm_simulate:\n%s" % e)
2051 	    except xml.etree.ElementTree.ParseError as e:
2052 	        return err("Unable to run crm_simulate:\n%s" % e)
2053 	
2054 	
2055 	# DEPRECATED
2056 	# please use pcs.lib.pacemaker.simulate.get_operations_from_transitions
2057 	def get_operations_from_transitions(transitions_dom):
2058 	    """
2059 	    Commandline options: no options
2060 	    """
2061 	    operation_list = []
2062 	    watched_operations = (
2063 	        "start",
2064 	        "stop",
2065 	        "promote",
2066 	        "demote",
2067 	        "migrate_from",
2068 	        "migrate_to",
2069 	    )
2070 	    for rsc_op in transitions_dom.getElementsByTagName("rsc_op"):
2071 	        primitives = rsc_op.getElementsByTagName("primitive")
2072 	        if not primitives:
2073 	            continue
2074 	        if rsc_op.getAttribute("operation").lower() not in watched_operations:
2075 	            continue
2076 	        for prim in primitives:
2077 	            prim_id = prim.getAttribute("id")
2078 	            operation_list.append(
2079 	                (
2080 	                    int(rsc_op.getAttribute("id")),
2081 	                    {
2082 	                        "id": prim_id,
2083 	                        "long_id": prim.getAttribute("long-id") or prim_id,
2084 	                        "operation": rsc_op.getAttribute("operation").lower(),
2085 	                        "on_node": rsc_op.getAttribute("on_node"),
2086 	                    },
2087 	                )
2088 	            )
2089 	    operation_list.sort(key=lambda x: x[0])
2090 	    return [op[1] for op in operation_list]
2091 	
2092 	
2093 	def get_resources_location_from_operations(cib_dom, resources_operations):
2094 	    """
2095 	    Commandline options:
2096 	      * --force - allow constraints on any resource, may not have any effect as
2097 	        an invalid constraint is ignored anyway
2098 	    """
2099 	    locations = {}
2100 	    for res_op in resources_operations:
2101 	        operation = res_op["operation"]
2102 	        if operation not in ("start", "promote", "migrate_from"):
2103 	            continue
2104 	        long_id = res_op["long_id"]
2105 	        if long_id not in locations:
2106 	            # Move clone instances as if they were non-cloned resources, it
2107 	            # really works with current pacemaker (1.1.13-6). Otherwise there
2108 	            # is probably no way to move them other then setting their
2109 	            # stickiness to 0.
2110 	            res_id = res_op["id"]
2111 	            if ":" in res_id:
2112 	                res_id = res_id.split(":")[0]
2113 	            id_for_constraint = validate_constraint_resource(cib_dom, res_id)[2]
2114 	            if not id_for_constraint:
2115 	                continue
2116 	            locations[long_id] = {
2117 	                "id": res_op["id"],
2118 	                "long_id": long_id,
2119 	                "id_for_constraint": id_for_constraint,
2120 	            }
2121 	        if operation in ("start", "migrate_from"):
2122 	            locations[long_id]["start_on_node"] = res_op["on_node"]
2123 	        if operation == "promote":
2124 	            locations[long_id]["promote_on_node"] = res_op["on_node"]
2125 	    return {
2126 	        key: val
2127 	        for key, val in locations.items()
2128 	        if "start_on_node" in val or "promote_on_node" in val
2129 	    }
2130 	
2131 	
2132 	def get_remote_quorumtool_output(node):
2133 	    """
2134 	    Commandline options:
2135 	      * --request-timeout - timeout for HTTP requests
2136 	    """
2137 	    return sendHTTPRequest(node, "remote/get_quorum_info", None, False, False)
2138 	
2139 	
2140 	# return True if quorumtool_output is a string returned when the node is off
2141 	def is_node_offline_by_quorumtool_output(quorum_info):
2142 	    """
2143 	    Commandline options: no options
2144 	    """
2145 	    return quorum_info.strip() == "Cannot initialize CMAP service"
2146 	
2147 	
2148 	def dom_prepare_child_element(dom_element, tag_name, id_candidate):
2149 	    """
2150 	    Commandline options: no options
2151 	    """
2152 	    child_elements = [
2153 	        child
2154 	        for child in dom_element.childNodes
2155 	        if child.nodeType == child.ELEMENT_NODE and child.tagName == tag_name
2156 	    ]
2157 	
2158 	    if not child_elements:
2159 	        dom = dom_element.ownerDocument
2160 	        child_element = dom.createElement(tag_name)
2161 	        child_element.setAttribute("id", find_unique_id(dom, id_candidate))
2162 	        dom_element.appendChild(child_element)
2163 	    else:
2164 	        child_element = child_elements[0]
2165 	    return child_element
2166 	
2167 	
2168 	def dom_update_nvset(dom_element, nvpair_tuples, tag_name, id_candidate):
2169 	    """
2170 	    Commandline options: no options
2171 	    """
2172 	    # Already ported to pcs.libcib.nvpair
2173 	
2174 	    # Do not ever remove the nvset element, even if it is empty. There may be
2175 	    # ACLs set in pacemaker which allow "write" for nvpairs (adding, changing
2176 	    # and removing) but not nvsets. In such a case, removing the nvset would
2177 	    # cause the whole change to be rejected by pacemaker with a "permission
2178 	    # denied" message.
2179 	    # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
2180 	    if not nvpair_tuples:
2181 	        return
2182 	
2183 	    only_removing = True
2184 	    for _, value in nvpair_tuples:
2185 	        if value != "":
2186 	            only_removing = False
2187 	            break
2188 	
2189 	    # Do not use dom.getElementsByTagName, that would get elements we do not
2190 	    # want to. For example if dom_element is a clone, we would get the clones's
2191 	    # as well as clone's primitive's attributes.
2192 	    nvset_element_list = _dom_get_children_by_tag_name(dom_element, tag_name)
2193 	
2194 	    # Do not create new nvset if we are only removing values from it.
2195 	    if not nvset_element_list and only_removing:
2196 	        return
2197 	
2198 	    if not nvset_element_list:
2199 	        dom = dom_element.ownerDocument
2200 	        nvset_element = dom.createElement(tag_name)
2201 	        nvset_element.setAttribute("id", find_unique_id(dom, id_candidate))
2202 	        dom_element.appendChild(nvset_element)
2203 	    else:
2204 	        nvset_element = nvset_element_list[0]
2205 	
2206 	    for name, value in nvpair_tuples:
2207 	        dom_update_nv_pair(
2208 	            nvset_element, name, value, nvset_element.getAttribute("id") + "-"
2209 	        )
2210 	
2211 	
2212 	def dom_update_nv_pair(dom_element, name, value, id_prefix=""):
2213 	    """
2214 	    Commandline options: no options
2215 	    """
2216 	    # Do not ever remove the nvset element, even if it is empty. There may be
2217 	    # ACLs set in pacemaker which allow "write" for nvpairs (adding, changing
2218 	    # and removing) but not nvsets. In such a case, removing the nvset would
2219 	    # cause the whole change to be rejected by pacemaker with a "permission
2220 	    # denied" message.
2221 	    # https://bugzilla.redhat.com/show_bug.cgi?id=1642514
2222 	
2223 	    dom = dom_element.ownerDocument
2224 	    element_found = False
2225 	    for el in dom_element.getElementsByTagName("nvpair"):
2226 	        if el.getAttribute("name") == name:
2227 	            element_found = True
2228 	            if value == "":
2229 	                dom_element.removeChild(el)
2230 	            else:
2231 	                el.setAttribute("value", value)
2232 	            break
2233 	    if not element_found and value != "":
2234 	        el = dom.createElement("nvpair")
2235 	        el.setAttribute("id", id_prefix + name)
2236 	        el.setAttribute("name", name)
2237 	        el.setAttribute("value", value)
2238 	        dom_element.appendChild(el)
2239 	    return dom_element
2240 	
2241 	
2242 	# Passed an array of strings ["a=b","c=d"], return array of tuples
2243 	# [("a","b"),("c","d")]
2244 	def convert_args_to_tuples(ra_values):
2245 	    """
2246 	    Commandline options: no options
2247 	    """
2248 	    ret = []
2249 	    for ra_val in ra_values:
2250 	        if ra_val.count("=") != 0:
2251 	            split_val = ra_val.split("=", 1)
2252 	            ret.append((split_val[0], split_val[1]))
2253 	    return ret
2254 	
2255 	
2256 	def is_int(val):
2257 	    try:
2258 	        int(val)
2259 	        return True
2260 	    except ValueError:
2261 	        return False
2262 	
2263 	
2264 	def dom_update_utilization(dom_element, attributes, id_prefix=""):
2265 	    """
2266 	    Commandline options: no options
2267 	    """
2268 	    attr_tuples = []
2269 	    for name, value in sorted(attributes.items()):
2270 	        if value != "" and not is_int(value):
2271 	            err(
2272 	                "Value of utilization attribute must be integer: "
2273 	                "'{0}={1}'".format(name, value)
2274 	            )
2275 	        attr_tuples.append((name, value))
2276 	    dom_update_nvset(
2277 	        dom_element,
2278 	        attr_tuples,
2279 	        "utilization",
2280 	        id_prefix + dom_element.getAttribute("id") + "-utilization",
2281 	    )
2282 	
2283 	
2284 	def dom_update_meta_attr(dom_element, attributes):
2285 	    """
2286 	    Commandline options: no options
2287 	    """
2288 	    dom_update_nvset(
2289 	        dom_element,
2290 	        attributes,
2291 	        "meta_attributes",
2292 	        dom_element.getAttribute("id") + "-meta_attributes",
2293 	    )
2294 	
2295 	
2296 	def dom_update_instance_attr(dom_element, attributes):
2297 	    """
2298 	    Commandline options: no options
2299 	    """
2300 	    dom_update_nvset(
2301 	        dom_element,
2302 	        attributes,
2303 	        "instance_attributes",
2304 	        dom_element.getAttribute("id") + "-instance_attributes",
2305 	    )
2306 	
2307 	
2308 	def get_utilization(element, filter_name=None):
2309 	    """
2310 	    Commandline options: no options
2311 	    """
2312 	    utilization = {}
2313 	    for e in element.getElementsByTagName("utilization"):
2314 	        for u in e.getElementsByTagName("nvpair"):
2315 	            name = u.getAttribute("name")
2316 	            if filter_name is not None and name != filter_name:
2317 	                continue
2318 	            utilization[name] = u.getAttribute("value")
2319 	        # Use just first element of utilization attributes. We don't support
2320 	        # utilization with rules just yet.
2321 	        break
2322 	    return utilization
2323 	
2324 	
2325 	def get_utilization_str(element, filter_name=None):
2326 	    """
2327 	    Commandline options: no options
2328 	    """
2329 	    output = []
2330 	    for name, value in sorted(get_utilization(element, filter_name).items()):
2331 	        output.append(name + "=" + value)
2332 	    return " ".join(output)
2333 	
2334 	
2335 	def get_lib_env() -> LibraryEnvironment:
2336 	    """
2337 	    Commandline options:
2338 	      * -f - CIB file
2339 	      * --corosync_conf - corosync.conf file
2340 	      * --request-timeout - timeout of HTTP requests
2341 	    """
2342 	    user = None
2343 	    groups = None
2344 	    if os.geteuid() == 0:
2345 	        for name in ("CIB_user", "CIB_user_groups"):
2346 	            if name in os.environ and os.environ[name].strip():
2347 	                value = os.environ[name].strip()
2348 	                if name == "CIB_user":
2349 	                    user = value
2350 	                else:
2351 	                    groups = value.split(" ")
2352 	
2353 	    cib_data = None
2354 	    if usefile:
2355 	        cib_data = get_cib()
2356 	
2357 	    corosync_conf_data = None
2358 	    if "--corosync_conf" in pcs_options:
2359 	        conf = pcs_options["--corosync_conf"]
2360 	        try:
2361 	            with open(conf) as corosync_conf_file:
2362 	                corosync_conf_data = corosync_conf_file.read()
2363 	        except OSError as e:
2364 	            err("Unable to read %s: %s" % (conf, e.strerror))
2365 	
2366 	    return LibraryEnvironment(
2367 	        logging.getLogger("pcs"),
2368 	        get_report_processor(),
2369 	        user,
2370 	        groups,
2371 	        cib_data,
2372 	        corosync_conf_data,
2373 	        known_hosts_getter=read_known_hosts_file,
2374 	        request_timeout=pcs_options.get("--request-timeout"),
2375 	    )
2376 	
2377 	
2378 	def get_cib_user_groups():
2379 	    """
2380 	    Commandline options: no options
2381 	    """
2382 	    user = None
2383 	    groups = None
2384 	    if os.geteuid() == 0:
2385 	        for name in ("CIB_user", "CIB_user_groups"):
2386 	            if name in os.environ and os.environ[name].strip():
2387 	                value = os.environ[name].strip()
2388 	                if name == "CIB_user":
2389 	                    user = value
2390 	                else:
2391 	                    groups = value.split(" ")
2392 	    return user, groups
2393 	
2394 	
2395 	def get_cli_env():
2396 	    """
2397 	    Commandline options:
2398 	      * --debug
2399 	      * --request-timeout
2400 	    """
2401 	    env = Env()
2402 	    env.user, env.groups = get_cib_user_groups()
2403 	    env.known_hosts_getter = read_known_hosts_file
2404 	    env.report_processor = get_report_processor()
2405 	    env.request_timeout = pcs_options.get("--request-timeout")
2406 	    return env
2407 	
2408 	
2409 	def get_middleware_factory():
2410 	    """
2411 	    Commandline options:
2412 	      * --corosync_conf
2413 	      * --name
2414 	      * --booth-conf
2415 	      * --booth-key
2416 	      * -f
2417 	    """
2418 	    return middleware.create_middleware_factory(
2419 	        cib=middleware.cib(filename if usefile else None, touch_cib_file),
2420 	        corosync_conf_existing=middleware.corosync_conf_existing(
2421 	            pcs_options.get("--corosync_conf")
2422 	        ),
2423 	        booth_conf=pcs.cli.booth.env.middleware_config(
2424 	            pcs_options.get("--booth-conf"),
2425 	            pcs_options.get("--booth-key"),
2426 	        ),
2427 	    )
2428 	
2429 	
2430 	def get_library_wrapper():
2431 	    """
2432 	    Commandline options:
2433 	      * --debug
2434 	      * --request-timeout
2435 	      * --corosync_conf
2436 	      * --name
2437 	      * --booth-conf
2438 	      * --booth-key
2439 	      * -f
2440 	    NOTE: usage of options may depend on used middleware for particular command
2441 	    """
2442 	    return Library(get_cli_env(), get_middleware_factory())
2443 	
2444 	
2445 	def exit_on_cmdline_input_error(
2446 	    error: CmdLineInputError, main_name: str, usage_name: StringSequence
2447 	) -> None:
2448 	    if error and error.message:
2449 	        reports_output.error(error.message)
2450 	    if error and error.hint:
2451 	        print_to_stderr(f"Hint: {error.hint}")
2452 	    if not error or (not error.message or error.show_both_usage_and_message):
2453 	        usage.show(main_name, list(usage_name))
2454 	    sys.exit(1)
2455 	
2456 	
2457 	def get_report_processor() -> ReportProcessor:
2458 	    return ReportProcessorToConsole(debug="--debug" in pcs_options)
2459 	
2460 	
2461 	def get_user_and_pass() -> tuple[str, str]:
2462 	    """
2463 	    Commandline options:
2464 	      * -u - username
2465 	      * -p - password
2466 	    """
2467 	    username = (
2468 	        pcs_options["-u"]
2469 	        if "-u" in pcs_options
2470 	        else get_terminal_input("Username: ")
2471 	    )
2472 	    password = (
2473 	        pcs_options["-p"] if "-p" in pcs_options else get_terminal_password()
2474 	    )
2475 	    return username, password
2476 	
2477 	
2478 	def get_input_modifiers() -> InputModifiers:
2479 	    return InputModifiers(pcs_options)
2480 	
2481 	
2482 	def get_token_from_file(file_name: str) -> str:
2483 	    try:
2484 	        with open(file_name, "rb") as file:
2485 	            # 256 to stay backwards compatible
2486 	            max_size = 256
2487 	            value_bytes = file.read(max_size + 1)
2488 	            if len(value_bytes) > max_size:
2489 	                err(f"Maximal token size of {max_size} bytes exceeded")
2490 	            if not value_bytes:
2491 	                err(f"File '{file_name}' is empty")
2492 	            return base64.b64encode(value_bytes).decode("utf-8")
2493 	    except OSError as e:
2494 	        err(f"Unable to read file '{file_name}': {e}", exit_after_error=False)
2495 	        raise SystemExit(1) from e
2496 	
2497 	
2498 	def print_warning_if_utilization_attrs_has_no_effect(
2499 	    properties_facade: PropertyConfigurationFacade,
2500 	) -> None:
2501 	    PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS = [
2502 	        "balanced",
2503 	        "minimal",
2504 	        "utilization",
2505 	    ]
2506 	    value = properties_facade.get_property_value_or_default(
2507 	        "placement-strategy"
2508 	    )
2509 	    if value not in PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS:
2510 	        reports_output.warn(
2511 	            "Utilization attributes configuration has no effect until cluster "
2512 	            "property option 'placement-strategy' is set to one of the "
2513 	            "values: "
2514 	            f"{format_list(PLACEMENT_STRATEGIES_USING_UTILIZATION_ATTRS)}"
2515 	        )
2516