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