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