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