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