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