pax_global_header00006660000000000000000000000064150041747070014517gustar00rootroot0000000000000052 comment=3a90b9189709c8198d496e4170b2beb54763ff2b pcs-0.12.0.2/000077500000000000000000000000001500417470700125245ustar00rootroot00000000000000pcs-0.12.0.2/.gitarchivever000066400000000000000000000000341500417470700153640ustar00rootroot00000000000000ref names: (tag: v0.12.0.2) pcs-0.12.0.2/.gitattributes000066400000000000000000000000661500417470700154210ustar00rootroot00000000000000configure.ac export-subst .gitarchivever export-subst pcs-0.12.0.2/.github/000077500000000000000000000000001500417470700140645ustar00rootroot00000000000000pcs-0.12.0.2/.github/CODEOWNERS000066400000000000000000000001511500417470700154540ustar00rootroot00000000000000# https://help.github.com/en/articles/about-code-owners # Default reviewers for everything * @tomjelinek pcs-0.12.0.2/.gitignore000066400000000000000000000020751500417470700145200ustar00rootroot00000000000000*.pyc *.swp pcs.spec /MANIFEST /dist/ /pcsd/logrotate/pcsd pcs/snmp/pcs_snmp_agent pcs/snmp/settings.py pcs/snmp/pcs_snmp_agent.service /pcsd/pcsd.service /pcsd/pcsd-ruby.service /pcs_bundled/ /.bundle /pcsd/vendor /pcsd/public/ui /Gemfile* /scripts/pcsd.sh pcs.pc pcs-* .mypy_cache/ pyproject.toml requirements.txt setup.py setup.cfg pcs/api_v2_client pcs_test/api_v2_client pcs/pcs pcs/pcs_internal pcs/settings.py pcs_test/pcs_for_tests pcs_test/settings.py pcs_test/suite pcs_test/smoke.sh pcs_test/tools/bin_mock/pcmk/crm_resource pcs_test/tools/bin_mock/pcmk/pacemaker_metadata pcs_test/tools/bin_mock/pcmk/pacemaker-fenced pcs_test/tools/bin_mock/pcmk/stonith_admin pcs_test/resources/*.tmp pcs_test/resources/temp*.xml pcs_test/resources/temp* *.8 pcsd/capabilities.xml pcsd/pcsd pcsd/pcsd-cli.rb pcsd/rserver.rb pcsd/settings.rb build/ *.egg-info rpm/*.gem rpm/*.tar* rpm/*.rpm stamps* aclocal.m4 autom4te.cache/ compile config.guess config.sub configure install-sh ltmain.sh missing Makefile Makefile.in config.log config.status libtool m4/lt* m4/lib* .version typos_new pcs-0.12.0.2/.gitlab-ci.yml000066400000000000000000000100011500417470700151500ustar00rootroot00000000000000default: before_script: - git remote add upstream https://github.com/ClusterLabs/pcs.git - git fetch upstream .parallel: parallel: matrix: - BASE_IMAGE_NAME: ["PcsRhel10Next"] OS_TAG: "centos10" - BASE_IMAGE_NAME: ["PcsFedoraCurrentRelease"] OS_TAG: "generic" tags: - ${OS_TAG} stages: - stage1 - stage2 rpm_build: extends: .parallel stage: stage1 script: - ./autogen.sh - ./configure --enable-local-build --enable-individual-bundling --enable-webui - make CI_BRANCH=${BASE_IMAGE_NAME} rpm/pcs.spec - dnf builddep -y rpm/pcs.spec - make CI_BRANCH=${BASE_IMAGE_NAME} rpm - mkdir -p rpms && cp -v $(find rpm -type f -name '*.rpm' -not -name '*.src.rpm') rpms artifacts: expire_in: 1 week paths: - rpms distcheck: extends: .parallel stage: stage1 script: - "pip3 install dacite tornado pyagentx " - ./autogen.sh - ./configure --enable-local-build --enable-individual-bundling --enable-webui - make distcheck DISTCHECK_CONFIGURE_FLAGS='--enable-local-build --enable-individual-bundling --enable-webui' - rename --verbose .tar. ".${BASE_IMAGE_NAME}.tar." pcs*.tar.* - mkdir -p dist && cp -v pcs*.tar.* dist/ artifacts: expire_in: 1 week paths: - dist typos: extends: .parallel stage: stage1 script: - ./autogen.sh - ./configure --enable-local-build --enable-typos-check --enable-individual-bundling - make - make typos_check black: extends: .parallel stage: stage1 script: - python3 -m pip install --upgrade -r dev_requirements.txt - ./autogen.sh - ./configure --enable-local-build --enable-dev-tests --enable-tests-only --enable-individual-bundling - make black_check isort: extends: .parallel stage: stage1 script: - python3 -m pip install --upgrade -r dev_requirements.txt - ./autogen.sh - ./configure --enable-local-build --enable-dev-tests --enable-tests-only --enable-individual-bundling - make isort_check pylint: extends: .parallel stage: stage1 script: - python3 -m pip install --upgrade -r dev_requirements.txt - ./autogen.sh - ./configure --enable-local-build --enable-dev-tests --enable-parallel-pylint --enable-individual-bundling - make - make pylint mypy: extends: .parallel stage: stage1 script: - python3 -m pip install --upgrade -r dev_requirements.txt - ./autogen.sh - ./configure --enable-local-build --enable-dev-tests --enable-individual-bundling - make - make mypy ruby_tests: extends: .parallel stage: stage1 script: - ./autogen.sh - ./configure --enable-local-build --enable-individual-bundling - make - make pcsd-tests python_tier0_tests: extends: .parallel stage: stage1 script: # make sure that tier0 tests run without cluster packages installed - dnf remove -y corosync* pacemaker* fence-agents* resource-agents* booth* sbd - python3 -m pip install concurrencytest - ./autogen.sh - ./configure --enable-local-build --enable-individual-bundling - make - make tests_tier0 python_tier1_tests: extends: .parallel stage: stage2 needs: - rpm_build script: - "dnf install -y rpms/pcs-*${BASE_IMAGE_NAME}*$(rpm -E %{dist}).*.rpm" - python3 -m pip install concurrencytest - ./autogen.sh - ./configure --enable-local-build --enable-destructive-tests --enable-tests-only --enable-individual-bundling --enable-webui - rm -rf pcs pcsd pcs_bundled # make sure we are testing installed package - pcs_test/suite -v --installed --tier1 python_smoke_tests: extends: .parallel stage: stage2 needs: - rpm_build script: - "dnf install -y rpms/pcs-*${BASE_IMAGE_NAME}*$(rpm -E %{dist}).*.rpm" - systemctl start pcsd - sleep 2 - ./autogen.sh - ./configure --enable-local-build --enable-individual-bundling --enable-webui - make - rm -rf pcs - pcs_test/smoke.sh artifacts: paths: - /var/log/pcsd/ when: on_failure expire_in: 1 week pcs-0.12.0.2/CHANGELOG.md000066400000000000000000002531151500417470700143440ustar00rootroot00000000000000# Change Log ## [0.12.0.2] - 2025-04-29 ### Fixed - Fixed failing test in the 0.12.0.1 tarball ## [0.12.0.1] - 2025-04-14 ### Fixed - Command `pcs resource restart` allows restarting bundle instances (broken since pcs-0.12.0b1) ([RHEL-79033]) - Do not end with traceback when using `pcs resource delete` to remove bundle resources when the bundle has no IP address specified ([RHEL-79090]) - Make install no longer fails on systems with merged /usr/sbin and /usr/bin [RHEL-79033]: https://issues.redhat.com/browse/RHEL-79033 [RHEL-79090]: https://issues.redhat.com/browse/RHEL-79090 ## [0.12.0] - 2025-01-09 ### Added - Command `pcs booth ticket cleanup` that enables removal of booth tickets still loaded in CIB after their removal from booth configuration ([RHEL-7602]) - Commands `pcs booth ticket standby` and `pcs booth ticket unstandby` which allow for managing the state of the ticket ([RHEL-12709]) ### Fixed - Specify the meaning of zero value timeout in `pcs status wait` ([RHEL-44719]) [RHEL-7602]: https://issues.redhat.com/browse/RHEL-7602 [RHEL-12709]: https://issues.redhat.com/browse/RHEL-12709 [RHEL-44719]: https://issues.redhat.com/browse/RHEL-44719 ## [0.12.0b1] - 2024-11-12 ### Removed - Following pacemaker 3, support for nagios and upstart resource classes has been removed ([RHEL-49520]) - Following pacemaker 3, support for rkt containers in bundle resources has been removed ([RHEL-49521]) - Following pacemaker 3, support for multiple rules in a single location constraint has been removed ([RHEL-62719]) - Removed pcsd configuration option `PCSD_DISABLE_GUI`. It is possible not to install webui support by running `./configure` without flag `--enable-webui`. ### Added - Support for output formats `json` and `cmd` to `pcs tag config` command ([RHEL-21047]) - Add lib commands `cluster.get_corosync_conf_struct` and `resource.get_configured_resources` to API v2 - Lib command `cib.remove_elements` can now remove resources - Add lib command `status.full_cluster_status_plaintext` to API v1 ([RHEL-61747]) - Support for exporting stonith levels in `json` and `cmd` formats in commands `pcs stonith config` and `pcs stonith level config` commands ([RHEL-38483]) ### Changed - Minimal supported version of pacemaker is 3.0 - Minimal required version of python has been changed to 3.12 - Minimal required version of ruby has been changed to 3.1 - Commands `pcs resource delete | remove` and `pcs stonith delete | remove` now allow ([RHEL-61889]): - deletion of multiple resources or stonith resources with one command - deletion of resources or stonith resources included in tags - Commands `pcs resource delete | remove` can no longer remove remote and guest node resources. The `pcs cluster node delete-remote | remove-remote` or `pcs cluster node delete-guest | remove-guest` should be used to properly delete remote or guest nodes. ([RHEL-61889]) - Commands `pcs cluster node delete-remote | remove-remote` no longer remove location constraints referencing the node name of the removed node. This new behavior is consistent with the other node removal commands. - Minimal required version of python3-pyparsing has been changed to 3.0.0 - With deprecation of installation using setup.py, the project was migrated to standard PEP517, PEP518 and PEP621 packaging mechanism. Setuptools is used as a build backend and pip as a build frontend. If you build pcs from source (not using a package), look at the changes in Makefile.am and rpm/spec.in. Update of your building processes might be needed. - Following pacemaker 3, date-spec and duration options in rules have been changed ([RHEL-49524], [RHEL-49527]) ### Fixed - Do not end with error when using the instances quantifier in `pcs status query resource is-state` command ([RHEL-55723]) - Do not display a warning in `pcs status` when a fence\_sbd stonith device has its `method` option set to `cycle` ([RHEL-44432]) - Do not display expired constraints in `pcs constraint location config resources` unless `--all` is specified ([RHEL-33386]) - Displaying status of local and remote cluster sites in `pcs dr status` command. ([RHEL-61747]) [RHEL-21047]: https://issues.redhat.com/browse/RHEL-21047 [RHEL-33386]: https://issues.redhat.com/browse/RHEL-33386 [RHEL-38483]: https://issues.redhat.com/browse/RHEL-38483 [RHEL-44432]: https://issues.redhat.com/browse/RHEL-44432 [RHEL-49520]: https://issues.redhat.com/browse/RHEL-49520 [RHEL-49521]: https://issues.redhat.com/browse/RHEL-49521 [RHEL-49524]: https://issues.redhat.com/browse/RHEL-49524 [RHEL-49527]: https://issues.redhat.com/browse/RHEL-49527 [RHEL-55723]: https://issues.redhat.com/browse/RHEL-55723 [RHEL-61747]: https://issues.redhat.com/browse/RHEL-61747 [RHEL-61889]: https://issues.redhat.com/browse/RHEL-61889 [RHEL-62719]: https://issues.redhat.com/browse/RHEL-62719 ## [0.12.0a1] - 2024-06-21 ### Removed - Using spaces in dates in location constraint rules ([rhbz#2163953]) - Delimiting stonith devices with a comma in `pcs stonith level add | clear | delete | remove` commands - Ambiguous syntax of `pcs stonith level clear | delete | remove` - Legacy role names are no longer accepted by pcs, use `Promoted`, `Unpromoted`, `--promoted`, `promotable`, `promoted-max` - Using stonith resources in `pcs resource` commands and resources in `pcs stonith` commands, as well as `--brief`, `--no-strict`, `--safe` and `--simulate` flags of `pcs stonith disable` command ([RHEL-35428]) - Ability to create stonith resource in a group from `pcs stonith create` command ([RHEL-35428]) - command `stonith.create_in_group` from API v1 and v2 ([RHEL-35428]) - Command `pcs cluster pcsd-status`, replaced with `pcs status pcsd` or `pcs pcsd status` (deprecated since 0.10.9) - Command `pcs cluster certkey`, replaced with `pcs pcsd certkey` (deprecated since 0.10.9) - Command `pcs resource | stonith [op] defaults =...`, replaced with `pcs resource | stonith [op] defaults update` (deprecated since 0.10.7) - Command `pcs acl show`, replaced with `pcs acl config` (deprecated since 0.10.9) - Command `pcs alert show`, replaced with `pcs alert config` (deprecated since 0.10.9) - Commands `pcs constraint [location | colocation | order | ticket] show | list`, replaced with `pcs constraint [location | colocation | order | ticket] config` (deprecated since 0.10.9) - Commands `pcs property show`, `pcs property list`, replaced with `pcs property config` (deprecated since 0.10.9) - Command `pcs tag list`, replaced with `pcs tag config` (deprecated since 0.10.9) - `--autodelete` flag of `pcs resource move` command (deprecated since 0.11) ### Added - Support for output formats `json` and `cmd` to resources/stonith defaults and resource/stonith op defaults config commands ([RHEL-38487]) - Add lib commands `cib_options.resource_defaults_config` and `cib_options.operation_defaults_config` to API v2 - Add lib command `cluster.wait_for_pcmk_idle` to API v2 - Add lib command `status.resources_status` to API v2 - Add `pcs status wait` CLI command ([RHEL-38491]) - Add `pcs status query resource` CLI commands ([RHEL-38489]) - Add commands `constraint.location.add_rule_to_constraint` and `constraint.location.create_with_rule` to API v2 - Using `--yes` to confirm `pcs cluster destroy`, `pcs quorum unblock`, `pcs stonith confirm`, `pcs stonith sbd device setup` and `pcs stonith sbd watchdog test` commands ([RHEL-36612]) - Using `--overwrite` to confirm `pcs cluster report` overwriting files ([RHEL-36612]) - Pkg-config with info for webui is now provided. - Command `resource.restart` in API v2 ### Fixed - Do not put empty uid/gid corosync configuration options to an uidgid file when not specified in `pcs cluster uidgid add` command. Empty options cause corosync start failure. ([ghissue#772]) - Do not allow fencing levels other than 1..9 ([RHEL-38479]) - Adjust OCF metadata processing to support the latest pacemaker changes ([RHEL-27492]) - Use different process creation method for multiprocessing module in order to avoid deadlock on process termination. ([ghissue#780], [RHEL-38478]) - Unified the way score is defined in constraints commands ([RHEL-34792]) - Do not wrap pcs output to terminal width if pcs's stdout is redirected ([RHEL-38481]) - Report an error when an invalid resource-discovery is specified ([RHEL-38480]) - `pcs booth destroy` now works for nodes without a cluster (such as arbitrators) ([RHEL-38486]) - Validate SBD\_DELAY\_START and SBD\_STARTMODE options ([RHEL-38484]) ### Changed - When creating or updating a resource or stonith, its options are now validated by the resource or stonith agent. Unless --agent-validation is specified, this does not prevent misconfiguring the resource or stonith. ([RHEL-35670]) - Standalone webui backend support in pcsd is now optional. It can be enabled by adding `--enable-webui` option to `./configure` command. ([RHEL-29739]) ### Deprecated - Pcs produces warnings about [features planned to be removed in pacemaker 3](https://projects.clusterlabs.org/w/projects/pacemaker/pacemaker_3.0_changes/pacemaker_3.0_configuration_changes/): - score in order constraints - using rkt in bundles - upstart and nagios resources - some date-spec and duration options in rules - Specifying rules as multiple arguments, use a single string argument instead - Specifying score as a standalone value in `pcs constraint location add` and `pcs constraint colocation add`, use score=value instead ([RHEL-34792]) - `--wait` option in resource commands except `pcs resource restart | move`, and in the commands `pcs cluster node add-guest | add-remote`. Instead use: - `pcs status wait` to wait for the cluster to settle into stable state - `pcs status query resource` commands to verify that the resource is in the expected state after the wait - Using `--force` to confirm `pcs cluster destroy`, `pcs quorum unblock`, `pcs stonith confirm`, `pcs stonith sbd device setup` and `pcs stonith sbd watchdog test` commands, use `--yes` instead ([RHEL-36612]) - Using `--force` to confirm overwriting files in `pcs cluster report`, use `--overwrite` instead ([RHEL-36612]) - Assigning and unassigning ACL roles without specifying `user` or `group` keyword [ghissue#772]: https://github.com/ClusterLabs/pcs/issues/772 [ghissue#780]: https://github.com/ClusterLabs/pcs/issues/780 [RHEL-27492]: https://issues.redhat.com/browse/RHEL-27492 [RHEL-29739]: https://issues.redhat.com/browse/RHEL-29739 [RHEL-34792]: https://issues.redhat.com/browse/RHEL-34792 [RHEL-35428]: https://issues.redhat.com/browse/RHEL-35428 [RHEL-35670]: https://issues.redhat.com/browse/RHEL-35670 [RHEL-36612]: https://issues.redhat.com/browse/RHEL-36612 [RHEL-38478]: https://issues.redhat.com/browse/RHEL-38478 [RHEL-38479]: https://issues.redhat.com/browse/RHEL-38479 [RHEL-38480]: https://issues.redhat.com/browse/RHEL-38480 [RHEL-38481]: https://issues.redhat.com/browse/RHEL-38481 [RHEL-38484]: https://issues.redhat.com/browse/RHEL-38484 [RHEL-38486]: https://issues.redhat.com/browse/RHEL-38486 [RHEL-38487]: https://issues.redhat.com/browse/RHEL-38487 [RHEL-38489]: https://issues.redhat.com/browse/RHEL-38489 [RHEL-38491]: https://issues.redhat.com/browse/RHEL-38491 [rhbz#2163953]: https://bugzilla.redhat.com/show_bug.cgi?id=2163953 ## [0.11.7] - 2024-01-11 ### Security - Make use of filters when extracting tarballs to enhance security if provided by Python (`pcs config restore` command) ([rhbz#2219407]) ### Added - Support ISO 8601 duration specifications for pacemaker "time" properties ([rhbz#2112268], [ghpull#712]) - It is now possible to move bundle resources (requires pacemaker 2.1.6 or newer) and clone resources ([RHEL-7744]) - Add lib command `cib.remove_elements` to API v2. ### Fixed - Exporting constraints with rules in form of pcs commands now escapes `#` and fixes spaces in dates to make the commands valid ([rhbz#2163953]) - Do not display duplicate records in commands `pcs property [config] --all` and `pcs property describe` ([rhbz#2217850]) - Commands `pcs property defaults` and `pcs property describe` print error message in case specified properties do not have metadata. ([rhbz#2222744]) - Clarify messages informing users that cluster must be stopped in order to change certain corosync options ([rhbz#2175797]) - Prevent disabling `auto_tie_breaker` when it would make SBD not working properly ([rhbz#2227230]) - Improved error messages and documentation of `pcs resource move` command ([rhbz#2219554]) - Do not create files in /tmp directory for commands running by ruby daemon ([ghissue#731]) - Do not display warning in `pcs status` for expired constraints that were created by moving resources ([rhbz#2111591]) - Fixed validation for interval and timeout option values of an operation specified for `pcs resource create` command ([rhbz#2179084]). - Improved error message of `pcs booth ticket grant|revoke` commands in case a booth site address parameter is needed ([rhbz#2232143]) - When moving or banning a resource in a bundle, pcs now errors out instead of creating a move / ban constraint which does nothing ([RHEL-7744]) ### Changed - When exporting constraints in form of pcs commands, constraints containing options unsupported by pcs are not exported and a warning is printed instead. Previously, the warnings were printed, but the constraints were exported regardless. ([rhbz#2163953]) - Allow `tls` and `keep_active_partition_tie_breaker` options for qdevice model "net" to be set using `pcs quorum device add` and `pcs quorum device update` commands ([rhbz#2234717]) ### Deprecated - Using spaces in dates in location constraint rules (using spaces in dates in rules in other parts of configuration was never allowed) ([rhbz#2163953]) - Using `--group`, `--after` and `--before` in `pcs resource create` command is deprecated in favor of `group`, `after` and `before`, respectively [ghissue#731]: https://github.com/ClusterLabs/pcs/issues/731 [ghpull#712]: https://github.com/ClusterLabs/pcs/pull/712 [rhbz#2112268]: https://bugzilla.redhat.com/show_bug.cgi?id=2112268 [rhbz#2163953]: https://bugzilla.redhat.com/show_bug.cgi?id=2163953 [rhbz#2175797]: https://bugzilla.redhat.com/show_bug.cgi?id=2175797 [rhbz#2179084]: https://bugzilla.redhat.com/show_bug.cgi?id=2179084 [rhbz#2217850]: https://bugzilla.redhat.com/show_bug.cgi?id=2217850 [rhbz#2219407]: https://bugzilla.redhat.com/show_bug.cgi?id=2219407 [rhbz#2219554]: https://bugzilla.redhat.com/show_bug.cgi?id=2219554 [rhbz#2222744]: https://bugzilla.redhat.com/show_bug.cgi?id=2222744 [rhbz#2227230]: https://bugzilla.redhat.com/show_bug.cgi?id=2227230 [rhbz#2111591]: https://bugzilla.redhat.com/show_bug.cgi?id=2111591 [rhbz#2232143]: https://bugzilla.redhat.com/show_bug.cgi?id=2232143 [rhbz#2234717]: https://bugzilla.redhat.com/show_bug.cgi?id=2234717 [RHEL-7744]: https://issues.redhat.com/browse/RHEL-7744 ## [0.11.6] - 2023-06-20 ### Added - Support for output formats `json` and `cmd` to constraints config commands ([rhbz#2179388], [rhbz#1423473], [rhbz#2163953]) - Automatic restarts of the Puma web server in the legacy Ruby daemon to reduce its memory footprint ([rhbz#1860626]) - New URL for listing pcsd capabilities: `/capabilities` - It is now possible to list pcsd capabilities even if pcsd is not running: `pcsd --version --full` - Add lib commands `cluster_property.get_properties` and `cluster_property.get_properties_metadata` to API v2 - Add `pcs property defaults` and `pcs property describe` CLI commands - Support for output formats `json` and `cmd` to property config command ([rhbz#2163914]) - Commands `pcs resource describe` and `pcs stonith describe` print detailed info about resource options (data type or allowed values, default value) - Add warning to `pcs resource utilization` and `pcs node utilization` for the case configuration is not in effect (cluster property `placement-strategy` is not set appropriately) ([rhbz#1465829]) - New format of `pcs resource create` command which requires `meta` keyword for specifying clone and promotable meta attributes is available to be enabled by specifying --future ([rhbz#2168155]) ### Fixed - Crash in commands that ask for user input (like `pcs cluster destroy`) when stdin is closed ([ghissue#612]) - Fix displaying differences between configuration checkpoints in `pcs config checkpoint diff` command ([rhbz#2175881]) - Fix `pcs stonith update-scsi-devices` command which was broken since Pacemaker-2.1.5-rc1 ([rhbz#2177996]) - Make `pcs resource disable --simulate --brief` documentation clearer ([rhbz#2109852]) - Fixed a regression causing crash in `pcs resource move` command (broken since pcs-0.11.5) ([rhbz#2210855]) - Using `--force` in `pcs resource meta` command had no effect on a specific error message even if the message suggested otherwise. ### Changed - Commands for displaying cluster configuration have been slightly updated: - Headings of empty sections are no longer displayed - Resource listing is more dense as operations options are shown in a single line - Specifying `--full` to show IDs of elements now shows IDs of nvpairs as well ### Deprecated - Specifying clone and promotable meta attributes without the `meta` keyword is now deprecated, i.e. `pcs resource clone myResource name=value` is deprecated by `pcs resource clone myResource meta name=value` ([rhbz#2168155], [ghpull#648]) [ghissue#612]: https://github.com/ClusterLabs/pcs/issues/612 [ghpull#648]: https://github.com/ClusterLabs/pcs/pull/648 [rhbz#1423473]: https://bugzilla.redhat.com/show_bug.cgi?id=1423473 [rhbz#1465829]: https://bugzilla.redhat.com/show_bug.cgi?id=1465829 [rhbz#1860626]: https://bugzilla.redhat.com/show_bug.cgi?id=1860626 [rhbz#2109852]: https://bugzilla.redhat.com/show_bug.cgi?id=2109852 [rhbz#2163914]: https://bugzilla.redhat.com/show_bug.cgi?id=2163914 [rhbz#2163953]: https://bugzilla.redhat.com/show_bug.cgi?id=2163953 [rhbz#2168155]: https://bugzilla.redhat.com/show_bug.cgi?id=2168155 [rhbz#2175881]: https://bugzilla.redhat.com/show_bug.cgi?id=2175881 [rhbz#2177996]: https://bugzilla.redhat.com/show_bug.cgi?id=2177996 [rhbz#2179388]: https://bugzilla.redhat.com/show_bug.cgi?id=2179388 [rhbz#2210855]: https://bugzilla.redhat.com/show_bug.cgi?id=2210855 ## [0.11.5] - 2023-03-01 ### Added - Warning to `pcs resource|stonith update` commands about not using agent self-validation feature when the resource is already misconfigured ([rhbz#2151524]) - Add lib command `cluster_property.set_properties` to API v2 - Commands for checking and creating qdevice certificates on the local node only ### Fixed - Graceful stopping pcsd service using `systemctl stop pcsd` command - Displaying bool and integer values in `pcs resource config` command ([rhbz#2151164], [ghissue#604]) - Allow time values in stonith-watchdog-time property ([rhbz#2158790]) - Enable/Disable sbd when cluster is not running ([rhbz#2166249]) - Confusing error message in `pcs constraint ticket add` command ([rhbz#2168617], [ghpull#559]) - Internal server error during cluster setup with Ruby 3.2 - Set `Content-Security-Policy: frame-ancestors 'self'; default-src 'self'` HTTP header for HTTP 404 responses as well ([rhbz#2160664]) - Validate dates in location constraint rules ([ghpull#644]) ### Changed - Resource/stonith agent self-validation of instance attributes is now disabled by default, as many agents do not work with it properly. Use flag '--agent-validation' to enable it in supported commands. ([rhbz#2159454]) [ghissue#604]: https://github.com/ClusterLabs/pcs/issues/604 [ghpull#559]: https://github.com/ClusterLabs/pcs/pull/559 [ghpull#644]: https://github.com/ClusterLabs/pcs/pull/644 [rhbz#2151164]: https://bugzilla.redhat.com/show_bug.cgi?id=2151164 [rhbz#2151524]: https://bugzilla.redhat.com/show_bug.cgi?id=2151524 [rhbz#2158790]: https://bugzilla.redhat.com/show_bug.cgi?id=2158790 [rhbz#2159454]: https://bugzilla.redhat.com/show_bug.cgi?id=2159454 [rhbz#2160664]: https://bugzilla.redhat.com/show_bug.cgi?id=2160664 [rhbz#2166249]: https://bugzilla.redhat.com/show_bug.cgi?id=2166249 [rhbz#2168617]: https://bugzilla.redhat.com/show_bug.cgi?id=2168617 ## [0.11.4] - 2022-11-21 ### Security - CVE-2022-2735 pcs: obtaining an authentication token for hacluster user could lead to privilege escalation ([rhbz#2116841]) ### Added - API v2 providing asynchronous interface for pcsd. Note that this feature is in tech-preview state and thus may be changed in the future - Support for resource/stonith agent self-validation of instance attributes via pacemaker ([rhbz#2112270]) - Support for booth 'enable-authfile' fix ([rhbz#2116295]) ### Fixed - `pcs resource manage --monitor` no longer enables monitor operation for all resources in a group if only one of the resources was requested to become managed ([rhbz#2092950]) - `pcs resource restart` command works again (broken in pcs-0.11.3) ([rhbz#2102663]) - Misleading error message from `pcs booth sync` when booth config directory (`/etc/booth`) is missing ([rhbz#1791670]) - Creating a promotable or globally-unique clones is not allowed for non-ocf resource agents ([rhbz#1493416]) - Improved cluster properties validators, OCF 1.1 now supported ([rhbz#2019464]) - `pcs property set/unset` forbid manipulation of specific cluster properties ([rhbz#1620043]) [rhbz#1493416]: https://bugzilla.redhat.com/show_bug.cgi?id=1493416 [rhbz#1620043]: https://bugzilla.redhat.com/show_bug.cgi?id=1620043 [rhbz#1791670]: https://bugzilla.redhat.com/show_bug.cgi?id=1791670 [rhbz#2019464]: https://bugzilla.redhat.com/show_bug.cgi?id=2019464 [rhbz#2092950]: https://bugzilla.redhat.com/show_bug.cgi?id=2092950 [rhbz#2102663]: https://bugzilla.redhat.com/show_bug.cgi?id=2102663 [rhbz#2112270]: https://bugzilla.redhat.com/show_bug.cgi?id=2112270 [rhbz#2116295]: https://bugzilla.redhat.com/show_bug.cgi?id=2116295 [rhbz#2116841]: https://bugzilla.redhat.com/show_bug.cgi?id=2116841 ## [0.11.3] - 2022-06-23 ### Security - CVE-2022-1049: Pcs daemon was allowing expired accounts, and accounts with expired passwords to login when using PAM auth. ([huntr#220307], [rhbz#2068457]) - Pcsd does not expose the server name in HTTP headers anymore ([rhbz#2059122]) - Set `Strict-Transport-Security: max-age=63072000` HTTP header for all responses ([rhbz#2097731]) - Set HTTP headers to prevent caching everything except static files ([rhbz#2097733]) - Set HTTP headers to prevent sending referrer ([rhbz#2097732]) - Set cookie option SameSite to Lax ([rhbz#2097730]) - Set `Content-Security-Policy: frame-ancestors 'self'; default-src 'self'` HTTP header for all responses ([rhbz#2097778]) ### Added - Add support for fence\_mpath to `pcs stonith update-scsi-devices` command ([rhbz#2024522]) - Support for cluster UUIDs. New clusters now get a UUID during setup. Existing clusters can get a UUID by running the new `pcs cluster config uuid generate` command ([rhbz#2054671]) - Add warning regarding move constraints to `pcs status` ([rhbz#2058247]) - Support for output formats `json` and `cmd` to `pcs resource config` and `pcs stonith config` commands ([rhbz#2058251], [rhbz#2058252]) ### Fixed - Booth ticket name validation ([rhbz#2053177]) - Adding booth ticket doesn't report 'mode' as an unknown option anymore ([rhbz#2058243]) - Preventing fence-loop caused when stonith-watchdog-timeout is set with wrong value ([rhbz#2058246]) - Do not allow to create an order constraint for resources in one group as that may block Pacemaker ([ghpull#509]) - `pcs quorum device remove` works again ([rhbz#2095695]) - Fixed description of full permission ([rhbz#2059177]) [ghpull#509]: https://github.com/ClusterLabs/pcs/pull/509 [rhbz#2024522]: https://bugzilla.redhat.com/show_bug.cgi?id=2024522 [rhbz#2053177]: https://bugzilla.redhat.com/show_bug.cgi?id=2053177 [rhbz#2054671]: https://bugzilla.redhat.com/show_bug.cgi?id=2054671 [rhbz#2058243]: https://bugzilla.redhat.com/show_bug.cgi?id=2058243 [rhbz#2058246]: https://bugzilla.redhat.com/show_bug.cgi?id=2058246 [rhbz#2058247]: https://bugzilla.redhat.com/show_bug.cgi?id=2058247 [rhbz#2058251]: https://bugzilla.redhat.com/show_bug.cgi?id=2058251 [rhbz#2058252]: https://bugzilla.redhat.com/show_bug.cgi?id=2058252 [rhbz#2059122]: https://bugzilla.redhat.com/show_bug.cgi?id=2059122 [rhbz#2059177]: https://bugzilla.redhat.com/show_bug.cgi?id=2059177 [rhbz#2068457]: https://bugzilla.redhat.com/show_bug.cgi?id=2068457 [rhbz#2095695]: https://bugzilla.redhat.com/show_bug.cgi?id=2095695 [rhbz#2097730]: https://bugzilla.redhat.com/show_bug.cgi?id=2097730 [rhbz#2097731]: https://bugzilla.redhat.com/show_bug.cgi?id=2097731 [rhbz#2097732]: https://bugzilla.redhat.com/show_bug.cgi?id=2097732 [rhbz#2097733]: https://bugzilla.redhat.com/show_bug.cgi?id=2097733 [rhbz#2097778]: https://bugzilla.redhat.com/show_bug.cgi?id=2097778 [huntr#220307]: https://huntr.dev/bounties/7aa921fc-a568-4fd8-96f4-7cd826246aa5/ ## [0.11.2] - 2022-02-01 ### Fixed - Pcs was not automatically enabling corosync-qdevice when adding a quorum device to a cluster (broken since pcs-0.10.9) ([rhbz#2028902]) - `resource update` command exiting with a traceback when updating a resource with a non-existing resource agent ([rhbz#2019836]) - pcs\_snmp\_agent is working again (broken since pcs-0.10.1) ([ghpull#431]) - Skip checking of scsi devices to be removed before unfencing to be added devices ([rhbz#2033248]) - Make `ocf:linbit:drbd` agent pass OCF standard validation ([ghissue#441], [rhbz#2036633]) - Multiple improvements of `pcs resource move` command ([rhbz#1996062]) - Pcs no longer creates Pacemaker-1.x CIB when `-f` is used, so running `pcs cluster cib-upgrade` manually is not needed ([rhbz#2022463]) ### Deprecated - Usage of `pcs resource` commands for stonith resources and vice versa ([rhbz#1301204]) [ghissue#441]: https://github.com/ClusterLabs/pcs/issues/441 [ghpull#431]: https://github.com/ClusterLabs/pcs/pull/431 [rhbz#1301204]: https://bugzilla.redhat.com/show_bug.cgi?id=1301204 [rhbz#1996062]: https://bugzilla.redhat.com/show_bug.cgi?id=1996062 [rhbz#2019836]: https://bugzilla.redhat.com/show_bug.cgi?id=2019836 [rhbz#2022463]: https://bugzilla.redhat.com/show_bug.cgi?id=2022463 [rhbz#2028902]: https://bugzilla.redhat.com/show_bug.cgi?id=2028902 [rhbz#2033248]: https://bugzilla.redhat.com/show_bug.cgi?id=2033248 [rhbz#2036633]: https://bugzilla.redhat.com/show_bug.cgi?id=2036633 ## [0.11.1] - 2021-11-30 ### Removed - Deprecated obsolete commands `pcs config import-cman` and `pcs config export pcs-commands|pcs-commands-verbose` have been removed ([rhbz#1881064]) - Unused and unmaintained pcsd urls: `/remote/config_backup`, `/remote/node_available`, `/remote/resource_status` - Pcsd no longer provides data in format used by web UI in pcs 0.9.142 and older ### Added - Explicit confirmation is now required to prevent accidental destroying of the cluster with `pcs cluster destroy` ([rhbz#1283805]) - Add add/remove cli syntax for command `pcs stonith update-scsi-devices` ([rhbz#1992668]) - Command `pcs resource move` is fully supported ([rhbz#1990787]) - Support for OCF 1.1 resource and stonith agents ([rhbz#2018969]) ### Changed - Pcs no longer depends on python3-distro package - 'pcs status xml' now prints cluster status in the new format provided by Pacemaker 2.1 ([rhbz#1985981]) - All errors, warning and progress related output is now printed to stderr instead of stdout - Make roles `Promoted` and `Unpromoted` default ([rhbz#1885293]) - Make auto-deleting constraint default for `pcs resource move` command ([rhbz#1996062]) - Deprecation warnings use a "Deprecation Warning:" prefix instead of "Warning:" on the command line - Minimal required version of python has been changed to 3.9 - Minimal required version of ruby has been changed to 2.5 - Minimal supported version of pacemaker is 2.1 ### Fixed - Do not unfence newly added devices on fenced cluster nodes ([rhbz#1991654]) - Fix displaying fencing levels with regular expression targets ([rhbz#1533090]) - Reject cloning of stonith resources ([rhbz#1811072]) - Do not show warning that no stonith device was detected and stonith-enabled is not false when a stonith device is in a group ([ghpull#370]) - Misleading error message from `pcs quorum unblock` when `wait_for_all=0` ([rhbz#1968088]) - Misleading error message from `pcs booth setup` and `pcs booth pull` when booth config directory (`/etc/booth`) is missing ([rhbz#1791670], [ghpull#411], [ghissue#225]) ### Deprecated - Legacy role names `Master` and `Slave` ([rhbz#1885293]) - Option `--master` is deprecated and has been replaced by option `--promoted` ([rhbz#1885293]) [ghissue#225]: https://github.com/ClusterLabs/pcs/issues/225 [ghpull#370]: https://github.com/ClusterLabs/pcs/pull/370 [ghpull#411]: https://github.com/ClusterLabs/pcs/pull/411 [rhbz#1283805]: https://bugzilla.redhat.com/show_bug.cgi?id=1283805 [rhbz#1533090]: https://bugzilla.redhat.com/show_bug.cgi?id=1533090 [rhbz#1791670]: https://bugzilla.redhat.com/show_bug.cgi?id=1791670 [rhbz#1811072]: https://bugzilla.redhat.com/show_bug.cgi?id=1811072 [rhbz#1881064]: https://bugzilla.redhat.com/show_bug.cgi?id=1881064 [rhbz#1885293]: https://bugzilla.redhat.com/show_bug.cgi?id=1885293 [rhbz#1968088]: https://bugzilla.redhat.com/show_bug.cgi?id=1968088 [rhbz#1985981]: https://bugzilla.redhat.com/show_bug.cgi?id=1985981 [rhbz#1990787]: https://bugzilla.redhat.com/show_bug.cgi?id=1990787 [rhbz#1991654]: https://bugzilla.redhat.com/show_bug.cgi?id=1991654 [rhbz#1992668]: https://bugzilla.redhat.com/show_bug.cgi?id=1992668 [rhbz#1996062]: https://bugzilla.redhat.com/show_bug.cgi?id=1996062 [rhbz#2018969]: https://bugzilla.redhat.com/show_bug.cgi?id=2018969 ## [0.10.10] - 2021-08-19 ### Added - Support for new role names introduced in pacemaker 2.1 ([rhbz#1885293]) ### Fixed - Traceback in some cases when --wait without timeout is used [rhbz#1885293]: https://bugzilla.redhat.com/show_bug.cgi?id=1885293 ## [0.10.9] - 2021-08-10 ### Added - Elliptic curve TLS certificates are now supported in pcsd ([ghissue#123]) - Support for corosync option `totem.block_unlisted_ips` ([rhbz#1720221]) - Support for displaying status of a single resource or tag ([rhbz#1290830]) - Support for displaying status of resources on a specified node ([rhbz#1285269]) - New option `--brief` for `pcs resource disable --safe` or its alias `pcs resource safe-disable` that only prints errors ([rhbz#1909901]) - Support for updating scsi fencing devices without affecting other resources added in the new command `pcs stonith update-scsi-devices` ([rhbz#1759995], [rhbz#1872378]) - Option `--autodelete` for `pcs resource move` command which removes a location constraint used for moving a resource, once the resource has been moved. This feature is in tech-preview state and thus may be changed in the future ([rhbz#1847102]) ### Fixed - Node attribute expressions are now correctly reported as not allowed in resource defaults rules ([rhbz#1896458]) - Upgreded to jquery 3.6.0 ([rhbz#1882291, rhbz#1886342]) - Man page and help: note that 'pcs resource unclone' accepts clone resources as well ([rhbz#1930886]) - Improved error messages when a host is found to be a part of a cluster already ([rhbz#1690419]) - `pcs cluster sync` command now warns reloading corosync config is necessary for changes to take effect ([rhbz#1750240]) - Show user friendly error if unable to delete a group (due to the group being referenced within configuration) when moving resources out of the the group. ([rhbz#1678273]) - Exit with an error if `on-fail=demote` is specified for a resource operation and pacemaker doesn't support it - The `pcs status nodes` command now correctly shows status of nodes that are both in maintenance and standby modes ([rhbz#1432097]) ### Changed - python3-openssl was replaced with python3-cryptography ([rhbz#1927404]) ### Deprecated - `pcs acl show` replaced with `pcs acl config` - `pcs alert show` replaced with `pcs alert config` - Undocumented command `pcs cluster certkey` replaced with `pcs pcsd certkey` - `pcs cluster pcsd-status` replaced with `pcs status pcsd` or `pcs pcsd status` - `pcs constraint [location | colocation | order | ticket] show | list` replaced with `pcs constraint [location | colocation | order | ticket] config` - `pcs property show`, `pcs property list` replaced with `pcs property config` - pcsd urls: `/remote/config_backup`, `/remote/node_available`, `/remote/node_restart`, `/remote/resource_status` - Undocumented syntax for constraint location rules: - `date start= gt` replaced with `date gt ` - `date end= lt` replaced with `date lt ` - `date start= end= in_range` replaced with `date in_range to ` - `operation=date_spec` replaced with `date-spec ` - converting invalid score to score-attribute=pingd - Delimiting stonith devices with a comma in `pcs stonith level add | clear | delete | remove` commands, use a space instead - `pcs stonith level delete | remove [] []...` replaced with `pcs stonith level delete | remove [target ] [stonith ]...` - `pcs stonith level clear [ | ]` replaced with `pcs stonith level clear [target | stonith ...]` - `pcs tag list` replaced with `pcs tag config` [ghissue#123]: https://github.com/ClusterLabs/pcs/issues/123 [rhbz#1285269]: https://bugzilla.redhat.com/show_bug.cgi?id=1285269 [rhbz#1290830]: https://bugzilla.redhat.com/show_bug.cgi?id=1290830 [rhbz#1432097]: https://bugzilla.redhat.com/show_bug.cgi?id=1432097 [rhbz#1678273]: https://bugzilla.redhat.com/show_bug.cgi?id=1678273 [rhbz#1690419]: https://bugzilla.redhat.com/show_bug.cgi?id=1690419 [rhbz#1720221]: https://bugzilla.redhat.com/show_bug.cgi?id=1720221 [rhbz#1750240]: https://bugzilla.redhat.com/show_bug.cgi?id=1750240 [rhbz#1759995]: https://bugzilla.redhat.com/show_bug.cgi?id=1759995 [rhbz#1847102]: https://bugzilla.redhat.com/show_bug.cgi?id=1847102 [rhbz#1872378]: https://bugzilla.redhat.com/show_bug.cgi?id=1872378 [rhbz#1882291]: https://bugzilla.redhat.com/show_bug.cgi?id=1882291 [rhbz#1886342]: https://bugzilla.redhat.com/show_bug.cgi?id=1886342 [rhbz#1896458]: https://bugzilla.redhat.com/show_bug.cgi?id=1896458 [rhbz#1909901]: https://bugzilla.redhat.com/show_bug.cgi?id=1909901 [rhbz#1927404]: https://bugzilla.redhat.com/show_bug.cgi?id=1927404 [rhbz#1930886]: https://bugzilla.redhat.com/show_bug.cgi?id=1930886 ## [0.10.8] - 2021-02-01 ### Added - Support for changing corosync configuration in an existing cluster ([rhbz#1457314], [rhbz#1667061], [rhbz#1856397], [rhbz#1774143]) - Command to show structured corosync configuration (see `pcs cluster config show` command) ([rhbz#1667066]) ### Fixed - Improved error message with a hint in `pcs cluster cib-push` ([ghissue#241]) - Option --wait was not working with pacemaker 2.0.5+ ([ghissue#260]) - Explicitly close libcurl connections to prevent stalled TCP connections in CLOSE-WAIT state ([ghissue#261], [rhbz#1885841]) - Fixed parsing negative float numbers on command line ([rhbz#1869399]) - Removed unwanted logging to system log (/var/log/messages) ([rhbz#1917286]) - Fixed rare race condition in `pcs cluster start --wait` ([rhbz#1794062]) - Better error message when unable to connect to pcsd ([rhbz#1619818]) ### Deprecated - Commands `pcs config import-cman` and `pcs config export pcs-commands|pcs-commands-verbose` have been deprecated ([rhbz#1851335]) - Entering values starting with '-' (negative numbers) without '--' on command line is now deprecated ([rhbz#1869399]) [ghissue#241]: https://github.com/ClusterLabs/pcs/issues/241 [ghissue#260]: https://github.com/ClusterLabs/pcs/issues/260 [ghissue#261]: https://github.com/ClusterLabs/pcs/issues/261 [rhbz#1457314]: https://bugzilla.redhat.com/show_bug.cgi?id=1457314 [rhbz#1619818]: https://bugzilla.redhat.com/show_bug.cgi?id=1619818 [rhbz#1667061]: https://bugzilla.redhat.com/show_bug.cgi?id=1667061 [rhbz#1667066]: https://bugzilla.redhat.com/show_bug.cgi?id=1667066 [rhbz#1774143]: https://bugzilla.redhat.com/show_bug.cgi?id=1774143 [rhbz#1794062]: https://bugzilla.redhat.com/show_bug.cgi?id=1794062 [rhbz#1851335]: https://bugzilla.redhat.com/show_bug.cgi?id=1851335 [rhbz#1856397]: https://bugzilla.redhat.com/show_bug.cgi?id=1856397 [rhbz#1869399]: https://bugzilla.redhat.com/show_bug.cgi?id=1869399 [rhbz#1885841]: https://bugzilla.redhat.com/show_bug.cgi?id=1885841 [rhbz#1917286]: https://bugzilla.redhat.com/show_bug.cgi?id=1917286 ## [0.10.7] - 2020-09-30 ### Added - Support for multiple sets of resource and operation defaults, including support for rules ([rhbz#1222691], [rhbz#1817547], [rhbz#1862966], [rhbz#1867516], [rhbz#1869399]) - Support for "demote" value of resource operation's "on-fail" option ([rhbz#1843079]) - Support for 'number' type in rules ([rhbz#1869399]) - It is possible to set custom (promotable) clone id in `pcs resource create` and `pcs resource clone/promotable` commands ([rhbz#1741056]) ### Fixed - Prevent removing non-empty tag by removing tagged resource group or clone ([rhbz#1857295]) - Clarify documentation for 'resource move' and 'resource ban' commands with regards to the 'lifetime' option. - Allow moving both promoted and demoted promotable clone resources ([rhbz#1875301]) ### Deprecated - `pcs resource [op] defaults =...` commands are deprecated now. Use `pcs resource [op] defaults update =...` if you only manage one set of defaults, or `pcs resource [op] defaults set` if you manage several sets of defaults. ([rhbz#1817547]) [rhbz#1222691]: https://bugzilla.redhat.com/show_bug.cgi?id=1222691 [rhbz#1741056]: https://bugzilla.redhat.com/show_bug.cgi?id=1741056 [rhbz#1817547]: https://bugzilla.redhat.com/show_bug.cgi?id=1817547 [rhbz#1843079]: https://bugzilla.redhat.com/show_bug.cgi?id=1843079 [rhbz#1857295]: https://bugzilla.redhat.com/show_bug.cgi?id=1857295 [rhbz#1862966]: https://bugzilla.redhat.com/show_bug.cgi?id=1862966 [rhbz#1867516]: https://bugzilla.redhat.com/show_bug.cgi?id=1867516 [rhbz#1869399]: https://bugzilla.redhat.com/show_bug.cgi?id=1869399 [rhbz#1875301]: https://bugzilla.redhat.com/show_bug.cgi?id=1875301 ## [0.10.6] - 2020-06-11 ### Security - Web UI sends HTTP headers: Content-Security-Policy, X-Frame-Options and X-Xss-Protection ### Added - When creating a cluster, verify the cluster name does not prevent mounting GFS2 volumes ([rhbz#1782553]) - An option to run 'pcs cluster setup' in a local mode (do not connect to any nodes, save corosync.conf to a specified file) ([rhbz#1839637]) - Support for pacemaker tags. Pcs provides commands for creating and removing tags, adding and/or removing IDs to/from tags, and listing current tag configuration. ([rhbz#1684676]) - Support for tag ids in commands resource enable/disable/manage/unmanage ([rhbz#1684676]) - `pcs resource [safe-]disable --simulate` has a new option `--brief` to print only a list of affected resources ([rhbz#1833114]) ### Fixed - Keep autogenerated IDs of set constraints reasonably short ([rhbz#1387358], [rhbz#1824206]) - Pcs is now compatible with Ruby 2.7 and Python 3.8. To achieve this, it newly depends on python3-distro package. - `pcs status` works on remote nodes again (broken since pcs-0.10.4) ([rhbz#1830552]) - Fixed inability to create colocation constraint from web ui ([rhbz#1832973]) - Actions going through pcsd no longer time out after 30s (broken since pcs-0.10.5) ([rhbz#1833506]) [rhbz#1387358]: https://bugzilla.redhat.com/show_bug.cgi?id=1387358 [rhbz#1684676]: https://bugzilla.redhat.com/show_bug.cgi?id=1684676 [rhbz#1782553]: https://bugzilla.redhat.com/show_bug.cgi?id=1782553 [rhbz#1824206]: https://bugzilla.redhat.com/show_bug.cgi?id=1824206 [rhbz#1830552]: https://bugzilla.redhat.com/show_bug.cgi?id=1830552 [rhbz#1832973]: https://bugzilla.redhat.com/show_bug.cgi?id=1832973 [rhbz#1833114]: https://bugzilla.redhat.com/show_bug.cgi?id=1833114 [rhbz#1833506]: https://bugzilla.redhat.com/show_bug.cgi?id=1833506 [rhbz#1839637]: https://bugzilla.redhat.com/show_bug.cgi?id=1839637 ## [0.10.5] - 2020-03-18 ### Added - It is possible to configure a disaster-recovery site and display its status ([rhbz#1676431]) ### Fixed - Error messages in cases when cluster is not set up ([rhbz#1743731]) - Improved documentation of configuring links in the 'pcs cluster setup' command - Safe-disabling clones and groups does not fail any more due to their inner resources get stopped ([rhbz#1781303]) - Booth documentation clarified ([ghissue#231]) - Detection of fence history support ([rhbz#1793574]) - Fix documentation and flags regarding bundled/cloned/grouped resources for `pcs (resource | stonith) (cleanup | refresh)` ([rhbz#1805082]) - Improved ACL documentation ([rhbz#1722970]) - Added missing Strict-Transport-Security headers to redirects ([rhbz#1810017]) - Improved pcsd daemon performance ([rhbz#1783106]) [ghissue#231]: https://github.com/ClusterLabs/pcs/issues/231 [rhbz#1676431]: https://bugzilla.redhat.com/show_bug.cgi?id=1676431 [rhbz#1722970]: https://bugzilla.redhat.com/show_bug.cgi?id=1722970 [rhbz#1743731]: https://bugzilla.redhat.com/show_bug.cgi?id=1743731 [rhbz#1781303]: https://bugzilla.redhat.com/show_bug.cgi?id=1781303 [rhbz#1783106]: https://bugzilla.redhat.com/show_bug.cgi?id=1783106 [rhbz#1793574]: https://bugzilla.redhat.com/show_bug.cgi?id=1793574 [rhbz#1805082]: https://bugzilla.redhat.com/show_bug.cgi?id=1805082 [rhbz#1810017]: https://bugzilla.redhat.com/show_bug.cgi?id=1810017 ## [0.10.4] - 2019-11-28 ### Added - New section in pcs man page summarizing changes in pcs-0.10. Commands removed or changed in pcs-0.10 print errors poiting to that section. ([rhbz#1728890]) - `pcs resource disable` can show effects of disabling resources and prevent disabling resources if any other resources would be affected ([rhbz#1631519]) - `pcs resource relations` command shows relations between resources such as ordering constraints, ordering set constraints and relations defined by resource hierarchy ([rhbz#1631514]) ### Changed - Expired location constraints are now hidden by default when listing constraints in any way. Using `--all` will list and denote them with `(expired)`. All expired rules are then marked the same way. ([rhbz#1442116]) ### Fixed - All node names and scores are validated when running `pcs constraint location avoids/prefers` before writing configuration to cib ([rhbz#1673835]) - Fixed crash when an invalid port is given in an address to the `pcs host auth` command ([rhbz#1698763]) - Command `pcs cluster verify` suggests `--full` option instead of `-V` option which is not recognized by pcs ([rhbz#1712347]) - It is now possible to authenticate remote clusters in web UI even if the local cluster is not authenticated ([rhbz#1743735]) - Documentation of `pcs constraint colocation add` ([rhbz#1734361]) - Empty constraint option are not allowed in `pcs constraint order` and `pcs constraint colocation add` commands ([rhbz#1734361]) - More fixes for the case when PATH environment variable is not set - Fixed crashes and other issues when UTF-8 characters are present in the corosync.conf file ([rhbz#1741586]) [rhbz#1442116]: https://bugzilla.redhat.com/show_bug.cgi?id=1442116 [rhbz#1631514]: https://bugzilla.redhat.com/show_bug.cgi?id=1631514 [rhbz#1631519]: https://bugzilla.redhat.com/show_bug.cgi?id=1631519 [rhbz#1673835]: https://bugzilla.redhat.com/show_bug.cgi?id=1673835 [rhbz#1698763]: https://bugzilla.redhat.com/show_bug.cgi?id=1698763 [rhbz#1712347]: https://bugzilla.redhat.com/show_bug.cgi?id=1712347 [rhbz#1728890]: https://bugzilla.redhat.com/show_bug.cgi?id=1728890 [rhbz#1734361]: https://bugzilla.redhat.com/show_bug.cgi?id=1734361 [rhbz#1741586]: https://bugzilla.redhat.com/show_bug.cgi?id=1741586 [rhbz#1743735]: https://bugzilla.redhat.com/show_bug.cgi?id=1743735 ## [0.10.3] - 2019-08-23 ### Fixed - Fixed crashes in the `pcs host auth` command ([rhbz#1676957]) - Fixed id conflict with current bundle configuration in `pcs resource bundle reset` ([rhbz#1657166]) - Options starting with - and -- are no longer ignored for non-root users (broken since pcs-0.10.2) ([rhbz#1725183]) - Fixed crashes when pcs is configured that no rubygems are bundled in pcs package ([ghissue#208]) - Standby nodes running resources are listed separately in `pcs status nodes` - Parsing arguments in the `pcs constraint order` and `pcs constraint colocation add` commands has been improved, errors which were previously silent are now reported ([rhbz#1734361]) - Fixed shebang correction in Makefile ([ghissue#206]) - Generate 256 bytes long corosync authkey, longer keys are not supported when FIPS is enabled ([rhbz#1740218]) ### Changed - Command `pcs resource bundle reset` no longer accepts the container type ([rhbz#1657166]) [ghissue#206]: https://github.com/ClusterLabs/pcs/issues/206 [ghissue#208]: https://github.com/ClusterLabs/pcs/issues/208 [rhbz#1657166]: https://bugzilla.redhat.com/show_bug.cgi?id=1657166 [rhbz#1676957]: https://bugzilla.redhat.com/show_bug.cgi?id=1676957 [rhbz#1725183]: https://bugzilla.redhat.com/show_bug.cgi?id=1725183 [rhbz#1734361]: https://bugzilla.redhat.com/show_bug.cgi?id=1734361 [rhbz#1740218]: https://bugzilla.redhat.com/show_bug.cgi?id=1740218 ## [0.10.2] - 2019-06-12 ### Added - Command `pcs config checkpoint diff` for displaying differences between two specified checkpoints ([rhbz#1655055]) - Support for resource instance attributes uniqueness check according to resource agent metadata ([rhbz#1665404]) - Command `pcs resource bundle reset` for a bundle configuration resetting ([rhbz#1657166]) - `pcs cluster setup` now checks if nodes' addresses match value of `ip_version` ([rhbz#1667053]) - Support for sbd option SBD\_TIMEOUT\_ACTION ([rhbz#1664828]) - Support for clearing expired moves and bans of resources ([rhbz#1625386]) - Commands for adding, changing and removing corosync links ([rhbz#1667058]) ### Fixed - Corosync config file parser updated and made more strict to match changes in corosync - Allow non-root users to read quorum status (commands `pcs status corosync`, `pcs status quorum`, `pcs quorum device status`, `pcs quorum status`) ([rhbz#1653316]) - Removed command `pcs resource show` dropped from usage and man page ([rhbz#1656953]) - Put proper link options' names to corosync.conf ([rhbz#1659051]) - Fixed issuses in configuring links in the 'create cluster' form in web UI ([rhbz#1664057]) - Pcs no longer removes empty `meta_attributes`, `instance_attributes` and other nvsets and similar elements from CIB. Such behavior was causing problems when pacemaker ACLs were in effect, leading to inability of pushing modified CIBs to pacemaker. ([rhbz#1659144]) - `ipv4-6` and `ipv6-4` are now valid values of `ip_version` in cluster setup ([rhbz#1667040]) - Crash when using unsupported options in commands `pcs status` and `pcs config` ([rhbz#1668422]) - `pcs resource group add` now fails gracefully instead of dumping an invalid CIB when a group ID is already occupied by a non-resource element ([rhbz#1668223]) - pcs no longer spawns unnecessary processes for reading known hosts ([rhbz#1676945]) - Lower load caused by periodical config files syncing in pcsd by making it sync less frequently ([rhbz#1676957]) - Improve logging of periodical config files syncing in pcsd - Knet link option `ip_version` has been removed, it was never supported by corosync. Transport option `ip_version` is still in place. ([rhbz#1674005]) - Several bugs in linklist validation in `pcs cluster setup` ([rhbz#1667090]) - Fixed a typo in documentation (regardles -> regardless) ([rhbz#1660702]) - Fixed pcsd crashes when non-ASCII characters are present in systemd journal - Pcs works even when PATH environment variable is not set ([rhbz#1673825]) - Fixed several "Unknown report" error messages - Pcsd SSL certificates are no longer synced across cluster nodes when creating new cluster or adding new node to an existing cluster. To enable the syncing, set `PCSD_SSL_CERT_SYNC_ENABLED` to `true` in pcsd config. ([rhbz#1673822]) - Pcs now reports missing node names in corosync.conf instead of failing silently - Fixed an issue where some pcs commands could not connect to cluster nodes over IPv6 - Fixed cluster setup problem in web UI when full domain names are used ([rhbz#1687965]) - Fixed inability to setup cluster in web UI when knet links are not specified ([rhbz#1687562]) - `--force` works correctly in `pcs quorum unblock` (broken since pcs-0.10.1) - Removed `3des` from allowed knet crypto ciphers since it is actually not supported by corosync - Improved validation of corosync options and their values ([rhbz#1679196], [rhbz#1679197]) ### Changed - Do not check whether watchdog is defined as an absolute path when enabling SBD. This check is not needed anymore as we are validating watchdog against list provided by SBD itself. ### Deprecated - Command `pcs resource show`, removed in pcs-0.10.1, has been readded as deprecated to ease transition to its replacements. It will be removed again in future. [rhbz#1661059] [rhbz#1625386]: https://bugzilla.redhat.com/show_bug.cgi?id=1625386 [rhbz#1653316]: https://bugzilla.redhat.com/show_bug.cgi?id=1653316 [rhbz#1655055]: https://bugzilla.redhat.com/show_bug.cgi?id=1655055 [rhbz#1656953]: https://bugzilla.redhat.com/show_bug.cgi?id=1656953 [rhbz#1657166]: https://bugzilla.redhat.com/show_bug.cgi?id=1657166 [rhbz#1659051]: https://bugzilla.redhat.com/show_bug.cgi?id=1659051 [rhbz#1659144]: https://bugzilla.redhat.com/show_bug.cgi?id=1659144 [rhbz#1660702]: https://bugzilla.redhat.com/show_bug.cgi?id=1660702 [rhbz#1661059]: https://bugzilla.redhat.com/show_bug.cgi?id=1661059 [rhbz#1664057]: https://bugzilla.redhat.com/show_bug.cgi?id=1664057 [rhbz#1664828]: https://bugzilla.redhat.com/show_bug.cgi?id=1664828 [rhbz#1665404]: https://bugzilla.redhat.com/show_bug.cgi?id=1665404 [rhbz#1667040]: https://bugzilla.redhat.com/show_bug.cgi?id=1667040 [rhbz#1667053]: https://bugzilla.redhat.com/show_bug.cgi?id=1667053 [rhbz#1667058]: https://bugzilla.redhat.com/show_bug.cgi?id=1667058 [rhbz#1667090]: https://bugzilla.redhat.com/show_bug.cgi?id=1667090 [rhbz#1668223]: https://bugzilla.redhat.com/show_bug.cgi?id=1668223 [rhbz#1668422]: https://bugzilla.redhat.com/show_bug.cgi?id=1668422 [rhbz#1673822]: https://bugzilla.redhat.com/show_bug.cgi?id=1673822 [rhbz#1673825]: https://bugzilla.redhat.com/show_bug.cgi?id=1673825 [rhbz#1674005]: https://bugzilla.redhat.com/show_bug.cgi?id=1674005 [rhbz#1676945]: https://bugzilla.redhat.com/show_bug.cgi?id=1676945 [rhbz#1676957]: https://bugzilla.redhat.com/show_bug.cgi?id=1676957 [rhbz#1679196]: https://bugzilla.redhat.com/show_bug.cgi?id=1679196 [rhbz#1679197]: https://bugzilla.redhat.com/show_bug.cgi?id=1679197 [rhbz#1687562]: https://bugzilla.redhat.com/show_bug.cgi?id=1687562 [rhbz#1687965]: https://bugzilla.redhat.com/show_bug.cgi?id=1687965 ## [0.10.1] - 2018-11-23 ### Removed - Pcs-0.10 removes support for CMAN, Corosync 1.x, Corosync 2.x and Pacemaker 1.x based clusters. For managing those clusters use pcs-0.9.x. - Pcs-0.10 requires Python 3.6 and Ruby 2.2, support for older Python and Ruby versions has been removed. - `pcs resource failcount reset` command has been removed as `pcs resource cleanup` is doing exactly the same job. ([rhbz#1427273]) - Deprecated commands `pcs cluster remote-node add | remove` have been removed as they were replaced with `pcs cluster node add-guest | remove-guest` - Ability to create master resources has been removed as they are deprecated in Pacemaker 2.x ([rhbz#1542288]) - Instead of `pcs resource create ... master` use `pcs resource create ... promotable` or `pcs resource create ... clone promotable=true` - Instead of `pcs resource master` use `pcs resource promotable` or `pcs resource clone ... promotable=true` - Deprecated --clone option from `pcs resource create` command - Ability to manage node attributes with `pcs property set|unset|show` commands (using `--node` option). The same functionality is still available using `pcs node attribute` command. - Undocumented version of the `pcs constraint colocation add` command, its syntax was `pcs constraint colocation add [score] [options]` - Deprecated commands `pcs cluster standby | unstandby`, use `pcs node standby | unstandby` instead - Deprecated command `pcs cluster quorum unblock` which was replaced by `pcs quorum unblock` - Subcommand `pcs status groups` as it was not showing a cluster status but cluster configuration. The same functionality is still available using command `pcs resource group list` - Undocumented command `pcs acl target`, use `pcs acl user` instead ### Added - Validation for an unaccessible resource inside a bundle ([rhbz#1462248]) - Options to filter failures by an operation and its interval in `pcs resource cleanup` and `pcs resource failcount show` commands ([rhbz#1427273]) - Commands for listing and testing watchdog devices ([rhbz#1578891]) - Commands for creating promotable clone resources `pcs resource promotable` and `pcs resource create ... promotable` ([rhbz#1542288]) - `pcs resource update` and `pcs resource meta` commands change master resources to promotable clone resources because master resources are deprecated in Pacemaker 2.x ([rhbz#1542288]) - Support for the `promoted-max` bundle option replacing the `masters` option in Pacemaker 2.x ([rhbz#1542288]) - Support for OP\_NO\_RENEGOTIATION option when OpenSSL supports it (even with Python 3.6) ([rhbz#1566430]) - Support for container types `rkt` and `podman` into bundle commands ([rhbz#1619620]) - Support for promotable clone resources in pcsd and web UI ([rhbz#1542288]) - Obsoleting parameters of resource and fence agents are now supported and preferred over deprecated parameters ([rhbz#1436217]) - `pcs status` now shows failed and pending fencing actions and `pcs status --full` shows the whole fencing history. Pacemaker supporting fencing history is required. ([rhbz#1615891]) - `pcs stonith history` commands for displaying, synchronizing and cleaning up fencing history. Pacemaker supporting fencing history is required. ([rhbz#1620190]) - Validation of node existence in a cluster when creating location constraints ([rhbz#1553718]) - Command `pcs client local-auth` for authentication of pcs client against local pcsd. This is required when a non-root user wants to execute a command which requires root permissions (e.g. `pcs cluster start`). ([rhbz#1554302]) - Command `pcs resource group list` which has the same functionality as removed command `pcs resource show --groups` ### Fixed - Fixed encoding of the CIB\_user\_groups cookie in communication between nodes. - `pcs cluster cib-push diff-against=` does not consider an empty diff as an error ([ghpull#166]) - `pcs cluster cib-push diff-against=` exits gracefully with an error message if crm\_feature\_set < 3.0.9 ([rhbz#1488044]) - `pcs resource update` does not create an empty meta\_attributes element any more ([rhbz#1568353]) - `pcs resource debug-*` commands provide debug messages even with pacemaker-1.1.18 and newer ([rhbz#1574898]) - Improve `pcs quorum device add` usage and man page ([rhbz#1476862]) - Removing resources using web UI when the operation takes longer than expected ([rhbz#1579911]) - Removing a cluster node no longer leaves the node in the CIB and therefore cluster status even if the removal is run on the node which is being removed ([rhbz#1595829]) - Possible race condition causing an HTTP 408 error when sending larger files via pcs ([rhbz#1600169]) - Configuring QDevice works even if NSS with the new db format (cert9.db, key4.db, pkcs11.txt) is used ([rhbz#1596721]) - Options starting with '-' and '--' are no longer accepted by commands for which those options have no effect ([rhbz#1533866]) - When a user makes an error in a pcs command, usage for that specific command is printed instead of printing the whole usage - Show more user friendly error message when testing watchdog device and multiple devices are present ([rhbz#1578891]) - Do not distinguish between supported and unsupported watchdog devices as SBD cannot reliably provide such information ([rhbz#1578891]) - `pcs config` no longer crashes when `crm_mon` prints something to stderr ([rhbz#1578955]) - `pcs resource bundle update` cmd for bundles which are using unsupported container backend ([rhbz#1619620]) - Do not crash if unable to load SSL certificate or key, log errors and exit gracefully instead ([rhbz#1638852]) - Fixed several issues in parsing `pcs constraint colocation add` command. - All `remove` subcommands now have `delete` aliases and vice versa. Previously, only some of them did and it was mostly undocumented. - The `pcs acl role delete` command no longer deletes ACL users and groups with no ACL roles assigned ### Changed - Authentication has been overhauled ([rhbz#1549535]): - The `pcs cluster auth` command only authenticates nodes in a local cluster and does not accept a node list. - The new command for authentication is `pcs host auth`. It allows to specify host names, addresses and pcsd ports. - Previously, running `pcs cluster auth A B C` caused A, B and C to be all authenticated against each other. Now, `pcs host auth A B C` makes the local host authenticated against A, B and C. This allows better control of what is authenticated against what. - The `pcs pcsd clear-auth` command has been replaced by `pcs pcsd deauth` and `pcs host deauth` commands. The new commands allows to deauthenticate a single host / token as well as all hosts / tokens. - These changes are not backward compatible. You should use the `pcs host auth` command to re-authenticate your hosts. - The `pcs cluster setup` command has been overhauled ([rhbz#1158816], [rhbz#1183103]): - It works with Corosync 3.x only and supports knet as well as udp/udpu. - Node names are now supported. - The number of Corosync options configurable by the command has been significantly increased. - The syntax of the command has been completely changed to accommodate the changes and new features. - Corosync encryption is enabled by default when knet is used ([rhbz#1648942]) - The `pcs cluster node add` command has been overhauled ([rhbz#1158816], [rhbz#1183103]) - It works with Corosync 3.x only and supports knet as well as udp/udpu. - Node names are now supported. - The syntax of the command has been changed to accommodate new features and to be consistent with other pcs commands. - The `pcs cluster node remove` has been overhauled ([rhbz#1158816], [rhbz#1595829]): - It works with Corosync 3.x only and supports knet as well as udp/udpu. - It is now possible to remove more than one node at once. - Removing a cluster node no longer leaves the node in the CIB and therefore cluster status even if the removal is run on the node which is being removed - Node names are fully supported now and are no longer coupled with node addresses. It is possible to set up a cluster where Corosync communicates over different addresses than pcs/pcsd. ([rhbz#1158816], [rhbz#1183103]) - Node names are now required while node addresses are optional in the `pcs cluster node add-guest` and `pcs cluster node add-remove` commands. Previously, it was the other way around. - Web UI has been updated following changes in authentication and support for Corosync 3.x ([rhbz#1158816], [rhbz#1183103], [rhbz#1549535]) - Commands related to resource failures have been overhauled to support changes in pacemaker. Failures are now tracked per resource operations on top of resources and nodes. ([rhbz#1427273], [rhbz#1588667]) - `--watchdog` and `--device` options of `pcs stonith sbd enable` and `pcs stonith sbd device setup` commands have been replaced with `watchdog` and `device` options respectively - Update pacemaker daemon names to match changes in pacemaker-2.0 ([rhbz#1573344]) - Watchdog devices are validated against a list provided by sbd ([rhbz#1578891]) - Resource operation option `requires` is no longer accepted to match changes in pacemaker-2.0 ([rhbz#1605185]) - Update pacemaker exit codes to match changes in pacemaker-2.0 ([rhbz#1536121]) - `pcs cluster cib-upgrade` no longer exits with an error if the CIB schema is already the latest available (this has been changed in pacemaker-2.0) - Pcs now configures corosync to put timestamps in its log ([rhbz#1615420]) - Option `-V` has been replaced with `--full` and a CIB file can be specified only using option `-f` in `pcs cluster verify` - Master resources are now called promotable clone resources to match changes in pacemaker-2.0 ([rhbz#1542288]) - Key size of default pcsd self-generated certificates increased from 2048b to 3072b ([rhbz#1638852]) - pcsd.service now depends on network-online.target ([rhbz#1640477]) - Split command `pcs resource [show]` into two new commands: - `pcs resource [status]` - same as `pcs resource [show]` - `pcs resource config` - same as `pcs resource [show] --full` or resource id specified instead of --full Respective changes have been made to `pcs stonith [show]` command. - Previously, `pcs cluster sync` synchronized only corosync configuration across all nodes configured in the cluster. This command will be changed in the future to sync all cluster configuration. New subcommand `pcs cluster sync corosync` has been introduced to sync only corosync configuration. For now, both commands have the same functionality. ### Security - CVE-2018-1086: Debug parameter removal bypass, allowing information disclosure ([rhbz#1557366]) - CVE-2018-1079: Privilege escalation via authorized user malicious REST call ([rhbz#1550243]) ### Deprecated - The `masters` bundle option is obsoleted by the `promoted-max` option in Pacemaker 2.x and therefore in pcs ([rhbz#1542288]) - `pcs cluster uidgid rm`, use `pcs cluster uidgid delete` or `pcs cluster uidgid remove` instead [ghpull#166]: https://github.com/ClusterLabs/pcs/pull/166 [rhbz#1158816]: https://bugzilla.redhat.com/show_bug.cgi?id=1158816 [rhbz#1183103]: https://bugzilla.redhat.com/show_bug.cgi?id=1183103 [rhbz#1427273]: https://bugzilla.redhat.com/show_bug.cgi?id=1427273 [rhbz#1436217]: https://bugzilla.redhat.com/show_bug.cgi?id=1436217 [rhbz#1462248]: https://bugzilla.redhat.com/show_bug.cgi?id=1462248 [rhbz#1476862]: https://bugzilla.redhat.com/show_bug.cgi?id=1476862 [rhbz#1488044]: https://bugzilla.redhat.com/show_bug.cgi?id=1488044 [rhbz#1533866]: https://bugzilla.redhat.com/show_bug.cgi?id=1533866 [rhbz#1536121]: https://bugzilla.redhat.com/show_bug.cgi?id=1536121 [rhbz#1542288]: https://bugzilla.redhat.com/show_bug.cgi?id=1542288 [rhbz#1549535]: https://bugzilla.redhat.com/show_bug.cgi?id=1549535 [rhbz#1550243]: https://bugzilla.redhat.com/show_bug.cgi?id=1550243 [rhbz#1553718]: https://bugzilla.redhat.com/show_bug.cgi?id=1553718 [rhbz#1554302]: https://bugzilla.redhat.com/show_bug.cgi?id=1554302 [rhbz#1557366]: https://bugzilla.redhat.com/show_bug.cgi?id=1557366 [rhbz#1566430]: https://bugzilla.redhat.com/show_bug.cgi?id=1566430 [rhbz#1568353]: https://bugzilla.redhat.com/show_bug.cgi?id=1568353 [rhbz#1573344]: https://bugzilla.redhat.com/show_bug.cgi?id=1573344 [rhbz#1574898]: https://bugzilla.redhat.com/show_bug.cgi?id=1574898 [rhbz#1578891]: https://bugzilla.redhat.com/show_bug.cgi?id=1578891 [rhbz#1578955]: https://bugzilla.redhat.com/show_bug.cgi?id=1578955 [rhbz#1579911]: https://bugzilla.redhat.com/show_bug.cgi?id=1579911 [rhbz#1588667]: https://bugzilla.redhat.com/show_bug.cgi?id=1588667 [rhbz#1595829]: https://bugzilla.redhat.com/show_bug.cgi?id=1595829 [rhbz#1596721]: https://bugzilla.redhat.com/show_bug.cgi?id=1596721 [rhbz#1600169]: https://bugzilla.redhat.com/show_bug.cgi?id=1600169 [rhbz#1605185]: https://bugzilla.redhat.com/show_bug.cgi?id=1605185 [rhbz#1615420]: https://bugzilla.redhat.com/show_bug.cgi?id=1615420 [rhbz#1615891]: https://bugzilla.redhat.com/show_bug.cgi?id=1615891 [rhbz#1619620]: https://bugzilla.redhat.com/show_bug.cgi?id=1619620 [rhbz#1620190]: https://bugzilla.redhat.com/show_bug.cgi?id=1620190 [rhbz#1638852]: https://bugzilla.redhat.com/show_bug.cgi?id=1638852 [rhbz#1640477]: https://bugzilla.redhat.com/show_bug.cgi?id=1640477 [rhbz#1648942]: https://bugzilla.redhat.com/show_bug.cgi?id=1648942 ## [0.9.163] - 2018-02-20 ### Added - Added `pcs status booth` as an alias to `pcs booth status` - A warning is displayed in `pcs status` and a stonith device detail in web UI when a stonith device has its `method` option set to `cycle` ([rhbz#1523378]) ### Fixed - `--skip-offline` is no longer ignored in the `pcs quorum device remove` command - pcs now waits up to 5 minutes (previously 10 seconds) for pcsd restart when synchronizing pcsd certificates - Usage and man page now correctly state it is possible to enable or disable several stonith devices at once - It is now possible to set the `action` option of stonith devices in web UI by using force ([rhbz#1421702]) - Do not crash when `--wait` is used in `pcs stonith create` ([rhbz#1522813]) - Nodes are now authenticated after running `pcs cluster auth` even if an existing corosync.conf defines no nodes ([ghissue#153], [rhbz#1517333]) - Pcs now properly exits with code 1 when an error occurs in `pcs cluster node add-remote` and `pcs cluster node add-guest` commands ([rhbz#1464781]) - Fixed a crash in the `pcs booth sync` command ([rhbz#1527530]) - Always replace the whole CIB instead of applying a diff when crm\_feature\_set <= 3.0.8 ([rhbz#1488044]) - Fixed `pcs cluster auth` in a cluster when not authenticated and using a non-default port ([rhbz#1415197]) - Fixed `pcs cluster auth` in a cluster when previously authenticated using a non-default port and reauthenticating using an implicit default port ([rhbz#1415197]) [ghissue#153]: https://github.com/ClusterLabs/pcs/issues/153 [rhbz#1415197]: https://bugzilla.redhat.com/show_bug.cgi?id=1415197 [rhbz#1421702]: https://bugzilla.redhat.com/show_bug.cgi?id=1421702 [rhbz#1464781]: https://bugzilla.redhat.com/show_bug.cgi?id=1464781 [rhbz#1488044]: https://bugzilla.redhat.com/show_bug.cgi?id=1488044 [rhbz#1517333]: https://bugzilla.redhat.com/show_bug.cgi?id=1517333 [rhbz#1522813]: https://bugzilla.redhat.com/show_bug.cgi?id=1522813 [rhbz#1523378]: https://bugzilla.redhat.com/show_bug.cgi?id=1523378 [rhbz#1527530]: https://bugzilla.redhat.com/show_bug.cgi?id=1527530 ## [0.9.162] - 2017-11-15 ### Added - `pcs status --full` now displays information about tickets ([rhbz#1389943]) - Support for managing qdevice heuristics ([rhbz#1389209]) - SNMP agent providing information about cluster to the master agent. It supports only python 2.7 for now ([rhbz#1367808]). ### Fixed - Fixed crash when loading a huge xml ([rhbz#1506864]) - Fixed adding an existing cluster into the web UI ([rhbz#1415197]) - False warnings about failed actions when resource is master/unmaster from the web UI ([rhbz#1506220]) ### Changed - `pcs resource|stonith cleanup` no longer deletes the whole operation history of resources. Instead, it only deletes failed operations from the history. The original functionality is available in the `pcs resource|stonith refresh` command. ([rhbz#1508351], [rhbz#1508350]) [rhbz#1367808]: https://bugzilla.redhat.com/show_bug.cgi?id=1367808 [rhbz#1389209]: https://bugzilla.redhat.com/show_bug.cgi?id=1389209 [rhbz#1389943]: https://bugzilla.redhat.com/show_bug.cgi?id=1389943 [rhbz#1415197]: https://bugzilla.redhat.com/show_bug.cgi?id=1415197 [rhbz#1506220]: https://bugzilla.redhat.com/show_bug.cgi?id=1506220 [rhbz#1506864]: https://bugzilla.redhat.com/show_bug.cgi?id=1506864 [rhbz#1508350]: https://bugzilla.redhat.com/show_bug.cgi?id=1508350 [rhbz#1508351]: https://bugzilla.redhat.com/show_bug.cgi?id=1508351 ## [0.9.161] - 2017-11-02 ### Added - List of pcs and pcsd capabilities ([rhbz#1230919]) ### Fixed - Fixed `pcs cluster auth` when already authenticated and using different port ([rhbz#1415197]) - It is now possible to restart a bundle resource on one node ([rhbz#1501274]) - `resource update` no longer exits with an error when the `remote-node` meta attribute is set to the same value that it already has ([rhbz#1502715], [ghissue#145]) - Listing and describing resource and stonith agents no longer crashes when agents' metadata contain non-ascii characters ([rhbz#1503110], [ghissue#151]) [ghissue#145]: https://github.com/ClusterLabs/pcs/issues/145 [ghissue#151]: https://github.com/ClusterLabs/pcs/issues/151 [rhbz#1230919]: https://bugzilla.redhat.com/show_bug.cgi?id=1230919 [rhbz#1415197]: https://bugzilla.redhat.com/show_bug.cgi?id=1415197 [rhbz#1501274]: https://bugzilla.redhat.com/show_bug.cgi?id=1501274 [rhbz#1502715]: https://bugzilla.redhat.com/show_bug.cgi?id=1502715 [rhbz#1503110]: https://bugzilla.redhat.com/show_bug.cgi?id=1503110 ## [0.9.160] - 2017-10-09 ### Added - Configurable pcsd port ([rhbz#1415197]) - Description of the `--force` option added to man page and help ([rhbz#1491631]) ### Fixed - Fixed some crashes when pcs encounters a non-ascii character in environment variables, command line arguments and so on ([rhbz#1435697]) - Fixed detecting if systemd is in use ([ghissue#118]) - Upgrade CIB schema version when `resource-discovery` option is used in location constraints ([rhbz#1420437]) - Fixed error messages in `pcs cluster report` ([rhbz#1388783]) - Increase request timeout when starting a cluster with large number of nodes to prevent timeouts ([rhbz#1463327]) - Fixed "Unable to update cib" error caused by invalid resource operation IDs - `pcs resource op defaults` now fails on an invalid option ([rhbz#1341582]) - Fixed behaviour of `pcs cluster verify` command when entered with the filename argument ([rhbz#1213946]) ### Changed - CIB changes are now pushed to pacemaker as a diff in commands overhauled to the new architecture (previously the whole CIB was pushed). This resolves race conditions and ACLs related errors when pushing CIB. ([rhbz#1441673]) - All actions / operations defined in resource agent's metadata (except meta-data, status and validate-all) are now copied to the CIB when creating a resource. ([rhbz#1418199], [ghissue#132]) - Improve documentation of the `pcs stonith confirm` command ([rhbz#1489682]) ### Deprecated - This is the last version fully supporting CMAN clusters and python 2.6. Support for these will be gradually dropped. [ghissue#118]: https://github.com/ClusterLabs/pcs/issues/118 [ghissue#132]: https://github.com/ClusterLabs/pcs/issues/132 [rhbz#1213946]: https://bugzilla.redhat.com/show_bug.cgi?id=1213946 [rhbz#1341582]: https://bugzilla.redhat.com/show_bug.cgi?id=1341582 [rhbz#1388783]: https://bugzilla.redhat.com/show_bug.cgi?id=1388783 [rhbz#1415197]: https://bugzilla.redhat.com/show_bug.cgi?id=1415197 [rhbz#1418199]: https://bugzilla.redhat.com/show_bug.cgi?id=1418199 [rhbz#1420437]: https://bugzilla.redhat.com/show_bug.cgi?id=1420437 [rhbz#1435697]: https://bugzilla.redhat.com/show_bug.cgi?id=1435697 [rhbz#1441673]: https://bugzilla.redhat.com/show_bug.cgi?id=1441673 [rhbz#1463327]: https://bugzilla.redhat.com/show_bug.cgi?id=1463327 [rhbz#1489682]: https://bugzilla.redhat.com/show_bug.cgi?id=1489682 [rhbz#1491631]: https://bugzilla.redhat.com/show_bug.cgi?id=1491631 ## [0.9.159] - 2017-06-30 ### Added - Option to create a cluster with or without corosync encryption enabled, by default the encryption is disabled ([rhbz#1165821]) - It is now possible to disable, enable, unmanage and manage bundle resources and set their meta attributes ([rhbz#1447910]) - Pcs now warns against using the `action` option of stonith devices ([rhbz#1421702]) ### Fixed - Fixed crash of the `pcs cluster setup` command when the `--force` flag was used ([rhbz#1176018]) - Fixed crash of the `pcs cluster destroy --all` command when the cluster was not running ([rhbz#1176018]) - Fixed crash of the `pcs config restore` command when restoring pacemaker authkey ([rhbz#1176018]) - Fixed "Error: unable to get cib" when adding a node to a stopped cluster ([rhbz#1176018]) - Fixed a crash in the `pcs cluster node add-remote` command when an id conflict occurs ([rhbz#1386114]) - Fixed creating a new cluster from the web UI ([rhbz#1284404]) - `pcs cluster node add-guest` now works with the flag `--skip-offline` ([rhbz#1176018]) - `pcs cluster node remove-guest` can be run again when the guest node was unreachable first time ([rhbz#1176018]) - Fixed "Error: Unable to read /etc/corosync/corosync.conf" when running `pcs resource create`([rhbz#1386114]) - It is now possible to set `debug` and `verbose` parameters of stonith devices ([rhbz#1432283]) - Resource operation ids are now properly validated and no longer ignored in `pcs resource create`, `pcs resource update` and `pcs resource op add` commands ([rhbz#1443418]) - Flag `--force` works correctly when an operation is not successful on some nodes during `pcs cluster node add-remote` or `pcs cluster node add-guest` ([rhbz#1464781]) ### Changed - Binary data are stored in corosync authkey ([rhbz#1165821]) - It is now mandatory to specify container type in the `resource bundle create` command - When creating a new cluster, corosync communication encryption is disabled by default (in 0.9.158 it was enabled by default, in 0.9.157 and older it was disabled) [rhbz#1165821]: https://bugzilla.redhat.com/show_bug.cgi?id=1165821 [rhbz#1176018]: https://bugzilla.redhat.com/show_bug.cgi?id=1176018 [rhbz#1284404]: https://bugzilla.redhat.com/show_bug.cgi?id=1284404 [rhbz#1386114]: https://bugzilla.redhat.com/show_bug.cgi?id=1386114 [rhbz#1421702]: https://bugzilla.redhat.com/show_bug.cgi?id=1421702 [rhbz#1432283]: https://bugzilla.redhat.com/show_bug.cgi?id=1432283 [rhbz#1443418]: https://bugzilla.redhat.com/show_bug.cgi?id=1443418 [rhbz#1447910]: https://bugzilla.redhat.com/show_bug.cgi?id=1447910 [rhbz#1464781]: https://bugzilla.redhat.com/show_bug.cgi?id=1464781 ## [0.9.158] - 2017-05-23 ### Added - Support for bundle resources (CLI only) ([rhbz#1433016]) - Commands for adding and removing guest and remote nodes including handling pacemaker authkey (CLI only) ([rhbz#1176018], [rhbz#1254984], [rhbz#1386114], [rhbz#1386512]) - Command `pcs cluster node clear` to remove a node from pacemaker's configuration and caches - Backing up and restoring cluster configuration by `pcs config backup` and `pcs config restore` commands now support corosync and pacemaker authkeys ([rhbz#1165821], [rhbz#1176018]) ### Deprecated - `pcs cluster remote-node add` and `pcs cluster remote-node remove `commands have been deprecated in favor of `pcs cluster node add-guest` and `pcs cluster node remove-guest` commands ([rhbz#1386512]) ### Fixed - Fixed a bug which under specific conditions caused pcsd to crash on start when running under systemd ([ghissue#134]) - `pcs resource unmanage` now sets the unmanaged flag to primitive resources even if a clone or master/slave resource is specified. Thus the primitive resources will not become managed just by uncloning. This also prevents some discrepancies between disabled monitor operations and the unmanaged flag. ([rhbz#1303969]) - `pcs resource unmanage --monitor` now properly disables monitor operations even if a clone or master/slave resource is specified. ([rhbz#1303969]) - `--help` option now shows help just for the specified command. Previously the usage for a whole group of commands was shown. - Fixed a crash when `pcs cluster cib-push` is called with an explicit value of the `--wait` flag ([rhbz#1422667]) - Handle pcsd crash when an unusable address is set in `PCSD_BIND_ADDR` ([rhbz#1373614]) - Removal of a pacemaker remote resource no longer causes the respective remote node to be fenced ([rhbz#1390609]) ### Changed - Newly created clusters are set up to encrypt corosync communication ([rhbz#1165821], [ghissue#98]) [ghissue#98]: https://github.com/ClusterLabs/pcs/issues/98 [ghissue#134]: https://github.com/ClusterLabs/pcs/issues/134 [rhbz#1176018]: https://bugzilla.redhat.com/show_bug.cgi?id=1176018 [rhbz#1254984]: https://bugzilla.redhat.com/show_bug.cgi?id=1254984 [rhbz#1303969]: https://bugzilla.redhat.com/show_bug.cgi?id=1303969 [rhbz#1373614]: https://bugzilla.redhat.com/show_bug.cgi?id=1373614 [rhbz#1386114]: https://bugzilla.redhat.com/show_bug.cgi?id=1386114 [rhbz#1386512]: https://bugzilla.redhat.com/show_bug.cgi?id=1386512 [rhbz#1390609]: https://bugzilla.redhat.com/show_bug.cgi?id=1390609 [rhbz#1422667]: https://bugzilla.redhat.com/show_bug.cgi?id=1422667 [rhbz#1433016]: https://bugzilla.redhat.com/show_bug.cgi?id=1433016 [rhbz#1165821]: https://bugzilla.redhat.com/show_bug.cgi?id=1165821 ## [0.9.157] - 2017-04-10 ### Added - Resources in location constraints now may be specified by resource name patterns in addition to resource names ([rhbz#1362493]) - Proxy settings description in pcsd configuration file ([rhbz#1315627]) - Man page for pcsd ([rhbz#1378742]) - Pcs now allows to set `trace_ra` and `trace_file` options of `ocf:heartbeat` and `ocf:pacemaker` resources ([rhbz#1421702]) - `pcs resource describe` and `pcs stonith describe` commands now show all information about the specified agent if the `--full` flag is used - `pcs resource manage | unmanage` enables respectively disables monitor operations when the `--monitor` flag is specified ([rhbz#1303969]) - Support for shared storage in SBD. Currently, there is very limited support in web UI ([rhbz#1413958]) ### Changed - It is now possible to specify more than one resource in the `pcs resource enable` and `pcs resource disable` commands. ### Fixed - Python 3: pcs no longer spams stderr with error messages when communicating with another node - Stopping a cluster does not timeout too early and it generally works better even if the cluster is running Virtual IP resources ([rhbz#1334429]) - `pcs booth remove` now works correctly even if the booth resource group is disabled (another fix) ([rhbz#1389941]) - Fixed Cross-site scripting (XSS) vulnerability in web UI ([CVE-2017-2661], [rhbz#1434111]) - Pcs no longer allows to create a stonith resource based on an agent whose name contains a colon ([rhbz#1415080]) - Pcs command now launches Python interpreter with "sane" options (python -Es) ([rhbz#1328882]) - Clufter is now supported on both Python 2 and Python 3 ([rhbz#1428350]) - Do not colorize clufter output if saved to a file [CVE-2017-2661]: https://access.redhat.com/security/cve/CVE-2017-2661 [rhbz#1303969]: https://bugzilla.redhat.com/show_bug.cgi?id=1303969 [rhbz#1315627]: https://bugzilla.redhat.com/show_bug.cgi?id=1315627 [rhbz#1328882]: https://bugzilla.redhat.com/show_bug.cgi?id=1328882 [rhbz#1334429]: https://bugzilla.redhat.com/show_bug.cgi?id=1334429 [rhbz#1362493]: https://bugzilla.redhat.com/show_bug.cgi?id=1362493 [rhbz#1378742]: https://bugzilla.redhat.com/show_bug.cgi?id=1378742 [rhbz#1389941]: https://bugzilla.redhat.com/show_bug.cgi?id=1389941 [rhbz#1413958]: https://bugzilla.redhat.com/show_bug.cgi?id=1413958 [rhbz#1415080]: https://bugzilla.redhat.com/show_bug.cgi?id=1415080 [rhbz#1421702]: https://bugzilla.redhat.com/show_bug.cgi?id=1421702 [rhbz#1428350]: https://bugzilla.redhat.com/show_bug.cgi?id=1428350 [rhbz#1434111]: https://bugzilla.redhat.com/show_bug.cgi?id=1434111 ## [0.9.156] - 2017-02-10 ### Added - Fencing levels now may be targeted in CLI by a node name pattern or a node attribute in addition to a node name ([rhbz#1261116]) - `pcs cluster cib-push` allows to push a diff obtained internally by comparing CIBs in specified files ([rhbz#1404233], [rhbz#1419903]) - Added flags `--wait`, `--disabled`, `--group`, `--after`, `--before` into the command `pcs stonith create` - Added commands `pcs stonith enable` and `pcs stonith disable` - Command line option --request-timeout ([rhbz#1292858]) - Check whenever proxy is set when unable to connect to a node ([rhbz#1315627]) ### Changed - `pcs node [un]standby` and `pcs node [un]maintenance` is now atomic even if more than one node is specified ([rhbz#1315992]) - Restarting pcsd initiated from pcs is now a synchronous operation ([rhbz#1284404]) - Stopped bundling fonts used in pcsd web UI ([ghissue#125]) - In `pcs resource create` flags `--master` and `--clone` changed to keywords `master` and `clone` - libcurl is now used for node to node communication ### Fixed - When upgrading CIB to the latest schema version, check for minimal common version across the cluster ([rhbz#1389443]) - `pcs booth remove` now works correctly even if the booth resource group is disabled ([rhbz#1389941]) - Adding a node in a CMAN cluster does not cause the new node to be fenced immediately ([rhbz#1394846]) - Show proper error message when there is an HTTP communication failure ([rhbz#1394273]) - Fixed searching for files to remove in the `/var/lib` directory ([ghpull#119], [ghpull#120]) - Fixed messages when managing services (start, stop, enable, disable...) - Fixed disabling services on systemd systems when using instances ([rhbz#1389501]) - Fixed parsing commandline options ([rhbz#1404229]) - Pcs does not exit with a false error message anymore when pcsd-cli.rb outputs to stderr ([ghissue#124]) - Pcs now exits with an error when both `--all` and a list of nodes is specified in the `pcs cluster start | stop | enable | disable` commands ([rhbz#1339355]) - built-in help and man page fixes and improvements ([rhbz#1347335]) - In `pcs resource create` the flag `--clone` no longer steals arguments from the keywords `meta` and `op` ([rhbz#1395226]) - `pcs resource create` does not produce invalid cib when group id is already occupied with non-resource element ([rhbz#1382004]) - Fixed misbehavior of the flag `--master` in `pcs resource create` command ([rhbz#1378107]) - Fixed tacit acceptance of invalid resource operation in `pcs resource create` ([rhbz#1398562]) - Fixed misplacing metadata for disabling when running `pcs resource create` with flags `--clone` and `--disabled` ([rhbz#1402475]) - Fixed incorrect acceptance of the invalid attribute of resource operation in `pcs resource create` ([rhbz#1382597]) - Fixed validation of options of resource operations in `pcs resource create` ([rhbz#1390071]) - Fixed silent omission of duplicate options ([rhbz#1390066]) - Added more validation for resource agent names ([rhbz#1387670]) - Fixed network communication issues in pcsd when a node was specified by an IPv6 address - Fixed JS error in web UI when empty cluster status is received ([rhbz#1396462]) - Fixed sending user group in cookies from Python 3 - Fixed pcsd restart in Python 3 - Fixed parsing XML in Python 3 (caused crashes when reading resource agents metadata) ([rhbz#1419639]) - Fixed the recognition of the structure of a resource agent name that contains a systemd instance ([rhbz#1419661]) ### Removed - Ruby 1.8 and 1.9 is no longer supported due to bad libcurl support [ghissue#124]: https://github.com/ClusterLabs/pcs/issues/124 [ghissue#125]: https://github.com/ClusterLabs/pcs/issues/125 [ghpull#119]: https://github.com/ClusterLabs/pcs/pull/119 [ghpull#120]: https://github.com/ClusterLabs/pcs/pull/120 [rhbz#1261116]: https://bugzilla.redhat.com/show_bug.cgi?id=1261116 [rhbz#1284404]: https://bugzilla.redhat.com/show_bug.cgi?id=1284404 [rhbz#1292858]: https://bugzilla.redhat.com/show_bug.cgi?id=1292858 [rhbz#1315627]: https://bugzilla.redhat.com/show_bug.cgi?id=1315627 [rhbz#1315992]: https://bugzilla.redhat.com/show_bug.cgi?id=1315992 [rhbz#1339355]: https://bugzilla.redhat.com/show_bug.cgi?id=1339355 [rhbz#1347335]: https://bugzilla.redhat.com/show_bug.cgi?id=1347335 [rhbz#1378107]: https://bugzilla.redhat.com/show_bug.cgi?id=1378107 [rhbz#1382004]: https://bugzilla.redhat.com/show_bug.cgi?id=1382004 [rhbz#1382597]: https://bugzilla.redhat.com/show_bug.cgi?id=1382597 [rhbz#1387670]: https://bugzilla.redhat.com/show_bug.cgi?id=1387670 [rhbz#1389443]: https://bugzilla.redhat.com/show_bug.cgi?id=1389443 [rhbz#1389501]: https://bugzilla.redhat.com/show_bug.cgi?id=1389501 [rhbz#1389941]: https://bugzilla.redhat.com/show_bug.cgi?id=1389941 [rhbz#1390066]: https://bugzilla.redhat.com/show_bug.cgi?id=1390066 [rhbz#1390071]: https://bugzilla.redhat.com/show_bug.cgi?id=1390071 [rhbz#1394273]: https://bugzilla.redhat.com/show_bug.cgi?id=1394273 [rhbz#1394846]: https://bugzilla.redhat.com/show_bug.cgi?id=1394846 [rhbz#1395226]: https://bugzilla.redhat.com/show_bug.cgi?id=1395226 [rhbz#1396462]: https://bugzilla.redhat.com/show_bug.cgi?id=1396462 [rhbz#1398562]: https://bugzilla.redhat.com/show_bug.cgi?id=1398562 [rhbz#1402475]: https://bugzilla.redhat.com/show_bug.cgi?id=1402475 [rhbz#1404229]: https://bugzilla.redhat.com/show_bug.cgi?id=1404229 [rhbz#1404233]: https://bugzilla.redhat.com/show_bug.cgi?id=1404233 [rhbz#1419639]: https://bugzilla.redhat.com/show_bug.cgi?id=1419639 [rhbz#1419661]: https://bugzilla.redhat.com/show_bug.cgi?id=1419661 [rhbz#1419903]: https://bugzilla.redhat.com/show_bug.cgi?id=1419903 ## [0.9.155] - 2016-11-03 ### Added - Show daemon status in `pcs status` on non-systemd machines - SBD support for cman clusters ([rhbz#1380352]) - Alerts management in pcsd ([rhbz#1376480]) ### Changed - Get all information about resource and stonith agents from pacemaker. Pcs now supports the same set of agents as pacemaker does. ([rhbz#1262001], [ghissue#81]) - `pcs resource create` now exits with an error if more than one resource agent matches the specified short agent name instead of randomly selecting one of the agents - Allow to remove multiple alerts and alert recipients at once ### Fixed - When stopping a cluster with some of the nodes unreachable, stop the cluster completely on all reachable nodes ([rhbz#1380372]) - Fixed pcsd crash when rpam rubygem is installed ([ghissue#109]) - Fixed occasional crashes / failures when using locale other than en\_US.UTF8 ([rhbz#1387106]) - Fixed starting and stopping cluster services on systemd machines without the `service` executable ([ghissue#115]) [ghissue#81]: https://github.com/ClusterLabs/pcs/issues/81 [ghissue#109]: https://github.com/ClusterLabs/pcs/issues/109 [ghissue#115]: https://github.com/ClusterLabs/pcs/issues/115 [rhbz#1262001]: https://bugzilla.redhat.com/show_bug.cgi?id=1262001 [rhbz#1376480]: https://bugzilla.redhat.com/show_bug.cgi?id=1376480 [rhbz#1380352]: https://bugzilla.redhat.com/show_bug.cgi?id=1380352 [rhbz#1380372]: https://bugzilla.redhat.com/show_bug.cgi?id=1380372 [rhbz#1387106]: https://bugzilla.redhat.com/show_bug.cgi?id=1387106 ## [0.9.154] - 2016-09-21 - There is no change log for this and previous releases. We are sorry. - Take a look at git history if you are interested. pcs-0.12.0.2/CONTRIBUTING.md000066400000000000000000000063551500417470700147660ustar00rootroot00000000000000# Contributing to the pcs project ## Running pcs and its test suite ### Python virtual environment * Using Python virtual environment (pyenv) is highly recommended, as it provides means of isolating development packages from system-wide packages. It allows to install specific versions of python packages, which pcs depends on, independently on the rest of the system. * In this tutorial, we choose to create a pyenv in `~/pyenvs/pcs` directory. * Create a base directory: `mkdir ~/pyenvs` * Create a pyenv: `python3 -m venv --system-site-packages ~/pyenvs/pcs` * To activate the pyenv, run `source ~/pyenvs/pcs/bin/activate` or `. ~/pyenvs/pcs/bin/activate` * To deactivate the pyenv, run `deactivate` ### Configure pcs * Go to pcs directory. * If you created a pyenv according to the previous section, make sure it is activated. * Run `./autogen.sh`. * This generates `configure` script based on `configure.ac` file. * It requires an annotated tag to be present in git repository. The easiest way to accomplish that is to add the upstream pcs repository as a remote repository. * Run `./configure`. * This checks all the dependencies and creates various files (including `Makefile` files) based on theirs `*.in` templates. * To list available options and their description, run `./configure -h`. * Recommended setup for development is to run `./configure --enable-local-build --enable-dev-tests --enable-destructive-tests --enable-concise-tests` * Run `make`. * This downloads and installs dependencies, such as python modules and rubygems. ### Run pcs and pcsd * To run pcs, type `pcs/pcs`. * To run pcsd, type `sripts/pcsd.sh`. ### Pcs test suite * To run all the tests, type `make check`. * You may run specific tests like this: * `make black_check` * `make isort_check` * `make mypy` * `make pylint` * `make tests_tier0` * `make tests_tier1` * `make pcsd-tests` * To run specific tests from python test suite, type `pcs_test/suite ` * When `make check` passes, you may want to run `make distcheck`. * This generates a distribution tarball and checks it. * The check is done by extracting files from the tarball, running `./configure` and `make check`. * Note, that `./configure` is run with no options, so it requires dependencies to be installed system wide. This can be overridden by running `make distcheck DISTCHECK_CONFIGURE_FLAGS='...'`. * The point of this test is to make sure all necessary files are present in the tarball. * To run black code formatter, type `make black`. * To run isort code formatter, type `make isort`. ### Distribution tarball * To create a tarball for distribution, run `make dist`. * The user of the tarball is supposed to run `./configure` with options they see fit. Then, they can run `make` with any target they need. ### Important notes * All system-dependent paths must be located in `pcs/settings.py.in` and `pcsd/settings.rb.in` files. * Do not forget to run `./configure` after changing any `*.in` file. * All files meant to be distributed must be listed in `EXTRA_DIST` variable in `Makefile.am` file in specific directory (`pcs`, `pcs/pcs`, `pcs/pcs_tests`, `pcs/pcsd`), with the exception of files created by autoconf / automake. pcs-0.12.0.2/COPYING000066400000000000000000000432541500417470700135670ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. pcs-0.12.0.2/MANIFEST.in000066400000000000000000000000741500417470700142630ustar00rootroot00000000000000include Makefile include COPYING graft pcsd prune pcsd/test pcs-0.12.0.2/Makefile.am000066400000000000000000000453141500417470700145670ustar00rootroot00000000000000EXTRA_DIST = \ autogen.sh \ CHANGELOG.md \ CONTRIBUTING.md \ dev_requirements.txt \ .gitlab-ci.yml \ make/gitlog-to-changelog \ make/git-version-gen \ make/release.mk \ MANIFEST.in \ mypy.ini \ pcs.pc.in \ rpm/pcs.spec.in \ scripts/pcsd.sh.in \ .version AUTOMAKE_OPTIONS = foreign MAINTAINERCLEANFILES = \ aclocal.m4 \ autoconf \ autoheader \ automake \ autoscan.log \ compile \ config.guess \ config.sub \ configure \ configure.scan \ depcomp \ Gemfile \ Gemfile.lock \ install-sh \ libtool \ libtoolize \ ltmain.sh \ Makefile.in \ missing \ rpm/requirements.txt SPEC = rpm/$(PACKAGE_NAME).spec TARFILES = $(PACKAGE_NAME)-$(VERSION).tar.bz2 \ $(PACKAGE_NAME)-$(VERSION).tar.gz \ $(PACKAGE_NAME)-$(VERSION).tar.xz ACLOCAL_AMFLAGS = -I m4 SUBDIRS = pcs pcsd pcs_test data PCS_PYTHON_PACKAGES = pcs/ pcs_test/ # options for any pip command pipopts = --disable-pip-version-check --verbose # options for pip install # --no-build-isolation - disables installing dependencies for embedded python # modules. All dependencies are installed by autotools in our case. pipinstallopts = --force-reinstall --no-build-isolation --no-warn-script-location # dependency management # 1 - sources directory - with python package sources # 2 - destination directory - python package will be installed into the # `packages` subdirectory of this destination directory # switched to pip installation since setup.py installation is deprecated # --no-binary - disallows downloading wheels define build_python_bundle cd $(1) && \ PYTHONPATH=$(2)/packages/ \ LC_ALL=C.utf8 \ ${PIP} ${pipopts} install ${pipinstallopts} --no-binary :all: --target $(2)/packages . endef PYAGENTX_URI="https://github.com/ondrejmular/pyagentx/archive/v${PYAGENTX_VERSION}.tar.gz" stamps/download_pyagentx: if ENABLE_DOWNLOAD if [ ! -f ${abs_top_builddir}/rpm/pyagentx-${PYAGENTX_VERSION}.tar.gz ]; then \ $(WGET) -q -O ${abs_top_builddir}/rpm/pyagentx-${PYAGENTX_VERSION}.tar.gz ${PYAGENTX_URI}; \ fi endif touch $@ stamps/download_python_deps: rpm/requirements.txt stamps/download_pyagentx dev_requirements.txt if ENABLE_DOWNLOAD PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $(PIP) $(pipopts) download --no-deps --no-build-isolation --dest rpm/ --no-binary :all: --requirement rpm/requirements.txt endif touch $@ stamps/install_python_devel_deps: dev_requirements.txt if DEV_TESTS PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $(PIP) install --upgrade -r $< endif touch $@ if ENABLE_DOWNLOAD stamps/untar_python_src: stamps/download_python_deps else stamps/untar_python_src: endif $(MKDIR_P) ${abs_top_builddir}/$(PCS_BUNDLED_DIR_LOCAL)/src/ src=`ls -1 ${abs_top_builddir}/rpm/*.tar.gz ${abs_top_srcdir}/rpm/*.tar.gz 2>/dev/null || true | sort -u | grep -v pcs- || true` && \ for i in $$src; do \ $(TAR) xvz -C ${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/src -f $$i; \ done; touch $@ stamps/install_python_embedded_mods_local: stamps/untar_python_src if LOCAL_BUILD for i in ${abs_top_builddir}/$(PCS_BUNDLED_DIR_LOCAL)/src/*; do \ $(call build_python_bundle,$$i,/${abs_top_builddir}/$(PCS_BUNDLED_DIR_LOCAL)); \ done; endif touch $@ install_python_embedded_mods: if LOCAL_BUILD for i in ${abs_top_builddir}/$(PCS_BUNDLED_DIR_LOCAL)/src/*; do \ $(call build_python_bundle,$$i,$(or ${DESTDIR}, /)/$(PCS_BUNDLED_DIR)); \ done; endif stamps/install_ruby_deps_local: if LOCAL_BUILD if INSTALL_EMBEDDED_GEMS if ENABLE_DOWNLOAD rm -rf .bundle Gemfile.lock $(MKDIR_P) .bundle echo '---' > .bundle/config echo 'BUNDLE_DISABLE_SHARED_GEMS: "true"' >> .bundle/config echo 'BUNDLE_PATH: "$(PCSD_BUNDLED_DIR_ROOT_LOCAL)"' >> .bundle/config echo 'BUNDLE_CACHE_PATH: "$(PCSD_BUNDLED_CACHE_DIR)"' >> .bundle/config echo 'BUNDLE_BUILD: \"$(ruby_LIBS)\"' >> .bundle/config echo 'BUNDLE_TIMEOUT: 30' >> .bundle/config echo 'BUNDLE_RETRY: 30' >> .bundle/config echo 'BUNDLE_JOBS: 1' >> .bundle/config echo 'BUNDLE_FORCE_RUBY_PLATFORM: "true"' >> .bundle/config $(BUNDLE) cp -rp $(PCSD_BUNDLED_DIR_LOCAL)/* $(PCSD_BUNDLED_DIR_ROOT_LOCAL)/ rm -rf $$(realpath $(PCSD_BUNDLED_DIR_LOCAL)/../) rm -rf .bundle Gemfile.lock else with_cflags=""; \ if test "x$(ruby_CFLAGS)" != "x"; then \ with_cflags='--with-cflags=$(ruby_CFLAGS)'; \ fi; \ gem_files=`$(FIND) "$(PCSD_BUNDLED_CACHE_DIR)" -type f -name '*.gem'` && \ if test "x$${gem_files}" != "x"; then \ $(GEM) install \ --force --verbose --no-document --local --no-user-install \ -i "$(PCSD_BUNDLED_DIR_ROOT_LOCAL)" \ $${gem_files} \ -- \ "$${with_cflags}" \ '--with-ldflags=$(ruby_LIBS)'; \ fi endif endif touch $@ endif stamps/download_rpm_ruby_deps: stamps/install_ruby_deps_local if ENABLE_DOWNLOAD cp ${PCSD_BUNDLED_CACHE_DIR}/*.gem rpm/ || true endif touch $@ # * Pip installs scripts into bin directory and that cannot be changed, so the # files are moved manually. Some of the folders do not exist in DESTDIR or # prefix locations, so they need to be created first, otherwise mv fails # * For systems where sbin and bin is merged, autotools should have correctly # defined the bindir and SBINDIR macros. If this isn't true, configure can be # run with --sbindir equal to bindir. It is assumed that setuptools also use # bindir location to output entry point script. install-exec-local: install_python_embedded_mods stamps/install_ruby_deps_local if $$(${PYTHON} -c "import sys; exit(sys.base_prefix == sys.prefix)"); then \ echo "WARNING: Virtual environment is activated, shebangs in Python " \ "entry points will point to this virtual environment. Fix shebangs " \ "manually to use the system interpreter. Entry points in rpm will be " \ "corrected automatically to point to the system interpreter"; \ fi if test -n "${DESTDIR}" || test "${prefix}" != "/usr"; then \ $(MKDIR_P) ${DESTDIR}/$(LIB_DIR)/pcs/; \ $(MKDIR_P) ${DESTDIR}/$(SBINDIR); \ fi ${PIP} ${pipopts} install ${pipinstallopts} \ --root=$(or ${DESTDIR}, /) \ --prefix=${prefix} ${EXTRA_SETUP_OPTS} . if test "$$(realpath ${DESTDIR}/$(prefix)/bin)" != "$$(realpath ${DESTDIR}/$(SBINDIR))"; then \ mv ${DESTDIR}/$(prefix)/bin/pcs ${DESTDIR}/$(SBINDIR)/ || \ mv ${DESTDIR}/$(prefix)/local/bin/pcs ${DESTDIR}/${prefix}/local/sbin/; \ mv ${DESTDIR}/$(prefix)/bin/pcsd ${DESTDIR}/$(SBINDIR)/ || \ mv ${DESTDIR}/$(prefix)/local/bin/pcsd ${DESTDIR}/${prefix}/local/sbin/; \ fi mv ${DESTDIR}/$(prefix)/bin/pcs_internal ${DESTDIR}/$(LIB_DIR)/pcs/ || \ mv ${DESTDIR}/$(prefix)/local/bin/pcs_internal ${DESTDIR}/${LIB_DIR}/pcs/ mv ${DESTDIR}/$(prefix)/bin/pcs_snmp_agent ${DESTDIR}/$(LIB_DIR)/pcs/ || \ mv ${DESTDIR}/$(prefix)/local/bin/pcs_snmp_agent ${DESTDIR}/${LIB_DIR}/pcs/ # * Uses PEP627 RECORD file - CSV, first column is a path relative to dist-info, # the path differs on Debian-like distros because packages that use system # Python are placed in dist-packages # * Before uninstallation, the manually moved scripts are deleted and not moved # to the original paths in the RECORD file. This is done because pcs installs # into different directories under different distros and the original location # cannot be determined easily. # * RECORD file only contains files not directories - deleting empty directory # tree by find is needed afterwards # * dist-info folder needs to be deleted for the same reason, pip uses it to # detect installed packages and if it's empty, pip reports error on every run # * On Debian-like distros, pcs is installed into /usr/local and the # uninstall also needs to handle that case. First we try to uninstall the # ${prefix}/local and only if nothing is there, try to uninstall from the # system. This is to prevent messing up system installation in these ditros # which are untouched by make install. uninstall-local: rm -rf ${DESTDIR}/$(PCS_BUNDLED_DIR) if test "$$(realpath ${DESTDIR}/$(prefix)/bin)" != "$$(realpath ${DESTDIR}/$(SBINDIR))"; then \ if ls ${DESTDIR}/$(prefix)/local/sbin/pcs; then \ rm -fv ${DESTDIR}/$(prefix)/local/sbin/pcs \ ${DESTDIR}/$(prefix)/local/sbin/pcsd; \ else \ rm -fv ${DESTDIR}/$(SBINDIR)/pcs \ ${DESTDIR}/$(SBINDIR)/pcsd; \ fi \ fi rm -fv ${DESTDIR}/$(LIB_DIR)/pcs/pcs_internal rm -fv ${DESTDIR}/$(LIB_DIR)/pcs/pcs_snmp_agent sitelib=${DESTDIR}/${prefix}/local/lib/python*/dist-packages; \ recordfile=$$(echo $${sitelib}/pcs*.dist-info/RECORD); \ if ! stat "$$recordfile"; then \ sitelib=${DESTDIR}/$(PYTHON_SITELIB); \ recordfile=$$(echo $${sitelib}/pcs*.dist-info/RECORD); \ fi; \ while read fname; do \ rm -rf $$(echo "$$sitelib/$$fname" | cut -d',' -f1); \ done < $$recordfile if test -n "${DESTDIR}" || test "${prefix}" != "/usr"; then \ rmdir ${DESTDIR}/$(SBINDIR); \ fi find ${DESTDIR}/$(LIB_DIR)/pcs -empty -type d -delete find ${DESTDIR}/$(LIB_DIR)/pcsd -empty -type d -delete find ${DESTDIR}/${PYTHON_SITELIB}/pcs -empty -type d -delete || : find ${DESTDIR}/${prefix}/local/lib/python*/dist-packages/pcs -empty -type d -delete || : rmdir ${DESTDIR}/$(PYTHON_SITELIB)/pcs*.dist-info || : rmdir ${DESTDIR}/${prefix}/local/lib/python*/dist-packages/pcs*.dist-info || : dist_doc_DATA = README.md CHANGELOG.md pkgconfigdir = $(LIB_DIR)/pkgconfig pkgconfig_DATA = pcs.pc # testing if CONCISE_TESTS python_test_options = else python_test_options = -v --vanilla endif pylint: if DEV_TESTS if PARALLEL_PYLINT pylint_options = --jobs=0 else pylint_options = endif export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ $(TIME) $(PYTHON) -m pylint --rcfile pyproject.toml ${pylint_options} ${PCS_PYTHON_PACKAGES} endif isort_check: pyproject.toml if DEV_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ $(TIME) $(PYTHON) -m isort --check-only ${PCS_PYTHON_PACKAGES} endif isort: pyproject.toml if DEV_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ $(TIME) $(PYTHON) -m isort ${PCS_PYTHON_PACKAGES} endif black_check: pyproject.toml if DEV_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ $(TIME) $(PYTHON) -m black --config pyproject.toml --check ${PCS_PYTHON_PACKAGES} endif black: pyproject.toml if DEV_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ $(PYTHON) -m black --config pyproject.toml ${PCS_PYTHON_PACKAGES} endif mypy: if DEV_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ $(TIME) $(PYTHON) -m mypy --config-file mypy.ini --package pcs --package pcs_test endif RUN_TYPOS=$(TYPOS) --color never --format brief | sed -e 's/:[0-9]\+:[0-9]\+:/:/' | $(SORT) .PHONY: typos_check typos_check: if TYPOS_CHECK $(RUN_TYPOS) > typos_new $(DIFF) typos_known typos_new endif .PHONY: typos_known typos_known: if TYPOS_CHECK $(RUN_TYPOS) > typos_known endif tests_tier0: export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ $(PYTHON) ${abs_builddir}/pcs_test/suite.py ${python_test_options} --tier0 tests_tier1: if EXECUTE_TIER1_TESTS export PYTHONPATH=${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL}/packages && \ $(PYTHON) ${abs_builddir}/pcs_test/suite.py $(python_test_options) --tier1 endif pcsd-tests: GEM_HOME=${abs_top_builddir}/${PCSD_BUNDLED_DIR_ROOT_LOCAL} \ $(RUBY) \ -I${abs_top_builddir}/pcsd \ -I${abs_top_builddir}/pcsd/test \ ${abs_top_builddir}/pcsd/test/test_all_suite.rb if LOCAL_BUILD check-local-deps: stamps/install_python_embedded_mods_local stamps/install_ruby_deps_local stamps/install_python_devel_deps else check-local-deps: endif all: check-local-deps test-tree-prep: if [ "${abs_top_builddir}" != "${abs_top_srcdir}" ]; then \ echo "Generating builddir symlinks for testing"; \ src_realpath=$(shell realpath ${abs_top_srcdir}); \ for i in `find "$$src_realpath/" -type d | \ grep -v "${abs_top_builddir}" | \ sed -e 's#^'$$src_realpath'/##g'`; do \ $(MKDIR_P) ${abs_top_builddir}/$${i}; \ done; \ find "$$src_realpath/" -type f | { while read src; do \ process=no; \ copy=no; \ case $$src in \ ${abs_top_builddir}*) \ ;; \ *Makefile.*|*.in) \ ;; \ *pcs_test/resources/*.conf) \ copy=yes; \ ;; \ *pcs_test/resources/qdevice-certs*) \ copy=yes; \ ;; \ *pcsd/test/*.conf*) \ copy=yes; \ ;; \ *) \ process=yes; \ ;; \ esac ; \ dst=`echo $$src | sed -e 's#^'$$src_realpath'/##g'`; \ if [ $${process} == yes ]; then \ rm -f ${abs_top_builddir}/$$dst; \ $(LN_S) $$src ${abs_top_builddir}/$$dst; \ fi; \ if [ $${copy} == yes ]; then \ rm -f ${abs_top_builddir}/$$dst; \ cp $$src ${abs_top_builddir}/$$dst; \ chmod u+w ${abs_top_builddir}/$$dst; \ fi; \ done; }; \ fi test-tree-clean: if [ "${abs_top_builddir}" != "${abs_top_srcdir}" ]; then \ echo "Cleaning symlinks for testing" ; \ find "${abs_top_builddir}/" -type l -delete; \ find ${abs_top_builddir} -type d -name qdevice-certs -exec rm -rf {} \; 2>/dev/null || : ;\ find ${abs_top_builddir} -type f -name "*.conf*" -exec rm -rf {} \; 2>/dev/null || : ;\ find "${abs_top_builddir}/" -type d -empty -delete; \ fi find ${abs_top_builddir} -type d -name __pycache__ -exec rm -rf {} \; 2>/dev/null || : check-local: check-local-deps test-tree-prep typos_check pylint isort_check black_check mypy tests_tier0 tests_tier1 pcsd-tests test-tree-clean # New setuptools use the build directory to build wheels clean-local: test-tree-clean rm -rf ./*.pyc ./*.egg-info ./*.dist-info build/ rm -rf ${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL} ${abs_top_builddir}/${PCSD_BUNDLED_DIR_ROOT_LOCAL} rm -rf Gemfile.lock .bundle pcs_test/resources/temp rm -rf $(PACKAGE_NAME)-$(VERSION).tar.* rpm/*tar* rpm/*.gem rpm/*.rpm rm -rf stamps/* # this will get rid of "libtoolized" m4 files distclean-local: rm -rf Gemfile rm -rf .mypy_cache rm -rf rpm/requirements.txt rpm/Gemfile rpm/pcs-* build/ rm -rf stamps rm -rf ${abs_top_builddir}/${PCS_BUNDLED_DIR_LOCAL} ${abs_top_builddir}/${PCSD_BUNDLED_DIR_ROOT_LOCAL} rm -rf $(filter-out \ $(top_builddir)/m4/ac_compare_versions.m4 \ $(top_builddir)/m4/ac_pip_module.m4 \ $(top_builddir)/m4/ac_ruby_gem.m4 \ $(top_builddir)/m4/ax_prog_date.m4, \ $(wildcard $(top_builddir)/m4/*.m4)) # release/versioning BUILT_SOURCES = .version .version: echo $(VERSION) > $@-t && mv $@-t $@ dist-hook: gen-ChangeLog echo $(VERSION) > $(distdir)/.tarball-version echo $(SOURCE_EPOCH) > $(distdir)/source_epoch gen_start_date = 2000-01-01 .PHONY: gen-ChangeLog gen-ChangeLog: if test -d $(abs_srcdir)/.git; then \ LC_ALL=C $(top_srcdir)/make/gitlog-to-changelog \ --since=$(gen_start_date) > $(distdir)/cl-t; \ rm -f $(distdir)/ChangeLog; \ mv $(distdir)/cl-t $(distdir)/ChangeLog; \ fi ## make rpm/srpm section. $(SPEC): $(SPEC).in .version config.status stamps/download_python_deps stamps/download_rpm_ruby_deps rm -f $@-t $@ date="`LC_ALL=C $(UTC_DATE_AT)$(SOURCE_EPOCH) "+%a %b %d %Y"`" && \ gvgver="`cd $(abs_srcdir); make/git-version-gen --fallback $(VERSION) .tarball-version .gitarchivever`" && \ if [ "$$gvgver" = "`echo $$gvgver | sed 's/-/./'`" ];then \ rpmver="$$gvgver" && \ alphatag="" && \ dirty="" && \ numcomm="0"; \ else \ gitver="`echo $$gvgver | sed 's/\(.*\)+/\1-/'`" && \ rpmver=`echo $$gitver | sed 's/-.*//g'` && \ alphatag=`echo $$gvgver | sed 's/[^-]*-\([^-]*\).*/\1/'` && \ numcomm=`echo $$gitver | sed 's/[^-]*-\([^-]*\).*/\1/'` && \ dirty="" && \ if [ "`echo $$gitver | sed 's/^.*-dirty$$//g'`" = "" ];then \ dirty="dirty"; \ fi \ fi && \ if [ -n "$$dirty" ]; then dirty="dirty"; else dirty=""; fi && \ if [ "$$numcomm" = "0" ]; then \ sed \ -e "s#@version@#$$rpmver#g" \ -e "s#%glo.*alpha.*##g" \ -e "s#%glo.*numcomm.*##g" \ -e "s#@dirty@#$$dirty#g" \ -e "s#@date@#$$date#g" \ -e "s#@pcs_bundled_dir@#${PCS_BUNDLED_DIR_LOCAL}#g" \ -e "s#@pyversion@#${PYVERSION}#g" \ -e "s#@cirpmworkarounds@#${CIRPMWORKAROUNDS}#g" \ $(abs_srcdir)/$@.in > $@-t; \ else \ sed \ -e "s#@version@#$$rpmver#g" \ -e "s#@alphatag@#$$alphatag#g" \ -e "s#@numcomm@#$$numcomm#g" \ -e "s#@dirty@#$$dirty#g" \ -e "s#@date@#$$date#g" \ -e "s#@pcs_bundled_dir@#${PCS_BUNDLED_DIR_LOCAL}#g" \ -e "s#@pyversion@#${PYVERSION}#g" \ -e "s#@cirpmworkarounds@#${CIRPMWORKAROUNDS}#g" \ $(abs_srcdir)/$@.in > $@-t; \ fi; \ if [ -z "$(CI_BRANCH)" ]; then \ sed -i -e "s#%glo.*cibranch.*##g" $@-t; \ else \ sed -i -e "s#@cibranch@#$(CI_BRANCH)#g" $@-t; \ fi && \ if [ -z "$$dirty" ]; then sed -i -e "s#%glo.*dirty.*##g" $@-t; fi && \ sed -i -e "s#@pyagentx_version@#${PYAGENTX_VERSION}#g" $@-t && \ pylist="`ls rpm/*.tar.gz | grep -v ^rpm/pyagentx- | grep -v ^rpm/pcs- | sed -e 's#rpm/##g' -e 's#.tar.gz##'`" && \ pysrc="`base=42; for i in $$pylist; do echo 'Source'$$base': '$$i'.tar.gz' && let "base=base+1"; done`" && \ $(AWK) -i inplace -v r="$$pysrc" '{gsub(/@pysrc@/,r)}1' $@-t; \ pybundle="`for i in $$pylist; do echo $$i | grep -v ^dataclasses- | sed 's/\(.*\)-\(.*\)/Provides: bundled(\1) = \2/'; done`" && \ $(AWK) -i inplace -v r="$$pybundle" '{gsub(/@pybundle@/,r)}1' $@-t; \ require_pycurl="`echo $$pylist | tr ' ' '\n' | grep pycurl | sed 's/.*-\(.*\)/Requires: libcurl%{?_isa} >= \1/'`" && \ $(AWK) -i inplace -v r="$$require_pycurl" '{gsub(/@require_pycurl@/,r)}1' $@-t; \ pycache="`echo $(MKDIR_P) $(PCS_BUNDLED_DIR_LOCAL)/src; base=41; for i in $$pylist pyagentx; do echo 'cp -f %SOURCE'$$base' rpm/' && let "base=base+1"; done`" && \ $(AWK) -i inplace -v r="$$pycache" '{gsub(/@pycache@/,r)}1' $@-t; \ gemlist="`for i in $$($(FIND) rpm/ -type f -name '*.gem'); do echo $$i | sed -e 's#rpm/##g' -e 's#.gem##g'; done`" && \ gemsrc="`base=80; for i in $$gemlist; do echo 'Source'$$base': '$$i'.gem' && let "base=base+1"; done`" && \ $(AWK) -i inplace -v r="$$gemsrc" '{gsub(/@gemsrc@/,r)}1' $@-t; \ gembundle="`for i in $$gemlist; do echo $$i | sed 's/\(.*\)-\(.*\)/Provides: bundled(\1) = \2/'; done`" && \ $(AWK) -i inplace -v r="$$gembundle" '{gsub(/@gembundle@/,r)}1' $@-t; \ gemcache="`echo $(MKDIR_P) $(PCSD_BUNDLED_CACHE_DIR); base=80; for i in $$gemlist; do echo 'cp -f %SOURCE'$$base' $(PCSD_BUNDLED_CACHE_DIR)' && let "base=base+1"; done`" && \ $(AWK) -i inplace -v r="$$gemcache" '{gsub(/@gemcache@/,r)}1' $@-t; chmod a-w $@-t mv $@-t $@ rm -f $@-t* $(TARFILES): $(MAKE) dist cp $(TARFILES) $(abs_top_builddir)/rpm RPMBUILDOPTS = --define "_sourcedir $(abs_top_builddir)/rpm" \ --define "_specdir $(abs_top_builddir)/rpm" \ --define "_builddir $(abs_top_builddir)/rpm" \ --define "_srcrpmdir $(abs_top_builddir)/rpm" \ --define "_rpmdir $(abs_top_builddir)/rpm" srpm: clean $(MAKE) $(SPEC) $(TARFILES) rpmbuild $(RPMBUILDOPTS) --nodeps -bs $(SPEC) rpm: clean $(MAKE) $(SPEC) $(TARFILES) rpmbuild $(RPMBUILDOPTS) -ba $(SPEC) clean-generic: rm -rf $(SPEC) $(TARFILES) $(PACKAGE_NAME)-$(VERSION) *.rpm pcs-0.12.0.2/README.md000066400000000000000000000113621500417470700140060ustar00rootroot00000000000000## PCS - Pacemaker/Corosync Configuration System Pcs is a Corosync and Pacemaker configuration tool. It permits users to easily view, modify and create Pacemaker based clusters. Pcs contains pcsd, a pcs daemon, which operates as a remote server for pcs. --- ### Pcs Branches * main * This is where pcs-0.12 lives. * Clusters running Pacemaker 3.x on top of Corosync 3.x are supported. * The main development happens here. * pcs-0.11 * Clusters running Pacemaker 2.1 on top of Corosync 3.x are supported. * This branch is in maintenance mode - bugs are being fixed but only a subset of new features lands here. * pcs-0.10 * Clusters running Pacemaker 2.0 on top of Corosync 3.x are supported. * Pacemaker 2.1 is supported, if it is compiled with `--enable-compat-2.0` option. * This branch is no longer maintained. * pcs-0.9 * Clusters running Pacemaker 1.x on top of Corosync 2.x or Corosync 1.x with CMAN are supported. * This branch is no longer maintained. --- ### Dependencies These are the runtime dependencies of pcs and pcsd: * python 3.12+ * python3-cryptography * python3-dateutil 2.7.0+ * python3-lxml * python3-pycurl * python3-pyparsing 3.0.0+ * python3-tornado 6.1.0+ * [dacite](https://github.com/konradhalas/dacite) * ruby 3.1.0+ * killall (package psmisc) * corosync 3.x * pacemaker 3.x --- ### Installation from Source Apart from the dependencies listed above, these are also required for installation: * python development files (packages python3-devel, python3-setuptools 66.1+, python3-pip, python3-wheel) * ruby development files (package ruby-devel) * rubygems * rubygem bundler (package rubygem-bundler or ruby-bundler or bundler) * autoconf, automake * pkgconf, pkgconfig(booth), pkgconfig(sbd) * pacemaker-libs-devel (or libpacemaker-devel and libpacemaker3-devel) * gcc * gcc-c++ * FFI development files (package libffi-devel or libffi-dev) * printf (package coreutils) * redhat-rpm-config (if you are using Fedora) * wget (to download bundled libraries) During the installation, all required rubygems are automatically downloaded and compiled. To install pcs and pcsd run the following in terminal: ```shell ./autogen.sh ./configure # alternatively './configure --enable-local-build' can be used to also download # missing dependencies make make install ``` If you are using GNU/Linux with systemd, it is now time to: ```shell systemctl daemon-reload ``` Start pcsd and make it start on boot: ```shell systemctl start pcsd systemctl enable pcsd ``` --- ### Packages Currently this is built into Fedora, RHEL, CentOS and Debian and its derivates. It is likely that other Linux distributions also contain pcs packages. * [Fedora package git repositories](https://src.fedoraproject.org/rpms/pcs) * [Current Fedora .spec](https://src.fedoraproject.org/rpms/pcs/blob/rawhide/f/pcs.spec) * [Debian-HA project home page](https://wiki.debian.org/Debian-HA) --- ### Quick Start * **Authenticate cluster nodes** Set the same password for the `hacluster` user on all nodes. ```shell passwd hacluster ``` To authenticate the nodes, run the following command on one of the nodes (replacing node1, node2, node3 with a list of nodes in your future cluster). Specify all your cluster nodes in the command. Make sure pcsd is running on all nodes. ```shell pcs host auth node1 node2 node3 -u hacluster ``` * **Create a cluster** To create a cluster run the following command on one node (replacing cluster\_name with a name of your cluster and node1, node2, node3 with a list of nodes in the cluster). `--start` and `--enable` will start your cluster and configure the nodes to start the cluster on boot respectively. ```shell pcs cluster setup cluster_name node1 node2 node3 --start --enable ``` * **Check the cluster status** After a few moments the cluster should startup and you can get the status of the cluster. ```shell pcs status ``` * **Add cluster resources** After this you can add stonith agents and resources: ```shell pcs stonith create --help ``` and ```shell pcs resource create --help ``` --- ### Further Documentation [ClusterLabs website](https://clusterlabs.org) is an excellent place to learn more about Pacemaker clusters. * [ClusterLabs quick start](https://clusterlabs.org/quickstart.html) * [Clusters from Scratch](https://clusterlabs.org/pacemaker/doc/2.1/Clusters_from_Scratch/html/) * [ClusterLabs documentation page](https://clusterlabs.org/pacemaker/doc/) --- ### Inquiries If you have any bug reports or feature requests please feel free to open a github issue on the pcs project. Alternatively you can use ClusterLabs [users mailinglist](https://lists.clusterlabs.org/mailman/listinfo/users) which is also a great place to ask Pacemaker clusters related questions. pcs-0.12.0.2/RELEASE.md000066400000000000000000000013551500417470700141320ustar00rootroot00000000000000## How to release new pcs version ### Bump changelog version * Run `make -f make/release.mk bump-changelog version=`. * This will create commit with updated CHANGELOGE.md * Merge commit to upstream (via PR or push it directly) ### Create tarballs with new release version * Run `make -f make/release.mk tarballs version= "configure_options=--enable-local-build"` * The should be next pcs version (e.g. version=0.10.9) * Test generated tarballs ### Create annotated tag * Run `make -f make/release.mk tag version= "configure_options=--enable-local-build" release=yes` * If your upstream remote branch is origin, run `make -f make/release.mk publish release=yes` or `git push ` pcs-0.12.0.2/SECURITY.md000066400000000000000000000014701500417470700143170ustar00rootroot00000000000000# Security Policy ## Supported Versions | Version | Supported | | ------- | ----------| | 0.12.x | yes | | 0.11.x | yes | | 0.10.x | no | | 0.9.x | no | ## Reporting a Vulnerability If you wish to report a security vulnerability, please send an email to lead developers tojeline@redhat.com and omular@redhat.com. Include following information in the email to help us fix the vulnerability: * Detailed description and / or a reproducer / exploit * Pcs version(s) where the vulnerability was discovered, upstream or downstream version * Setup details, configuration or conditions required to trigger the vulnerability, if any * Whether the vulnerability has been disclosed, where and when * The name and affiliation of security researchers who discovered the vulnerability Thank you pcs-0.12.0.2/autogen.sh000077500000000000000000000002651500417470700145300ustar00rootroot00000000000000#!/bin/sh # # Copyright (C) 2020 Red Hat, Inc. All rights reserved. # # Run this to generate all the initial makefiles, etc. autoreconf -i -v && echo Now run ./configure and make pcs-0.12.0.2/configure.ac000066400000000000000000000563501500417470700150230ustar00rootroot00000000000000# Process this file with autoconf to produce a configure script. AC_PREREQ([2.63]) AC_INIT([pcs], m4_esyscmd([make/git-version-gen .tarball-version .gitarchivever]), [developers@clusterlabs.org]) AC_CONFIG_AUX_DIR([.]) AM_INIT_AUTOMAKE([dist-bzip2 dist-xz -Wno-portability tar-pax]) AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_SRCDIR([pyproject.toml.in]) AC_CANONICAL_HOST AC_LANG([C]) # Sanitize path if test "$prefix" = "NONE"; then prefix="/usr" if test "$localstatedir" = "\${prefix}/var"; then localstatedir="/var" fi if test "$sysconfdir" = "\${prefix}/etc"; then sysconfdir="/etc" fi if test "$libdir" = "\${exec_prefix}/lib"; then if test -e /usr/lib64; then libdir="/usr/lib64" else libdir="/usr/lib" fi fi fi case $exec_prefix in NONE) exec_prefix=$prefix;; prefix) exec_prefix=$prefix;; esac # need to expand a bunch of paths to make sure # the embedded values in files are absolute paths eval SBINDIR="`eval echo ${sbindir}`" AC_SUBST([SBINDIR]) eval LOCALSTATEDIR="`eval echo ${localstatedir}`" AC_SUBST([LOCALSTATEDIR]) eval LIBDIR="`eval echo ${libdir}`" # Checks for programs. # check stolen from gnulib/m4/gnu-make.m4 if ! ${MAKE-make} --version /cannot/make/this >/dev/null 2>&1; then AC_MSG_ERROR([you don't seem to have GNU make; it is required]) fi AC_PROG_LN_S AC_PROG_INSTALL AC_PROG_MAKE_SET AC_PROG_AWK AC_PROG_MKDIR_P PKG_PROG_PKG_CONFIG # check for python AM_PATH_PYTHON([3.12]) eval PYTHON_SITELIB="`eval echo ${pythondir}`" AC_SUBST([PYTHON_SITELIB]) # required to detect / install python modules if ! $PYTHON -m pip > /dev/null 2>&1; then AC_MSG_ERROR([Python module pip not found]) fi PIP="$PYTHON -m pip" AC_SUBST([PIP]) # use a wrapper to call into PKG_CHECK_VAR to allow to set a default AC_DEFUN([PCS_PKG_CHECK_VAR], [ varname=$1 default=$4 AC_MSG_CHECKING([for pkg-conf $2 var $3]) PKG_CHECK_VAR([$1], [$2], [$3]) AS_VAR_IF([$1], [""], [AS_VAR_IF([default], [""], AC_MSG_ERROR([not found]), [AS_VAR_COPY([$varname], [default]) && AC_MSG_RESULT([not found, using default ${!varname}])])], [AC_MSG_RESULT([yes (detected: ${!varname})])]) ]) # check for systemd PKG_CHECK_MODULES([systemd], [systemd]) PCS_PKG_CHECK_VAR([SYSTEMD_UNIT_DIR_TMP], [systemd], [systemdsystemunitdir], [/usr/lib/systemd/system]) if test "${prefix}" != "/usr"; then SYSTEMD_UNIT_DIR="${prefix}/$SYSTEMD_UNIT_DIR_TMP" else SYSTEMD_UNIT_DIR="$SYSTEMD_UNIT_DIR_TMP" fi AC_SUBST([SYSTEMD_UNIT_DIR]) PCS_PKG_CHECK_VAR([SYSTEMD_UNIT_PATH], [systemd], [systemdsystemunitpath], [/etc/systemd/system:/etc/systemd/system:/run/systemd/system:/usr/local/lib/systemd/system:/usr/lib/systemd/system:/usr/lib/systemd/system:/lib/systemd/system]) AC_SUBST([SYSTEMD_UNIT_PATH]) # check for ruby AC_PATH_PROG([RUBY], [ruby]) if test x$RUBY = x; then AC_MSG_ERROR([Unable to find ruby binary]) fi # opensuse has a versioned ruby-$version.pc file # that does not match fedora or rhel ruby.pc # so we need to detect it rubymod=`pkg-config --list-all | awk '{print $1}' | grep ^ruby | sort -n | tail -n 1` PKG_CHECK_MODULES([ruby], [$rubymod >= 3.1]) PCS_PKG_CHECK_VAR([RUBY_VER], [$rubymod], [ruby_version]) AC_CHECK_PROGS([GEM], [gem]) if test "x$GEM" = "x"; then AC_MSG_ERROR([Unable to find gem binary]) fi # used to measure time for some tests, not critical if not available AC_CHECK_PROGS([TIME], [time]) # required to build rpm and pyagentx AC_CHECK_PROGS([TAR], [tar]) if test "x$TAR" = "x"; then AC_MSG_ERROR([Unable to find tar binary.]) fi # configure options section AC_ARG_ENABLE([dev-tests], [AS_HELP_STRING([--enable-dev-tests], [Enable extra developers tests (black, isort, mypy, pylint) (default: no)])], [dev_tests="yes"]) AM_CONDITIONAL([DEV_TESTS], [test "x$dev_tests" = "xyes"]) AC_ARG_ENABLE([destructive-tests], [AS_HELP_STRING([--enable-destructive-tests], [Automatically execute potentially dangerous tests when running make check (default: no)])], [destructive_tests="yes"]) AM_CONDITIONAL([EXECUTE_TIER1_TESTS], [test "x$destructive_tests" = "xyes"]) AC_ARG_ENABLE([concise-tests], [AS_HELP_STRING([--enable-concise-tests], [Make tests output brief by not printing a name of each test (default: no)])], [concise_tests="yes"]) AM_CONDITIONAL([CONCISE_TESTS], [test "x$concise_tests" = "xyes"]) AC_ARG_ENABLE([parallel-pylint], [AS_HELP_STRING([--enable-parallel-pylint], [Enable running pylint in multiple threads (default: no)])], [parallel_pylint="yes"]) AM_CONDITIONAL([PARALLEL_PYLINT], [test "x$parallel_pylint" = "xyes"]) AC_ARG_ENABLE([typos-check], [AS_HELP_STRING([--enable-typos-check], [Enable checking source code for typos (needs https://github.com/crate-ci/typos to be installed) (default: no)])], [typos_check="yes"]) AM_CONDITIONAL([TYPOS_CHECK], [test "x$typos_check" = "xyes"]) AC_ARG_ENABLE([local-build], [AS_HELP_STRING([--enable-local-build], [Download and install all dependencies as user / bundles])], [local_build="yes"]) AM_CONDITIONAL([LOCAL_BUILD], [test "x$local_build" = "xyes"]) AC_ARG_ENABLE([tests-only], [AS_HELP_STRING([--enable-tests-only], [Check only for tests dependencies])], [tests_only="yes"]) AC_ARG_ENABLE([individual-bundling], [AS_HELP_STRING([--enable-individual-bundling], [Bundle only missing python packages /ruby gems instead of all of them])], [individual_bundling="yes"]) AC_ARG_ENABLE([use-local-cache-only], [AS_HELP_STRING([--enable-use-local-cache-only], [Use only local cache to build bundles and disable downloads])], [cache_only="yes"]) # This should be removed in the next major release AC_ARG_ENABLE([booth-enable-authfile-set], [AS_HELP_STRING([--enable-booth-enable-authfile-set], [Enable support for setting enable-authfile booth option])], [booth_enable_authfile_set="yes"]) # This should stay here for at least another major release after booth-enable-authfile-set is removed AC_ARG_ENABLE([booth-enable-authfile-unset], [AS_HELP_STRING([--enable-booth-enable-authfile-unset], [Enable support for removing enable-authfile booth option])], [booth_enable_authfile_unset="yes"]) AC_ARG_ENABLE([webui], [AS_HELP_STRING([--enable-webui], [Include pcsd web UI backend module in pcs package])], [webui="yes"]) # this will catch both ID and ID_LIKE from os-release AC_ARG_WITH([distro], [AS_HELP_STRING([--with-distro=DIR], [Set defaults to specified distro. Default: autodetected])], [DISTRO="$withval"]) AC_ARG_WITH([pcsd-default-cipherlist], [AS_HELP_STRING([--with-pcsd-default-cipherlist=CIPHERLIST], [Default pcsd server OpenSSL cipher list. Default: DEFAULT:!RC4:!3DES:@STRENGTH])], [PCSD_DEFAULT_CIPHERLIST="$withval"], [PCSD_DEFAULT_CIPHERLIST="DEFAULT:!RC4:!3DES:@STRENGTH"]) AC_SUBST([PCSD_DEFAULT_CIPHERLIST]) AC_ARG_WITH([python-version], [AS_HELP_STRING([--with-python-version=X.Y], [Set an alternative Python interpreter version for generated rpm.])], [PYVERSION="$withval"], [PYVERSION=""]) AC_SUBST([PYVERSION]) AC_ARG_ENABLE([ci-rpm-workarounds], [AS_HELP_STRING([--enable-ci-rpm-workarounds], [NOT TO BE USED UNLESS YOUR NAME IS JENKINS])], [CIRPMWORKAROUNDS="yes"], [CIRPMWORKAROUNDS=""]) AC_SUBST([CIRPMWORKAROUNDS]) if test "x$cache_only" != "xyes"; then AC_CHECK_PROGS([WGET], [wget]) if test "x$WGET" = "x"; then AC_MSG_ERROR([Unable to find wget binary.]) fi fi if test "x$typos_check" = "xyes"; then AC_PATH_PROG([DIFF], [diff]) if test "x$DIFF" = "x"; then AC_MSG_ERROR([Unable to find diff in $PATH]) fi AC_PATH_PROG([SORT], [sort]) if test "x$SORT" = "x"; then AC_MSG_ERROR([Unable to find sort in $PATH]) fi AC_PATH_PROG([TYPOS], [typos]) if test "x$TYPOS" = "x"; then AC_MSG_ERROR([Unable to find typos in $PATH]) fi fi if test "x$DISTRO" = "x"; then AC_MSG_CHECKING([linux distribution]) if test -f /etc/os-release; then DISTRO=$(cat /etc/os-release | grep ^ID= | cut -d "=" -f 2 | sed -s 's#"##g') if test "x$DISTRO" = "x"; then AC_MSG_ERROR([Unable to detect linux distribution. Please specify --with-distro=]) fi DISTROS=$(cat /etc/os-release | grep ^ID_LIKE= | cut -d "=" -f 2 | sed -s 's#"##g') fi AC_MSG_RESULT([$DISTRO]) else AC_MSG_RESULT([Distro detection disabled. Setting forced to: $DISTRO]) fi AC_MSG_CHECKING([default settings for $DISTRO $DISTROS]) for i in $DISTRO $DISTROS; do case $i in debian|ubuntu) FOUND_DISTRO=1 CONFIGDIR="$sysconfdir/default" PCSLIBDIR="$prefix/share" PCMKDAEMONDIR="$prefix/lib/pacemaker" COROSYNCLOGDIR="$localstatedir/log/corosync" DISTROEXT=debian break ;; fedora*|rhel|centos|centos-stream*|opensuse*) FOUND_DISTRO=1 CONFIGDIR="$sysconfdir/sysconfig" PCSLIBDIR="$LIBDIR" PCMKDAEMONDIR="$prefix/libexec/pacemaker" COROSYNCLOGDIR="$localstatedir/log/cluster" DISTROEXT=fedora break ;; opencloudos) FOUND_DISTRO=1 CONFIGDIR="$sysconfdir/sysconfig" PCSLIBDIR="$LIBDIR" PCMKDAEMONDIR="$prefix/libexec/pacemaker" COROSYNCLOGDIR="$localstatedir/log/cluster" DISTROEXT=opencloudos break ;; esac done if test "x$FOUND_DISTRO" = "x"; then AC_MSG_RESULT([not found]) AC_MSG_ERROR([Unknown distribution $DISTRO. Please contact pcs upstream project to add support, or check --with-distro= value]) else AC_MSG_RESULT([$i (or alike) default settings will be used]) fi AC_SUBST([DISTROEXT]) AC_ARG_WITH([default-config-dir], [AS_HELP_STRING([--with-default-config-dir=DIR], [pcs config directory. Default: autodetected])], [CONF_DIR="$withval"], [CONF_DIR="$CONFIGDIR"]) AC_SUBST([CONF_DIR]) AC_ARG_WITH([pcs-lib-dir], [AS_HELP_STRING([--with-pcs-lib-dir=DIR], [pcs lib directory. Default: autodetected])], [LIB_DIR="$withval"], [LIB_DIR="$PCSLIBDIR"]) AC_SUBST([LIB_DIR]) AC_ARG_WITH([snmp-mibs-dir], [AS_HELP_STRING([--with-snmp-mibs-dir=DIR], [snmp MIB directory. Default: $prefix/share/snmp/mibs])], [SNMP_MIB_DIR="$withval"], [SNMP_MIB_DIR="$prefix/share/snmp/mibs"]) AC_SUBST([SNMP_MIB_DIR]) # python detection section PCS_BUNDLED_DIR_LOCAL="pcs_bundled" AC_SUBST([PCS_BUNDLED_DIR_LOCAL]) PCS_BUNDLED_DIR="$LIB_DIR/pcs/$PCS_BUNDLED_DIR_LOCAL" AC_SUBST([PCS_BUNDLED_DIR]) mkdir -p $ac_pwd/stamps mkdir -p $ac_pwd/rpm/ rm -rf $ac_pwd/rpm/requirements.txt touch $ac_pwd/rpm/requirements.txt if test "x$CIRPMWORKAROUNDS" = "xyes"; then echo "pyparsing>=3.0.0" >> $ac_pwd/rpm/requirements.txt fi # PCS_BUNDLE_PYMOD([module], [version]) AC_DEFUN([PCS_BUNDLE_PYMOD], [ echo "$1 $2" | sed -e 's# ##g' >> $ac_pwd/rpm/requirements.txt if test "x$cache_only" = "xyes"; then src=`ls rpm/$1-*` if test "x$src" = "x"; then AC_MSG_ERROR([cache only build required but no source detected in rpm/]) fi fi ]) # PCS_CHECK_PYMOD([module], [version], [embedded=yes]) AC_DEFUN([PCS_CHECK_PYMOD], [ if test "x$local_build" = "xyes" && test "x$3" = "xyes"; then AC_PIP_MODULE([$1], [$2], [bundle_module=no], [bundle_module=yes], [bundle_module=yes]) if test "x$bundle_module" = "xyes" || test "x$individual_bundling" != "xyes"; then PCS_BUNDLE_PYMOD([$1], [$2]) fi else AC_PIP_MODULE([$1], [$2], [], [AC_MSG_ERROR([Python module $1 not found])]) fi ]) # required by rpm build PYAGENTX_VERSION=0.4.pcs.2 AC_SUBST([PYAGENTX_VERSION]) # those MUST be available in BaseOS # pip 19.0 required for PEP517 support PCS_CHECK_PYMOD([pip], [>= 23.0]) # pip builds a wheel first PCS_CHECK_PYMOD([wheel]) if test "x$tests_only" != "xyes"; then # setuptools 61.0.0 required for PEP621 support PCS_CHECK_PYMOD([setuptools], [>= 66.1]) PCS_CHECK_PYMOD([cryptography]) PCS_CHECK_PYMOD([lxml]) PCS_CHECK_PYMOD([pyparsing], [>= 3.0.0]) # those are kind of problematic. # use them all from the BaseOS or embedded them all as necessary (--enable-local-build) PCS_CHECK_PYMOD([dacite], [], [yes]) PCS_CHECK_PYMOD([tornado], [>= 6.0.0], [yes]) PCS_CHECK_PYMOD([python-dateutil], [>= 2.7.0], [yes]) # setuptoools_scm is needed for bundling dateutil, this uses variables # set by the PCS_CHECK_PYMOD macro, so it must be executed right after it if test "x$bundle_module" = "xyes" && test "x$local_build" = "xyes"; then PCS_CHECK_PYMOD([setuptools-scm]) PCS_CHECK_PYMOD([six]) fi PCS_CHECK_PYMOD([pycurl], [], [yes]) # Building pycurl requires devel packages that contain pkg-config, so devel # packages must be installed for these checks to pass. # This uses variables set by the PCS_CHECK_PYMOD macro, so it must be executed # right after it. if test "x$bundle_module" = "xyes"; then PKG_CHECK_MODULES([LIBCURL], [libcurl]) PKG_CHECK_MODULES([OPENSSL], [openssl]) fi # special case, because we need to download from github AC_PIP_MODULE([pyagentx]) if test "x$HAVE_PIPMOD_PYAGENTX" = "xno" && test "x$local_build" != "xyes"; then AC_MSG_ERROR([Python module pyagentx not found]) fi fi # ruby gem section PCSD_BUNDLED_DIR_ROOT_LOCAL="pcsd/vendor/bundle/" PCSD_BUNDLED_DIR_LOCAL="$PCSD_BUNDLED_DIR_ROOT_LOCAL/ruby/$RUBY_VER/" PCSD_BUNDLED_CACHE_DIR="$PCSD_BUNDLED_DIR_ROOT_LOCAL/cache" AC_SUBST([PCSD_BUNDLED_DIR_ROOT_LOCAL]) AC_SUBST([PCSD_BUNDLED_DIR_LOCAL]) AC_SUBST([PCSD_BUNDLED_CACHE_DIR]) rm -rf Gemfile Gemfile.lock echo "source 'https://rubygems.org'" > Gemfile echo "" >> Gemfile # PCS_BUNDLE_GEM([module]) AC_DEFUN([PCS_BUNDLE_GEM], [ echo "gem '$1'" >> Gemfile if test "x$cache_only" = "xyes"; then src=`ls $PCSD_BUNDLED_CACHE_DIR/$1-*` if test "x$src" = "x"; then AC_MSG_ERROR([cache only build required but no source detected in $PCSD_BUNDLED_CACHE_DIR]) fi fi ]) # PCS_CHECK_GEM([module], [version]) AC_DEFUN([PCS_CHECK_GEM], [ if test "x$local_build" = "xyes"; then AC_RUBY_GEM([$1], [$2], [], [PCS_BUNDLE_GEM([$1])]) else AC_RUBY_GEM([$1], [$2], [], [AC_MSG_ERROR([ruby gem $1 not found])]) fi ]) # PCS_GEM_ACTION([curversion], [op], [cmpversion][, action-if-true] [, action-if-false]) AC_DEFUN([PCS_GEM_ACTION], [ if test -n "$1"; then AC_COMPARE_VERSIONS([$1], [$2], [$3], [$4], [$5]) else true $5 fi ]) PCS_CHECK_GEM([power_assert]) PCS_CHECK_GEM([test-unit]) RACK_HANDLER="Rackup" REQUIRE_RACKUP="require 'rackup'" if test "x$tests_only" != "xyes"; then PCS_CHECK_GEM([backports]) PCS_CHECK_GEM([childprocess]) PCS_CHECK_GEM([ethon]) PCS_CHECK_GEM([ffi]) PCS_CHECK_GEM([json]) PCS_CHECK_GEM([ruby2_keywords]) PCS_CHECK_GEM([mustermann]) PCS_CHECK_GEM([rack]) PCS_GEM_ACTION([$HAVE_RUBYGEM_RACK_VERSION], [<], [3.0], [RACK_HANDLER=Rack REQUIRE_RACKUP=""], [PCS_CHECK_GEM([rackup])]) PCS_CHECK_GEM([rack-protection]) PCS_GEM_ACTION([$HAVE_RUBYGEM_RACK_PROTECTION_VERSION], [<], [3.2.0], [], [PCS_CHECK_GEM([base64], [>= 0.1.0])]) PCS_CHECK_GEM([rack-test]) PCS_CHECK_GEM([sinatra]) PCS_GEM_ACTION([$HAVE_RUBYGEM_SINATRA_VERSION], [<], [4.0.0], [], [PCS_CHECK_GEM([rack-session], [>= 2.0.0])]) PCS_CHECK_GEM([tilt]) PCS_CHECK_GEM([nio4r]) PCS_CHECK_GEM([puma]) PCS_CHECK_GEM([rexml]) fi AC_SUBST([RACK_HANDLER]) AC_SUBST([REQUIRE_RACKUP]) if test "x$local_build" = "xyes" && test $(wc -l < Gemfile) -gt 2; then GEM_HOME="$LIB_DIR/$PCSD_BUNDLED_DIR_ROOT_LOCAL" SYSTEMD_GEM_HOME="Environment=GEM_HOME=$GEM_HOME" if test "x$cache_only" != "xyes"; then AC_CHECK_PROGS([BUNDLE], [bundle]) if test "x$BUNDLE" = "x"; then AC_MSG_ERROR([Unable to find bundle binary required to install missing ruby gems]) fi fi fi AC_SUBST([BUNDLE]) AC_SUBST([GEM]) AC_SUBST([GEM_HOME]) AC_SUBST([SYSTEMD_GEM_HOME]) AM_CONDITIONAL([INSTALL_EMBEDDED_GEMS], [test -n "$GEM_HOME"]) AM_CONDITIONAL([ENABLE_DOWNLOAD], [test "x$cache_only" != "xyes"]) if test "x$booth_enable_authfile_set" = "xyes"; then BOOTH_ENABLE_AUTHFILE_SET_DOC=" .TP enable-authfile Add option 'enable-authfile' to booth configuration. In some versions of booth, auhfile is not used by default and explicit enabling is required." fi if test "x$booth_enable_authfile_unset" = "xyes"; then BOOTH_ENABLE_AUTHFILE_UNSET_DOC=" .TP clean-enable-authfile Remove 'enable-authfile' option from booth configuration. This is useful when upgrading from booth that required the option to be present to a new version which doesn't tolerate the option." fi _AM_SUBST_NOTMAKE([BOOTH_ENABLE_AUTHFILE_SET_DOC]) _AM_SUBST_NOTMAKE([BOOTH_ENABLE_AUTHFILE_UNSET_DOC]) AC_SUBST([BOOTH_ENABLE_AUTHFILE_SET_DOC]) AC_SUBST([BOOTH_ENABLE_AUTHFILE_UNSET_DOC]) AC_SUBST([BOOTH_ENABLE_AUTHFILE_SET_ENABLED], [$(if test "x$booth_enable_authfile_set" = "xyes"; then echo "True"; else echo "False"; fi)]) AC_SUBST([BOOTH_ENABLE_AUTHFILE_UNSET_ENABLED], [$(if test "x$booth_enable_authfile_unset" = "xyes"; then echo "True"; else echo "False"; fi)]) AC_SUBST([BOOTH_ENABLE_AUTHFILE_SET_CAPABILITY], [$(test "x$booth_enable_authfile_set" != "xyes"; echo "$?")]) AC_SUBST([BOOTH_ENABLE_AUTHFILE_UNSET_CAPABILITY], [$(test "x$booth_enable_authfile_unset" != "xyes"; echo "$?")]) AC_SUBST([PACKAGE_WEBUI_BACKEND], [$(if test "x$webui" != "xyes"; then echo "pcs.daemon.app.webui*"; fi)]) PCSD_PUBLIC_DIR="$LIB_DIR/pcsd/public" AC_SUBST([PCSD_PUBLIC_DIR]) PCSD_WEBUI_DIR="$PCSD_PUBLIC_DIR/ui" AC_SUBST([PCSD_WEBUI_DIR]) PCSD_UNIX_SOCKET="$LOCALSTATEDIR/run/pcsd.socket" AC_SUBST([PCSD_UNIX_SOCKET]) OUTPUT_FORMAT_SYNTAX_DOC="\fB\-\-output\-format\fR text|cmd|json" OUTPUT_FORMAT_DESC_DOC="There are 3 formats of output available: 'cmd', 'json' and 'text', default is 'text'. Format 'text' is a human friendly output. Format 'cmd' prints pcs commands which can be used to recreate the same configuration. Format 'json' is a machine oriented output of the configuration." AC_SUBST([OUTPUT_FORMAT_SYNTAX_DOC]) AC_SUBST([OUTPUT_FORMAT_DESC_DOC]) # detect different paths required to generate default settings AC_PATH_PROG([BASH], [bash]) if test "x$BASH" = "x"; then AC_MSG_ERROR([Unable to find bash in $PATH]) fi AC_PATH_PROG([SYSTEMCTL], [systemctl]) if test "x$SYSTEMCTL" = "x"; then AC_PATH_PROG([SERVICE], [service]) if test "x$SERVICE" = "x"; then AC_MSG_ERROR([Unable to find systemctl or service in $PATH]) fi fi if test "x$tests_only" != "xyes"; then AC_PATH_PROG([KILLALL], [killall]) if test "x$KILLALL" = "x"; then AC_MSG_ERROR([Unable to find killall in $PATH]) fi fi # yes this is absurd but we need full path for some # python calls AC_PATH_PROG([RM], [rm]) if test "x$RM" = "x"; then AC_MSG_ERROR([Unable to find rm in $PATH]) fi AC_PATH_PROG([FIND], [find]) if test "x$FIND" = "x"; then AC_MSG_ERROR([Unable to find find in $PATH]) fi AC_PATH_PROG([CERTUTIL], [certutil]) if test "x$CERTUTIL" = "x"; then AC_MSG_ERROR([Unable to find certutil in $PATH]) fi # NOTE: some of those pacemaker var are only available # in pacemaker.pc with pacemaker >= 2.0.5 PCS_PKG_CHECK_VAR([PCMK_USER], [pacemaker], [daemon_user], [hacluster]) PCS_PKG_CHECK_VAR([PCMK_GROUP], [pacemaker], [daemon_group], [haclient]) PCS_PKG_CHECK_VAR([PCMK_DAEMON_DIR], [pacemaker], [daemondir], [$PCMKDAEMONDIR]) PCS_PKG_CHECK_VAR([PCMKEXECPREFIX], [pacemaker], [exec_prefix], [/usr]) PCS_PKG_CHECK_VAR([PCMKPREFIX], [pacemaker], [prefix], [/usr]) if test "$PCMKPREFIX" = "/usr"; then PCMKCONFDIR="/etc" PCMKLOCALSTATEDIR="/var" else PCMKCONFDIR="$PCMKPREFIX/etc" PCMKLOCALSTATEDIR="$PCMKPREFIX/var" fi AC_SUBST([PCMKCONFDIR]) AC_SUBST([PCMKLOCALSTATEDIR]) PCS_PKG_CHECK_VAR([PCMK_CIB_DIR], [pacemaker], [configdir], [/var/lib/pacemaker/cib]) PCS_PKG_CHECK_VAR([PCMK_SCHEMA_DIR], [pacemaker], [schemadir], [/usr/share/pacemaker]) PCS_PKG_CHECK_VAR([COROEXECPREFIX], [corosync], [exec_prefix], [/usr]) PCS_PKG_CHECK_VAR([COROPREFIX], [corosync], [prefix], [/usr]) if test "$COROPREFIX" = "/usr"; then COROCONFDIR="/etc" else COROCONFDIR="$COROPREFIX/etc" fi AC_SUBST([COROCONFDIR]) eval COROSYNCLOGDIR="`eval echo ${COROSYNCLOGDIR}`" PCS_PKG_CHECK_VAR([COROLOGDIR], [corosync], [logdir], [$COROSYNCLOGDIR]) PCS_PKG_CHECK_VAR([COROQDEVEXECPREFIX], [corosync-qdevice], [exec_prefix], [/usr]) PCS_PKG_CHECK_VAR([COROQDEVCONFDIR], [corosync-qdevice], [confdir], [/etc/corosync]) PCS_PKG_CHECK_VAR([SBDCONFDIR], [sbd], [confdir], [$CONFIGDIR]) PCS_PKG_CHECK_VAR([SBDEXECPREFIX], [sbd], [exec_prefix], [/usr]) PCS_PKG_CHECK_VAR([FASEXECPREFIX], [fence-agents], [exec_prefix], [/usr]) PCS_PKG_CHECK_VAR([RA_API_DTD], [resource-agents], [ra_api_dtd], [/usr/share/resource-agents/ra-api-1.dtd]) PCS_PKG_CHECK_VAR([RA_TMP_DIR], [resource-agents], [ra_tmp_dir], [/run/resource-agents]) PCS_PKG_CHECK_VAR([BOOTHCONFDIR], [booth], [confdir], [/etc/booth]) PCS_PKG_CHECK_VAR([BOOTHEXECPREFIX], [booth], [exec_prefix], [/usr]) # required for man page and spec file generation AX_PROG_DATE AS_IF([test "$ax_cv_prog_date_gnu_date:$ax_cv_prog_date_gnu_utc" = yes:yes], [UTC_DATE_AT="date -u -d@"], [AS_IF([test "x$ax_cv_prog_date_bsd_date" = xyes], [UTC_DATE_AT="date -u -r"], [AC_MSG_ERROR([date utility unable to convert epoch to UTC])])]) AC_SUBST([UTC_DATE_AT]) AC_ARG_VAR([SOURCE_EPOCH],[last modification date of the source]) AC_MSG_NOTICE([trying to determine source epoch]) AC_MSG_CHECKING([for source epoch in \$SOURCE_EPOCH]) AS_IF([test -n "$SOURCE_EPOCH"], [AC_MSG_RESULT([yes])], [AC_MSG_RESULT([no]) AC_MSG_CHECKING([for source epoch in source_epoch file]) AS_IF([test -e "$srcdir/source_epoch"], [read SOURCE_EPOCH <"$srcdir/source_epoch" AC_MSG_RESULT([yes])], [AC_MSG_RESULT([no]) AC_MSG_CHECKING([for source epoch baked in by gitattributes export-subst]) SOURCE_EPOCH='1745941920' # template for rewriting by git-archive AS_CASE([$SOURCE_EPOCH], [?Format:*], # was not rewritten [AC_MSG_RESULT([no]) AC_MSG_CHECKING([for source epoch in \$SOURCE_DATE_EPOCH]) AS_IF([test "x$SOURCE_DATE_EPOCH" != x], [SOURCE_EPOCH="$SOURCE_DATE_EPOCH" AC_MSG_RESULT([yes])], [AC_MSG_RESULT([no]) AC_MSG_CHECKING([whether git log can provide a source epoch]) SOURCE_EPOCH=f${SOURCE_EPOCH#\$F} # convert into git log --pretty format SOURCE_EPOCH=$(cd "$srcdir" && git log -1 --pretty=${SOURCE_EPOCH%$} 2>/dev/null) AS_IF([test -n "$SOURCE_EPOCH"], [AC_MSG_RESULT([yes])], [AC_MSG_RESULT([no, using current time and breaking reproducibility]) SOURCE_EPOCH=$(date +%s)])])], [AC_MSG_RESULT([yes])] )]) ]) AC_MSG_NOTICE([using source epoch $($UTC_DATE_AT$SOURCE_EPOCH +'%F')]) UTC_DATE=$($UTC_DATE_AT$SOURCE_EPOCH +'%F') AC_SUBST([UTC_DATE]) AC_CONFIG_FILES([Makefile pcs.pc pyproject.toml data/Makefile pcs/Makefile pcs/settings.py pcs/snmp/pcs_snmp_agent.service pcs/snmp/settings.py pcs/snmp/pcs_snmp_agent.8 pcs/pcs.8 pcs_test/Makefile pcs_test/settings.py pcsd/capabilities.xml pcsd/Makefile pcsd/pcsd.8 pcsd/pcsd-cli.rb pcsd/pcsd-ruby.service pcsd/pcsd.service pcsd/rserver.rb pcsd/settings.rb pcsd/logrotate/pcsd]) AC_CONFIG_FILES([pcs/pcs], [chmod +x pcs/pcs]) AC_CONFIG_FILES([pcs/pcs_internal], [chmod +x pcs/pcs_internal]) AC_CONFIG_FILES([pcs/snmp/pcs_snmp_agent], [chmod +x pcs/snmp/pcs_snmp_agent]) AC_CONFIG_FILES([pcs_test/api_v2_client], [chmod +x pcs_test/api_v2_client]) AC_CONFIG_FILES([pcs_test/smoke.sh], [chmod +x pcs_test/smoke.sh]) AC_CONFIG_FILES([pcs_test/pcs_for_tests], [chmod +x pcs_test/pcs_for_tests]) AC_CONFIG_FILES([pcs_test/suite], [chmod +x pcs_test/suite]) AC_CONFIG_FILES([pcs_test/tools/bin_mock/pcmk/crm_resource], [chmod +x pcs_test/tools/bin_mock/pcmk/crm_resource]) AC_CONFIG_FILES([pcs_test/tools/bin_mock/pcmk/pacemaker-fenced], [chmod +x pcs_test/tools/bin_mock/pcmk/pacemaker-fenced]) AC_CONFIG_FILES([pcs_test/tools/bin_mock/pcmk/stonith_admin], [chmod +x pcs_test/tools/bin_mock/pcmk/stonith_admin]) AC_CONFIG_FILES([pcsd/pcsd], [chmod +x pcsd/pcsd]) AC_CONFIG_FILES([scripts/pcsd.sh], [chmod +x scripts/pcsd.sh]) AC_OUTPUT pcs-0.12.0.2/data/000077500000000000000000000000001500417470700134355ustar00rootroot00000000000000pcs-0.12.0.2/data/Makefile.am000066400000000000000000000003051500417470700154670ustar00rootroot00000000000000MAINTAINERCLEANFILES = Makefile.in pcsdatadir = $(LIB_DIR)/pcs/data dist_pcsdata_DATA = \ ocf-1.0.rng \ ocf-1.1.rng uninstall-local: rmdir $(DESTDIR)/$(pcsdatadir) 2>/dev/null || : pcs-0.12.0.2/data/ocf-1.0.rng000066400000000000000000000140051500417470700152100ustar00rootroot00000000000000 0 1 Master Slave Promoted Unpromoted select pcs-0.12.0.2/data/ocf-1.1.rng000066400000000000000000000136201500417470700152130ustar00rootroot00000000000000 0 1 boolean string integer duration epoch_time nonnegative_integer percentage port score timeout version time select pcs-0.12.0.2/dev_requirements.txt000066400000000000000000000005611500417470700166500ustar00rootroot00000000000000lxml-stubs pylint==3.3.2 astroid==3.3.5 mypy==1.13.0 black==24.10.0 isort types-cryptography types-dataclasses # later versions remove type annotations from a few functions causing # error: Call to untyped function "getinfo" in typed context [no-untyped-call] # so we are stuck with this version until there's a fix types-pycurl==7.45.2.20240311 types-python-dateutil pcs-0.12.0.2/m4/000077500000000000000000000000001500417470700130445ustar00rootroot00000000000000pcs-0.12.0.2/m4/ac_compare_versions.m4000066400000000000000000000025671500417470700173410ustar00rootroot00000000000000dnl @synopsis AC_COMPARE_VERSIONS([verA], [op], [verB] [, action-if-true] [, action-if-false]) dnl dnl Compare two versions based on "op" dnl dnl op can be: dnl dnl lt or < dnl le or <= dnl eq or = dnl ge or >= dnl gt or > dnl dnl @category InstalledPackages dnl @author Fabio M. Di Nitto . dnl @version 2020-11-19 dnl @license AllPermissive AC_DEFUN([AC_COMPARE_VERSIONS],[ result=false verA="$1" op="$2" verB="$3" if test "x$verA" = "x" || test "x$verB" = "x" || test "x$op" = x; then AC_MSG_ERROR([ac_compare_versions: Missing parameters]) fi case "$op" in "lt"|"<") printf '%s\n%s\n' "$verA" "$verB" | sort -V -C if test $? -eq 0 && test "$verA" != "$verB"; then result=true fi ;; "le"|"<=") printf '%s\n%s\n' "$verA" "$verB" | sort -V -C if test $? -eq 0; then result=true fi ;; "eq"|"=") if test "$verB" = "$verA"; then result=true fi ;; "ge"|">=") printf '%s\n%s\n' "$verB" "$verA" | sort -V -C if test $? -eq 0; then result=true fi ;; "gt"|">") printf '%s\n%s\n' "$verB" "$verA" | sort -V -C if test $? -eq 0 && test "$verA" != "$verB"; then result=true fi ;; *) AC_MSG_ERROR([Unknown operand: $op]) ;; esac if test "x$result" = "xtrue"; then true # need to make shell happy if 4 is empty $4 else true # need to make shell happy if 5 is empty $5 fi ]) pcs-0.12.0.2/m4/ac_pip_module.m4000066400000000000000000000032221500417470700161050ustar00rootroot00000000000000dnl @synopsis AC_PIP_MODULE(modname[, version][, action-if-found][, action-if-not-found][, action-if-version-mismatch][, pythonpath]) dnl dnl Checks for pip module. dnl dnl If fatal is non-empty then absence of a module will trigger an dnl error. dnl dnl @category InstalledPackages dnl @author Fabio M. Di Nitto . dnl @version 2020-11-19 dnl @license AllPermissive AC_DEFUN([AC_PIP_MODULE],[ module="$1" reqversion="$2" AC_MSG_CHECKING([pip module: $module $reqversion]) pipcommonopts="list --format freeze --disable-pip-version-check" if test -n "$6"; then pipoutput=$(PYTHONPATH=$6 $PIP $pipcommonopts | grep ^${module}==) else pipoutput=$($PIP $pipcommonopts | grep ^${module}==) fi if test "x$pipoutput" != "x"; then curversion=$(echo $pipoutput | sed -e 's#.*==##g') checkver=ok if test "x$reqversion" != x; then comp=$(echo $reqversion | cut -d " " -f 1) tmpversion=$(echo $reqversion | cut -d " " -f 2) AC_COMPARE_VERSIONS([$curversion], [$comp], [$tmpversion], [checkver=ok], [checkver=nok]) fi if test "x$checkver" = "xok"; then AC_MSG_RESULT([yes (detected: $curversion)]) eval AS_TR_CPP(HAVE_PIPMOD_$module)=yes eval AS_TR_CPP(HAVE_PIPMOD_$module_version)=$curversion $3 else if test -n "$5"; then AC_MSG_RESULT([no (detected: $curversion)]) eval AS_TR_CPP(HAVE_PIPMOD_$module)=no eval AS_TR_CPP(HAVE_PIPMOD_$module_version)=$curversion $5 else AC_MSG_ERROR([python $module version $curversion detected. Requested "$comp $tmpversion"]) fi fi else AC_MSG_RESULT([no]) eval AS_TR_CPP(HAVE_PIPMOD_$module)=no eval AS_TR_CPP(HAVE_PIPMOD_$module_version)="" $4 fi ]) pcs-0.12.0.2/m4/ac_ruby_gem.m4000066400000000000000000000025671500417470700155740ustar00rootroot00000000000000dnl @synopsis AC_RUBY_GEM(gem[, version][, action-if-found][, action-if-not-found][, gemhome]) dnl dnl Checks for Ruby gem. dnl dnl @category InstalledPackages dnl @author Fabio M. Di Nitto . dnl @version 2020-11-23 dnl @license AllPermissive AC_DEFUN([AC_RUBY_GEM],[ module="$1" reqversion="$2" AC_MSG_CHECKING([ruby gem: $module]) if test -n "$5"; then gemoutput=$(GEM_HOME=$5 $GEM list --local | grep "^$module " 2>/dev/null) else gemoutput=$($GEM list --local | grep "^$module " 2>/dev/null) fi if test "x$gemoutput" != "x"; then curversionlist=$(echo $gemoutput | sed -e 's#.*(##g' -e 's#)##'g -e 's#default: ##g' | tr ',' ' ') curversion=0.0.0 for version in $curversionlist; do AC_COMPARE_VERSIONS([$curversion], [lt], [$version], [curversion=$version],) done if test "x$reqversion" != x; then comp=$(echo $reqversion | cut -d " " -f 1) tmpversion=$(echo $reqversion | cut -d " " -f 2) AC_COMPARE_VERSIONS([$curversion], [$comp], [$tmpversion],, [AC_MSG_ERROR([ruby gem $module version $curversion detected. Requested "$comp $tmpversion"])]) fi AC_MSG_RESULT([yes (detected: $curversion)]) eval AS_TR_CPP(HAVE_RUBYGEM_$module)=yes eval AS_TR_CPP(HAVE_RUBYGEM_${module}_version)=$curversion $3 else AC_MSG_RESULT([no]) eval AS_TR_CPP(HAVE_RUBYGEM_$module)=no eval AS_TR_CPP(HAVE_RUBYGEM_${module}_version)="" $4 fi ]) pcs-0.12.0.2/m4/ax_prog_date.m4000066400000000000000000000114241500417470700157440ustar00rootroot00000000000000# =========================================================================== # https://www.gnu.org/software/autoconf-archive/ax_prog_date.html # =========================================================================== # # SYNOPSIS # # AX_PROG_DATE() # # DESCRIPTION # # This macro tries to determine the type of the date (1) command and some # of its non-standard capabilities. # # The type is determined as follow: # # * If the version string contains "GNU", then: # - The variable ax_cv_prog_date_gnu is set to "yes". # - The variable ax_cv_prog_date_type is set to "gnu". # # * If date supports the "-v 1d" option, then: # - The variable ax_cv_prog_date_bsd is set to "yes". # - The variable ax_cv_prog_date_type is set to "bsd". # # * If both previous checks fail, then: # - The variable ax_cv_prog_date_type is set to "unknown". # # The following capabilities of GNU date are checked: # # * If date supports the --date arg option, then: # - The variable ax_cv_prog_date_gnu_date is set to "yes". # # * If date supports the --utc arg option, then: # - The variable ax_cv_prog_date_gnu_utc is set to "yes". # # The following capabilities of BSD date are checked: # # * If date supports the -v 1d option, then: # - The variable ax_cv_prog_date_bsd_adjust is set to "yes". # # * If date supports the -r arg option, then: # - The variable ax_cv_prog_date_bsd_date is set to "yes". # # All the aforementioned variables are set to "no" before a check is # performed. # # LICENSE # # Copyright (c) 2017 Enrico M. Crisostomo # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or (at your # option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program. If not, see . # # As a special exception, the respective Autoconf Macro's copyright owner # gives unlimited permission to copy, distribute and modify the configure # scripts that are the output of Autoconf when processing the Macro. You # need not follow the terms of the GNU General Public License when using # or distributing such scripts, even though portions of the text of the # Macro appear in them. The GNU General Public License (GPL) does govern # all other use of the material that constitutes the Autoconf Macro. # # This special exception to the GPL applies to versions of the Autoconf # Macro released by the Autoconf Archive. When you make and distribute a # modified version of the Autoconf Macro, you may extend this special # exception to the GPL to apply to your modified version as well. #serial 3 AC_DEFUN([AX_PROG_DATE], [dnl AC_CACHE_CHECK([for GNU date], [ax_cv_prog_date_gnu], [ ax_cv_prog_date_gnu=no if date --version 2>/dev/null | head -1 | grep -q GNU then ax_cv_prog_date_gnu=yes fi ]) AC_CACHE_CHECK([for BSD date], [ax_cv_prog_date_bsd], [ ax_cv_prog_date_bsd=no if date -v 1d > /dev/null 2>&1 then ax_cv_prog_date_bsd=yes fi ]) AC_CACHE_CHECK([for date type], [ax_cv_prog_date_type], [ ax_cv_prog_date_type=unknown if test "x${ax_cv_prog_date_gnu}" = "xyes" then ax_cv_prog_date_type=gnu elif test "x${ax_cv_prog_date_bsd}" = "xyes" then ax_cv_prog_date_type=bsd fi ]) AS_VAR_IF([ax_cv_prog_date_gnu], [yes], [ AC_CACHE_CHECK([whether GNU date supports --date], [ax_cv_prog_date_gnu_date], [ ax_cv_prog_date_gnu_date=no if date --date=@1512031231 > /dev/null 2>&1 then ax_cv_prog_date_gnu_date=yes fi ]) AC_CACHE_CHECK([whether GNU date supports --utc], [ax_cv_prog_date_gnu_utc], [ ax_cv_prog_date_gnu_utc=no if date --utc > /dev/null 2>&1 then ax_cv_prog_date_gnu_utc=yes fi ]) ]) AS_VAR_IF([ax_cv_prog_date_bsd], [yes], [ AC_CACHE_CHECK([whether BSD date supports -r], [ax_cv_prog_date_bsd_date], [ ax_cv_prog_date_bsd_date=no if date -r 1512031231 > /dev/null 2>&1 then ax_cv_prog_date_bsd_date=yes fi ]) ]) AS_VAR_IF([ax_cv_prog_date_bsd], [yes], [ AC_CACHE_CHECK([whether BSD date supports -v], [ax_cv_prog_date_bsd_adjust], [ ax_cv_prog_date_bsd_adjust=no if date -v 1d > /dev/null 2>&1 then ax_cv_prog_date_bsd_adjust=yes fi ]) ]) ])dnl AX_PROG_DATE pcs-0.12.0.2/make/000077500000000000000000000000001500417470700134415ustar00rootroot00000000000000pcs-0.12.0.2/make/git-version-gen000077500000000000000000000223671500417470700164160ustar00rootroot00000000000000#!/bin/sh # Print a version string. scriptversion=2018-08-31.20; # UTC # Copyright (C) 2012-2020 Red Hat, Inc. # Copyright (C) 2007-2016 Free Software Foundation, Inc. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # This script is derived from GIT-VERSION-GEN from GIT: http://git.or.cz/. # It may be run two ways: # - from a git repository in which the "git describe" command below # produces useful output (thus requiring at least one signed tag) # - from a non-git-repo directory containing a .tarball-version file, which # presumes this script is invoked like "./git-version-gen .tarball-version". # In order to use intra-version strings in your project, you will need two # separate generated version string files: # # .tarball-version - present only in a distribution tarball, and not in # a checked-out repository. Created with contents that were learned at # the last time autoconf was run, and used by git-version-gen. Must not # be present in either $(srcdir) or $(builddir) for git-version-gen to # give accurate answers during normal development with a checked out tree, # but must be present in a tarball when there is no version control system. # Therefore, it cannot be used in any dependencies. GNUmakefile has # hooks to force a reconfigure at distribution time to get the value # correct, without penalizing normal development with extra reconfigures. # # .version - present in a checked-out repository and in a distribution # tarball. Usable in dependencies, particularly for files that don't # want to depend on config.h but do want to track version changes. # Delete this file prior to any autoconf run where you want to rebuild # files to pick up a version string change; and leave it stale to # minimize rebuild time after unrelated changes to configure sources. # # As with any generated file in a VC'd directory, you should add # /.version to .gitignore, so that you don't accidentally commit it. # .tarball-version is never generated in a VC'd directory, so needn't # be listed there. # # In order to use git archive versions another two files has to be presented: # # .gitarchive-version - present in checked-out repository and git # archive tarball, but not in the distribution tarball. Used as a last # option for version. File must contain special string $Format:%d$, # which is substituted by git on archive operation. # # .gitattributes - present in checked-out repository and git archive # tarball, but not in the distribution tarball. Must set export-subst # attribute for .gitarchive-version file. # # Use the following line in your configure.ac, so that $(VERSION) will # automatically be up-to-date each time configure is run (and note that # since configure.ac no longer includes a version string, Makefile rules # should not depend on configure.ac for version updates). # # AC_INIT([GNU project], # m4_esyscmd([build-aux/git-version-gen .tarball-version]), # [bug-project@example]) # # Then use the following lines in your Makefile.am, so that .version # will be present for dependencies, and so that .version and # .tarball-version will exist in distribution tarballs. # # EXTRA_DIST = $(top_srcdir)/.version # BUILT_SOURCES = $(top_srcdir)/.version # $(top_srcdir)/.version: # echo $(VERSION) > $@-t && mv $@-t $@ # dist-hook: # echo $(VERSION) > $(distdir)/.tarball-version me=$0 version="git-version-gen $scriptversion Copyright 2011 Free Software Foundation, Inc. There is NO warranty. You may redistribute this software under the terms of the GNU General Public License. For more information about these matters, see the files named COPYING." usage="\ Usage: $me [OPTION]... \$srcdir/.tarball-version [\$srcdir/.gitarchive-version] [TAG-NORMALIZATION-SED-SCRIPT] Print a version string. Options: --prefix PREFIX prefix of git tags (default 'v') --fallback VERSION fallback version to use if \"git --version\" fails --help display this help and exit --version output version information and exit Running without arguments will suffice in most cases." prefix=v fallback= while test $# -gt 0; do case $1 in --help) echo "$usage"; exit 0;; --version) echo "$version"; exit 0;; --prefix) shift; prefix="$1";; --fallback) shift; fallback="$1";; -*) echo "$0: Unknown option '$1'." >&2 echo "$0: Try '--help' for more information." >&2 exit 1;; *) if test "x$tarball_version_file" = x; then tarball_version_file="$1" elif test "x$gitarchive_version_file" = x; then gitarchive_version_file="$1" elif test "x$tag_sed_script" = x; then tag_sed_script="$1" else echo "$0: extra non-option argument '$1'." >&2 exit 1 fi;; esac shift done if test "x$tarball_version_file" = x; then echo "$usage" exit 1 fi tag_sed_script="${tag_sed_script:-s/x/x/}" nl=' ' # Avoid meddling by environment variable of the same name. v= v_from_git= # First see if there is a tarball-only version file. # then try "git describe", then default. if test -f $tarball_version_file then v=`cat $tarball_version_file` || v= case $v in *$nl*) v= ;; # reject multi-line output [0-9]*) ;; *) v= ;; esac test "x$v" = x \ && echo "$0: WARNING: $tarball_version_file is missing or damaged" 1>&2 fi if test "x$v" != x then : # use $v # Otherwise, if there is at least one git commit involving the working # directory, and "git describe" output looks sensible, use that to # derive a version string. elif test "`git log -1 --pretty=format:x . 2>&1`" = x \ && v=`git describe --abbrev=4 --match="$prefix*" HEAD 2>/dev/null \ || git describe --abbrev=4 HEAD 2>/dev/null` \ && v=`printf '%s\n' "$v" | sed "$tag_sed_script"` \ && case $v in $prefix[0-9]*) ;; *) (exit 1) ;; esac then # Is this a new git that lists number of commits since the last # tag or the previous older version that did not? # Newer: v6.10-77-g0f8faeb # Older: v6.10-g0f8faeb case $v in *-*-*) : git describe is okay three part flavor ;; *-*) : git describe is older two part flavor # Recreate the number of commits and rewrite such that the # result is the same as if we were using the newer version # of git describe. vtag=`echo "$v" | sed 's/-.*//'` commit_list=`git rev-list "$vtag"..HEAD 2>/dev/null` \ || { commit_list=failed; echo "$0: WARNING: git rev-list failed" 1>&2; } numcommits=`echo "$commit_list" | wc -l` v=`echo "$v" | sed "s/\(.*\)-\(.*\)/\1-$numcommits-\2/"`; test "$commit_list" = failed && v=UNKNOWN ;; esac # Change the first '-' to a '.', so version-comparing tools work properly. # Remove the "g" in git describe's output string, to save a byte. v=`echo "$v" | sed 's/-/+/;s/\(.*\)-g/\1-/'`; v_from_git=1 elif test "x$fallback" = x || git --version >/dev/null 2>&1; then if test -f $gitarchive_version_file then v=`sed "s/^.*tag: \($prefix[0-9)][^,)]*\).*\$/\1/" $gitarchive_version_file \ | sed "$tag_sed_script"` || exit 1 case $v in *$nl*) v= ;; # reject multi-line output $prefix[0-9]*) ;; *) v= ;; esac test -z "$v" \ && echo "$0: WARNING: $gitarchive_version_file doesn't contain valid version tag" 1>&2 \ && v=UNKNOWN elif test "x$fallback" = x; then v=UNKNOWN else v=$fallback fi else v=$fallback fi if test "x$fallback" = x -a "$v" = "UNKNOWN" then echo "$0: ERROR: Can't find valid version. Please use valid git repository," \ "released tarball or version tagged archive" 1>&2 exit 1 fi v=`echo "$v" |sed "s/^$prefix//"` # Test whether to append the "-dirty" suffix only if the version # string we're using came from git. I.e., skip the test if it's "UNKNOWN" # or if it came from .tarball-version. if test "x$v_from_git" != x; then # Don't declare a version "dirty" merely because a time stamp has changed. git update-index --refresh > /dev/null 2>&1 dirty=`exec 2>/dev/null;git diff-index --name-only HEAD` || dirty= case "$dirty" in '') ;; *) # Append the suffix only if there isn't one already. case $v in *-dirty) ;; *) v="$v-dirty" ;; esac ;; esac fi # Omit the trailing newline, so that m4_esyscmd can use the result directly. printf %s "$v" # Local variables: # eval: (add-hook 'write-file-hooks 'time-stamp) # time-stamp-start: "scriptversion=" # time-stamp-format: "%:y-%02m-%02d.%02H" # time-stamp-time-zone: "UTC0" # time-stamp-end: "; # UTC" # End: pcs-0.12.0.2/make/gitlog-to-changelog000077500000000000000000000126451500417470700172310ustar00rootroot00000000000000eval '(exit $?0)' && eval 'exec perl -wS "$0" ${1+"$@"}' & eval 'exec perl -wS "$0" $argv:q' if 0; # Convert git log output to ChangeLog format. my $VERSION = '2009-10-30 13:46'; # UTC # The definition above must lie within the first 8 lines in order # for the Emacs time-stamp write hook (at end) to update it. # If you change this file with Emacs, please let the write hook # do its job. Otherwise, update this string manually. # Copyright (C) 2008-2010 Free Software Foundation, Inc. # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # Written by Jim Meyering use strict; use warnings; use Getopt::Long; use POSIX qw(strftime); (my $ME = $0) =~ s|.*/||; # use File::Coda; # http://meyering.net/code/Coda/ END { defined fileno STDOUT or return; close STDOUT and return; warn "$ME: failed to close standard output: $!\n"; $? ||= 1; } sub usage ($) { my ($exit_code) = @_; my $STREAM = ($exit_code == 0 ? *STDOUT : *STDERR); if ($exit_code != 0) { print $STREAM "Try `$ME --help' for more information.\n"; } else { print $STREAM < ChangeLog $ME -- -n 5 foo > last-5-commits-to-branch-foo EOF } exit $exit_code; } # If the string $S is a well-behaved file name, simply return it. # If it contains white space, quotes, etc., quote it, and return the new string. sub shell_quote($) { my ($s) = @_; if ($s =~ m![^\w+/.,-]!) { # Convert each single quote to '\'' $s =~ s/\'/\'\\\'\'/g; # Then single quote the string. $s = "'$s'"; } return $s; } sub quoted_cmd(@) { return join (' ', map {shell_quote $_} @_); } { my $since_date = '1970-01-01 UTC'; my $format_string = '%s%n%b%n'; GetOptions ( help => sub { usage 0 }, version => sub { print "$ME version $VERSION\n"; exit }, 'since=s' => \$since_date, 'format=s' => \$format_string, ) or usage 1; my @cmd = (qw (git log --log-size), "--since=$since_date", '--pretty=format:%ct %an <%ae>%n%n'.$format_string, @ARGV); open PIPE, '-|', @cmd or die ("$ME: failed to run `". quoted_cmd (@cmd) ."': $!\n" . "(Is your Git too old? Version 1.5.1 or later is required.)\n"); my $prev_date_line = ''; while (1) { defined (my $in = ) or last; $in =~ /^log size (\d+)$/ or die "$ME:$.: Invalid line (expected log size):\n$in"; my $log_nbytes = $1; my $log; my $n_read = read PIPE, $log, $log_nbytes; $n_read == $log_nbytes or die "$ME:$.: unexpected EOF\n"; my @line = split "\n", $log; my $author_line = shift @line; defined $author_line or die "$ME:$.: unexpected EOF\n"; $author_line =~ /^(\d+) (.*>)$/ or die "$ME:$.: Invalid line " . "(expected date/author/email):\n$author_line\n"; my $date_line = sprintf "%s $2\n", strftime ("%F", localtime ($1)); # If this line would be the same as the previous date/name/email # line, then arrange not to print it. if ($date_line ne $prev_date_line) { $prev_date_line eq '' or print "\n"; print $date_line; } $prev_date_line = $date_line; # Omit "Signed-off-by..." lines. @line = grep !/^Signed-off-by: .*>$/, @line; # If there were any lines if (@line == 0) { warn "$ME: warning: empty commit message:\n $date_line\n"; } else { # Remove leading and trailing blank lines. while ($line[0] =~ /^\s*$/) { shift @line; } while ($line[$#line] =~ /^\s*$/) { pop @line; } # Prefix each non-empty line with a TAB. @line = map { length $_ ? "\t$_" : '' } @line; print "\n", join ("\n", @line), "\n"; } defined ($in = ) or last; $in ne "\n" and die "$ME:$.: unexpected line:\n$in"; } close PIPE or die "$ME: error closing pipe from " . quoted_cmd (@cmd) . "\n"; # FIXME-someday: include $PROCESS_STATUS in the diagnostic } # Local Variables: # mode: perl # indent-tabs-mode: nil # eval: (add-hook 'write-file-hooks 'time-stamp) # time-stamp-start: "my $VERSION = '" # time-stamp-format: "%:y-%02m-%02d %02H:%02M" # time-stamp-time-zone: "UTC" # time-stamp-end: "'; # UTC" # End: pcs-0.12.0.2/make/release.mk000066400000000000000000000037401500417470700154160ustar00rootroot00000000000000# to build official release tarballs, handle tagging and publish. project = pcs deliverables = $(project)-$(version).sha256 \ $(project)-$(version).tar.bz2 \ $(project)-$(version).tar.gz \ $(project)-$(version).tar.xz changelogfile = CHANGELOG.md .PHONY: all all: tag tarballs .PHONY: checks checks: ifeq (,$(version)) @echo ERROR: need to define version= @exit 1 endif @if [ ! -d .git ]; then \ echo This script needs to be executed from top level cluster git tree; \ exit 1; \ fi @if [ -n "$$(git status --untracked-files=no --porcelain 2>/dev/null)" ]; then \ echo Stash or rollback the uncommitted changes in git first; \ exit 1; \ fi .PHONY: setup setup: checks ./autogen.sh ./configure $(configure_options) $(MAKE) maintainer-clean .PHONY: tag tag: setup ./tag-$(version) tag-$(version): ifeq (,$(release)) @echo Building test release $(version), no tagging echo '$(version)' > .tarball-version else # following will be captured by git-version-gen automatically git tag -a -m "v$(version) release" v$(version) HEAD @touch $@ endif .PHONY: tarballs tarballs: tag ./autogen.sh ./configure $(configure_options) $(MAKE) distcheck "DISTCHECK_CONFIGURE_FLAGS=$(configure_options)" .PHONY: sha256 sha256: $(project)-$(version).sha256 $(deliverables): tarballs $(project)-$(version).sha256: # checksum anything from deliverables except for in-prep checksums file sha256sum $(deliverables:$@=) | sort -k2 > $@ .PHONY: publish publish: ifeq (,$(release)) @echo Building test release $(version), no publishing! else git push --follow-tags origin @echo Hey you! Yeah you, looking somewhere else! @echo Remember to notify cluster-devel/RH and users/ClusterLabs MLs. endif .PHONY: bump-changelog bump-changelog: checks sed -i 's/\#\# \[Unreleased\]/\#\# \[$(version)\] - $(shell date +%Y-%m-%d)/' \ $(changelogfile) git commit -a -m "Bumped to $(version)" .PHONY: clean clean: rm -rf $(project)* tag-* .tarball-version pcs-0.12.0.2/mypy.ini000066400000000000000000000120241500417470700142220ustar00rootroot00000000000000[mypy] mypy_path = ./pcs/bundled/packages # Modules with issues in typehinting: # TODO: fix [mypy-pcs.daemon.*] ignore_errors = True # Modules and packages with full support have more strict checks enabled [mypy-pcs.cli.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.cli.booth.env] disallow_untyped_defs = False disallow_untyped_calls = False [mypy-pcs.cli.common.env_cli] disallow_untyped_defs = False disallow_untyped_calls = False [mypy-pcs.cli.common.lib_wrapper] disallow_untyped_defs = False disallow_untyped_calls = False [mypy-pcs.cli.common.middleware] disallow_untyped_defs = False disallow_untyped_calls = False [mypy-pcs.cli.file.metadata] disallow_untyped_defs = False [mypy-pcs.common.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.entry_points.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.daemon.app.api_v2] disallow_untyped_defs = True disallow_untyped_calls = True ignore_errors = False [mypy-pcs.daemon.app.api_v1] disallow_untyped_defs = True disallow_untyped_calls = True ignore_errors = False [mypy-pcs.daemon.app.auth] disallow_untyped_defs = True disallow_untyped_calls = True ignore_errors = False [mypy-pcs.daemon.async_tasks.*] disallow_untyped_defs = True disallow_untyped_calls = True ignore_errors = False [mypy-pcs.daemon.session] disallow_untyped_defs = True disallow_untyped_calls = True ignore_errors = False [mypy-pcs.lib.auth.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.auth.pam] disallow_untyped_defs = False disallow_untyped_calls = False [mypy-pcs.lib.booth.cib] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cluster_property] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.constraint.duplicates] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.constraint.location] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.fencing_topology] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.nvpair_multi] disallow_untyped_defs = True [mypy-pcs.lib.cib.remove_elements] disallow_untyped_defs = True [mypy-pcs.lib.cib.resource.clone] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.resource.common] disallow_untyped_defs = True [mypy-pcs.lib.cib.resource.group] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.resource.hierarchy] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.resource.relations] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.resource.validations] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.resource_agent.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.rule.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.cib.tag] disallow_untyped_defs = True [mypy-pcs.lib.commands.booth] disallow_untyped_defs = True [mypy-pcs.lib.commands.cib] disallow_untyped_defs = True [mypy-pcs.lib.commands.cib_options] disallow_untyped_defs = True [mypy-pcs.lib.commands.cluster_property] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.commands.dr] disallow_untyped_defs = True [mypy-pcs.lib.commands.fencing_topology] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.commands.status] disallow_untyped_defs = True [mypy-pcs.lib.commands.tag] disallow_untyped_defs = True [mypy-pcs.lib.corosync.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.corosync.qdevice_net] disallow_untyped_defs = True disallow_untyped_calls = False [mypy-pcs.lib.dr.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.file.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.pacemaker.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.pacemaker.state] disallow_untyped_defs = False disallow_untyped_calls = False [mypy-pcs.lib.permissions.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.resource_agent.*] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.services] disallow_untyped_defs = True [mypy-pcs.lib.tools] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.validate] disallow_untyped_defs = True disallow_untyped_calls = True [mypy-pcs.lib.xml_tools] disallow_untyped_defs = True disallow_untyped_calls = True # We don't want to type check tests [mypy-pcs_test.*] ignore_errors = True # But we want to type check the test framework [mypy-pcs_test.tools.*] ignore_errors = False [mypy-pcs_test.suite] ignore_errors = False # External libraries [mypy-dacite] ignore_missing_imports = True [mypy-lxml.doctestcompare] ignore_missing_imports = True [mypy-pyagentx] ignore_errors = True ignore_missing_imports = True [mypy-pyparsing] ignore_missing_imports = True [mypy-tornado.*] ignore_missing_imports = True ignore_errors = True [mypy-xml.dom.*] ignore_missing_imports = True pcs-0.12.0.2/pcs.pc.in000066400000000000000000000002221500417470700142360ustar00rootroot00000000000000webui_dir=@PCSD_WEBUI_DIR@ pcsd_unix_socket=@PCSD_UNIX_SOCKET@ Name: pcs Description: Pacemaker/Corosync Configuration System Version: @VERSION@ pcs-0.12.0.2/pcs/000077500000000000000000000000001500417470700133115ustar00rootroot00000000000000pcs-0.12.0.2/pcs/COPYING000066400000000000000000000432541500417470700143540ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. pcs-0.12.0.2/pcs/Makefile.am000066400000000000000000000323041500417470700153470ustar00rootroot00000000000000MAINTAINERCLEANFILES = Makefile.in # install bashcompletiondir = $(prefix)/share/bash-completion/completions/ dist_bashcompletion_DATA= bash_completion/pcs snmpmibsdir = $(SNMP_MIB_DIR) dist_snmpmibs_DATA = snmp/mibs/PCMK-PCS-MIB.txt \ snmp/mibs/PCMK-PCS-V1-MIB.txt man8_MANS = pcs.8 snmp/pcs_snmp_agent.8 sysconfigdir = $(CONF_DIR) dist_sysconfig_DATA = snmp/conf/pcs_snmp_agent servicedir = $(SYSTEMD_UNIT_DIR) service_DATA = snmp/pcs_snmp_agent.service EXTRA_DIST = \ acl.py \ alert.py \ app.py \ cli/booth/command.py \ cli/booth/env.py \ cli/booth/__init__.py \ cli/cluster/__init__.py \ cli/cluster/command.py \ cli/cluster_property/__init__.py \ cli/cluster_property/command.py \ cli/cluster_property/output.py \ cli/common/completion.py \ cli/common/env_cli.py \ cli/common/errors.py \ cli/common/__init__.py \ cli/common/lib_wrapper.py \ cli/common/middleware.py \ cli/common/parse_args.py \ cli/common/printable_tree.py \ cli/common/routing.py \ cli/common/tools.py \ cli/common/output.py \ cli/constraint_colocation/command.py \ cli/constraint_colocation/__init__.py \ cli/constraint/command.py \ cli/constraint/location/__init__.py \ cli/constraint/location/command.py \ cli/constraint/output/__init__.py \ cli/constraint/output/all.py \ cli/constraint/output/colocation.py \ cli/constraint/output/location.py \ cli/constraint/output/order.py \ cli/constraint/output/set.py \ cli/constraint/output/ticket.py \ cli/constraint/__init__.py \ cli/constraint_order/command.py \ cli/constraint_order/__init__.py \ cli/constraint/parse_args.py \ cli/constraint_ticket/command.py \ cli/constraint_ticket/__init__.py \ cli/constraint_ticket/parse_args.py \ cli/dr.py \ client.py \ cli/fencing_topology.py \ cli/file/__init__.py \ cli/file/metadata.py \ cli/__init__.py \ cli/nvset.py \ cli/query/__init__.py \ cli/query/resource.py \ cli/reports/__init__.py \ cli/reports/messages.py \ cli/reports/output.py \ cli/reports/preprocessor.py \ cli/reports/processor.py \ cli/resource/__init__.py \ cli/resource/command.py \ cli/resource/parse_args.py \ cli/resource/output.py \ cli/resource/relations.py \ cli/resource_agent.py \ cli/routing/acl.py \ cli/routing/alert.py \ cli/routing/booth.py \ cli/routing/client.py \ cli/routing/cluster.py \ cli/routing/config.py \ cli/routing/constraint.py \ cli/routing/dr.py \ cli/routing/host.py \ cli/routing/__init__.py \ cli/routing/node.py \ cli/routing/pcsd.py \ cli/routing/prop.py \ cli/routing/qdevice.py \ cli/routing/quorum.py \ cli/routing/resource.py \ cli/routing/resource_stonith_common.py \ cli/routing/status.py \ cli/routing/stonith.py \ cli/routing/tag.py \ cli/rule.py \ cli/status/command.py \ cli/status/__init__.py \ cli/stonith/__init__.py \ cli/stonith/command.py \ cli/stonith/levels/__init__.py \ cli/stonith/levels/command.py \ cli/stonith/levels/output.py \ cli/tag/__init__.py \ cli/tag/command.py \ cli/tag/output.py \ cluster.py \ common/corosync_conf.py \ common/const.py \ common/dr.py \ common/fencing_topology.py \ common/file.py \ common/file_type_codes.py \ common/host.py \ common/__init__.py \ common/async_tasks/__init__.py \ common/async_tasks/dto.py \ common/async_tasks/types.py \ common/capabilities.py \ common/communication/__init__.py \ common/communication/const.py \ common/communication/dto.py \ common/communication/types.py \ common/interface/dto.py \ common/interface/__init__.py \ common/node_communicator.py \ common/pacemaker/__init__.py \ common/pacemaker/cluster_property.py \ common/pacemaker/constraint/__init__.py \ common/pacemaker/constraint/all.py \ common/pacemaker/constraint/colocation.py \ common/pacemaker/constraint/location.py \ common/pacemaker/constraint/order.py \ common/pacemaker/constraint/set.py \ common/pacemaker/constraint/ticket.py \ common/pacemaker/defaults.py \ common/pacemaker/fencing_topology.py \ common/pacemaker/nvset.py \ common/pacemaker/resource/__init__.py \ common/pacemaker/resource/bundle.py \ common/pacemaker/resource/clone.py \ common/pacemaker/resource/group.py \ common/pacemaker/resource/list.py \ common/pacemaker/resource/operations.py \ common/pacemaker/resource/primitive.py \ common/pacemaker/resource/relations.py \ common/pacemaker/role.py \ common/pacemaker/rule.py \ common/pacemaker/tag.py \ common/pacemaker/tools.py \ common/pacemaker/types.py \ common/permissions/__init__.py \ common/permissions/types.py \ common/pcs_pycurl.py \ common/reports/codes.py \ common/reports/const.py \ common/reports/conversions.py \ common/reports/deprecated_codes.py \ common/reports/dto.py \ common/reports/__init__.py \ common/reports/item.py \ common/reports/messages.py \ common/reports/processor.py \ common/reports/types.py \ common/resource_agent/__init__.py \ common/resource_agent/const.py \ common/resource_agent/dto.py \ common/services/common.py \ common/services/drivers/__init__.py \ common/services/drivers/systemd.py \ common/services/drivers/sysvinit_rhel.py \ common/services_dto.py \ common/services/errors.py \ common/services/__init__.py \ common/services/interfaces/executor.py \ common/services/interfaces/__init__.py \ common/services/interfaces/manager.py \ common/services/types.py \ common/ssl.py \ common/str_tools.py \ common/tools.py \ common/types.py \ common/validate.py \ common/status_dto.py \ common/resource_status.py \ config.py \ constraint.py \ daemon/app/common.py \ daemon/app/__init__.py \ daemon/app/api_v0.py \ daemon/app/api_v1.py \ daemon/app/api_v2.py \ daemon/app/auth.py \ daemon/app/capabilities.py \ daemon/app/sinatra_common.py \ daemon/app/sinatra_remote.py \ daemon/app/sinatra_ui.py \ daemon/app/ui_common.py \ daemon/app/webui/__init__.py \ daemon/app/webui/auth.py \ daemon/app/webui/core.py \ daemon/app/webui/session.py \ daemon/app/webui/sinatra_ui.py \ daemon/async_tasks/__init__.py \ daemon/async_tasks/scheduler.py \ daemon/async_tasks/task.py \ daemon/async_tasks/types.py \ daemon/async_tasks/worker/command_mapping.py \ daemon/async_tasks/worker/communicator.py \ daemon/async_tasks/worker/executor.py \ daemon/async_tasks/worker/__init__.py \ daemon/async_tasks/worker/logging.py \ daemon/async_tasks/worker/report_processor.py \ daemon/async_tasks/worker/types.py \ daemon/env.py \ daemon/http_server.py \ daemon/__init__.py \ daemon/log.py \ daemon/ruby_pcsd.py \ daemon/run.py \ daemon/ssl.py \ daemon/systemd.py \ entry_points/cli.py \ entry_points/common.py \ entry_points/daemon.py \ entry_points/__init__.py \ entry_points/internal.py \ entry_points/snmp_agent.py \ host.py \ __init__.py \ lib/auth/config/__init__.py \ lib/auth/config/exporter.py \ lib/auth/config/facade.py \ lib/auth/config/parser.py \ lib/auth/config/types.py \ lib/auth/__init__.py \ lib/auth/const.py \ lib/auth/pam.py \ lib/auth/provider.py \ lib/auth/tools.py \ lib/auth/types.py \ lib/booth/cib.py \ lib/booth/config_facade.py \ lib/booth/config_files.py \ lib/booth/config_parser.py \ lib/booth/config_validators.py \ lib/booth/constants.py \ lib/booth/env.py \ lib/booth/__init__.py \ lib/booth/resource.py \ lib/booth/status.py \ lib/booth/sync.py \ lib/cib/acl.py \ lib/cib/alert.py \ lib/cib/const.py \ lib/cib/constraint/colocation.py \ lib/cib/constraint/common.py \ lib/cib/constraint/constraint.py \ lib/cib/constraint/location.py \ lib/cib/constraint/__init__.py \ lib/cib/constraint/order.py \ lib/cib/constraint/resource_set.py \ lib/cib/constraint/ticket.py \ lib/cib/fencing_topology.py \ lib/cib/__init__.py \ lib/cib/node.py \ lib/cib/nvpair_multi.py \ lib/cib/nvpair.py \ lib/cib/remove_elements.py \ lib/cib/resource/agent.py \ lib/cib/resource/bundle.py \ lib/cib/resource/clone.py \ lib/cib/resource/common.py \ lib/cib/resource/const.py \ lib/cib/resource/group.py \ lib/cib/resource/guest_node.py \ lib/cib/resource/hierarchy.py \ lib/cib/resource/__init__.py \ lib/cib/resource/operations.py \ lib/cib/resource/primitive.py \ lib/cib/resource/relations.py \ lib/cib/resource/remote_node.py \ lib/cib/resource/stonith.py \ lib/cib/resource/types.py \ lib/cib/resource/validations.py \ lib/cib/rule/cib_to_dto.py \ lib/cib/rule/cib_to_str.py \ lib/cib/rule/expression_part.py \ lib/cib/rule/in_effect.py \ lib/cib/rule/__init__.py \ lib/cib/rule/parsed_to_cib.py \ lib/cib/rule/parser.py \ lib/cib/rule/validator.py \ lib/cib/sections.py \ lib/cib/status.py \ lib/cib/tag.py \ lib/cib/tools.py \ lib/cluster_property.py \ lib/commands/acl.py \ lib/commands/alert.py \ lib/commands/booth.py \ lib/commands/cib.py \ lib/commands/cib_options.py \ lib/commands/cluster.py \ lib/commands/cluster_property.py \ lib/commands/constraint/colocation.py \ lib/commands/constraint/common.py \ lib/commands/constraint/__init__.py \ lib/commands/constraint/location.py \ lib/commands/constraint/order.py \ lib/commands/constraint/ticket.py \ lib/commands/dr.py \ lib/commands/fencing_topology.py \ lib/commands/__init__.py \ lib/commands/node.py \ lib/commands/pcsd.py \ lib/commands/qdevice.py \ lib/commands/quorum.py \ lib/commands/remote_node.py \ lib/commands/resource_agent.py \ lib/commands/resource.py \ lib/commands/sbd.py \ lib/commands/scsi.py \ lib/commands/services.py \ lib/commands/status.py \ lib/commands/stonith_agent.py \ lib/commands/stonith.py \ lib/commands/tag.py \ lib/communication/booth.py \ lib/communication/cluster.py \ lib/communication/corosync.py \ lib/communication/__init__.py \ lib/communication/nodes.py \ lib/communication/qdevice_net.py \ lib/communication/qdevice.py \ lib/communication/sbd.py \ lib/communication/scsi.py \ lib/communication/status.py \ lib/communication/tools.py \ lib/corosync/config_facade.py \ lib/corosync/config_parser.py \ lib/corosync/config_validators.py \ lib/corosync/constants.py \ lib/corosync/__init__.py \ lib/corosync/live.py \ lib/corosync/node.py \ lib/corosync/qdevice_client.py \ lib/corosync/qdevice_net.py \ lib/dr/config/facade.py \ lib/dr/config/__init__.py \ lib/dr/env.py \ lib/dr/__init__.py \ lib/env.py \ lib/errors.py \ lib/exchange_formats.md \ lib/external.py \ lib/file/__init__.py \ lib/file/instance.py \ lib/file/json.py \ lib/file/metadata.py \ lib/file/raw_file.py \ lib/file/toolbox.py \ lib/__init__.py \ lib/interface/config.py \ lib/interface/__init__.py \ lib/node_communication_format.py \ lib/node_communication.py \ lib/node.py \ lib/pacemaker/api_result.py \ lib/pacemaker/__init__.py \ lib/pacemaker/live.py \ lib/pacemaker/simulate.py \ lib/pacemaker/state.py \ lib/pacemaker/status.py \ lib/pacemaker/values.py \ lib/permissions/__init__.py \ lib/permissions/checker.py \ lib/permissions/config/__init__.py \ lib/permissions/config/exporter.py \ lib/permissions/config/facade.py \ lib/permissions/config/parser.py \ lib/permissions/config/types.py \ lib/resource_agent/const.py \ lib/resource_agent/error.py \ lib/resource_agent/facade.py \ lib/resource_agent/__init__.py \ lib/resource_agent/list.py \ lib/resource_agent/name.py \ lib/resource_agent/ocf_transform.py \ lib/resource_agent/pcs_transform.py \ lib/resource_agent/types.py \ lib/resource_agent/xml.py \ lib/sbd.py \ lib/services.py \ lib/tools.py \ lib/validate.py \ lib/xml_tools.py \ node.py \ pcsd.py \ pcs_internal.py \ py.typed \ qdevice.py \ quorum.py \ resource.py \ snmp/agentx/__init__.py \ snmp/agentx/types.py \ snmp/agentx/updater.py \ snmp/conf/pcs_snmp_agent \ snmp/__init__.py \ snmp/mibs/PCMK-PCS-MIB.txt \ snmp/mibs/PCMK-PCS-V1-MIB.txt \ snmp/pcs_snmp_agent.8 \ snmp/pcs_snmp_agent.py \ snmp/updaters/__init__.py \ snmp/updaters/v1.py \ status.py \ stonith.py \ usage.py \ utils.py pcs-0.12.0.2/pcs/__init__.py000066400000000000000000000000001500417470700154100ustar00rootroot00000000000000pcs-0.12.0.2/pcs/acl.py000066400000000000000000000212731500417470700144270ustar00rootroot00000000000000from pcs.cli.cluster_property.output import PropertyConfigurationFacade from pcs.cli.common.errors import CmdLineInputError from pcs.cli.reports.output import deprecation_warning from pcs.common.str_tools import indent from pcs.lib.pacemaker.values import is_true def _print_list_of_objects(obj_list, transformation_fn): out = [] for obj in obj_list: out += transformation_fn(obj) if out: print("\n".join(out)) def acl_config(lib, argv, modifiers): """ Options: * -f - CIB file """ # TODO move to lib once lib supports cluster properties # enabled/disabled should be part of the structure returned # by lib.acl.get_config modifiers.ensure_only_supported("-f") if argv: raise CmdLineInputError() properties_facade = PropertyConfigurationFacade.from_properties_dtos( lib.cluster_property.get_properties(), lib.cluster_property.get_properties_metadata(), ) acl_enabled = properties_facade.get_property_value_or_default( "enable-acl", "" ) if is_true(acl_enabled): print("ACLs are enabled") else: print("ACLs are disabled, run 'pcs acl enable' to enable") print() data = lib.acl.get_config() _print_list_of_objects(data.get("target_list", []), target_to_str) _print_list_of_objects(data.get("group_list", []), group_to_str) _print_list_of_objects(data.get("role_list", []), role_to_str) def acl_enable(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if argv: raise CmdLineInputError() lib.cluster_property.set_properties({"enable-acl": "true"}) def acl_disable(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if argv: raise CmdLineInputError() lib.cluster_property.set_properties({"enable-acl": "false"}) def user_create(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() user_name, role_list = argv[0], argv[1:] lib.acl.create_target(user_name, role_list) def user_delete(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) != 1: raise CmdLineInputError() lib.acl.remove_target(argv[0]) def group_create(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() group_name, role_list = argv[0], argv[1:] lib.acl.create_group(group_name, role_list) def group_delete(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) != 1: raise CmdLineInputError() lib.acl.remove_group(argv[0]) def argv_to_permission_info_list(argv): """ Commandline options: no options """ if len(argv) % 3 != 0: raise CmdLineInputError() # wrapping by list, # because in python3 zip() returns an iterator instead of a list # and the loop below makes iteration over it permission_info_list = list( zip( [permission.lower() for permission in argv[::3]], [scope_type.lower() for scope_type in argv[1::3]], argv[2::3], ) ) for permission, scope_type, dummy_scope in permission_info_list: if permission not in ["read", "write", "deny"] or scope_type not in [ "xpath", "id", ]: raise CmdLineInputError() return permission_info_list def role_create(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() role_id = argv.pop(0) description = "" desc_key = "description=" if argv and argv[0].startswith(desc_key) and len(argv[0]) > len(desc_key): description = argv.pop(0)[len(desc_key) :] permission_info_list = argv_to_permission_info_list(argv) lib.acl.create_role(role_id, permission_info_list, description) def role_delete(lib, argv, modifiers): """ Options: * -f - CIB file * --autodelete - autodelete empty targets, groups """ modifiers.ensure_only_supported("-f", "--autodelete") if len(argv) != 1: raise CmdLineInputError() lib.acl.remove_role( argv[0], autodelete_users_groups=modifiers.get("--autodelete") ) def _role_assign_unassign(argv, keyword, not_specific_fn, user_fn, group_fn): """ Commandline options: no options """ # DEPRECATED ambiguous syntax in the first 0.12 version # - pcs role assign [to] [user|group] # - pcs role unassign [from] [user|group] # The problem is, that 'user|group' is optional, therefore pcs guesses # which one it is. argv_len = len(argv) if argv_len < 2: raise CmdLineInputError() not_specific_fn_deprecated = ( "Assigning / unassigning a role to a user / group without specifying " "'user' or 'group' keyword is deprecated and might be removed in a " "future release." ) if argv_len == 2: deprecation_warning(not_specific_fn_deprecated) not_specific_fn(*argv) elif argv_len == 3: role_id, something, ug_id = argv if something == keyword: deprecation_warning(not_specific_fn_deprecated) not_specific_fn(role_id, ug_id) elif something == "user": user_fn(role_id, ug_id) elif something == "group": group_fn(role_id, ug_id) else: raise CmdLineInputError() elif argv_len == 4 and argv[1] == keyword and argv[2] in ["group", "user"]: role_id, _, user_group, ug_id = argv if user_group == "user": user_fn(role_id, ug_id) else: group_fn(role_id, ug_id) else: raise CmdLineInputError() def role_assign(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") _role_assign_unassign( argv, "to", # DEPRECATED ambiguous syntax in the first 0.12 version # Use assign_role_to_target or assign_role_to_group instead. lib.acl.assign_role_not_specific, lib.acl.assign_role_to_target, lib.acl.assign_role_to_group, ) def role_unassign(lib, argv, modifiers): """ Options: * -f - CIB file * --autodelete - autodelete empty targets, groups """ modifiers.ensure_only_supported("-f", "--autodelete") _role_assign_unassign( argv, "from", # DEPRECATED ambiguous syntax in the first 0.12 version # Use unassign_role_from_target or unassign_role_from_group instead. lambda role_id, ug_id: lib.acl.unassign_role_not_specific( role_id, ug_id, modifiers.get("--autodelete") ), lambda role_id, ug_id: lib.acl.unassign_role_from_target( role_id, ug_id, modifiers.get("--autodelete") ), lambda role_id, ug_id: lib.acl.unassign_role_from_group( role_id, ug_id, modifiers.get("--autodelete") ), ) def permission_add(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) < 4: raise CmdLineInputError() role_id, argv_next = argv[0], argv[1:] lib.acl.add_permission(role_id, argv_to_permission_info_list(argv_next)) def run_permission_delete(lib, argv, modifiers): """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) != 1: raise CmdLineInputError() lib.acl.remove_permission(argv[0]) def _target_group_to_str(type_name, obj): return [f"{type_name.title()}: {obj.get('id')}"] + indent( [" ".join(["Roles:"] + obj.get("role_list", []))] ) def target_to_str(target): return _target_group_to_str("user", target) def group_to_str(group): return _target_group_to_str("group", group) def role_to_str(role): out = [] if role.get("description"): out.append(f"Description: {role.get('description')}") out += map(_permission_to_str, role.get("permission_list", [])) return [f"Role: {role.get('id')}"] + indent(out) def _permission_to_str(permission): out = ["Permission:", permission.get("kind")] if permission.get("xpath") is not None: out += ["xpath", permission.get("xpath")] elif permission.get("reference") is not None: out += ["id", permission.get("reference")] out.append(f"({permission.get('id')})") return " ".join(out) pcs-0.12.0.2/pcs/alert.py000066400000000000000000000151341500417470700147760ustar00rootroot00000000000000import json from typing import Any from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, KeyValueParser, group_by_keywords, ) from pcs.common.str_tools import indent def alert_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file (in lib wrapper) """ modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() sections = group_by_keywords( argv, set(["options", "meta"]), implicit_first_keyword="main" ) parser = KeyValueParser(sections.get_args_flat("main")) parser.check_allowed_keys(["id", "description", "path"]) main_args = parser.get_unique() lib.alert.create_alert( main_args.get("id", None), main_args.get("path", None), KeyValueParser(sections.get_args_flat("options")).get_unique(), KeyValueParser(sections.get_args_flat("meta")).get_unique(), main_args.get("description", None), ) def alert_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file (in lib wrapper) """ modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() alert_id = argv[0] sections = group_by_keywords( argv[1:], set(["options", "meta"]), implicit_first_keyword="main" ) parser = KeyValueParser(sections.get_args_flat("main")) parser.check_allowed_keys(["description", "path"]) main_args = parser.get_unique() lib.alert.update_alert( alert_id, main_args.get("path", None), KeyValueParser(sections.get_args_flat("options")).get_unique(), KeyValueParser(sections.get_args_flat("meta")).get_unique(), main_args.get("description", None), ) def alert_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file (in lib wrapper) """ modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() lib.alert.remove_alert(argv) def recipient_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file (in lib wrapper) * --force - allows not unique recipient values """ modifiers.ensure_only_supported("-f", "--force") if len(argv) < 2: raise CmdLineInputError() alert_id = argv[0] sections = group_by_keywords( argv[1:], set(["options", "meta"]), implicit_first_keyword="main" ) parser = KeyValueParser(sections.get_args_flat("main")) parser.check_allowed_keys(["description", "id", "value"]) main_args = parser.get_unique() lib.alert.add_recipient( alert_id, main_args.get("value", None), KeyValueParser(sections.get_args_flat("options")).get_unique(), KeyValueParser(sections.get_args_flat("meta")).get_unique(), recipient_id=main_args.get("id", None), description=main_args.get("description", None), allow_same_value=modifiers.get("--force"), ) def recipient_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file (in lib wrapper) * --force - allows not unique recipient values """ modifiers.ensure_only_supported("-f", "--force") if not argv: raise CmdLineInputError() recipient_id = argv[0] sections = group_by_keywords( argv[1:], set(["options", "meta"]), implicit_first_keyword="main" ) parser = KeyValueParser(sections.get_args_flat("main")) parser.check_allowed_keys(["description", "value"]) main_args = parser.get_unique() lib.alert.update_recipient( recipient_id, KeyValueParser(sections.get_args_flat("options")).get_unique(), KeyValueParser(sections.get_args_flat("meta")).get_unique(), recipient_value=main_args.get("value", None), description=main_args.get("description", None), allow_same_value=modifiers.get("--force"), ) def recipient_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file (in lib wrapper) """ modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() lib.alert.remove_recipient(argv) def _nvset_to_str(nvset_obj): # TODO duplicite to pcs.resource._nvpairs_strings key_val = { nvpair_obj["name"]: nvpair_obj["value"] for nvpair_obj in nvset_obj } output = [] for name, value in sorted(key_val.items()): if " " in value: value = f'"{value}"' output.append(f"{name}={value}") return " ".join(output) def __description_attributes_to_str(obj): output = [] if obj.get("description"): output.append(f"Description: {obj['description']}") if obj.get("instance_attributes"): attributes = _nvset_to_str(obj["instance_attributes"]) output.append(f"Options: {attributes}") if obj.get("meta_attributes"): attributes = _nvset_to_str(obj["meta_attributes"]) output.append(f"Meta options: {attributes}") return output def _alert_to_str(alert): content = [] content.extend(__description_attributes_to_str(alert)) recipients = [] for recipient in alert.get("recipient_list", []): recipients.extend(_recipient_to_str(recipient)) if recipients: content.append("Recipients:") content.extend(indent(recipients, 1)) return [f"Alert: {alert['id']} (path={alert['path']})"] + indent(content, 1) def _recipient_to_str(recipient): return [ f"Recipient: {recipient['id']} (value={recipient['value']})" ] + indent(__description_attributes_to_str(recipient), 1) def print_alert_config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file (in lib wrapper) """ modifiers.ensure_only_supported("-f") if argv: raise CmdLineInputError() lines = alert_config_lines(lib) if lines: print("\n".join(lines)) def alert_config_lines(lib: Any) -> list[str]: lines = [] alert_list = lib.alert.get_all_alerts() if alert_list: lines.append("Alerts:") for alert in alert_list: lines.extend(indent(_alert_to_str(alert), 1)) return lines def print_alerts_in_json( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: """ This is used only by pcsd, will be removed in new architecture Options: * -f - CIB file (in lib wrapper) """ modifiers.ensure_only_supported("-f") if argv: raise CmdLineInputError() print(json.dumps(lib.alert.get_all_alerts())) pcs-0.12.0.2/pcs/app.py000066400000000000000000000212031500417470700144410ustar00rootroot00000000000000import getopt import logging import os import sys from pcs import ( settings, usage, utils, ) from pcs.cli.common import ( completion, errors, parse_args, routing, ) from pcs.cli.reports import process_library_reports from pcs.cli.reports.output import ( deprecation_warning, error, print_to_stderr, ) from pcs.cli.routing import ( acl, alert, booth, client, cluster, config, constraint, dr, host, node, pcsd, prop, qdevice, quorum, resource, status, stonith, tag, ) from pcs.common import capabilities from pcs.lib.errors import LibraryError def _non_root_run(argv_cmd): """ This function will run commands which has to be run as root for users which are not root. If it required to run such command as root it will do that by sending it to the local pcsd and then it will exit. """ options = [] for option, value in utils.pcs_options.items(): if parse_args.is_option_expecting_value(option): options.extend([option, value]) else: options.append(option) # specific commands need to be run under root account, pass them to pcsd # don't forget to allow each command in pcsd.rb in "post /run_pcs do" root_command_list = [ ["cluster", "auth", "..."], ["cluster", "corosync", "..."], ["cluster", "destroy", "..."], ["cluster", "disable", "..."], ["cluster", "enable", "..."], ["cluster", "node", "..."], ["cluster", "start", "..."], ["cluster", "stop", "..."], ["cluster", "sync", "..."], # ['config', 'restore', '...'], # handled in config.config_restore ["host", "auth", "..."], ["host", "deauth", "..."], ["pcsd", "deauth", "..."], ["pcsd", "status", "..."], ["pcsd", "sync-certificates"], ["quorum", "device", "status", "..."], ["quorum", "status", "..."], ["status"], ["status", "corosync", "..."], ["status", "pcsd", "..."], ["status", "quorum", "..."], ["status", "status", "..."], ] for root_cmd in root_command_list: if (argv_cmd == root_cmd) or ( root_cmd[-1] == "..." and argv_cmd[: len(root_cmd) - 1] == root_cmd[:-1] ): # handle interactivity of 'pcs cluster auth' if argv_cmd[0:2] in [["cluster", "auth"], ["host", "auth"]]: if "-u" not in utils.pcs_options: username = utils.get_terminal_input("Username: ") options.extend(["-u", username]) if "-p" not in utils.pcs_options: password = utils.get_terminal_password() options.extend(["-p", password]) # call the local pcsd err_msgs, exitcode, std_out, std_err = utils.call_local_pcsd( argv_cmd, options ) if err_msgs: for msg in err_msgs: utils.err(msg, False) sys.exit(1) if std_out.strip(): print(std_out) if std_err.strip(): sys.stderr.write(std_err) sys.exit(exitcode) usefile = False filename = "" def main(argv=None): # pylint: disable=global-statement # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements if completion.has_applicable_environment(os.environ): print( completion.make_suggestions( os.environ, usage.generate_completion_tree_from_usage() ) ) sys.exit() argv = argv if argv else sys.argv[1:] utils.subprocess_setup() global filename, usefile utils.pcs_options = {} # we want to support optional arguments for --wait, so if an argument # is specified with --wait (ie. --wait=30) then we use them waitsecs = None new_argv = [] for arg in argv: if arg.startswith("--wait="): tempsecs = arg.replace("--wait=", "") if tempsecs: waitsecs = tempsecs arg = "--wait" new_argv.append(arg) argv = new_argv try: if "--" in argv: pcs_options, argv = getopt.gnu_getopt( argv, parse_args.PCS_SHORT_OPTIONS, parse_args.PCS_LONG_OPTIONS ) else: # DEPRECATED # TODO remove # We want to support only the -- version ( args_without_negative_nums, args_filtered_out, ) = parse_args.filter_out_non_option_negative_numbers(argv) if args_filtered_out: options_str = "', '".join(args_filtered_out) deprecation_warning( f"Using '{options_str}' without '--' is deprecated, those " "parameters will be considered position independent " "options in future pcs versions" ) pcs_options, dummy_argv = getopt.gnu_getopt( args_without_negative_nums, parse_args.PCS_SHORT_OPTIONS, parse_args.PCS_LONG_OPTIONS, ) argv = parse_args.filter_out_options(argv) except getopt.GetoptError as err: error(str(err)) print_to_stderr(usage.main()) sys.exit(1) full = False for option, dummy_value in pcs_options: if option == "--full": full = True break for opt, val in pcs_options: if not opt in utils.pcs_options: utils.pcs_options[opt] = val else: # If any options are a list then they've been entered twice which # isn't valid utils.err(f"{opt} can only be used once") if opt in ("-h", "--help"): if not argv: print(usage.main()) sys.exit() else: argv = [argv[0], "help"] + argv[1:] elif opt == "-f": usefile = True filename = val utils.usefile = usefile utils.filename = filename elif opt == "--corosync_conf": settings.corosync_conf_file = val elif opt == "--version": try: print(settings.pcs_version) if full: print( capabilities.capabilities_to_codes_str( capabilities.get_pcs_capabilities() ) ) sys.exit() except capabilities.CapabilitiesError as e: raise error(e.msg) from e elif opt == "--fullhelp": usage.full_usage() sys.exit() elif opt == "--wait": utils.pcs_options[opt] = waitsecs elif opt == "--request-timeout": request_timeout_valid = False try: timeout = int(val) if timeout > 0: utils.pcs_options[opt] = timeout request_timeout_valid = True except ValueError: pass if not request_timeout_valid: utils.err( f"'{val}' is not a valid --request-timeout value, use " "a positive integer" ) # initialize logger logging.getLogger("pcs") if (os.getuid() != 0) and (argv and argv[0] != "help") and not usefile: _non_root_run(argv) cmd_map = { "resource": resource.resource_cmd, "cluster": cluster.cluster_cmd, "stonith": stonith.stonith_cmd, "property": prop.property_cmd, "constraint": constraint.constraint_cmd, "acl": acl.acl_cmd, "status": status.status_cmd, "config": config.config_cmd, "pcsd": pcsd.pcsd_cmd, "node": node.node_cmd, "quorum": quorum.quorum_cmd, "qdevice": qdevice.qdevice_cmd, "alert": alert.alert_cmd, "booth": booth.booth_cmd, "host": host.host_cmd, "client": client.client_cmd, "dr": dr.dr_cmd, "tag": tag.tag_cmd, "help": lambda lib, argv, modifiers: print(usage.main()), } try: routing.create_router(cmd_map, [])( utils.get_library_wrapper(), argv, utils.get_input_modifiers() ) except LibraryError as e: if e.output: sys.stderr.write(e.output) sys.exit(1) process_library_reports(e.args) except errors.CmdLineInputError: if argv and argv[0] in cmd_map: usage.show(argv[0], []) else: print_to_stderr(usage.main()) sys.exit(1) pcs-0.12.0.2/pcs/bash_completion/000077500000000000000000000000001500417470700164575ustar00rootroot00000000000000pcs-0.12.0.2/pcs/bash_completion/pcs000066400000000000000000000020051500417470700171640ustar00rootroot00000000000000# bash completion for pcs _pcs_completion(){ LENGTHS=() for WORD in "${COMP_WORDS[@]}"; do LENGTHS+=(${#WORD}) done COMPREPLY=( $( \ env COMP_WORDS="${COMP_WORDS[*]}" \ COMP_LENGTHS="${LENGTHS[*]}" \ COMP_CWORD=$COMP_CWORD \ PCS_AUTO_COMPLETE=1 pcs \ ) ) #examples what we get: #pcs #COMP_WORDS: pcs COMP_LENGTHS: 3 #pcs co #COMP_WORDS: pcs co COMP_LENGTHS: 3 2 # pcs config #COMP_WORDS: pcs config COMP_LENGTHS: 3 6 # pcs config " #COMP_WORDS: pcs config " COMP_LENGTHS: 3 6 4 # pcs config "'\\n #COMP_WORDS: pcs config "'\\n COMP_LENGTHS: 3 6 5'" } # -o default # Use readline's default filename completion if the compspec generates no # matches. # -F function # The shell function function is executed in the current shell environment. # When it finishes, the possible completions are retrieved from the value of # the COMPREPLY array variable. complete -o default -F _pcs_completion pcs pcs-0.12.0.2/pcs/cli/000077500000000000000000000000001500417470700140605ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/__init__.py000066400000000000000000000000001500417470700161570ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/booth/000077500000000000000000000000001500417470700151735ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/booth/__init__.py000066400000000000000000000000001500417470700172720ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/booth/command.py000066400000000000000000000251401500417470700171650ustar00rootroot00000000000000from typing import ( Any, Optional, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, KeyValueParser, group_by_keywords, ) from pcs.common.reports import codes as report_codes def config_setup(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ create booth config Options: * --force - overwrite existing * --booth-conf - booth config file * --booth-key - booth authkey file * --name - name of a booth instance """ modifiers.ensure_only_supported( "--force", "--booth-conf", "--booth-key", "--name", ) peers = group_by_keywords(arg_list, set(["sites", "arbitrators"])) peers.ensure_unique_keywords() if not peers.has_keyword("sites") or not peers.get_args_flat("sites"): raise CmdLineInputError() lib.booth.config_setup( peers.get_args_flat("sites"), peers.get_args_flat("arbitrators"), instance_name=modifiers.get("--name"), overwrite_existing=modifiers.get("--force"), ) def config_destroy(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ destroy booth config Options: --force - ignore config load issues --name - name of a booth instance """ modifiers.ensure_only_supported("--force", "--name") if arg_list: raise CmdLineInputError() lib.booth.config_destroy( instance_name=modifiers.get("--name"), ignore_config_load_problems=modifiers.get("--force"), ) def config_show(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ print booth config Options: * --name - name of a booth instance * --request-timeout - HTTP timeout for getting config from remote host """ modifiers.ensure_only_supported("--name", "--request-timeout") if len(arg_list) > 1: raise CmdLineInputError() node = None if not arg_list else arg_list[0] print( lib.booth.config_text( instance_name=modifiers.get("--name"), node_name=node ) .decode("utf-8") .rstrip() ) def config_ticket_add( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ add ticket to current configuration Options: * --force * --booth-conf - booth config file * --booth-key - booth auth key file * --name - name of a booth instance """ modifiers.ensure_only_supported( "--force", "--booth-conf", "--name", "--booth-key" ) if not arg_list: raise CmdLineInputError lib.booth.config_ticket_add( arg_list[0], KeyValueParser(arg_list[1:]).get_unique(), instance_name=modifiers.get("--name"), allow_unknown_options=modifiers.get("--force"), ) def config_ticket_remove( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ add ticket to current configuration Options: * --booth-conf - booth config file * --booth-key - booth auth key file * --name - name of a booth instance """ modifiers.ensure_only_supported("--booth-conf", "--name", "--booth-key") if len(arg_list) != 1: raise CmdLineInputError lib.booth.config_ticket_remove( arg_list[0], instance_name=modifiers.get("--name"), ) def enable_authfile( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ Options: * --booth-conf - booth config file * --booth-key - booth auth key file * --name - name of a booth instance """ modifiers.ensure_only_supported("--booth-conf", "--name", "--booth-key") if len(arg_list): raise CmdLineInputError() lib.booth.config_set_enable_authfile(instance_name=modifiers.get("--name")) def enable_authfile_clean( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ Options: * --booth-conf - booth config file * --booth-key - booth auth key file * --name - name of a booth instance """ modifiers.ensure_only_supported("--booth-conf", "--name", "--booth-key") if len(arg_list): raise CmdLineInputError() lib.booth.config_unset_enable_authfile( instance_name=modifiers.get("--name") ) def _parse_ticket_operation(arg_list: Argv) -> tuple[str, Optional[str]]: site_ip = None if len(arg_list) == 2: site_ip = arg_list[1] elif len(arg_list) != 1: raise CmdLineInputError() ticket = arg_list[0] return ticket, site_ip def ticket_revoke(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of a booth instance """ modifiers.ensure_only_supported("--name") ticket, site_ip = _parse_ticket_operation(arg_list) lib.booth.ticket_revoke( ticket, site_ip=site_ip, instance_name=modifiers.get("--name") ) def ticket_grant(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of a booth instance """ modifiers.ensure_only_supported("--name") ticket, site_ip = _parse_ticket_operation(arg_list) lib.booth.ticket_grant( ticket, site_ip=site_ip, instance_name=modifiers.get("--name") ) def ticket_cleanup(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of a booth instance """ if not arg_list: modifiers.ensure_only_supported("--name") lib.booth.ticket_cleanup_auto(instance_name=modifiers.get("--name")) return if len(arg_list) != 1: raise CmdLineInputError() modifiers.ensure_only_supported() lib.booth.ticket_cleanup(arg_list[0]) def ticket_unstandby( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ Options: None """ modifiers.ensure_only_supported() if len(arg_list) != 1: raise CmdLineInputError() lib.booth.ticket_unstandby(arg_list[0]) def ticket_standby(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: None """ modifiers.ensure_only_supported() if len(arg_list) != 1: raise CmdLineInputError() lib.booth.ticket_standby(arg_list[0]) def create_in_cluster( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ Options: * --force - allows to create booth resource even if its agent is not installed * -f - CIB file * --name - name of a booth instance """ modifiers.ensure_only_supported("--force", "-f", "--name") if len(arg_list) != 2 or arg_list[0] != "ip": raise CmdLineInputError() lib.booth.create_in_cluster( arg_list[1], instance_name=modifiers.get("--name"), allow_absent_resource_agent=modifiers.get("--force"), ) def remove_from_cluster( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ Options: * --force - allow remove of multiple * -f - CIB file * --name - name of a booth instance """ modifiers.ensure_only_supported("--force", "-f", "--name") if arg_list: raise CmdLineInputError() force_flags = [] if modifiers.get("--force"): force_flags.append(report_codes.FORCE) lib.booth.remove_from_cluster( instance_name=modifiers.get("--name"), force_flags=force_flags ) def restart(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - allow multiple * --name - name of a booth instance """ modifiers.ensure_only_supported("--force", "--name") if arg_list: raise CmdLineInputError() lib.booth.restart( instance_name=modifiers.get("--name"), allow_multiple=modifiers.get("--force"), ) def sync(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --skip-offline - skip offline nodes * --name - name of a booth instance * --booth-conf - booth config file * --booth-key - booth authkey file * --request-timeout - HTTP timeout for file distribution """ modifiers.ensure_only_supported( "--skip-offline", "--name", "--booth-conf", "--booth-key", "--request-timeout", ) if arg_list: raise CmdLineInputError() lib.booth.config_sync( instance_name=modifiers.get("--name"), skip_offline_nodes=modifiers.get("--skip-offline"), ) def enable(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of a booth instance """ modifiers.ensure_only_supported("--name") if arg_list: raise CmdLineInputError() lib.booth.enable_booth(instance_name=modifiers.get("--name")) def disable(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of a booth instance """ modifiers.ensure_only_supported("--name") if arg_list: raise CmdLineInputError() lib.booth.disable_booth(instance_name=modifiers.get("--name")) def start(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of a booth instance """ modifiers.ensure_only_supported("--name") if arg_list: raise CmdLineInputError() lib.booth.start_booth(instance_name=modifiers.get("--name")) def stop(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of a booth instance """ modifiers.ensure_only_supported("--name") if arg_list: raise CmdLineInputError() lib.booth.stop_booth(instance_name=modifiers.get("--name")) def pull(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of a booth instance * --request-timeout - HTTP timeout for file distribution """ modifiers.ensure_only_supported("--name", "--request-timeout") if len(arg_list) != 1: raise CmdLineInputError() lib.booth.pull_config( arg_list[0], instance_name=modifiers.get("--name"), ) def status(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --name - name of booth instance """ modifiers.ensure_only_supported("--name") if arg_list: raise CmdLineInputError() booth_status = lib.booth.get_status(instance_name=modifiers.get("--name")) if booth_status.get("ticket"): print("TICKETS:") print(booth_status["ticket"]) if booth_status.get("peers"): print("PEERS:") print(booth_status["peers"]) if booth_status.get("status"): print("DAEMON STATUS:") print(booth_status["status"]) pcs-0.12.0.2/pcs/cli/booth/env.py000066400000000000000000000065601500417470700163440ustar00rootroot00000000000000from pcs.cli.file import metadata from pcs.cli.reports import output from pcs.common import file as pcs_file from pcs.common import ( file_type_codes, reports, ) from pcs.common.reports.item import ReportItem from pcs.lib.errors import LibraryError def middleware_config(config_path, key_path): if config_path and not key_path: raise output.error( "When --booth-conf is specified, " "--booth-key must be specified as well" ) if key_path and not config_path: raise output.error( "When --booth-key is specified, " "--booth-conf must be specified as well" ) is_mocked_environment = config_path and key_path config_file = None key_file = None if is_mocked_environment: config_file = pcs_file.RawFile( metadata.for_file_type(file_type_codes.BOOTH_CONFIG, config_path) ) key_file = pcs_file.RawFile( metadata.for_file_type(file_type_codes.BOOTH_KEY, key_path) ) def create_booth_env(): try: config_data = ( config_file.read() if config_file and config_file.exists() else None ) key_data = ( key_file.read() if key_file and key_file.exists() else None ) # TODO write custom error handling, do not use pcs.lib specific code # and LibraryError except pcs_file.RawFileError as e: raise LibraryError( ReportItem.error( reports.messages.FileIoError( e.metadata.file_type_code, e.action, e.reason, file_path=e.metadata.path, ) ) ) from e return { "config_data": config_data, "key_data": key_data, "key_path": key_path, } def flush(modified_env): if not is_mocked_environment: return if not modified_env: # TODO now this would not happen # for more information see comment in # pcs.cli.common.lib_wrapper.lib_env_to_cli_env raise output.error("Error during library communication") try: if modified_env["key_file"]["content"] is not None: key_file.write( modified_env["key_file"]["content"], can_overwrite=True ) config_file.write( modified_env["config_file"]["content"], can_overwrite=True ) # TODO write custom error handling, do not use pcs.lib specific code # and LibraryError except pcs_file.RawFileError as e: raise LibraryError( ReportItem.error( reports.messages.FileIoError( e.metadata.file_type_code, e.action, e.reason, file_path=e.metadata.path, ) ) ) from e def apply(next_in_line, env, *args, **kwargs): env.booth = create_booth_env() if is_mocked_environment else {} result_of_next = next_in_line(env, *args, **kwargs) if is_mocked_environment: flush(env.booth["modified_env"]) return result_of_next return apply pcs-0.12.0.2/pcs/cli/cluster/000077500000000000000000000000001500417470700155415ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/cluster/__init__.py000066400000000000000000000000001500417470700176400ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/cluster/command.py000066400000000000000000000126421500417470700175360ustar00rootroot00000000000000from typing import ( Any, Optional, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, KeyValueParser, ) from pcs.cli.resource.parse_args import ( parse_primitive as parse_primitive_resource, ) from pcs.common.reports import codes as report_codes def _node_add_remote_separate_name_and_addr( arg_list: Argv, ) -> tuple[str, Optional[str], list[str]]: """ Commandline options: no options """ node_name = arg_list[0] if len(arg_list) == 1: node_addr = None rest_args = [] elif "=" in arg_list[1] or arg_list[1] in ["op", "meta"]: node_addr = None rest_args = arg_list[1:] else: node_addr = arg_list[1] rest_args = arg_list[2:] return node_name, node_addr, rest_args def node_add_remote( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ Options: * --wait * --force - allow incomplete distribution of files, allow pcmk remote service to fail * --skip-offline - skip offline nodes * --request-timeout - HTTP request timeout * --no-default-ops - do not use default operations For tests: * --corosync_conf * -f """ modifiers.ensure_only_supported( "--wait", "--force", "--skip-offline", "--request-timeout", "--corosync_conf", "-f", "--no-default-ops", ) if not arg_list: raise CmdLineInputError() node_name, node_addr, rest_args = _node_add_remote_separate_name_and_addr( arg_list ) parts = parse_primitive_resource(rest_args) force = modifiers.get("--force") lib.remote_node.node_add_remote( node_name, node_addr, parts.operations, parts.meta_attrs, parts.instance_attrs, skip_offline_nodes=modifiers.get("--skip-offline"), allow_incomplete_distribution=force, allow_pacemaker_remote_service_fail=force, allow_invalid_operation=force, allow_invalid_instance_attributes=force, use_default_operations=not modifiers.get("--no-default-ops"), wait=modifiers.get("--wait"), ) def node_remove_remote( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ Options: * --force - allow multiple nodes removal, allow pcmk remote service to fail, don't stop a resource before its deletion (this is side effect of old resource delete command used here) * --skip-offline - skip offline nodes * --request-timeout - HTTP request timeout For tests: * --corosync_conf * -f """ modifiers.ensure_only_supported( "--force", "--skip-offline", "--request-timeout", "--corosync_conf", "-f", ) if len(arg_list) != 1: raise CmdLineInputError() force_flags = [] if modifiers.get("--force"): force_flags.append(report_codes.FORCE) if modifiers.get("--skip-offline"): force_flags.append(report_codes.SKIP_OFFLINE_NODES) lib.remote_node.node_remove_remote(arg_list[0], force_flags) def node_add_guest(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --wait * --force - allow incomplete distribution of files, allow pcmk remote service to fail * --skip-offline - skip offline nodes * --request-timeout - HTTP request timeout For tests: * --corosync_conf * -f """ modifiers.ensure_only_supported( "--wait", "--force", "--skip-offline", "--request-timeout", "--corosync_conf", "-f", ) if len(arg_list) < 2: raise CmdLineInputError() node_name = arg_list[0] resource_id = arg_list[1] meta_options = KeyValueParser(arg_list[2:]).get_unique() lib.remote_node.node_add_guest( node_name, resource_id, meta_options, skip_offline_nodes=modifiers.get("--skip-offline"), allow_incomplete_distribution=modifiers.get("--force"), allow_pacemaker_remote_service_fail=modifiers.get("--force"), wait=modifiers.get("--wait"), ) def node_remove_guest( lib: Any, arg_list: Argv, modifiers: InputModifiers ) -> None: """ Options: * --wait * --force - allow multiple nodes removal, allow pcmk remote service to fail * --skip-offline - skip offline nodes * --request-timeout - HTTP request timeout For tests: * --corosync_conf * -f """ modifiers.ensure_only_supported( "--wait", "--force", "--skip-offline", "--request-timeout", "--corosync_conf", "-f", ) if len(arg_list) != 1: raise CmdLineInputError() lib.remote_node.node_remove_guest( arg_list[0], skip_offline_nodes=modifiers.get("--skip-offline"), allow_remove_multiple_nodes=modifiers.get("--force"), allow_pacemaker_remote_service_fail=modifiers.get("--force"), wait=modifiers.get("--wait"), ) def node_clear(lib: Any, arg_list: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - allow to clear a cluster node """ modifiers.ensure_only_supported("--force") if len(arg_list) != 1: raise CmdLineInputError() lib.cluster.node_clear( arg_list[0], allow_clear_cluster_node=modifiers.get("--force") ) pcs-0.12.0.2/pcs/cli/cluster_property/000077500000000000000000000000001500417470700175055ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/cluster_property/__init__.py000066400000000000000000000000001500417470700216040ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/cluster_property/command.py000066400000000000000000000203731500417470700215020ustar00rootroot00000000000000import json from typing import Any from pcs.cli.cluster_property.output import ( PropertyConfigurationFacade, cluster_property_metadata_to_text, properties_defaults_to_text, properties_to_cmd, properties_to_text, properties_to_text_with_default_mark, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.output import smart_wrap_text from pcs.cli.common.parse_args import ( OUTPUT_FORMAT_OPTION, OUTPUT_FORMAT_VALUE_CMD, OUTPUT_FORMAT_VALUE_JSON, OUTPUT_FORMAT_VALUE_TEXT, Argv, InputModifiers, KeyValueParser, ensure_unique_args, ) from pcs.cli.reports.output import deprecation_warning from pcs.common import reports from pcs.common.interface import dto from pcs.common.pacemaker.cluster_property import ClusterPropertyMetadataDto from pcs.common.pacemaker.nvset import ListCibNvsetDto from pcs.common.str_tools import ( format_list, format_plural, ) def set_property(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - allow unknown options * -f - CIB file """ modifiers.ensure_only_supported("--force", "-f") if not argv: raise CmdLineInputError() force_flags = set() if modifiers.get("--force"): force_flags.add(reports.codes.FORCE) cluster_options = KeyValueParser(argv).get_unique() lib.cluster_property.set_properties(cluster_options, force_flags) def unset_property(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - no error when removing not existing properties * -f - CIB file """ modifiers.ensure_only_supported("--force", "-f") if not argv: raise CmdLineInputError() force_flags = set() if modifiers.get("--force"): force_flags.add(reports.codes.FORCE) else: ensure_unique_args(argv) lib.cluster_property.set_properties( {name: "" for name in argv}, force_flags ) def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --all - list configured properties with values and properties with default values if not in configuration * --defaults - list only default values of properties, only properties with a default value are listed * --output-format - supported formats: text, cmd, json """ modifiers.ensure_only_supported( "-f", "--all", "--defaults", output_format_supported=True ) mutually_exclusive_options = ["--all", "--defaults", "--output-format"] if argv and modifiers.is_specified_any(mutually_exclusive_options): raise CmdLineInputError( "cannot specify properties when using {}".format( format_list(mutually_exclusive_options) ) ) modifiers.ensure_not_mutually_exclusive(*mutually_exclusive_options) output_format = modifiers.get_output_format() if ( argv or output_format == OUTPUT_FORMAT_VALUE_CMD or modifiers.get("--all") ): properties_facade = PropertyConfigurationFacade.from_properties_dtos( lib.cluster_property.get_properties(), lib.cluster_property.get_properties_metadata(), ) elif modifiers.get("--defaults"): # do not load set properties # --defaults should work without a cib file properties_facade = ( PropertyConfigurationFacade.from_properties_metadata( lib.cluster_property.get_properties_metadata() ) ) else: # json or default text # do not load properties metadata, only configured properties are needed properties_facade = PropertyConfigurationFacade.from_properties_config( lib.cluster_property.get_properties() ) if argv: output = "\n".join( properties_to_text_with_default_mark( properties_facade, property_names=argv ) ) elif modifiers.get("--all"): output = "\n".join( properties_to_text_with_default_mark(properties_facade) ) elif modifiers.get("--defaults"): deprecation_warning( "Option --defaults is deprecated and will be removed. " "Please use command 'pcs property defaults' instead." ) output = "\n".join( properties_defaults_to_text( properties_facade.get_defaults(include_advanced=True) ) ) elif output_format == OUTPUT_FORMAT_VALUE_CMD: output = " \\\n".join(properties_to_cmd(properties_facade)) elif output_format == OUTPUT_FORMAT_VALUE_JSON: output = json.dumps( dto.to_dict(ListCibNvsetDto(properties_facade.properties[0:1])) ) else: output = "\n".join(properties_to_text(properties_facade)) if output: print(output) def defaults(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --full - also list advanced cluster properties """ modifiers.ensure_only_supported("--full") if argv and modifiers.is_specified("--full"): raise CmdLineInputError("cannot specify properties when using '--full'") properties_facade = PropertyConfigurationFacade.from_properties_metadata( lib.cluster_property.get_properties_metadata() ) defaults_dict = properties_facade.get_defaults( argv, include_advanced=modifiers.is_specified("--full") ) extra_args = set(argv) - defaults_dict.keys() if extra_args: raise CmdLineInputError( "No default value for {property_pl}: {name_list}".format( property_pl=format_plural(extra_args, "property"), name_list=format_list(list(extra_args)), ) ) output = "\n".join(properties_defaults_to_text(defaults_dict)) if output: print(output) def describe(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --full - also list advanced cluster properties * --output-format - supported formats: text, json """ modifiers.ensure_only_supported("--full", output_format_supported=True) if argv and modifiers.is_specified("--full"): raise CmdLineInputError("cannot specify properties when using '--full'") output_format = modifiers.get_output_format( supported_formats={OUTPUT_FORMAT_VALUE_TEXT, OUTPUT_FORMAT_VALUE_JSON} ) if output_format == OUTPUT_FORMAT_VALUE_JSON and ( argv or modifiers.is_specified("--full") ): raise CmdLineInputError( "property filtering is not supported with " f"{OUTPUT_FORMAT_OPTION}={OUTPUT_FORMAT_VALUE_JSON}" ) properties_facade = PropertyConfigurationFacade.from_properties_metadata( lib.cluster_property.get_properties_metadata() ) if output_format == OUTPUT_FORMAT_VALUE_JSON: output = json.dumps( dto.to_dict( ClusterPropertyMetadataDto( properties_metadata=properties_facade.properties_metadata, readonly_properties=properties_facade.readonly_properties, ) ) ) else: filtered_metadata = properties_facade.get_properties_metadata( argv, include_advanced=modifiers.is_specified("--full") ) extra_args = set(argv) - { metadata.name for metadata in filtered_metadata } if extra_args: raise CmdLineInputError( "No description for {property_pl}: {name_list}".format( property_pl=format_plural(extra_args, "property"), name_list=format_list(list(extra_args)), ) ) output = "\n".join( smart_wrap_text( cluster_property_metadata_to_text( sorted(filtered_metadata, key=lambda x: x.name) ) ) ) if output: print(output) def print_cluster_properties_definition_legacy( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: """ Options: no options """ modifiers.ensure_only_supported() if argv: raise CmdLineInputError() print( json.dumps( lib.cluster_property.get_cluster_properties_definition_legacy() ) ) pcs-0.12.0.2/pcs/cli/cluster_property/output.py000066400000000000000000000173611500417470700214270ustar00rootroot00000000000000from shlex import quote from typing import ( Optional, Sequence, ) from pcs.cli.nvset import nvset_dto_to_lines from pcs.cli.resource.output import resource_agent_parameter_metadata_to_text from pcs.common.pacemaker.cluster_property import ClusterPropertyMetadataDto from pcs.common.pacemaker.nvset import ( CibNvsetDto, ListCibNvsetDto, ) from pcs.common.resource_agent.dto import ResourceAgentParameterDto from pcs.common.str_tools import ( format_name_value_default_list, format_name_value_list, indent, ) from pcs.common.types import ( StringCollection, StringSequence, ) class PropertyConfigurationFacade: def __init__( self, properties: Sequence[CibNvsetDto], properties_metadata: Sequence[ResourceAgentParameterDto], readonly_properties: StringCollection, ) -> None: self._properties = properties self._first_nvpair_set = ( self._properties[0].nvpairs if self._properties else [] ) self._properties_metadata = properties_metadata self._readonly_properties = readonly_properties self._defaults_map = self.get_defaults(include_advanced=True) self._name_nvpair_dto_map = { nvpair_dto.name: nvpair_dto for nvpair_dto in self._first_nvpair_set } @classmethod def from_properties_dtos( cls, properties_dto: ListCibNvsetDto, properties_metadata_dto: ClusterPropertyMetadataDto, ) -> "PropertyConfigurationFacade": return cls( properties_dto.nvsets, properties_metadata_dto.properties_metadata, properties_metadata_dto.readonly_properties, ) @classmethod def from_properties_config( cls, properties_dto: ListCibNvsetDto ) -> "PropertyConfigurationFacade": return cls( properties_dto.nvsets, [], [], ) @classmethod def from_properties_metadata( cls, properties_metadata_dto: ClusterPropertyMetadataDto ) -> "PropertyConfigurationFacade": return cls( [], properties_metadata_dto.properties_metadata, properties_metadata_dto.readonly_properties, ) @property def properties(self) -> Sequence[CibNvsetDto]: return self._properties @property def properties_metadata(self) -> Sequence[ResourceAgentParameterDto]: return self._properties_metadata @property def readonly_properties(self) -> StringCollection: return self._readonly_properties def get_property_value( self, property_name: str, custom_default: Optional[str] = None ) -> Optional[str]: nvpair = self._name_nvpair_dto_map.get(property_name) return nvpair.value if nvpair else custom_default def get_property_value_or_default( self, property_name: str, custom_default: Optional[str] = None ) -> Optional[str]: value = self.get_property_value(property_name) if value is not None: return value return self._defaults_map.get(property_name, custom_default) def get_defaults( self, property_names: Optional[StringSequence] = None, include_advanced: bool = False, ) -> dict[str, str]: return { metadata.name: metadata.default for metadata in self.get_properties_metadata( property_names, include_advanced ) if metadata.default is not None } def get_properties_metadata( self, property_names: Optional[StringSequence] = None, include_advanced: bool = False, ) -> Sequence[ResourceAgentParameterDto]: if property_names: filtered_metadata = [ metadata for metadata in self._properties_metadata if metadata.name in property_names ] else: filtered_metadata = [ metadata for metadata in self._properties_metadata if include_advanced or not metadata.advanced ] deduplicated_metadata = { metadata.name: metadata for metadata in filtered_metadata } return list(deduplicated_metadata.values()) def get_name_value_default_list(self) -> list[tuple[str, str, bool]]: name_value_default_list = [ (nvpair_dto.name, nvpair_dto.value, False) for nvpair_dto in self._first_nvpair_set ] name_value_default_list.extend( [ (metadata_dto.name, metadata_dto.default, True) for metadata_dto in self.get_properties_metadata( include_advanced=True ) if metadata_dto.name not in self._name_nvpair_dto_map and metadata_dto.default is not None ] ) return name_value_default_list def properties_to_text( properties_facade: PropertyConfigurationFacade, ) -> list[str]: """ Return a text format of configured properties. properties_facade -- cluster property configuration and metadata """ if properties_facade.properties: return nvset_dto_to_lines( properties_facade.properties[0], nvset_label="Cluster Properties", ) return [] def properties_to_text_with_default_mark( properties_facade: PropertyConfigurationFacade, property_names: Optional[StringSequence] = None, ) -> list[str]: """ Return text format of configured properties or property default values. If default property value is missing then property is not displayed at all. If property_names is specified, then only properties from the list is displayed. properties_facade -- cluster property configuration and metadata property_names -- properties to be displayed """ lines: list[str] = [] id_part = ( f" {properties_facade.properties[0].id}" if properties_facade.properties else "" ) lines = [f"Cluster Properties:{id_part}"] tuple_list = [ item for item in properties_facade.get_name_value_default_list() if not property_names or item[0] in property_names ] lines.extend(indent(format_name_value_default_list(sorted(tuple_list)))) return lines def properties_to_cmd( properties_facade: PropertyConfigurationFacade, ) -> list[str]: """ Convert configured properties to the `pcs property set` command. properties_facade -- cluster property configuration and metadata """ if properties_facade.properties and properties_facade.properties[0].nvpairs: options = [ quote("=".join([nvpair.name, nvpair.value])) for nvpair in properties_facade.properties[0].nvpairs if nvpair.name not in properties_facade.readonly_properties ] if options: return ["pcs property set --force --"] + indent(options) return [] def properties_defaults_to_text(property_dict: dict[str, str]) -> list[str]: """ Convert property default values to lines of text. property_dict -- name to default value map """ return format_name_value_list(sorted(property_dict.items())) def cluster_property_metadata_to_text( metadata: Sequence[ResourceAgentParameterDto], ) -> list[str]: """ Convert cluster property metadata to lines of description text. Output example: property-name Description: Type: / Allowed values: Default: metadata - list of ResourceAgentParameterDto which is used for cluster property metadata """ text: list[str] = [] for parameter_dto in metadata: text.extend(resource_agent_parameter_metadata_to_text(parameter_dto)) return text pcs-0.12.0.2/pcs/cli/common/000077500000000000000000000000001500417470700153505ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/common/__init__.py000066400000000000000000000000001500417470700174470ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/common/completion.py000066400000000000000000000067711500417470700201060ustar00rootroot00000000000000from typing import Mapping from pcs.common.types import StringSequence SuggestionTree = Mapping[str, "SuggestionTree"] def has_applicable_environment(environment: Mapping[str, str]) -> bool: """ Check if environment variables for shell command completion are set environment -- very likely os.environ """ if not all( key in environment for key in [ "COMP_WORDS", "COMP_LENGTHS", "COMP_CWORD", "PCS_AUTO_COMPLETE", ] ): return False if environment["PCS_AUTO_COMPLETE"].strip() in ("0", ""): return False try: int(environment["COMP_CWORD"]) except ValueError: return False return True def make_suggestions( environment: Mapping[str, str], suggestion_tree: SuggestionTree ) -> str: """ Suggest possible shell command completions environment -- very likely os.environ suggestion_tree -- {'acl': {'role': {'create': ...}}}... """ if not has_applicable_environment(environment): raise EnvironmentError("Environment is not completion ready") try: typed_word_list = _split_words( environment["COMP_WORDS"], environment["COMP_LENGTHS"].split(" "), ) except EnvironmentError: return "" return "\n".join( _find_suggestions( suggestion_tree, typed_word_list, int(environment["COMP_CWORD"]) ) ) def _split_words(joined_words: str, word_lengths: StringSequence) -> list[str]: cursor_position = 0 words_string_len = len(joined_words) word_list = [] for length in word_lengths: try: next_position = cursor_position + int(length) except ValueError as e: raise EnvironmentError( f"Length of word '{length}' is not digit" ) from e if next_position > words_string_len: raise EnvironmentError( "Expected lengths are bigger than word lengths" ) if ( next_position != words_string_len and not joined_words[next_position].isspace() ): raise EnvironmentError("Words separator is not expected space") word_list.append(joined_words[cursor_position:next_position]) cursor_position = next_position + 1 if words_string_len > next_position: raise EnvironmentError("Expected lengths are smaller then word lengths") return word_list def _find_suggestions( suggestion_tree: SuggestionTree, typed_word_list: StringSequence, word_under_cursor_idx: int, ) -> list[str]: if not 1 <= word_under_cursor_idx <= len(typed_word_list): return [] if len(typed_word_list) == word_under_cursor_idx: # not started type the last word yet word_under_cursor = "" else: word_under_cursor = typed_word_list[word_under_cursor_idx] words_for_current_cursor_position = _get_subcommands( suggestion_tree, typed_word_list[1:word_under_cursor_idx] ) return [ word for word in words_for_current_cursor_position if word.startswith(word_under_cursor) ] def _get_subcommands( suggestion_tree: SuggestionTree, previous_subcommand_list: StringSequence ) -> list[str]: subcommand_tree = suggestion_tree for subcommand in previous_subcommand_list: if subcommand not in subcommand_tree: return [] subcommand_tree = subcommand_tree[subcommand] return sorted(list(subcommand_tree.keys())) pcs-0.12.0.2/pcs/cli/common/env_cli.py000066400000000000000000000006211500417470700173400ustar00rootroot00000000000000class Env: # pylint: disable=too-many-instance-attributes def __init__(self): self.cib_data = None self.user = None self.groups = None self.corosync_conf_data = None self.booth = None self.pacemaker = None self.known_hosts_getter = None self.debug = False self.request_timeout = None self.report_processor = None pcs-0.12.0.2/pcs/cli/common/errors.py000066400000000000000000000045441500417470700172450ustar00rootroot00000000000000from typing import Optional from pcs.common.str_tools import ( format_list_base, quote_items, ) from pcs.common.types import StringSequence ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE = ( "Cannot specify both --all and a list of nodes." ) SEE_MAN_CHANGES = "See 'man pcs' -> Changes in pcs-{}." class CmdLineInputError(Exception): """ an incorrect command has been entered in the command line """ def __init__( self, message: Optional[str] = None, hint: Optional[str] = None, show_both_usage_and_message: bool = False, ) -> None: """ message -- explains what was wrong with the entered command hint -- provides an additional hint how to proceed show_both_usage_and_message -- show both the message and usage The routine which handles this exception behaves according to whether the message was specified (prints this message to user) or not (prints appropriate part of documentation). If show_both_usage_and_message is True, documentation will be printed first and the message will be printed after that. Hint is printed every time as the last item. """ super().__init__(message) self.message = message self.hint = hint self.show_both_usage_and_message = show_both_usage_and_message def _msg_command_replaced( new_commands: StringSequence, pcs_version: str ) -> str: commands = format_list_base(quote_items(new_commands)) changes = SEE_MAN_CHANGES.format(pcs_version) return f"This command has been replaced with {commands}. {changes}" def _msg_command_removed(pcs_version: str) -> str: changes = SEE_MAN_CHANGES.format(pcs_version) return f"This command has been removed. {changes}" def command_replaced( new_commands: StringSequence, pcs_version: str ) -> CmdLineInputError: return CmdLineInputError( message=_msg_command_replaced(new_commands, pcs_version=pcs_version) ) def command_removed(pcs_version: str) -> CmdLineInputError: return CmdLineInputError( message=_msg_command_removed(pcs_version=pcs_version) ) def raise_command_replaced( new_commands: StringSequence, pcs_version: str ) -> None: raise command_replaced(new_commands, pcs_version) def raise_command_removed(pcs_version: str) -> None: raise command_removed(pcs_version) pcs-0.12.0.2/pcs/cli/common/lib_wrapper.py000066400000000000000000000511721500417470700202360ustar00rootroot00000000000000import logging from collections import namedtuple from pcs import settings from pcs.cli.common import middleware from pcs.lib.commands import ( acl, alert, booth, cib, cib_options, cluster, cluster_property, dr, fencing_topology, node, pcsd, qdevice, quorum, remote_node, resource, resource_agent, sbd, scsi, services, status, stonith, stonith_agent, tag, ) from pcs.lib.commands.constraint import colocation as constraint_colocation from pcs.lib.commands.constraint import common as constraint_common from pcs.lib.commands.constraint import location as constraint_location from pcs.lib.commands.constraint import order as constraint_order from pcs.lib.commands.constraint import ticket as constraint_ticket from pcs.lib.env import LibraryEnvironment def wrapper(dictionary): return namedtuple("wrapper", dictionary.keys())(**dictionary) def cli_env_to_lib_env(cli_env): return LibraryEnvironment( logging.getLogger("pcs"), cli_env.report_processor, cli_env.user, cli_env.groups, cli_env.cib_data, cli_env.corosync_conf_data, booth_files_data=cli_env.booth, known_hosts_getter=cli_env.known_hosts_getter, request_timeout=cli_env.request_timeout, ) def lib_env_to_cli_env(lib_env, cli_env): if not lib_env.is_cib_live: cli_env.cib_data = lib_env.final_mocked_cib_content if not lib_env.is_corosync_conf_live: cli_env.corosync_conf_data = lib_env.get_corosync_conf_data() # TODO # We expect that when there is booth set up in cli_env then there is booth # set up in lib_env as well. The code works like that now. Once we start # communicate over the network, we must do extra checks in here to make # sure what the status really is. # this applies generally, not only for booth # corosync_conf and cib suffers with this problem as well but in this cases # it is dangerously hidden: when inconsistency between cli and lib # environment occurs, original content is put to file (which is wrong) if cli_env.booth: cli_env.booth["modified_env"] = lib_env.get_booth_env(name="").export() return cli_env def bind(cli_env, run_with_middleware, run_library_command): def run(cli_env, *args, **kwargs): lib_env = cli_env_to_lib_env(cli_env) lib_call_result = run_library_command(lib_env, *args, **kwargs) # midlewares needs finish its work and they see only cli_env # so we need reflect some changes to cli_env lib_env_to_cli_env(lib_env, cli_env) return lib_call_result def decorated_run(*args, **kwargs): return run_with_middleware(run, cli_env, *args, **kwargs) return decorated_run def bind_all(env, run_with_middleware, dictionary): return wrapper( dict( (exposed_fn, bind(env, run_with_middleware, library_fn)) for exposed_fn, library_fn in dictionary.items() ) ) def load_module(env, middleware_factory, name): # pylint: disable=too-many-branches # pylint: disable=too-many-return-statements if name == "acl": return bind_all( env, middleware.build(middleware_factory.cib), { "create_role": acl.create_role, "remove_role": acl.remove_role, "assign_role_not_specific": acl.assign_role_not_specific, "assign_role_to_target": acl.assign_role_to_target, "assign_role_to_group": acl.assign_role_to_group, "unassign_role_not_specific": acl.unassign_role_not_specific, "unassign_role_from_target": acl.unassign_role_from_target, "unassign_role_from_group": acl.unassign_role_from_group, "create_target": acl.create_target, "create_group": acl.create_group, "remove_target": acl.remove_target, "remove_group": acl.remove_group, "add_permission": acl.add_permission, "remove_permission": acl.remove_permission, "get_config": acl.get_config, }, ) if name == "alert": return bind_all( env, middleware.build(middleware_factory.cib), { "create_alert": alert.create_alert, "update_alert": alert.update_alert, "remove_alert": alert.remove_alert, "add_recipient": alert.add_recipient, "update_recipient": alert.update_recipient, "remove_recipient": alert.remove_recipient, "get_all_alerts": alert.get_all_alerts, }, ) if name == "booth": bindings = { "config_destroy": booth.config_destroy, "config_setup": booth.config_setup, "config_sync": booth.config_sync, "config_text": booth.config_text, "config_ticket_add": booth.config_ticket_add, "config_ticket_remove": booth.config_ticket_remove, "create_in_cluster": booth.create_in_cluster, "disable_booth": booth.disable_booth, "enable_booth": booth.enable_booth, "get_status": booth.get_status, "pull_config": booth.pull_config, "remove_from_cluster": booth.remove_from_cluster, "restart": booth.restart, "start_booth": booth.start_booth, "stop_booth": booth.stop_booth, "ticket_cleanup_auto": booth.ticket_cleanup_auto, "ticket_cleanup": booth.ticket_cleanup, "ticket_grant": booth.ticket_grant, "ticket_revoke": booth.ticket_revoke, "ticket_standby": booth.ticket_standby, "ticket_unstandby": booth.ticket_unstandby, } if settings.booth_enable_authfile_set_enabled: bindings["config_set_enable_authfile"] = ( booth.config_set_enable_authfile ) if settings.booth_enable_authfile_unset_enabled: bindings["config_unset_enable_authfile"] = ( booth.config_unset_enable_authfile ) return bind_all( env, middleware.build( middleware_factory.booth_conf, middleware_factory.cib ), bindings, ) if name == "cib": return bind_all( env, middleware.build(middleware_factory.cib), { "remove_elements": cib.remove_elements, }, ) if name == "cluster": return bind_all( env, middleware.build(middleware_factory.cib), { "add_link": cluster.add_link, "add_nodes": cluster.add_nodes, "corosync_authkey_change": cluster.corosync_authkey_change, "config_update": cluster.config_update, "config_update_local": cluster.config_update_local, "get_corosync_conf_struct": cluster.get_corosync_conf_struct, "node_clear": cluster.node_clear, "remove_links": cluster.remove_links, "remove_nodes": cluster.remove_nodes, "remove_nodes_from_cib": cluster.remove_nodes_from_cib, "setup": cluster.setup, "setup_local": cluster.setup_local, "update_link": cluster.update_link, "verify": cluster.verify, "generate_cluster_uuid": cluster.generate_cluster_uuid, "generate_cluster_uuid_local": cluster.generate_cluster_uuid_local, "wait_for_pcmk_idle": cluster.wait_for_pcmk_idle, }, ) if name == "dr": return bind_all( env, middleware.build(middleware_factory.corosync_conf_existing), { "get_config": dr.get_config, "destroy": dr.destroy, "set_recovery_site": dr.set_recovery_site, "status_all_sites_plaintext": dr.status_all_sites_plaintext, }, ) if name == "remote_node": return bind_all( env, middleware.build( middleware_factory.cib, middleware_factory.corosync_conf_existing, ), { "node_add_remote": remote_node.node_add_remote, "node_add_guest": remote_node.node_add_guest, "node_remove_remote": remote_node.node_remove_remote, "node_remove_guest": remote_node.node_remove_guest, }, ) if name == "constraint_colocation": return bind_all( env, middleware.build(middleware_factory.cib), { "create_with_set": constraint_colocation.create_with_set, }, ) if name == "constraint_location": return bind_all( env, middleware.build(middleware_factory.cib), { "create_plain_with_rule": constraint_location.create_plain_with_rule, }, ) if name == "constraint_order": return bind_all( env, middleware.build(middleware_factory.cib), { "create_with_set": constraint_order.create_with_set, }, ) if name == "constraint_ticket": return bind_all( env, middleware.build(middleware_factory.cib), { "create_with_set": constraint_ticket.create_with_set, "create": constraint_ticket.create, "remove": constraint_ticket.remove, }, ) if name == "constraint": return bind_all( env, middleware.build(middleware_factory.cib), { "get_config": constraint_common.get_config, }, ) if name == "fencing_topology": return bind_all( env, middleware.build(middleware_factory.cib), { "add_level": fencing_topology.add_level, "get_config": fencing_topology.get_config, "get_config_dto": fencing_topology.get_config_dto, "remove_all_levels": fencing_topology.remove_all_levels, "remove_levels_by_params": ( fencing_topology.remove_levels_by_params ), "verify": fencing_topology.verify, }, ) if name == "node": return bind_all( env, middleware.build(middleware_factory.cib), { "maintenance_unmaintenance_all": ( node.maintenance_unmaintenance_all ), "maintenance_unmaintenance_list": ( node.maintenance_unmaintenance_list ), "maintenance_unmaintenance_local": ( node.maintenance_unmaintenance_local ), "standby_unstandby_all": node.standby_unstandby_all, "standby_unstandby_list": node.standby_unstandby_list, "standby_unstandby_local": node.standby_unstandby_local, }, ) if name == "pcsd": return bind_all( env, middleware.build(), {"synchronize_ssl_certificate": pcsd.synchronize_ssl_certificate}, ) if name == "qdevice": return bind_all( env, middleware.build(), { "qdevice_status_text": qdevice.qdevice_status_text, "qdevice_setup": qdevice.qdevice_setup, "qdevice_destroy": qdevice.qdevice_destroy, "qdevice_start": qdevice.qdevice_start, "qdevice_stop": qdevice.qdevice_stop, "qdevice_kill": qdevice.qdevice_kill, "qdevice_enable": qdevice.qdevice_enable, "qdevice_disable": qdevice.qdevice_disable, # following commands are internal use only, called from pcsd "client_net_setup": qdevice.client_net_setup, "client_net_import_certificate": ( qdevice.client_net_import_certificate ), "client_net_destroy": qdevice.client_net_destroy, "qdevice_net_sign_certificate_request": ( qdevice.qdevice_net_sign_certificate_request ), }, ) if name == "quorum": return bind_all( env, middleware.build(middleware_factory.corosync_conf_existing), { "add_device": quorum.add_device, "get_config": quorum.get_config, "remove_device": quorum.remove_device, "remove_device_heuristics": quorum.remove_device_heuristics, "set_expected_votes_live": quorum.set_expected_votes_live, "set_options": quorum.set_options, "status_text": quorum.status_text, "status_device_text": quorum.status_device_text, "update_device": quorum.update_device, # used by ha_cluster system role "device_net_certificate_check_local": quorum.device_net_certificate_check_local, "device_net_certificate_setup_local": quorum.device_net_certificate_setup_local, }, ) if name == "resource_agent": return bind_all( env, middleware.build(), { "describe_agent": resource_agent.describe_agent, "get_agent_default_operations": resource_agent.get_agent_default_operations, "get_agent_metadata": resource_agent.get_agent_metadata, "get_agents_list": resource_agent.get_agents_list, "get_structured_agent_name": resource_agent.get_structured_agent_name, "list_agents_for_standard_and_provider": ( resource_agent.list_agents_for_standard_and_provider ), "list_agents": resource_agent.list_agents, "list_ocf_providers": resource_agent.list_ocf_providers, "list_standards": resource_agent.list_standards, }, ) if name == "resource": return bind_all( env, middleware.build( middleware_factory.cib, middleware_factory.corosync_conf_existing, ), { "ban": resource.ban, "bundle_create": resource.bundle_create, "bundle_reset": resource.bundle_reset, "bundle_update": resource.bundle_update, "create": resource.create, "create_as_clone": resource.create_as_clone, "create_in_group": resource.create_in_group, "create_into_bundle": resource.create_into_bundle, "disable": resource.disable, "disable_safe": resource.disable_safe, "disable_simulate": resource.disable_simulate, "enable": resource.enable, "get_configured_resources": resource.get_configured_resources, "get_failcounts": resource.get_failcounts, "get_resource_relations_tree": ( resource.get_resource_relations_tree ), "group_add": resource.group_add, "is_any_resource_except_stonith": resource.is_any_resource_except_stonith, "is_any_stonith": resource.is_any_stonith, "manage": resource.manage, "move": resource.move, "move_autoclean": resource.move_autoclean, "restart": resource.restart, "unmanage": resource.unmanage, "unmove_unban": resource.unmove_unban, }, ) if name == "cib_options": return bind_all( env, middleware.build( middleware_factory.cib, ), { "operation_defaults_config": cib_options.operation_defaults_config, "operation_defaults_create": cib_options.operation_defaults_create, "operation_defaults_remove": cib_options.operation_defaults_remove, "operation_defaults_update": cib_options.operation_defaults_update, "resource_defaults_config": cib_options.resource_defaults_config, "resource_defaults_create": cib_options.resource_defaults_create, "resource_defaults_remove": cib_options.resource_defaults_remove, "resource_defaults_update": cib_options.resource_defaults_update, }, ) if name == "status": return bind_all( env, middleware.build( middleware_factory.cib, middleware_factory.corosync_conf_existing, ), { "pacemaker_status_xml": status.pacemaker_status_xml, "full_cluster_status_plaintext": ( status.full_cluster_status_plaintext ), "resources_status": status.resources_status, }, ) if name == "stonith": return bind_all( env, middleware.build( middleware_factory.cib, middleware_factory.corosync_conf_existing, ), { "create": stonith.create, "history_get_text": stonith.history_get_text, "history_cleanup": stonith.history_cleanup, "history_update": stonith.history_update, "update_scsi_devices": stonith.update_scsi_devices, "update_scsi_devices_add_remove": stonith.update_scsi_devices_add_remove, }, ) if name == "sbd": return bind_all( env, middleware.build(), { "enable_sbd": sbd.enable_sbd, "disable_sbd": sbd.disable_sbd, "get_cluster_sbd_status": sbd.get_cluster_sbd_status, "get_cluster_sbd_config": sbd.get_cluster_sbd_config, "get_local_sbd_config": sbd.get_local_sbd_config, "initialize_block_devices": sbd.initialize_block_devices, "get_local_devices_info": sbd.get_local_devices_info, "set_message": sbd.set_message, "get_local_available_watchdogs": ( sbd.get_local_available_watchdogs ), "test_local_watchdog": sbd.test_local_watchdog, }, ) if name == "services": return bind_all( env, middleware.build(), { "start_service": services.start_service, "stop_service": services.stop_service, "enable_service": services.enable_service, "disable_service": services.disable_service, "get_services_info": services.get_services_info, }, ) if name == "scsi": return bind_all( env, middleware.build(), { "unfence_node": scsi.unfence_node, "unfence_node_mpath": scsi.unfence_node_mpath, }, ) if name == "stonith_agent": return bind_all( env, middleware.build(), { "describe_agent": stonith_agent.describe_agent, "list_agents": stonith_agent.list_agents, }, ) if name == "tag": return bind_all( env, middleware.build(middleware_factory.cib), { "config": tag.config, "create": tag.create, "get_config_dto": tag.get_config_dto, "remove": tag.remove, "update": tag.update, }, ) if name == "cluster_property": return bind_all( env, middleware.build(middleware_factory.cib), { "set_properties": cluster_property.set_properties, "get_properties": cluster_property.get_properties, "get_properties_metadata": cluster_property.get_properties_metadata, "get_cluster_properties_definition_legacy": cluster_property.get_cluster_properties_definition_legacy, }, ) raise ValueError(f"No library part '{name}'") class Library: def __init__(self, env, middleware_factory): self.env = env self.middleware_factory = middleware_factory def __getattr__(self, name): return load_module(self.env, self.middleware_factory, name) pcs-0.12.0.2/pcs/cli/common/middleware.py000066400000000000000000000065441500417470700200500ustar00rootroot00000000000000import fcntl from collections import namedtuple from functools import partial from pcs.cli.reports.output import error def build(*middleware_list): def run(command, env, *args, **kwargs): next_in_line = command for next_command in reversed(middleware_list): next_in_line = partial(next_command, next_in_line) return next_in_line(env, *args, **kwargs) return run def cib(filename, touch_cib_file): """ return configured middleware that cares about local cib bool use_local_cib is flag if local cib was required callable load_cib_content returns local cib content, take no params callable write_cib put content of cib to required place """ def apply(next_in_line, env, *args, **kwargs): if filename: touch_cib_file(filename) try: with open(filename, mode="r") as cib_file: # the lock is released when the file gets closed on leaving # the with statement fcntl.flock(cib_file.fileno(), fcntl.LOCK_SH) original_content = cib_file.read() except EnvironmentError as e: raise error(f"Cannot read cib file '{filename}': '{e}'") from e env.cib_data = original_content result_of_next = next_in_line(env, *args, **kwargs) if filename and env.cib_data != original_content: try: with open(filename, mode="w") as cib_file: # the lock is released when the file gets closed on leaving # the with statement fcntl.flock(cib_file.fileno(), fcntl.LOCK_EX) cib_file.write(env.cib_data) except EnvironmentError as e: raise error(f"Cannot write cib file '{filename}': '{e}'") from e return result_of_next return apply def corosync_conf_existing(local_file_path): def apply(next_in_line, env, *args, **kwargs): if local_file_path: try: with open(local_file_path, "r") as local_file: # the lock is released when the file gets closed on leaving # the with statement fcntl.flock(local_file.fileno(), fcntl.LOCK_SH) original_content = local_file.read() except EnvironmentError as e: raise error( f"Unable to read {local_file_path}: {e.strerror}" ) from e env.corosync_conf_data = original_content result_of_next = next_in_line(env, *args, **kwargs) if local_file_path and env.corosync_conf_data != original_content: try: with open(local_file_path, "w") as local_file: # the lock is released when the file gets closed on leaving # the with statement fcntl.flock(local_file.fileno(), fcntl.LOCK_EX) local_file.write(env.corosync_conf_data) except EnvironmentError as e: raise error( f"Unable to write {local_file_path}: {e.strerror}" ) from e return result_of_next return apply def create_middleware_factory(**kwargs): """ Commandline options: no options """ return namedtuple("MiddlewareFactory", kwargs.keys())(**kwargs) pcs-0.12.0.2/pcs/cli/common/output.py000066400000000000000000000053441500417470700172700ustar00rootroot00000000000000import sys import textwrap from shlex import quote from shutil import get_terminal_size from typing import ( Iterable, List, ) from pcs.common.types import ( StringIterable, StringSequence, ) INDENT_STEP = 2 SUBSEQUENT_INDENT_STEP = 4 def bool_to_cli_value(value: bool) -> str: return "1" if value else "0" def _smart_wrap( text: str, subsequent_indent: int = SUBSEQUENT_INDENT_STEP ) -> List[str]: initial_indent = len(text) - len(text.lstrip(" ")) return format_wrap_for_terminal( text, subsequent_indent=subsequent_indent + initial_indent ) def smart_wrap_text( lines: StringSequence, subsequent_indent: int = SUBSEQUENT_INDENT_STEP ) -> List[str]: output = [] for line in lines: if not line: output.append("") continue output.extend(_smart_wrap(line, subsequent_indent=subsequent_indent)) return output def format_wrap_for_terminal( text: str, subsequent_indent: int = SUBSEQUENT_INDENT_STEP, trim: int = 0, ) -> List[str]: """ Returns text as a list of lines. Length of a line is determined by a terminal size if not explicitly specified. text -- string to format subsequent_indent -- number of spaces all subsequent lines will be indented compared to the first one. trim -- number which will be substracted from terminal size. Can be used in cases lines will be indented later by this number of spaces. """ # This function is used for stdout only - we don't care about wrapping # error messages and debug info. So it checks stdout and not stderr. # Checking stderr would enable wrapping in case of 'pcs ... | grep ...' # (stderr is connected to a terminal), which we don't want. (RHEL-36514) if sys.stdout is not None and sys.stdout.isatty(): return format_wrap( text, # minimal line length is 40 max(get_terminal_size()[0] - trim, 40), subsequent_indent=subsequent_indent, ) return [text] def format_wrap( text: str, max_length: int, subsequent_indent: int = SUBSEQUENT_INDENT_STEP, ) -> List[str]: return textwrap.wrap( text, max_length, subsequent_indent=" " * subsequent_indent, ) def options_to_cmd(options: StringIterable) -> str: return " ".join(quote(option) for option in options) def pair_to_cmd(pair: tuple[str, str]) -> str: return quote("=".join(pair)) def pairs_to_cmd(pairs: Iterable[tuple[str, str]]) -> str: return " ".join(pair_to_cmd(item) for item in pairs) def lines_to_str(lines: StringSequence) -> str: return "\n".join(smart_wrap_text(lines)) def format_cmd_list(cmd_lines: StringSequence) -> str: return ";\n".join(cmd_lines) pcs-0.12.0.2/pcs/cli/common/parse_args.py000066400000000000000000000610531500417470700200550ustar00rootroot00000000000000from collections import Counter from functools import partial from typing import ( Final, Mapping, Optional, Union, ) from pcs.cli.common.errors import ( SEE_MAN_CHANGES, CmdLineInputError, ) from pcs.cli.reports.output import deprecation_warning from pcs.common.const import INFINITY from pcs.common.str_tools import ( format_list, format_list_custom_last_separator, format_plural, ) from pcs.common.tools import timeout_to_seconds from pcs.common.types import ( StringCollection, StringIterable, StringSequence, ) # sys.argv always returns a list, we don't need StringSequence in here Argv = list[str] ModifierValueType = Union[None, bool, str] _FUTURE_OPTION_STR: Final = "future" FUTURE_OPTION: Final = f"--{_FUTURE_OPTION_STR}" _OUTPUT_FORMAT_OPTION_STR: Final = "output-format" OUTPUT_FORMAT_OPTION: Final = f"--{_OUTPUT_FORMAT_OPTION_STR}" OUTPUT_FORMAT_VALUE_CMD: Final = "cmd" OUTPUT_FORMAT_VALUE_JSON: Final = "json" OUTPUT_FORMAT_VALUE_TEXT: Final = "text" OUTPUT_FORMAT_VALUES: Final = frozenset( ( OUTPUT_FORMAT_VALUE_CMD, OUTPUT_FORMAT_VALUE_JSON, OUTPUT_FORMAT_VALUE_TEXT, ) ) ARG_TYPE_DELIMITER: Final = "%" # h = help, f = file, # p = password (cluster auth), u = user (cluster auth), PCS_SHORT_OPTIONS: Final = "hf:p:u:" PCS_LONG_OPTIONS: Final = [ "debug", "version", "help", "fullhelp", "force", "skip-offline", "autodelete", "simulate", "all", "full", "local", "wait", "config", "start", "enable", "disabled", "off", "request-timeout=", "brief", _FUTURE_OPTION_STR, # resource (safe-)disable "safe", "no-strict", # resource cleanup | refresh "strict", "pacemaker", "corosync", "no-default-ops", "defaults", "nodesc", "promoted", "name=", "group=", "node=", "from=", "to=", "after=", "before=", "corosync_conf=", "booth-conf=", "booth-key=", "no-watchdog-validation", # pcs cluster setup "no-cluster-uuid", "no-keys-sync", # in pcs status - do not display resource status on inactive node "hide-inactive", # pcs resource (un)manage - enable or disable monitor operations "monitor", # TODO remove # used only in deprecated 'pcs resource|stonith show' "groups", # "pcs resource clear --expired" - only clear expired moves and bans "expired", # disable evaluating whether rules are expired "no-expire-check", # allow overwriting existing files, currently meant for / used in CLI only "overwrite", # output format of commands, e.g: json, cmd, text, ... f"{_OUTPUT_FORMAT_OPTION_STR}=", # auth token "token=", # enable agent self validation "agent-validation", # disable text output in query commands "quiet", # proceed with dangerous actions, meant for / used in CLI only "yes", ] def split_list(arg_list: Argv, separator: str) -> list[Argv]: """ split a list of arguments to several lists using separator as a delimiter arg_list -- list of command line arguments to split separator -- delimiter """ separator_indexes = [i for i, x in enumerate(arg_list) if x == separator] bounds = zip( [0] + [i + 1 for i in separator_indexes], separator_indexes + [None] ) return [arg_list[i:j] for i, j in bounds] def split_list_by_any_keywords( arg_list: Argv, keyword_label: str ) -> dict[str, Argv]: """ split a list of arguments using any argument not containing = as a delimiter arg_list -- list of command line arguments to split keyword_label -- description of all keywords """ groups: dict[str, Argv] = {} if not arg_list: return groups if "=" in arg_list[0]: raise CmdLineInputError( f"Invalid character '=' in {keyword_label} '{arg_list[0]}'" ) current_keyword = arg_list[0] groups[current_keyword] = [] for arg in arg_list[1:]: if "=" in arg: groups[current_keyword].append(arg) else: current_keyword = arg if current_keyword in groups: raise CmdLineInputError( "{} '{}' defined multiple times".format( keyword_label.capitalize(), current_keyword ) ) groups[current_keyword] = [] return groups def split_option(arg: str, allow_empty_value: bool = True) -> tuple[str, str]: """ Get (key, value) from a key=value commandline argument. Split the argument by the first = and return resulting parts. Raise CmdLineInputError if the argument cannot be split. arg -- commandline argument to split allow_empty_value -- if False, raise CmdLineInputError on empty value """ if "=" not in arg: raise CmdLineInputError(f"missing value of '{arg}' option") if arg.startswith("="): raise CmdLineInputError(f"missing key in '{arg}' option") key, value = arg.split("=", 1) if not (value or allow_empty_value): raise CmdLineInputError(f"value of '{key}' option is empty") return key, value def ensure_unique_args(cmdline_args: Argv) -> None: """ Raises in case there are duplicate args """ duplicities = [ item for item, count in Counter(cmdline_args).items() if count > 1 ] if duplicities: argument_pl = format_plural(duplicities, "argument") duplicities_list = format_list(duplicities) raise CmdLineInputError(f"duplicate {argument_pl}: {duplicities_list}") class KeyValueParser: """ Parse and check key=value options """ def __init__(self, arg_list: Argv, repeatable: StringCollection = ()): """ arg_list -- commandline arguments to be parsed repeatable -- keys that are allowed to be specified several times """ self._repeatable_keys = repeatable self._key_value_map: dict[str, list[str]] = {} for arg in arg_list: name, value = split_option(arg) if name not in self._key_value_map: self._key_value_map[name] = [value] else: self._key_value_map[name].append(value) def check_allowed_keys(self, allowed_keys: StringCollection) -> None: """ Check that only allowed keys were specified allowed_keys -- list of allowed keys """ unknown_options = set(self._key_value_map.keys()) - set(allowed_keys) if unknown_options: raise CmdLineInputError( "Unknown option{s} '{options}'".format( s=("s" if len(unknown_options) > 1 else ""), options="', '".join(sorted(unknown_options)), ) ) def get_unique(self) -> dict[str, str]: """ Get all non-repeatable keys and their values; raise if a key has more values """ result: dict[str, str] = {} for key, values in self._key_value_map.items(): if key in self._repeatable_keys: continue values_uniq = set(values) if len(values_uniq) > 1: raise CmdLineInputError( f"duplicate option '{key}' with different values " f"{format_list_custom_last_separator(values_uniq, ' and ')}" ) result[key] = values[0] return result def get_repeatable(self) -> dict[str, list[str]]: """ Get all repeatable keys and their values """ return { key: self._key_value_map[key] for key in self._repeatable_keys if key in self._key_value_map } class ArgsByKeywords: def __init__(self, groups: Mapping[str, list[Argv]]): self._groups = groups self._flat_cache: dict[str, Argv] = {} def allow_repetition_only_for(self, keyword_set: StringCollection) -> None: """ Raise CmdLineInputError if a keyword has been repetead when not allowed keyword_set -- repetition is allowed for these keywords """ for keyword, arg_groups in self._groups.items(): if len(arg_groups) > 1 and keyword not in keyword_set: raise CmdLineInputError( f"'{keyword}' cannot be used more than once" ) def ensure_unique_keywords(self) -> None: """ Raise CmdLineInputError if any keyword has been repetead """ return self.allow_repetition_only_for(set()) def is_empty(self) -> bool: """ Check if any args have been specified """ return not self._groups def has_keyword(self, keyword: str) -> bool: """ Check if a keyword has been specified keyword -- a keyword to check """ return keyword in self._groups def has_empty_keyword(self, keyword: str) -> bool: """ Check if a keyword has been specified without any following args keyword -- a keyword to check """ return self.has_keyword(keyword) and not self.get_args_flat(keyword) def get_args_flat(self, keyword: str) -> Argv: """ Get arguments of a keyword in one sequence """ if keyword in self._groups: if keyword not in self._flat_cache: self._flat_cache[keyword] = [ arg for one_group in self._groups[keyword] for arg in one_group ] return self._flat_cache[keyword] return [] def get_args_groups(self, keyword: str) -> list[Argv]: """ Get arguments of a keyword, one group for each keyword occurrence """ if keyword in self._groups: return self._groups[keyword] return [] def group_by_keywords( arg_list: Argv, keyword_set: StringCollection, implicit_first_keyword: Optional[str] = None, ) -> ArgsByKeywords: """ Separate argv into groups delimited by specified keywords arg_list -- commandline arguments containing keywords keyword_set -- all expected keywords implicit_first_keyword -- key for capturing args before the first keyword """ args_by_keywords: dict[str, list[Argv]] = {} def new_keyword(keyword: str) -> None: if keyword not in args_by_keywords: args_by_keywords[keyword] = [] args_by_keywords[keyword].append([]) if arg_list: if arg_list[0] not in keyword_set: if not implicit_first_keyword: raise CmdLineInputError() current_keyword = implicit_first_keyword new_keyword(current_keyword) for arg in arg_list: if arg in keyword_set: current_keyword = arg new_keyword(current_keyword) else: args_by_keywords[current_keyword][-1].append(arg) return ArgsByKeywords(args_by_keywords) def parse_typed_arg( arg: str, allowed_types: StringSequence, default_type: str ) -> tuple[str, str]: """ Get (type, value) from a typed commandline argument. Split the argument by the type separator and return the type and the value. Raise CmdLineInputError in the argument format or type is not valid. string arg -- commandline argument Iterable allowed_types -- list of allowed argument types string default_type -- type to return if the argument doesn't specify a type """ if ARG_TYPE_DELIMITER not in arg: return default_type, arg arg_type, arg_value = arg.split(ARG_TYPE_DELIMITER, 1) if not arg_type: return default_type, arg_value if arg_type not in allowed_types: raise CmdLineInputError( ( "'{arg_type}' is not an allowed type for '{arg_full}', use " "{hint}" ).format( arg_type=arg_type, arg_full=arg, hint=", ".join(sorted(allowed_types)), ) ) return arg_type, arg_value def _is_num(arg: str) -> bool: if arg.lower() == INFINITY.lower(): return True try: int(arg) return True except ValueError: return False def _is_float(arg: str) -> bool: try: float(arg) return True except ValueError: return False def _is_negative_num(arg: str) -> bool: return arg.startswith("-") and (_is_num(arg[1:]) or _is_float(arg)) def is_short_option_expecting_value(arg: str) -> bool: return len(arg) == 2 and arg[0] == "-" and f"{arg[1]}:" in PCS_SHORT_OPTIONS def is_long_option_expecting_value(arg: str) -> bool: return ( len(arg) > 2 and arg[0:2] == "--" and f"{arg[2:]}=" in PCS_LONG_OPTIONS ) def is_option_expecting_value(arg: str) -> bool: return is_short_option_expecting_value( arg ) or is_long_option_expecting_value(arg) # DEPRECATED # TODO remove # This function is called only by deprecated code for parsing argv containing # negative numbers without -- prepending them. def filter_out_non_option_negative_numbers(arg_list: Argv) -> tuple[Argv, Argv]: """ Return arg_list without non-option negative numbers. Negative numbers following the option expecting value are kept. There is the problematic legacy: Argument "--" has special meaning: it can be used to signal that no more options will follow. This would solve the problem with negative numbers in a standard way: there would be no special approach to negative numbers, everything would be left in the hands of users. We cannot use "--" as it would be a backward incompatible change: * "pcs ... -infinity" would not work any more, users would have to switch to "pcs ... -- ... -infinity" * previously, position of some --options mattered, for example "--clone ", this syntax would not be possible with the "--" in place Currently used --options, which may be problematic when switching to "--": * --group , --before | --after * pcs resource | stonith create, pcs resource group add, pcs tag update * They have a single argument, so they would work even with --. But the command may look weird: pcs resource create --group G --after R2 -- R3 ocf:pacemaker:Dummy vs. current command pcs resource create R3 ocf:pacemaker:Dummy --group G --after R2 list arg_list contains command line arguments """ args_without_negative_nums = [] args_filtered_out = [] for i, arg in enumerate(arg_list): prev_arg = arg_list[i - 1] if i > 0 else "" if not _is_negative_num(arg) or is_option_expecting_value(prev_arg): args_without_negative_nums.append(arg) else: args_filtered_out.append(arg) return args_without_negative_nums, args_filtered_out # DEPRECATED # TODO remove # This function is called only by deprecated code for parsing argv containing # negative numbers without -- prepending them. def filter_out_options(arg_list: Argv) -> Argv: """ Return arg_list without options and negative numbers See a comment in filter_out_non_option_negative_numbers. arg_list -- command line arguments """ args_without_options = [] for i, arg in enumerate(arg_list): prev_arg = arg_list[i - 1] if i > 0 else "" if not is_option_expecting_value(prev_arg) and ( not arg.startswith("-") or arg == "-" or _is_negative_num(arg) ): args_without_options.append(arg) return args_without_options def wait_to_timeout(wait: Union[bool, str, None]) -> int: if wait is False: return -1 if wait is None: return 0 timeout = timeout_to_seconds(wait) if timeout is None: raise CmdLineInputError(f"'{wait}' is not a valid interval value") return timeout class InputModifiers: def __init__(self, options: Mapping[str, ModifierValueType]): self._defined_options = set(options.keys()) self._options = dict(options) self._options.update( { # boolean values "--all": "--all" in options, "--agent-validation": "--agent-validation" in options, "--autodelete": "--autodelete" in options, "--brief": "--brief" in options, "--config": "--config" in options, "--corosync": "--corosync" in options, "--debug": "--debug" in options, "--defaults": "--defaults" in options, "--disabled": "--disabled" in options, "--enable": "--enable" in options, "--expired": "--expired" in options, "--force": "--force" in options, "--full": "--full" in options, "--quiet": "--quiet" in options, FUTURE_OPTION: FUTURE_OPTION in options, # TODO remove # used only in deprecated 'pcs resource|stonith show' "--groups": "--groups" in options, "--hide-inactive": "--hide-inactive" in options, "--local": "--local" in options, "--monitor": "--monitor" in options, "--no-default-ops": "--no-default-ops" in options, "--nodesc": "--nodesc" in options, "--no-expire-check": "--no-expire-check" in options, "--no-cluster-uuid": "--no-cluster-uuid" in options, "--no-keys-sync": "--no-keys-sync" in options, "--no-strict": "--no-strict" in options, "--no-watchdog-validation": ( "--no-watchdog-validation" in options ), "--off": "--off" in options, "--overwrite": "--overwrite" in options, "--pacemaker": "--pacemaker" in options, "--promoted": "--promoted" in options, "--safe": "--safe" in options, "--simulate": "--simulate" in options, "--skip-offline": "--skip-offline" in options, "--start": "--start" in options, "--strict": "--strict" in options, "--yes": "--yes" in options, # string values "--after": options.get("--after", None), "--before": options.get("--before", None), "--booth-conf": options.get("--booth-conf", None), "--booth-key": options.get("--booth-key", None), "--corosync_conf": options.get("--corosync_conf", None), "--from": options.get("--from", None), # TODO remove # used in resource create and stonith create, deprecated in both "--group": options.get("--group", None), "--name": options.get("--name", None), "--node": options.get("--node", None), OUTPUT_FORMAT_OPTION: options.get( OUTPUT_FORMAT_OPTION, OUTPUT_FORMAT_VALUE_TEXT ), "--request-timeout": options.get("--request-timeout", None), "--to": options.get("--to", None), "--token": options.get("--token", None), "--wait": options.get("--wait", False), "-f": options.get("-f", None), "-p": options.get("-p", None), "-u": options.get("-u", None), } ) def get_subset( self, *options: str, **custom_options: ModifierValueType ) -> "InputModifiers": opt_dict = { opt: self.get(opt) for opt in options if self.is_specified(opt) } opt_dict.update(custom_options) return InputModifiers(opt_dict) def ensure_only_supported( self, *supported_options: str, hint_syntax_changed: Optional[str] = None, output_format_supported: bool = False, ) -> None: # --debug is supported in all commands supported_options_set = set(supported_options) | {"--debug"} if output_format_supported: supported_options_set.add(OUTPUT_FORMAT_OPTION) unsupported_options = self._defined_options - supported_options_set if unsupported_options: pluralize = partial(format_plural, unsupported_options) raise CmdLineInputError( "Specified {option} {option_list} {_is} not supported in this " "command".format( option=pluralize("option"), option_list=format_list(sorted(unsupported_options)), _is=pluralize("is"), ), hint=( "Syntax has changed from previous version. {}".format( SEE_MAN_CHANGES.format(hint_syntax_changed) ) if hint_syntax_changed else None ), ) def ensure_not_mutually_exclusive(self, *mutually_exclusive: str) -> None: """ Raise CmdLineInputError if several exclusive options were specified mutually_exclusive -- mutually exclusive options """ options_to_report = self._defined_options & set(mutually_exclusive) if len(options_to_report) > 1: raise CmdLineInputError( "Only one of {} can be used".format( format_list(sorted(options_to_report)) ) ) def ensure_not_incompatible( self, checked: str, incompatible: StringCollection ) -> None: """ Raise CmdLineInputError if both the checked and an incompatible option were specified checked -- option incompatible with any of incompatible options incompatible -- set of options incompatible with checked """ if not checked in self._defined_options: return disallowed = self._defined_options & set(incompatible) if disallowed: raise CmdLineInputError( "'{}' cannot be used with {}".format( checked, format_list(sorted(disallowed)) ) ) def ensure_dependency_satisfied( self, main_option: str, dependent_options: StringCollection ) -> None: """ Raise CmdLineInputError if any of dependent_options is present and main_option is not present. main_option -- option on which dependent_options depend dependent_options -- none of these options can be specified if main_option is not specified """ if main_option in self._defined_options: return disallowed = self._defined_options & set(dependent_options) if disallowed: raise CmdLineInputError( "{} cannot be used without '{}'".format( format_list(sorted(disallowed)), main_option ) ) def is_specified(self, option: str) -> bool: return option in self._defined_options def is_specified_any(self, option_list: StringIterable) -> bool: for option in option_list: if self.is_specified(option): return True return False def get( self, option: str, default: ModifierValueType = None ) -> ModifierValueType: if option in self._defined_options: return self._options[option] if default is not None: return default if option in self._options: return self._options[option] raise AssertionError(f"Non existing default value for '{option}'") def get_output_format( self, supported_formats: StringCollection = OUTPUT_FORMAT_VALUES, ) -> str: output_format = self.get(OUTPUT_FORMAT_OPTION) if output_format in supported_formats: return str(output_format) raise CmdLineInputError( ( "Unknown value '{value}' for '{option}' option. Supported " "{value_pl} {is_pl}: {supported}" ).format( value=output_format, option=OUTPUT_FORMAT_OPTION, value_pl=format_plural(supported_formats, "value"), is_pl=format_plural(supported_formats, "is"), supported=format_list(list(supported_formats)), ) ) def get_rule_str(argv: Argv) -> Optional[str]: if argv: if len(argv) > 1: # deprecated after 0.11.7 deprecation_warning( "Specifying a rule as multiple arguments is deprecated and " "might be removed in a future release, specify the rule as " "a single string instead" ) return " ".join(argv) return argv[0] return None pcs-0.12.0.2/pcs/cli/common/printable_tree.py000066400000000000000000000026651500417470700207320ustar00rootroot00000000000000from typing import Sequence class PrintableTreeNode: @property def members(self) -> Sequence["PrintableTreeNode"]: raise NotImplementedError() @property def detail(self) -> list[str]: raise NotImplementedError() @property def is_leaf(self) -> bool: raise NotImplementedError() def get_title(self, verbose: bool) -> str: raise NotImplementedError() def tree_to_lines( node: PrintableTreeNode, verbose: bool = False, title_prefix: str = "", indent: str = "", ) -> list[str]: """ Return sequence of strings representing lines to print out tree structure on command line. """ result = [] note = "" if node.is_leaf: note = " [displayed elsewhere]" title = node.get_title(verbose) result.append(f"{title_prefix}{title}{note}") if node.is_leaf: return result _indent = "| " if not node.members: _indent = " " for line in node.detail: result.append(f"{indent}{_indent}{line}") _indent = "| " _title_prefix = "|- " for member in node.members: if member == node.members[-1]: _indent = " " _title_prefix = "`- " result.extend( tree_to_lines( member, verbose, indent=f"{indent}{_indent}", title_prefix=f"{indent}{_title_prefix}", ) ) return result pcs-0.12.0.2/pcs/cli/common/routing.py000066400000000000000000000021761500417470700174170ustar00rootroot00000000000000from typing import ( Any, Callable, List, Mapping, Optional, ) from pcs import utils from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import InputModifiers CliCmdInterface = Callable[[Any, List[str], InputModifiers], None] def create_router( cmd_map: Mapping[str, CliCmdInterface], usage_sub_cmd: List[str], default_cmd: Optional[str] = None, ) -> CliCmdInterface: def _router(lib: Any, argv: List[str], modifiers: InputModifiers) -> None: if argv: sub_cmd, *argv_next = argv else: if default_cmd is None: raise CmdLineInputError() sub_cmd, argv_next = default_cmd, [] try: if sub_cmd not in cmd_map: sub_cmd = "" raise CmdLineInputError() return cmd_map[sub_cmd](lib, argv_next, modifiers) except CmdLineInputError as e: if not usage_sub_cmd: raise return utils.exit_on_cmdline_input_error( e, usage_sub_cmd[0], (usage_sub_cmd[1:] + [sub_cmd]) ) return _router pcs-0.12.0.2/pcs/cli/common/tools.py000066400000000000000000000014041500417470700170610ustar00rootroot00000000000000import sys from typing import Union from pcs.common.tools import timeout_to_seconds def timeout_to_seconds_legacy( timeout: Union[int, str] ) -> Union[int, str, None]: """ Transform pacemaker style timeout to number of seconds. If timeout is not valid then `timeout` is returned. timeout -- timeout string """ parsed_timeout = timeout_to_seconds(timeout) if parsed_timeout is None: return timeout return parsed_timeout def print_to_stderr(output: str, end: str = "\n") -> None: """ Prints output to stderr and flushes str output -- a string that is printed to stderr str end -- an optional ending, newline by default as Python's print """ sys.stderr.write(f"{output}{end}") sys.stderr.flush() pcs-0.12.0.2/pcs/cli/constraint/000077500000000000000000000000001500417470700162445ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint/__init__.py000066400000000000000000000000001500417470700203430ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint/command.py000066400000000000000000000034611500417470700202400ustar00rootroot00000000000000from typing import ( Any, Callable, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, ensure_unique_args, ) from pcs.cli.constraint import parse_args from pcs.common.pacemaker.constraint import ( get_all_constraints_ids, get_all_location_rules_ids, ) from pcs.common.str_tools import format_list def create_with_set( create_with_set_library_call: Callable[..., Any], argv: Argv, modifiers: InputModifiers, ) -> None: """ callable create_with_set_library_call create constraint with set argv -- part of comandline args modifiers -- can contain "force" allowing resources in clone/promotable and constraint duplicity Commandline options: * --force - allow a resource inside clone or promotable, allow duplicate element * -f - CIB file """ resource_set_list, constraint_options = parse_args.prepare_set_args(argv) create_with_set_library_call( resource_set_list, constraint_options, resource_in_clone_alowed=modifiers.get("--force"), duplication_alowed=modifiers.get("--force"), ) def remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() ensure_unique_args(argv) constraints_dto = lib.constraint.get_config(evaluate_rules=False) missing_ids = set(argv) - ( get_all_constraints_ids(constraints_dto) | get_all_location_rules_ids(constraints_dto) ) if missing_ids: raise CmdLineInputError( "Unable to find constraints or constraint rules: " f"{format_list(missing_ids)}" ) lib.cib.remove_elements(argv) pcs-0.12.0.2/pcs/cli/constraint/location/000077500000000000000000000000001500417470700200545ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint/location/__init__.py000066400000000000000000000000001500417470700221530ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint/location/command.py000066400000000000000000000102251500417470700220440ustar00rootroot00000000000000from typing import Any from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, ensure_unique_args, get_rule_str, parse_typed_arg, ) from pcs.cli.reports.output import deprecation_warning from pcs.cli.reports.preprocessor import ( get_duplicate_constraint_exists_preprocessor, ) from pcs.common import ( const, reports, ) from pcs.common.pacemaker.constraint import get_all_location_constraints_ids from pcs.common.str_tools import format_list from pcs.common.types import StringIterable RESOURCE_TYPE_RESOURCE = "resource" RESOURCE_TYPE_REGEXP = "regexp" _RESOURCE_TYPE_MAP = { RESOURCE_TYPE_RESOURCE: const.RESOURCE_ID_TYPE_PLAIN, RESOURCE_TYPE_REGEXP: const.RESOURCE_ID_TYPE_REGEXP, } def remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file """ # deprecated since pcs-0.11.7 deprecation_warning( "This command is deprecated and will be removed. " "Please use 'pcs constraint delete' or 'pcs constraint remove' " "instead." ) modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() ensure_unique_args(argv) missing_ids = set(argv) - get_all_location_constraints_ids( lib.constraint.get_config(evaluate_rules=False) ) if missing_ids: raise CmdLineInputError( f"Unable to find location constraints: {format_list(missing_ids)}" ) lib.cib.remove_elements(argv) def _extract_options( argv: Argv, options: StringIterable, ignored_options: StringIterable = () ) -> dict[str, str]: result: dict[str, str] = {} for argument in argv: if "=" not in argument: break key, value = argument.split("=", 1) if key in options: result[key] = value continue if key not in ignored_options: break return result def _extract_rule_options( argv: Argv, extract_constraint_options: bool = True ) -> tuple[dict[str, str], dict[str, str]]: rule_options_def = {"id", "role", "score", "score-attribute"} constraint_options_def = {"constraint-id", "resource-discovery"} rule_options = _extract_options( argv, rule_options_def, ignored_options=( constraint_options_def if extract_constraint_options else set() ), ) constraint_options = dict() if extract_constraint_options: constraint_options = _extract_options( argv, constraint_options_def, ignored_options=rule_options_def ) processed_options = set(rule_options_def) if extract_constraint_options: processed_options |= constraint_options_def while ( argv and "=" in argv[0] and argv[0].split("=")[0] in processed_options ): argv.pop(0) if "constraint-id" in constraint_options: constraint_options["id"] = constraint_options["constraint-id"] del constraint_options["constraint-id"] return rule_options, constraint_options def create_with_rule(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --force - allow constraint on any resource type, allow duplicate constraints """ modifiers.ensure_only_supported("-f", "--force") if len(argv) < 3: raise CmdLineInputError() force_flags = set() if modifiers.get("--force"): force_flags.add(reports.codes.FORCE) argv = argv[:] # eliminate side-effect - do not modify the original argv rsc_type, rsc_value = parse_typed_arg( argv.pop(0), list(_RESOURCE_TYPE_MAP.keys()), RESOURCE_TYPE_RESOURCE ) if argv[0] == "rule": argv.pop(0) else: raise CmdLineInputError() rule_options, constraint_options = _extract_rule_options(argv) lib.env.report_processor.set_report_item_preprocessor( get_duplicate_constraint_exists_preprocessor(lib) ) lib.constraint_location.create_plain_with_rule( _RESOURCE_TYPE_MAP[rsc_type], rsc_value, get_rule_str(argv) or "", rule_options, constraint_options, force_flags, ) pcs-0.12.0.2/pcs/cli/constraint/output/000077500000000000000000000000001500417470700176045ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint/output/__init__.py000066400000000000000000000003571500417470700217220ustar00rootroot00000000000000from . import ( colocation, location, order, ticket, ) from .all import ( CibConstraintLocationAnyDto, constraints_to_cmd, constraints_to_text, filter_constraints_by_rule_expired_status, print_config, ) pcs-0.12.0.2/pcs/cli/constraint/output/all.py000066400000000000000000000121731500417470700207320ustar00rootroot00000000000000import json from typing import ( Iterable, TypeVar, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.output import lines_to_str from pcs.cli.common.parse_args import ( OUTPUT_FORMAT_VALUE_CMD, OUTPUT_FORMAT_VALUE_JSON, InputModifiers, ) from pcs.cli.reports.output import warn from pcs.common.interface import dto from pcs.common.pacemaker.constraint import ( CibConstraintLocationDto, CibConstraintLocationSetDto, CibConstraintsDto, ) from pcs.common.types import CibRuleInEffectStatus from . import ( colocation, location, order, ticket, ) def constraints_to_text( constraints_dto: CibConstraintsDto, with_id: bool ) -> list[str]: return ( location.constraints_to_text( constraints_dto.location, constraints_dto.location_set, with_id ) + colocation.constraints_to_text( constraints_dto.colocation, constraints_dto.colocation_set, with_id ) + order.constraints_to_text( constraints_dto.order, constraints_dto.order_set, with_id ) + ticket.constraints_to_text( constraints_dto.ticket, constraints_dto.ticket_set, with_id ) ) def constraints_to_cmd(constraints_dto: CibConstraintsDto) -> list[list[str]]: for location_set_dto in constraints_dto.location_set: warn( "Location set constraint with id " f"'{location_set_dto.attributes.constraint_id}' configured but it's " "not supported by this command." " Command for creating the constraint is omitted." ) location_cmds = [] for location_dto in constraints_dto.location: location_cmds.extend(location.plain_constraint_to_cmd(location_dto)) return list( filter( None, ( location_cmds + [ colocation.plain_constraint_to_cmd(colocation_dto) for colocation_dto in constraints_dto.colocation ] + [ colocation.set_constraint_to_cmd(colocation_set_dto) for colocation_set_dto in constraints_dto.colocation_set ] + [ order.plain_constraint_to_cmd(order_dto) for order_dto in constraints_dto.order ] + [ order.set_constraint_to_cmd(order_set_dto) for order_set_dto in constraints_dto.order_set ] + [ ticket.plain_constraint_to_cmd(ticket_dto) for ticket_dto in constraints_dto.ticket ] + [ ticket.set_constraint_to_cmd(ticket_set_dto) for ticket_set_dto in constraints_dto.ticket_set ] ), ) ) CibConstraintLocationAnyDto = TypeVar( "CibConstraintLocationAnyDto", CibConstraintLocationDto, CibConstraintLocationSetDto, ) def _filter_out_expired_base( constraint_dtos: Iterable[CibConstraintLocationAnyDto], ) -> list[CibConstraintLocationAnyDto]: return [ constraint_dto for constraint_dto in constraint_dtos if not constraint_dto.attributes.rules or not all( rule.in_effect == CibRuleInEffectStatus.EXPIRED for rule in constraint_dto.attributes.rules ) ] def filter_constraints_by_rule_expired_status( constraints_dto: CibConstraintsDto, include_expired: bool ) -> CibConstraintsDto: return CibConstraintsDto( location=( constraints_dto.location if include_expired else _filter_out_expired_base(constraints_dto.location) ), location_set=( constraints_dto.location_set if include_expired else _filter_out_expired_base(constraints_dto.location_set) ), colocation=constraints_dto.colocation, colocation_set=constraints_dto.colocation_set, order=constraints_dto.order, order_set=constraints_dto.order_set, ticket=constraints_dto.ticket, ticket_set=constraints_dto.ticket_set, ) def print_config( constraints_dto: CibConstraintsDto, modifiers: InputModifiers ) -> None: constraints_dto = filter_constraints_by_rule_expired_status( constraints_dto, include_expired=modifiers.is_specified("--all"), ) if modifiers.get_output_format() == OUTPUT_FORMAT_VALUE_JSON: if modifiers.is_specified("--full"): raise CmdLineInputError( f"Option '--full' is not compatible with '{modifiers.get_output_format()}' output format." ) print(json.dumps(dto.to_dict(constraints_dto), indent=2)) return if modifiers.get_output_format() == OUTPUT_FORMAT_VALUE_CMD: print( ";\n".join( " \\\n".join(cmd) for cmd in constraints_to_cmd(constraints_dto) ) ) return result = lines_to_str( constraints_to_text(constraints_dto, modifiers.is_specified("--full")) ) if result: print(result) pcs-0.12.0.2/pcs/cli/constraint/output/colocation.py000066400000000000000000000150011500417470700223050ustar00rootroot00000000000000from shlex import quote from typing import ( Iterable, Optional, ) from pcs.cli.common.output import ( INDENT_STEP, pairs_to_cmd, ) from pcs.cli.reports.output import warn from pcs.cli.rule import rule_expression_dto_to_lines from pcs.common.pacemaker.constraint import ( CibConstraintColocationAttributesDto, CibConstraintColocationDto, CibConstraintColocationSetDto, ) from pcs.common.str_tools import ( format_name_value_list, format_optional, indent, ) from pcs.common.types import StringCollection from . import set as _set def _attributes_to_pairs( attributes_dto: CibConstraintColocationAttributesDto, ) -> list[tuple[str, str]]: pairs = [] if attributes_dto.score: pairs.append(("score", attributes_dto.score)) if attributes_dto.influence: pairs.append(("influence", attributes_dto.influence)) return pairs def _attributes_to_text( attributes_dto: CibConstraintColocationAttributesDto, with_id: bool, ) -> list[str]: result = [ " ".join(format_name_value_list(_attributes_to_pairs(attributes_dto))) ] if attributes_dto.lifetime: result.append("Lifetime:") for rule_dto in attributes_dto.lifetime: result.extend( indent( rule_expression_dto_to_lines(rule_dto, with_id), indent_step=INDENT_STEP, ) ) return result def plain_constraint_to_text( constraint_dto: CibConstraintColocationDto, with_id: bool, ) -> list[str]: result = [ "{resource_role}resource '{resource_id}' with {with_resource_role}resource '{with_resource_id}'".format( resource_role=format_optional(constraint_dto.resource_role), resource_id=constraint_dto.resource_id, with_resource_role=format_optional( constraint_dto.with_resource_role ), with_resource_id=constraint_dto.with_resource_id, ) ] if with_id: result[0] += f" (id: {constraint_dto.attributes.constraint_id})" result.extend( indent( _attributes_to_text(constraint_dto.attributes, with_id), indent_step=INDENT_STEP, ) ) return result def set_constraint_to_text( constraint_dto: CibConstraintColocationSetDto, with_id: bool, ) -> list[str]: return _set.set_constraint_to_text( constraint_dto.attributes.constraint_id, _attributes_to_text(constraint_dto.attributes, with_id), constraint_dto.resource_sets, with_id, ) def constraints_to_text( plain_dtos: Iterable[CibConstraintColocationDto], set_dtos: Iterable[CibConstraintColocationSetDto], with_id: bool, ) -> list[str]: result = [] if plain_dtos: result.append("Colocation Constraints:") for constraint_dto in plain_dtos: result.extend( indent( plain_constraint_to_text(constraint_dto, with_id), indent_step=INDENT_STEP, ) ) if set_dtos: result.append("Colocation Set Constraints:") for set_constraint_dto in set_dtos: result.extend( indent( set_constraint_to_text(set_constraint_dto, with_id), indent_step=INDENT_STEP, ) ) return result def _attributes_to_cmd_pairs( attributes_dto: CibConstraintColocationAttributesDto, filter_out: StringCollection = tuple(), ) -> Optional[list[tuple[str, str]]]: if attributes_dto.lifetime: warn( "Lifetime configuration detected in constraint " f"'{attributes_dto.constraint_id}' but not supported by this " "command." " Command for creating the constraint is omitted." ) return None unsupported_options = {"influence"} result = [] for pair in [("id", attributes_dto.constraint_id)] + _attributes_to_pairs( attributes_dto ): if pair[0] in unsupported_options: warn( f"Option '{pair[0]}' detected in constraint " f"'{attributes_dto.constraint_id}' but not supported by this " "command." " Command for creating the constraint is omitted." ) return None if pair[0] in filter_out: continue result.append(pair) return result def plain_constraint_to_cmd( constraint_dto: CibConstraintColocationDto, ) -> list[str]: if ( constraint_dto.resource_instance is not None or constraint_dto.with_resource_instance is not None ): warn( "Resource instance(s) detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." " Command for creating the constraint is omitted." ) return [] if constraint_dto.node_attribute is not None: warn( "Option 'node_attribute' detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." " Command for creating the constraint is omitted." ) return [] result = [ "pcs -- constraint colocation add {resource_role}{resource_id} with {with_resource_role}{with_resource_id}".format( resource_role=format_optional(constraint_dto.resource_role), resource_id=quote(constraint_dto.resource_id), with_resource_role=format_optional( constraint_dto.with_resource_role ), with_resource_id=quote(constraint_dto.with_resource_id), ) ] pairs = _attributes_to_cmd_pairs(constraint_dto.attributes) if pairs is None: return [] params = pairs_to_cmd(pairs) if params: result.extend(indent([params], indent_step=INDENT_STEP)) return result def set_constraint_to_cmd( constraint_dto: CibConstraintColocationSetDto, ) -> list[str]: result = ["pcs -- constraint colocation"] for resource_set in constraint_dto.resource_sets: set_cmd_part = _set.resource_set_to_cmd(resource_set) if not set_cmd_part: return [] result.extend(indent(set_cmd_part, indent_step=INDENT_STEP)) pairs = _attributes_to_cmd_pairs(constraint_dto.attributes) if pairs is None: return [] params = pairs_to_cmd(pairs) if params: result.extend(indent([f"setoptions {params}"], indent_step=INDENT_STEP)) return result pcs-0.12.0.2/pcs/cli/constraint/output/location.py000066400000000000000000000347351500417470700220020ustar00rootroot00000000000000import shlex from collections import defaultdict from typing import ( Callable, Iterable, ) from pcs.cli.common.output import ( INDENT_STEP, pairs_to_cmd, ) from pcs.cli.reports.output import warn from pcs.cli.rule import rule_expression_dto_to_lines from pcs.common.pacemaker.constraint import ( CibConstraintLocationAttributesDto, CibConstraintLocationDto, CibConstraintLocationSetDto, ) from pcs.common.pacemaker.rule import CibRuleExpressionDto from pcs.common.pacemaker.tools import ( abs_score, is_negative_score, ) from pcs.common.str_tools import ( format_optional, indent, pairs_to_text, ) from . import set as _set def _attributes_to_pairs( attributes_dto: CibConstraintLocationAttributesDto, ) -> list[tuple[str, str]]: pairs = [] if attributes_dto.resource_discovery: pairs.append( ("resource-discovery", str(attributes_dto.resource_discovery)) ) return pairs def _attributes_to_text( attributes_dto: CibConstraintLocationAttributesDto, with_id: bool, ) -> list[str]: result = pairs_to_text(_attributes_to_pairs(attributes_dto)) if attributes_dto.rules: result.append("Rules:") for rule_dto in attributes_dto.rules: result.extend( indent( rule_expression_dto_to_lines(rule_dto, with_ids=with_id), indent_step=INDENT_STEP, ) ) if attributes_dto.lifetime: result.append("Lifetime:") for rule_dto in attributes_dto.lifetime: result.extend( indent( rule_expression_dto_to_lines(rule_dto, with_ids=with_id), indent_step=INDENT_STEP, ) ) return result def plain_constraint_to_text( constraint_dto: CibConstraintLocationDto, with_id: bool, ) -> list[str]: prefers_part = "" score = constraint_dto.attributes.score or "INFINITY" if not constraint_dto.attributes.rules: prefers_part = " {prefers} node '{node}' with score {score}".format( prefers=("avoids" if is_negative_score(score) else "prefers"), node=constraint_dto.attributes.node, score=abs_score(score), ) result = [ "{role}resource{resource_pattern} '{resource}'{prefers_part}".format( role=format_optional(constraint_dto.role), resource_pattern=("" if constraint_dto.resource_id else " pattern"), resource=( constraint_dto.resource_id or constraint_dto.resource_pattern ), prefers_part=prefers_part, ) ] if with_id: result[0] += f" (id: {constraint_dto.attributes.constraint_id})" result.extend( indent( _attributes_to_text(constraint_dto.attributes, with_id), indent_step=INDENT_STEP, ) ) return result def set_constraint_to_text( constraint_dto: CibConstraintLocationSetDto, with_id: bool, ) -> list[str]: return _set.set_constraint_to_text( constraint_dto.attributes.constraint_id, _attributes_to_text(constraint_dto.attributes, with_id), constraint_dto.resource_sets, with_id, ) def constraints_to_text( plain_dtos: Iterable[CibConstraintLocationDto], set_dtos: Iterable[CibConstraintLocationSetDto], with_id: bool, ) -> list[str]: result = [] if plain_dtos: result.append("Location Constraints:") for constraint_dto in plain_dtos: result.extend( indent( plain_constraint_to_text(constraint_dto, with_id), indent_step=INDENT_STEP, ) ) if set_dtos: result.append("Location Set Constraints:") for set_constraint_dto in set_dtos: result.extend( indent( set_constraint_to_text(set_constraint_dto, with_id), indent_step=INDENT_STEP, ) ) return result def _plain_constraint_get_resource_for_cmd( constraint_dto: CibConstraintLocationDto, ) -> str: if constraint_dto.resource_id: resource = f"resource%{constraint_dto.resource_id}" else: resource = f"regexp%{constraint_dto.resource_pattern}" return shlex.quote(resource) def _plain_constraint_to_cmd( constraint_dto: CibConstraintLocationDto, ) -> list[str]: score = constraint_dto.attributes.score or "INFINITY" result = [ "pcs -- constraint location add {id} {resource} {node} {score}".format( id=shlex.quote(constraint_dto.attributes.constraint_id), resource=_plain_constraint_get_resource_for_cmd(constraint_dto), node=shlex.quote(str(constraint_dto.attributes.node)), score=shlex.quote(f"score={score}"), ) ] if constraint_dto.attributes.resource_discovery: result.extend( indent( [pairs_to_cmd(_attributes_to_pairs(constraint_dto.attributes))], indent_step=INDENT_STEP, ) ) return result def _rule_to_cmd_pairs(rule: CibRuleExpressionDto) -> list[tuple[str, str]]: pairs = [] if rule.options.get("role"): pairs.append(("role", rule.options["role"])) if rule.options.get("score"): pairs.append(("score", rule.options["score"])) elif rule.options.get("score-attribute"): pairs.append(("score-attribute", rule.options["score-attribute"])) return pairs def _plain_constraint_rule_to_cmd( constraint_dto: CibConstraintLocationDto, ) -> list[list[str]]: if len(constraint_dto.attributes.rules) > 1: warn( f"Constraint '{constraint_dto.attributes.constraint_id}' contains " "more than one rule, which is no longer supported. Instead, each " "rule will be put in a separate constraint." ) result = [] for rule_index, one_rule in enumerate(constraint_dto.attributes.rules): command = [ "pcs -- constraint location {resource} rule".format( resource=_plain_constraint_get_resource_for_cmd(constraint_dto) ) ] pairs = [("id", one_rule.id)] if rule_index == 0: # Constraint ID must be unique. Since we split additional rules # into their own constraints, we cannot use the original constraint # ID for the newly created constraints. pairs.append( ( "constraint-id", constraint_dto.attributes.constraint_id, ) ) command.extend( indent( [ pairs_to_cmd( pairs + _attributes_to_pairs(constraint_dto.attributes) + _rule_to_cmd_pairs(one_rule) ), shlex.quote(one_rule.as_string), ], indent_step=INDENT_STEP, ) ) result.append(command) return result def plain_constraint_to_cmd( constraint_dto: CibConstraintLocationDto, ) -> list[list[str]]: if constraint_dto.attributes.lifetime: warn( "Lifetime configuration detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." " Command for creating the constraint is omitted." ) return [] if constraint_dto.role: warn( f"Resource role detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." " Command for creating the constraint is omitted." ) return [] if constraint_dto.attributes.rules: return _plain_constraint_rule_to_cmd(constraint_dto) return [_plain_constraint_to_cmd(constraint_dto)] def _split_based_on_score( constraint_dtos: Iterable[CibConstraintLocationDto], ) -> tuple[list[CibConstraintLocationDto], list[CibConstraintLocationDto]]: prefers: list[CibConstraintLocationDto] = [] avoids: list[CibConstraintLocationDto] = [] for constraint_dto in constraint_dtos: if constraint_dto.attributes.score is not None and is_negative_score( constraint_dto.attributes.score ): avoids.append(constraint_dto) else: prefers.append(constraint_dto) return prefers, avoids def _per_resource_constraint_to_str( constraint_dto: CibConstraintLocationDto, with_ids: bool ) -> str: return "node '{node}' with score {score}{id}".format( node=constraint_dto.attributes.node, score=abs_score(constraint_dto.attributes.score or "0"), id=( f" (id: {constraint_dto.attributes.constraint_id})" if with_ids else "" ), ) def _split_with_rules( constraint_dtos: Iterable[CibConstraintLocationDto], ) -> tuple[list[CibConstraintLocationDto], list[CibConstraintLocationDto]]: without_rules: list[CibConstraintLocationDto] = [] with_rules: list[CibConstraintLocationDto] = [] for constraint_dto in constraint_dtos: if constraint_dto.attributes.rules: with_rules.append(constraint_dto) else: without_rules.append(constraint_dto) return without_rules, with_rules def _split_patterns( constraint_dtos: Iterable[CibConstraintLocationDto], ) -> tuple[list[CibConstraintLocationDto], list[CibConstraintLocationDto]]: ids: list[CibConstraintLocationDto] = [] patterns: list[CibConstraintLocationDto] = [] for constraint_dto in constraint_dtos: if constraint_dto.resource_pattern: patterns.append(constraint_dto) else: ids.append(constraint_dto) return ids, patterns def _labeled_constraints_list_to_text( label: str, constraint_dtos: Iterable[CibConstraintLocationDto], render_fn: Callable[[CibConstraintLocationDto], str], ) -> list[str]: result = [] if constraint_dtos: result.extend( [f"{label}:"] + indent( [ render_fn(constraint_dto) for constraint_dto in constraint_dtos ], indent_step=INDENT_STEP, ), ) return result def _with_rules_to_text( constraint: CibConstraintLocationDto, with_ids: bool ) -> list[str]: result = [ "Constraint:{id}".format( id=f" {constraint.attributes.constraint_id}" if with_ids else "" ) ] rules_lines = [] for rule_dto in constraint.attributes.rules: rules_lines.extend( indent( rule_expression_dto_to_lines(rule_dto, with_ids), indent_step=INDENT_STEP, ) ) result.extend(indent(["Rules:"] + rules_lines, indent_step=INDENT_STEP)) return result def _per_resource_constraints_to_text( constraint_dtos: Iterable[CibConstraintLocationDto], with_ids: bool ) -> list[str]: def render_fn(constraint: CibConstraintLocationDto) -> str: return _per_resource_constraint_to_str(constraint, with_ids) result = [] without_rules, with_rules = _split_with_rules(constraint_dtos) prefers, avoids = _split_based_on_score(without_rules) result.extend( _labeled_constraints_list_to_text("Prefers", prefers, render_fn) ) result.extend( _labeled_constraints_list_to_text("Avoids", avoids, render_fn) ) for constraint in with_rules: result.extend(_with_rules_to_text(constraint, with_ids)) return result def constraints_to_grouped_by_resource_text( constraint_dtos: Iterable[CibConstraintLocationDto], with_ids: bool, ) -> list[str]: result = [] patterns: dict[str, list[CibConstraintLocationDto]] = defaultdict(list) resources: dict[str, list[CibConstraintLocationDto]] = defaultdict(list) for constraint_dto in constraint_dtos: if constraint_dto.resource_pattern: patterns[constraint_dto.resource_pattern].append(constraint_dto) else: resources[str(constraint_dto.resource_id)].append(constraint_dto) for resource_id, constraints in resources.items(): result.append(f"Resource: {resource_id}") result.extend( indent( _per_resource_constraints_to_text(constraints, with_ids), indent_step=INDENT_STEP, ) ) for resource_pattern, constraints in patterns.items(): result.append(f"Resource pattern: {resource_pattern}") result.extend( indent( _per_resource_constraints_to_text(constraints, with_ids), indent_step=INDENT_STEP, ) ) return result def _per_node_constraint_to_str( constraint_dto: CibConstraintLocationDto, with_ids: bool ) -> str: return "resource{pattern} '{id}' with score {score}{constraint_id}".format( pattern=" pattern" if constraint_dto.resource_pattern else "", id=constraint_dto.resource_pattern or constraint_dto.resource_id, score=abs_score(constraint_dto.attributes.score or "0"), constraint_id=( f" (id: {constraint_dto.attributes.constraint_id})" if with_ids else "" ), ) def constraints_to_grouped_by_node_text( constraints_dtos: Iterable[CibConstraintLocationDto], with_ids: bool, ) -> list[str]: constraint_dtos, with_rules = _split_with_rules(constraints_dtos) if with_rules: warn("Constraints with rules are not displayed.") nodes: dict[str, list[CibConstraintLocationDto]] = defaultdict(list) for constraint in constraint_dtos: if constraint.attributes.node: nodes[constraint.attributes.node].append(constraint) result = [] def _render_fn(constraint: CibConstraintLocationDto) -> str: return _per_node_constraint_to_str(constraint, with_ids) for node, constraints in nodes.items(): result.extend([f"Node: {node}"]) prefers, avoids = _split_based_on_score(constraints) result.extend( indent( _labeled_constraints_list_to_text( "Preferred by", prefers, _render_fn ), indent_step=INDENT_STEP, ) ) result.extend( indent( _labeled_constraints_list_to_text( "Avoided by", avoids, _render_fn ), indent_step=INDENT_STEP, ) ) return result pcs-0.12.0.2/pcs/cli/constraint/output/order.py000066400000000000000000000125771500417470700213050ustar00rootroot00000000000000from shlex import quote from typing import Iterable from pcs.cli.common.output import ( INDENT_STEP, bool_to_cli_value, pairs_to_cmd, ) from pcs.cli.reports.output import warn from pcs.common.pacemaker.constraint import ( CibConstraintOrderAttributesDto, CibConstraintOrderDto, CibConstraintOrderSetDto, ) from pcs.common.str_tools import ( format_optional, indent, pairs_to_text, ) from . import set as _set def _attributes_to_pairs( attributes_dto: CibConstraintOrderAttributesDto, ) -> list[tuple[str, str]]: pairs = [] if attributes_dto.symmetrical is not None: pairs.append( ("symmetrical", bool_to_cli_value(attributes_dto.symmetrical)) ) if attributes_dto.require_all is not None: pairs.append( ("require-all", bool_to_cli_value(attributes_dto.require_all)) ) if attributes_dto.score: pairs.append(("score", attributes_dto.score)) if attributes_dto.kind: pairs.append(("kind", attributes_dto.kind)) return pairs def _attributes_to_text( attributes_dto: CibConstraintOrderAttributesDto, ) -> list[str]: return pairs_to_text(_attributes_to_pairs(attributes_dto)) def plain_constraint_to_text( constraint_dto: CibConstraintOrderDto, with_id: bool, ) -> list[str]: result = [ "{first_action}resource '{first_resource}' then {then_action}resource '{then_resource}'".format( first_action=format_optional(constraint_dto.first_action), first_resource=constraint_dto.first_resource_id, then_action=format_optional(constraint_dto.then_action), then_resource=constraint_dto.then_resource_id, ) ] if with_id: result[0] += f" (id: {constraint_dto.attributes.constraint_id})" result.extend( indent( _attributes_to_text(constraint_dto.attributes), indent_step=INDENT_STEP, ) ) return result def set_constraint_to_text( constraint_dto: CibConstraintOrderSetDto, with_id: bool, ) -> list[str]: return _set.set_constraint_to_text( constraint_dto.attributes.constraint_id, _attributes_to_text(constraint_dto.attributes), constraint_dto.resource_sets, with_id, ) def constraints_to_text( plain_dtos: Iterable[CibConstraintOrderDto], set_dtos: Iterable[CibConstraintOrderSetDto], with_id: bool, ) -> list[str]: result = [] if plain_dtos: result.append("Order Constraints:") for constraint_dto in plain_dtos: result.extend( indent( plain_constraint_to_text(constraint_dto, with_id), indent_step=INDENT_STEP, ) ) if set_dtos: result.append("Order Set Constraints:") for set_constraint_dto in set_dtos: result.extend( indent( set_constraint_to_text(set_constraint_dto, with_id), indent_step=INDENT_STEP, ) ) return result def _attributes_to_cmd_pairs( attributes_dto: CibConstraintOrderAttributesDto, ) -> list[tuple[str, str]]: return [("id", attributes_dto.constraint_id)] + _attributes_to_pairs( attributes_dto ) def plain_constraint_to_cmd( constraint_dto: CibConstraintOrderDto, ) -> list[str]: if ( constraint_dto.first_resource_instance is not None or constraint_dto.then_resource_instance is not None ): warn( "Resource instance(s) detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not supported by " "this command." " Command for creating the constraint is omitted." ) return [] result = [ "pcs -- constraint order {first_action}{first_resource_id} then {then_action}{then_resource_id}".format( first_action=format_optional(constraint_dto.first_action), first_resource_id=quote(constraint_dto.first_resource_id), then_action=format_optional(constraint_dto.then_action), then_resource_id=quote(constraint_dto.then_resource_id), ) ] params = pairs_to_cmd(_attributes_to_cmd_pairs(constraint_dto.attributes)) if params: result.extend(indent([params], indent_step=INDENT_STEP)) return result def set_constraint_to_cmd( constraint_dto: CibConstraintOrderSetDto, ) -> list[str]: result = ["pcs -- constraint order"] for resource_set in constraint_dto.resource_sets: set_cmd_part = _set.resource_set_to_cmd(resource_set) if not set_cmd_part: return [] result.extend(indent(set_cmd_part, indent_step=INDENT_STEP)) pairs = [] for pair in _attributes_to_cmd_pairs(constraint_dto.attributes): # this list is based on pcs.lib.cib.constraint.order.ATTRIB if pair[0] not in ("symmetrical", "kind", "id"): warn( f"Option '{pair[0]}' detected in constraint " f"'{constraint_dto.attributes.constraint_id}' but not " "supported by this command." " Command for creating the constraint is omitted." ) return [] pairs.append(pair) if pairs: result.extend( indent( [f"setoptions {pairs_to_cmd(pairs)}"], indent_step=INDENT_STEP ) ) return result pcs-0.12.0.2/pcs/cli/constraint/output/set.py000066400000000000000000000064301500417470700207540ustar00rootroot00000000000000from typing import ( Optional, Sequence, ) from pcs.cli.common.output import ( INDENT_STEP, bool_to_cli_value, options_to_cmd, pairs_to_cmd, ) from pcs.cli.reports.output import warn from pcs.common.pacemaker.constraint import CibResourceSetDto from pcs.common.str_tools import ( format_list, format_optional, indent, pairs_to_text, ) from pcs.common.types import StringSequence def _resource_set_options_to_pairs( resource_set_dto: CibResourceSetDto, ) -> list[tuple[str, str]]: pairs = [] if resource_set_dto.sequential is not None: pairs.append( ("sequential", bool_to_cli_value(resource_set_dto.sequential)) ) if resource_set_dto.require_all is not None: pairs.append( ("require-all", bool_to_cli_value(resource_set_dto.require_all)) ) if resource_set_dto.ordering: pairs.append(("ordering", resource_set_dto.ordering)) if resource_set_dto.action: pairs.append(("action", resource_set_dto.action)) if resource_set_dto.role: pairs.append(("role", resource_set_dto.role)) if resource_set_dto.score: pairs.append(("score", resource_set_dto.score)) if resource_set_dto.kind: pairs.append(("kind", resource_set_dto.kind.capitalize())) return pairs def resource_set_to_text( resource_set_dto: CibResourceSetDto, with_id: bool, ) -> list[str]: output = [ "Resource Set:{id}".format( id=f" {resource_set_dto.set_id}" if with_id else "" ) ] set_options = [ "Resources: {resources}".format( resources=format_list(resource_set_dto.resources_ids) ) ] + pairs_to_text(_resource_set_options_to_pairs(resource_set_dto)) output.extend(indent(set_options, indent_step=INDENT_STEP)) return output def set_constraint_to_text( constraint_id: str, constraint_attrs_lines: StringSequence, resource_sets: Sequence[CibResourceSetDto], with_id: bool, ) -> list[str]: header = "Set Constraint:" if with_id: header += f" {constraint_id}" result = [header] result.extend(indent(constraint_attrs_lines, indent_step=INDENT_STEP)) for res_set_dto in resource_sets: result.extend( indent( resource_set_to_text(res_set_dto, with_id), indent_step=INDENT_STEP, ) ) return result def resource_set_to_cmd(resource_set: CibResourceSetDto) -> Optional[list[str]]: filtered_pairs = [] for pair in _resource_set_options_to_pairs(resource_set): # this list is based on pcs.lib.cib.constraint.resource_set._ATTRIBUTES if pair[0] not in ("action", "require-all", "role", "sequential"): warn( f"Option '{pair[0]}' detected in resource set " f"'{resource_set.set_id}' but not " "supported by this command." " Command for creating the constraint is omitted." ) return None filtered_pairs.append(pair) return [ "set {resources}{options}".format( resources=options_to_cmd(resource_set.resources_ids), options=format_optional( pairs_to_cmd(filtered_pairs), template=" {}", ), ) ] pcs-0.12.0.2/pcs/cli/constraint/output/ticket.py000066400000000000000000000075061500417470700214510ustar00rootroot00000000000000from shlex import quote from typing import Iterable from pcs.cli.common.output import ( INDENT_STEP, pairs_to_cmd, ) from pcs.common.pacemaker.constraint import ( CibConstraintTicketAttributesDto, CibConstraintTicketDto, CibConstraintTicketSetDto, ) from pcs.common.str_tools import ( format_optional, indent, pairs_to_text, ) from . import set as _set def _attributes_to_pairs( attributes_dto: CibConstraintTicketAttributesDto, ) -> list[tuple[str, str]]: pairs = [] if attributes_dto.loss_policy: pairs.append(("loss-policy", str(attributes_dto.loss_policy))) return pairs def plain_constraint_to_text( constraint_dto: CibConstraintTicketDto, with_id: bool, ) -> list[str]: result = [ "{role}resource '{resource}' depends on ticket '{ticket}'".format( role=format_optional(constraint_dto.role), resource=constraint_dto.resource_id, ticket=constraint_dto.attributes.ticket, ) ] if with_id: result[0] += f" (id: {constraint_dto.attributes.constraint_id})" result.extend( indent( pairs_to_text(_attributes_to_pairs(constraint_dto.attributes)), indent_step=INDENT_STEP, ) ) return result def set_constraint_to_text( constraint_dto: CibConstraintTicketSetDto, with_id: bool, ) -> list[str]: return _set.set_constraint_to_text( constraint_dto.attributes.constraint_id, pairs_to_text( [("ticket", constraint_dto.attributes.ticket)] + _attributes_to_pairs(constraint_dto.attributes) ), constraint_dto.resource_sets, with_id, ) def constraints_to_text( plain_dtos: Iterable[CibConstraintTicketDto], set_dtos: Iterable[CibConstraintTicketSetDto], with_id: bool, ) -> list[str]: result = [] if plain_dtos: result.append("Ticket Constraints:") for constraint_dto in plain_dtos: result.extend( indent( plain_constraint_to_text(constraint_dto, with_id), indent_step=INDENT_STEP, ) ) if set_dtos: result.append("Ticket Set Constraints:") for set_constraint_dto in set_dtos: result.extend( indent( set_constraint_to_text(set_constraint_dto, with_id), indent_step=INDENT_STEP, ) ) return result def _attributes_to_cmd_pairs( attributes_dto: CibConstraintTicketAttributesDto, ) -> list[tuple[str, str]]: return [("id", attributes_dto.constraint_id)] + _attributes_to_pairs( attributes_dto ) def plain_constraint_to_cmd( constraint_dto: CibConstraintTicketDto, ) -> list[str]: result = [ "pcs -- constraint ticket add {ticket} {role}{resource_id}".format( ticket=quote(constraint_dto.attributes.ticket), role=format_optional(constraint_dto.role), resource_id=quote(constraint_dto.resource_id), ) ] params = pairs_to_cmd(_attributes_to_cmd_pairs(constraint_dto.attributes)) if params: result.extend(indent([params], indent_step=INDENT_STEP)) return result def set_constraint_to_cmd( constraint_dto: CibConstraintTicketSetDto, ) -> list[str]: result = ["pcs -- constraint ticket"] for resource_set in constraint_dto.resource_sets: set_cmd_part = _set.resource_set_to_cmd(resource_set) if not set_cmd_part: return [] result.extend(indent(set_cmd_part, indent_step=INDENT_STEP)) params = pairs_to_cmd( _attributes_to_cmd_pairs(constraint_dto.attributes) + [("ticket", constraint_dto.attributes.ticket)] ) if params: result.extend(indent([f"setoptions {params}"], indent_step=INDENT_STEP)) return result pcs-0.12.0.2/pcs/cli/constraint/parse_args.py000066400000000000000000000026641500417470700207540ustar00rootroot00000000000000from typing import Union from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, KeyValueParser, split_list, ) def prepare_resource_sets( cmdline_args: Argv, ) -> list[dict[str, Union[list[str], dict[str, str]]]]: return [ { "ids": [id for id in args if "=" not in id], "options": KeyValueParser( [opt for opt in args if "=" in opt] ).get_unique(), } for args in split_list(cmdline_args, "set") ] def prepare_set_args( argv: Argv, ) -> tuple[list[dict[str, Union[list[str], dict[str, str]]]], dict[str, str]]: args_groups = split_list(argv, "setoptions") if len(args_groups) > 2: raise CmdLineInputError( "Keyword 'setoptions' may be mentioned at most once" ) resource_set_args = args_groups[0] constraint_options_args = args_groups[1] if len(args_groups) == 2 else [] if not resource_set_args: raise CmdLineInputError() resource_set_list = prepare_resource_sets(resource_set_args) if not resource_set_list or not all( resource_set["ids"] for resource_set in resource_set_list ): raise CmdLineInputError() constraint_options = {} if constraint_options_args: constraint_options = KeyValueParser( constraint_options_args ).get_unique() return (resource_set_list, constraint_options) pcs-0.12.0.2/pcs/cli/constraint_colocation/000077500000000000000000000000001500417470700204565ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint_colocation/__init__.py000066400000000000000000000000001500417470700225550ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint_colocation/command.py000066400000000000000000000062741500417470700224570ustar00rootroot00000000000000from typing import ( Any, cast, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, ) from pcs.cli.constraint import command from pcs.cli.constraint.output import print_config from pcs.cli.reports.output import deprecation_warning from pcs.cli.reports.preprocessor import ( get_duplicate_constraint_exists_preprocessor, ) from pcs.common.pacemaker.constraint import CibConstraintsDto def create_with_set(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ create colocation constraint with resource set object lib exposes library list argv see usage for "constraint colocation set" dict like object modifiers can contain "force" allows resource in clone/master and constraint duplicity Options: * --force - allow resource inside clone (or master), allow duplicate element * -f - CIB file """ modifiers.ensure_only_supported("-f", "--force") lib.env.report_processor.set_report_item_preprocessor( get_duplicate_constraint_exists_preprocessor(lib) ) command.create_with_set( lib.constraint_colocation.create_with_set, argv, modifiers, ) def config_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: modifiers.ensure_only_supported("-f", "--output-format", "--full") if argv: raise CmdLineInputError() constraints_dto = cast( CibConstraintsDto, lib.constraint.get_config(evaluate_rules=True), ) print_config( CibConstraintsDto( colocation=constraints_dto.colocation, colocation_set=constraints_dto.colocation_set, ), modifiers, ) def remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) != 2: raise CmdLineInputError() source_rsc_id, target_rsc_id = argv constraint_ids_to_remove: list[str] = [] constraint_dto = lib.constraint.get_config(evaluate_rules=False) for colocation_dto in constraint_dto.colocation: if ( colocation_dto.resource_id == source_rsc_id and colocation_dto.with_resource_id == target_rsc_id ): constraint_ids_to_remove.append( colocation_dto.attributes.constraint_id ) elif ( colocation_dto.resource_id == target_rsc_id and colocation_dto.with_resource_id == source_rsc_id ): # deprecated since pcs-0.11.7 deprecation_warning( "Removing colocation constraint with interchanged source " "resource id and targert resource id. This behavior is " "deprecated and will be removed." ) constraint_ids_to_remove.append( colocation_dto.attributes.constraint_id ) if not constraint_ids_to_remove: raise CmdLineInputError( f"Unable to find colocation constraint with source resource " f"'{source_rsc_id}' and target resource '{target_rsc_id}'" ) lib.cib.remove_elements(constraint_ids_to_remove) pcs-0.12.0.2/pcs/cli/constraint_order/000077500000000000000000000000001500417470700174375ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint_order/__init__.py000066400000000000000000000000001500417470700215360ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint_order/command.py000066400000000000000000000031521500417470700214300ustar00rootroot00000000000000from typing import ( Any, cast, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, ) from pcs.cli.constraint import command from pcs.cli.constraint.output import print_config from pcs.cli.reports.preprocessor import ( get_duplicate_constraint_exists_preprocessor, ) from pcs.common.pacemaker.constraint import CibConstraintsDto def create_with_set(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ create order constraint with resource set object lib exposes library list argv see usage for "constraint colocation set" dict like object modifiers can contain "force" allows resource in clone/master and constraint duplicity Options: * --force - allow resource inside clone (or master), allow duplicate element * -f - CIB file """ modifiers.ensure_only_supported("--force", "-f") lib.env.report_processor.set_report_item_preprocessor( get_duplicate_constraint_exists_preprocessor(lib) ) command.create_with_set( lib.constraint_order.create_with_set, argv, modifiers ) def config_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: modifiers.ensure_only_supported("-f", "--output-format", "--full") if argv: raise CmdLineInputError() constraints_dto = cast( CibConstraintsDto, lib.constraint.get_config(evaluate_rules=True), ) print_config( CibConstraintsDto( order=constraints_dto.order, order_set=constraints_dto.order_set, ), modifiers, ) pcs-0.12.0.2/pcs/cli/constraint_ticket/000077500000000000000000000000001500417470700176075ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint_ticket/__init__.py000066400000000000000000000000001500417470700217060ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/constraint_ticket/command.py000066400000000000000000000101371500417470700216010ustar00rootroot00000000000000import dataclasses from typing import ( Any, Optional, cast, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, ) from pcs.cli.constraint import command from pcs.cli.constraint.output import print_config from pcs.cli.constraint_ticket import parse_args from pcs.cli.reports.output import error from pcs.cli.reports.preprocessor import ( get_duplicate_constraint_exists_preprocessor, ) from pcs.common import reports from pcs.common.pacemaker.constraint import CibConstraintsDto from pcs.common.reports.messages import InvalidOptions def create_with_set(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ create ticket constraint with resource set object lib exposes library list argv see usage for "constraint colocation set" dict like object modifiers can contain "force" allows resource in clone/master and constraint duplicity Options: * --force - allow resource inside clone (or master), allow duplicate element * -f - CIB file """ modifiers.ensure_only_supported("--force", "-f") lib.env.report_processor.set_report_item_preprocessor( get_duplicate_constraint_exists_preprocessor(lib) ) command.create_with_set( lib.constraint_ticket.create_with_set, argv, modifiers, ) def add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ create ticket constraint object lib exposes library list argv see usage for "constraint colocation add" dict like object modifiers can contain "force" allows resource in clone/master and constraint duplicity Options: * --force - allow resource inside clone (or master), allow duplicate element * -f - CIB file """ generic_preprocessor = get_duplicate_constraint_exists_preprocessor(lib) def _rsc_role_preprocessor( report_item: reports.ReportItem, ) -> Optional[reports.ReportItem]: if isinstance(report_item.message, InvalidOptions): new_message = dataclasses.replace( report_item.message, allowed=sorted(set(report_item.message.allowed) - {"rsc-role"}), ) return dataclasses.replace(report_item, message=new_message) return report_item def _report_item_preprocessor( report_item: reports.ReportItem, ) -> Optional[reports.ReportItem]: report_item_2 = generic_preprocessor(report_item) if not report_item_2: return None return _rsc_role_preprocessor(report_item_2) modifiers.ensure_only_supported("--force", "-f") ticket, resource_id, resource_role, options = parse_args.parse_add(argv) if "rsc-role" in options: raise CmdLineInputError( "Resource role must not be specified among options" + ", specify it before resource id" ) if resource_role: options["rsc-role"] = resource_role lib.env.report_processor.set_report_item_preprocessor( _report_item_preprocessor ) lib.constraint_ticket.create( ticket, resource_id, options, resource_in_clone_alowed=modifiers.get("--force"), duplication_alowed=modifiers.get("--force"), ) def remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) != 2: raise CmdLineInputError() ticket, resource_id = argv if not lib.constraint_ticket.remove(ticket, resource_id): raise error("no matching ticket constraint found") def config_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: modifiers.ensure_only_supported("-f", "--output-format", "--full") if argv: raise CmdLineInputError() constraints_dto = cast( CibConstraintsDto, lib.constraint.get_config(evaluate_rules=True), ) print_config( CibConstraintsDto( ticket=constraints_dto.ticket, ticket_set=constraints_dto.ticket_set, ), modifiers, ) pcs-0.12.0.2/pcs/cli/constraint_ticket/parse_args.py000066400000000000000000000022151500417470700223070ustar00rootroot00000000000000from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, KeyValueParser, ) def separate_tail_option_candidates( arg_list: Argv, ) -> tuple[list[str], list[str]]: for i, arg in enumerate(arg_list): if "=" in arg: return arg_list[:i], arg_list[i:] return arg_list, [] def parse_add(arg_list: Argv) -> tuple[str, str, str, dict[str, str]]: info, option_candidates = separate_tail_option_candidates(arg_list) if not info: raise CmdLineInputError("Ticket not specified") ticket, resource_specification = info[0], info[1:] if len(resource_specification) not in (1, 2): raise CmdLineInputError( "invalid resource specification: '{0}'".format( " ".join(resource_specification) ) ) if len(resource_specification) == 2: resource_role, resource_id = resource_specification else: resource_role = "" resource_id = resource_specification[0] return ( ticket, resource_id, resource_role, KeyValueParser(option_candidates).get_unique(), ) pcs-0.12.0.2/pcs/cli/dr.py000066400000000000000000000105141500417470700150400ustar00rootroot00000000000000from typing import ( Any, List, ) from dacite import DaciteError from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import InputModifiers from pcs.cli.reports.output import error from pcs.common.dr import ( DrConfigDto, DrConfigSiteDto, DrSiteStatusDto, ) from pcs.common.interface import dto from pcs.common.reports import codes as report_codes from pcs.common.str_tools import indent from pcs.common.types import StringSequence def config( lib: Any, argv: StringSequence, modifiers: InputModifiers, ) -> None: """ Options: None """ modifiers.ensure_only_supported() if argv: raise CmdLineInputError() config_raw = lib.dr.get_config() try: config_dto = dto.from_dict(DrConfigDto, config_raw) except ( KeyError, TypeError, ValueError, DaciteError, dto.PayloadConversionError, ) as e: raise error( "Unable to communicate with pcsd, received response:\n" f"{config_raw}" ) from e lines = ["Local site:"] lines.extend(indent(_config_site_lines(config_dto.local_site))) for site_dto in config_dto.remote_site_list: lines.append("Remote site:") lines.extend(indent(_config_site_lines(site_dto))) print("\n".join(lines)) def _config_site_lines(site_dto: DrConfigSiteDto) -> List[str]: lines = [f"Role: {site_dto.site_role.capitalize()}"] if site_dto.node_list: lines.append("Nodes:") lines.extend(indent(sorted([node.name for node in site_dto.node_list]))) return lines def set_recovery_site( lib: Any, argv: StringSequence, modifiers: InputModifiers, ) -> None: """ Options: * --request-timeout - HTTP timeout for node authorization check """ modifiers.ensure_only_supported("--request-timeout") if len(argv) != 1: raise CmdLineInputError() lib.dr.set_recovery_site(argv[0]) def status( lib: Any, argv: StringSequence, modifiers: InputModifiers, ) -> None: """ Options: * --full - show full details, node attributes and failcount * --hide-inactive - hide inactive resources * --request-timeout - HTTP timeout for node authorization check """ modifiers.ensure_only_supported( "--full", "--hide-inactive", "--request-timeout", ) if argv: raise CmdLineInputError() status_list_raw = lib.dr.status_all_sites_plaintext( hide_inactive_resources=modifiers.get("--hide-inactive"), verbose=modifiers.get("--full"), ) try: status_list = [ dto.from_dict(DrSiteStatusDto, status_raw) for status_raw in status_list_raw ] except ( KeyError, TypeError, ValueError, DaciteError, dto.PayloadConversionError, ) as e: raise error( "Unable to communicate with pcsd, received response:\n" f"{status_list_raw}" ) from e has_errors = False plaintext_parts = [] for site_status in status_list: plaintext_parts.append( "--- {local_remote} cluster - {role} site ---".format( local_remote=("Local" if site_status.local_site else "Remote"), role=site_status.site_role.capitalize(), ) ) if site_status.status_successfully_obtained: plaintext_parts.append(site_status.status_plaintext.strip()) plaintext_parts.extend(["", ""]) else: has_errors = True plaintext_parts.extend( ["Error: Unable to get status of the cluster from any node", ""] ) print("\n".join(plaintext_parts).strip()) if has_errors: raise error("Unable to get status of all sites") def destroy( lib: Any, argv: StringSequence, modifiers: InputModifiers, ) -> None: """ Options: * --skip-offline - skip unreachable nodes (including missing auth token) * --request-timeout - HTTP timeout for node authorization check """ modifiers.ensure_only_supported("--skip-offline", "--request-timeout") if argv: raise CmdLineInputError() force_flags = [] if modifiers.get("--skip-offline"): force_flags.append(report_codes.SKIP_OFFLINE_NODES) lib.dr.destroy(force_flags=force_flags) pcs-0.12.0.2/pcs/cli/fencing_topology.py000066400000000000000000000006001500417470700177730ustar00rootroot00000000000000from pcs.common.fencing_topology import ( TARGET_TYPE_ATTRIBUTE, TARGET_TYPE_NODE, TARGET_TYPE_REGEXP, ) __target_type_map = { "attrib": TARGET_TYPE_ATTRIBUTE, "node": TARGET_TYPE_NODE, "regexp": TARGET_TYPE_REGEXP, } target_type_map_cli_to_lib = __target_type_map target_type_map_lib_to_cli = { value: key for key, value in __target_type_map.items() } pcs-0.12.0.2/pcs/cli/file/000077500000000000000000000000001500417470700147775ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/file/__init__.py000066400000000000000000000000001500417470700170760ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/file/metadata.py000066400000000000000000000022611500417470700171320ustar00rootroot00000000000000import os.path from pcs.common import file_type_codes as code from pcs.common.file import FileMetadata _metadata = { code.BOOTH_CONFIG: lambda path: FileMetadata( file_type_code=code.BOOTH_CONFIG, path=path, owner_user_name=None, owner_group_name=None, permissions=None, is_binary=False, ), code.BOOTH_KEY: lambda path: FileMetadata( file_type_code=code.BOOTH_KEY, path=path, owner_user_name=None, owner_group_name=None, permissions=0o600, is_binary=True, ), code.COROSYNC_CONF: lambda path: FileMetadata( file_type_code=code.COROSYNC_CONF, path=path, owner_user_name=None, owner_group_name=None, permissions=0o644, is_binary=False, ), code.PCS_KNOWN_HOSTS: lambda: FileMetadata( file_type_code=code.PCS_KNOWN_HOSTS, path=os.path.join(os.path.expanduser("~/.pcs"), "known-hosts"), owner_user_name=None, owner_group_name=None, permissions=0o600, is_binary=False, ), } def for_file_type(file_type_code, *args, **kwargs): return _metadata[file_type_code](*args, **kwargs) pcs-0.12.0.2/pcs/cli/nvset.py000066400000000000000000000040621500417470700155730ustar00rootroot00000000000000from typing import ( Iterable, List, ) from pcs.cli.rule import ( get_in_effect_label, rule_expression_dto_to_lines, ) from pcs.common.pacemaker.nvset import CibNvsetDto from pcs.common.str_tools import ( format_name_value_id_list, format_name_value_list, format_optional, indent, ) from pcs.common.types import CibRuleInEffectStatus def filter_out_expired_nvset( nvset_dto_list: Iterable[CibNvsetDto], ) -> list[CibNvsetDto]: return [ nvset_dto for nvset_dto in nvset_dto_list if not nvset_dto.rule or nvset_dto.rule.in_effect != CibRuleInEffectStatus.EXPIRED ] def nvset_dto_list_to_lines( nvset_dto_list: Iterable[CibNvsetDto], nvset_label: str, with_ids: bool = False, ) -> List[str]: return [ line for nvset_dto in nvset_dto_list for line in nvset_dto_to_lines( nvset_dto, nvset_label=nvset_label, with_ids=with_ids ) ] def nvset_dto_to_lines( nvset: CibNvsetDto, nvset_label: str = "Options Set", with_ids: bool = False ) -> List[str]: in_effect_label = get_in_effect_label(nvset.rule) if nvset.rule else None heading_parts = [ "{label}{in_effect}:{id}".format( label=nvset_label, in_effect=format_optional(in_effect_label, " ({})"), id=format_optional(nvset.id, " {}"), ) ] if nvset.options: heading_parts.append( " ".join(format_name_value_list(sorted(nvset.options.items()))) ) if with_ids: lines = format_name_value_id_list( sorted( [ (nvpair.name, nvpair.value, nvpair.id) for nvpair in nvset.nvpairs ] ) ) else: lines = format_name_value_list( sorted([(nvpair.name, nvpair.value) for nvpair in nvset.nvpairs]) ) if nvset.rule: lines.extend( rule_expression_dto_to_lines(nvset.rule, with_ids=with_ids) ) return [" ".join(heading_parts)] + indent(lines) pcs-0.12.0.2/pcs/cli/query/000077500000000000000000000000001500417470700152255ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/query/__init__.py000066400000000000000000000000001500417470700173240ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/query/resource.py000066400000000000000000000351701500417470700174340ustar00rootroot00000000000000from typing import ( Any, Optional, cast, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( ArgsByKeywords, Argv, InputModifiers, group_by_keywords, ) from pcs.common import reports from pcs.common.resource_status import ( EXACT_CHECK_STATES, InstancesQuantifierUnsupportedException, MembersQuantifierUnsupportedException, MoreChildrenQuantifierType, QueryException, ResourceException, ResourceNonExistentException, ResourceNotInGroupException, ResourcesStatusFacade, ResourceState, ResourceStateExactCheck, ResourceType, ResourceUnexpectedTypeException, can_be_promotable, can_be_unique, ) from pcs.common.str_tools import ( format_list, format_optional, ) def _handle_query_result(result: bool, quiet: bool) -> SystemExit: if not quiet: print(result) if result: return SystemExit(0) return SystemExit(2) def _handle_resource_exception(e: ResourceException) -> None: resource_id = f"{e.resource_id}{format_optional(e.instance_id, ':{}')}" if isinstance(e, ResourceNonExistentException): raise CmdLineInputError(f"Resource '{resource_id}' does not exist") if isinstance(e, ResourceNotInGroupException): raise CmdLineInputError(f"Resource '{resource_id}' is not in a group") if isinstance(e, ResourceUnexpectedTypeException): raise CmdLineInputError( ( "Resource '{id}' has unexpected type '{real}'. This command " "works only for resources of type {expected}" ).format( id=resource_id, real=e.resource_type.value, expected=format_list(t.value for t in e.expected_types), ) ) raise CmdLineInputError(f"Unknown error with resource '{resource_id}'") def _handle_query_exception(e: QueryException) -> None: if isinstance(e, MembersQuantifierUnsupportedException): raise CmdLineInputError( "'members' quantifier can be used only on group resources or " "group instances of cloned groups" ) if isinstance(e, InstancesQuantifierUnsupportedException): raise CmdLineInputError( ( "'instances' quantifier can be used only on clone resources " "and their instances, or on bundle resources and their replicas" ) ) raise CmdLineInputError( "Unknown error with the query", show_both_usage_and_message=True ) def _handle_is_modifiers(modifiers: InputModifiers) -> bool: modifiers.ensure_only_supported("--quiet", "-f") return modifiers.is_specified("--quiet") def _handle_get_modifiers(modifiers: InputModifiers) -> None: modifiers.ensure_only_supported("-f") def _get_resource_status_facade(lib: Any) -> ResourcesStatusFacade: dto = lib.status.resources_status() return ResourcesStatusFacade.from_resources_status_dto(dto) def _parse_more_members_quantifier( sections: ArgsByKeywords, keyword: str, ) -> Optional[MoreChildrenQuantifierType]: if not sections.has_keyword(keyword): return None args = sections.get_args_flat(keyword) if len(args) != 1: raise CmdLineInputError() try: return MoreChildrenQuantifierType[args[0].upper()] except KeyError as e: raise CmdLineInputError( reports.messages.InvalidOptionValue( keyword, args[0], [ quantifier.name.lower() for quantifier in MoreChildrenQuantifierType ], ).message ) from e def _parse_expected_state( state_section: Argv, ) -> tuple[ResourceState, Optional[str]]: if not state_section or len(state_section) > 2: raise CmdLineInputError() try: expected_state = ResourceState[state_section[0].upper()] except KeyError as e: raise CmdLineInputError( reports.messages.InvalidOptionValue( "state", state_section[0], [state.name.lower() for state in ResourceState], ).message ) from e if len(state_section) == 1: return expected_state, None if expected_state not in EXACT_CHECK_STATES: raise CmdLineInputError() return expected_state, state_section[1] def _pop_resource_id(argv: Argv) -> tuple[str, Optional[str]]: if len(argv) < 1: raise CmdLineInputError() resource_id = argv.pop(0) if not resource_id: raise CmdLineInputError() if ":" in resource_id: resource_id, instance_id = resource_id.rsplit(":", 1) return resource_id, instance_id return resource_id, None def exists(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if argv: raise CmdLineInputError() quiet = _handle_is_modifiers(modifiers) raise _handle_query_result( _get_resource_status_facade(lib).exists(resource_id, instance_id), quiet, ) def is_type(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) quiet = _handle_is_modifiers(modifiers) sections = group_by_keywords(argv, ["unique", "promotable"], "type") sections.ensure_unique_keywords() type_section = sections.get_args_flat("type") if len(type_section) != 1: raise CmdLineInputError() try: expected_type = ResourceType[type_section[0].upper()] except KeyError as e: raise CmdLineInputError( reports.messages.InvalidOptionValue( option_name="resource type", option_value=type_section[0], allowed_values=[ resource_type.value for resource_type in ResourceType ], ).message ) from e check_unique = sections.has_keyword("unique") if check_unique: if sections.get_args_flat("unique"): raise CmdLineInputError() if not can_be_unique(expected_type): raise CmdLineInputError( f"type '{expected_type.value}' cannot be unique" ) check_promotable = sections.has_keyword("promotable") if check_promotable: if sections.get_args_flat("promotable"): raise CmdLineInputError() if not can_be_promotable(expected_type): raise CmdLineInputError( f"type '{expected_type.value}' cannot be promotable" ) resources_status = _get_resource_status_facade(lib) try: result = ( resources_status.get_type(resource_id, instance_id) == expected_type ) except ResourceException as e: _handle_resource_exception(e) if result and check_unique: result = resources_status.is_unique(resource_id, instance_id) if result and check_promotable: result = resources_status.is_promotable(resource_id, instance_id) raise _handle_query_result(result, quiet) def get_type(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if argv: raise CmdLineInputError() _handle_get_modifiers(modifiers) resource_status = _get_resource_status_facade(lib) try: resource_type = resource_status.get_type(resource_id, instance_id) except ResourceException as e: _handle_resource_exception(e) output = [resource_type.value] if can_be_unique(resource_type) and resource_status.is_unique( resource_id, instance_id ): output.append("unique") if can_be_promotable(resource_type) and resource_status.is_promotable( resource_id, instance_id ): output.append("promotable") print(" ".join(output)) def is_stonith(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if argv: raise CmdLineInputError() quiet = _handle_is_modifiers(modifiers) try: result = _get_resource_status_facade(lib).is_stonith( resource_id, instance_id ) except ResourceException as e: _handle_resource_exception(e) raise _handle_query_result(result, quiet) def get_members(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if argv: raise CmdLineInputError() _handle_get_modifiers(modifiers) try: members = _get_resource_status_facade(lib).get_members( resource_id, instance_id ) except ResourceException as e: _handle_resource_exception(e) print("\n".join(members)) def get_nodes(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if argv: raise CmdLineInputError() _handle_get_modifiers(modifiers) try: nodes = _get_resource_status_facade(lib).get_nodes( resource_id, instance_id ) except ResourceException as e: _handle_resource_exception(e) print("\n".join(nodes)) def is_state(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ # pylint: disable=too-many-locals resource_id, instance_id = _pop_resource_id(argv) sections = group_by_keywords( argv, set( ["on-node", "members", "instances"], ), implicit_first_keyword="state", ) sections.ensure_unique_keywords() expected_state, expected_value = _parse_expected_state( sections.get_args_flat("state") ) expected_node_name = None if sections.has_keyword("on-node"): node_section = sections.get_args_flat("on-node") if len(node_section) != 1: raise CmdLineInputError() expected_node_name = node_section[0] members_quantifier = _parse_more_members_quantifier(sections, "members") instances_quantifier = _parse_more_members_quantifier(sections, "instances") quiet = _handle_is_modifiers(modifiers) resource_status = _get_resource_status_facade(lib) try: if expected_value is not None and ( expected_state in (ResourceState.LOCKED_TO, ResourceState.PENDING) ): result = resource_status.is_state_exact_value( resource_id, instance_id, cast(ResourceStateExactCheck, expected_state), expected_value, expected_node_name, members_quantifier, instances_quantifier, ) else: result = resource_status.is_state( resource_id, instance_id, expected_state, expected_node_name, members_quantifier, instances_quantifier, ) except ResourceException as e: _handle_resource_exception(e) except QueryException as e: _handle_query_exception(e) except NotImplementedError as e: raise CmdLineInputError(str(e)) from e raise _handle_query_result(result, quiet) def _handle_is_in_container( real_id: Optional[str], expected_id: Optional[str], quiet: bool ) -> SystemExit: is_in_container = real_id is not None and ( expected_id is None or real_id == expected_id ) if not quiet: print(is_in_container) if real_id is not None: print(real_id) if not is_in_container: return SystemExit(2) return SystemExit(0) def is_in_group(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if len(argv) > 1: raise CmdLineInputError() expected_group_id = argv[0] if argv else None quiet = _handle_is_modifiers(modifiers) try: group_id = _get_resource_status_facade(lib).get_parent_group_id( resource_id, instance_id ) except ResourceException as e: _handle_resource_exception(e) raise _handle_is_in_container(group_id, expected_group_id, quiet) def is_in_clone(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if len(argv) > 1: raise CmdLineInputError() expected_clone_id = argv[0] if argv else None quiet = _handle_is_modifiers(modifiers) try: clone_id = _get_resource_status_facade(lib).get_parent_clone_id( resource_id, instance_id ) except ResourceException as e: _handle_resource_exception(e) raise _handle_is_in_container(clone_id, expected_clone_id, quiet) def is_in_bundle(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if len(argv) > 1: raise CmdLineInputError() expected_bundle_id = argv[0] if argv else None quiet = _handle_is_modifiers(modifiers) try: bundle_id = _get_resource_status_facade(lib).get_parent_bundle_id( resource_id, instance_id ) except ResourceException as e: _handle_resource_exception(e) raise _handle_is_in_container(bundle_id, expected_bundle_id, quiet) def get_index_in_group(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --quiet - do not print anything to output """ resource_id, instance_id = _pop_resource_id(argv) if argv: raise CmdLineInputError() _handle_get_modifiers(modifiers) try: index = _get_resource_status_facade(lib).get_index_in_group( resource_id, instance_id ) except ResourceException as e: _handle_resource_exception(e) print(index) pcs-0.12.0.2/pcs/cli/reports/000077500000000000000000000000001500417470700155565ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/reports/__init__.py000066400000000000000000000002101500417470700176600ustar00rootroot00000000000000from . import ( messages, output, ) from .output import process_library_reports from .processor import ReportProcessorToConsole pcs-0.12.0.2/pcs/cli/reports/messages.py000066400000000000000000000523741500417470700177520ustar00rootroot00000000000000from functools import partial from typing import ( Any, Dict, Mapping, Optional, get_type_hints, ) from pcs.common import file_type_codes from pcs.common.reports import ( codes, const, dto, item, messages, types, ) from pcs.common.str_tools import ( format_list, format_optional, format_plural, indent, transform, ) from pcs.common.tools import get_all_subclasses class CliReportMessage: def __init__(self, dto_obj: dto.ReportItemMessageDto) -> None: self._dto_obj = dto_obj @property def code(self) -> str: return self._dto_obj.code @property def message(self) -> str: return self._dto_obj.message @property def payload(self) -> Mapping[str, Any]: return self._dto_obj.payload def get_message_with_force_text( self, force_code: Optional[types.ForceCode] ) -> str: force_text_map = { codes.SKIP_OFFLINE_NODES: ", use --skip-offline to override", } force_text = ( force_text_map.get(force_code, ", use --force to override") if force_code else "" ) return f"{self.message}{force_text}" class CliReportMessageCustom(CliReportMessage): _obj: item.ReportItemMessage def __init__(self, dto_obj: dto.ReportItemMessageDto) -> None: super().__init__(dto_obj) self._obj = get_type_hints(self.__class__).get("_obj")( # type: ignore **dto_obj.payload ) @property def message(self) -> str: raise NotImplementedError() class ResourceManagedNoMonitorEnabled(CliReportMessageCustom): _obj: messages.ResourceManagedNoMonitorEnabled @property def message(self) -> str: return ( f"Resource '{self._obj.resource_id}' has no enabled monitor " "operations. Re-run with '--monitor' to enable them." ) class ResourceUnmoveUnbanPcmkExpiredNotSupported(CliReportMessageCustom): _obj: messages.ResourceUnmoveUnbanPcmkExpiredNotSupported @property def message(self) -> str: return "--expired not supported, please upgrade pacemaker" class CannotUnmoveUnbanResourceMasterResourceNotPromotable( CliReportMessageCustom ): _obj: messages.CannotUnmoveUnbanResourceMasterResourceNotPromotable @property def message(self) -> str: return _resource_move_ban_clear_master_resource_not_promotable( self._obj.promotable_id ) class InvalidCibContent(CliReportMessageCustom): _obj: messages.InvalidCibContent @property def message(self) -> str: return "invalid cib:\n{report}{more_verbose}".format( report=self._obj.report, more_verbose=format_optional( self._obj.can_be_more_verbose, "\n\nUse --full for more details.", ), ) class NodeCommunicationErrorNotAuthorized(CliReportMessageCustom): _obj: messages.NodeCommunicationErrorNotAuthorized @property def message(self) -> str: return ( f"Unable to authenticate to {self._obj.node} ({self._obj.reason})" f", try running 'pcs host auth {self._obj.node}'" ) class NodeCommunicationErrorTimedOut(CliReportMessageCustom): _obj: messages.NodeCommunicationErrorTimedOut @property def message(self) -> str: return ( f"{self._obj.node}: Connection timeout, check if pcsd is running " "there or try setting higher timeout with --request-timeout option " f"({self._obj.reason})" ) class CannotBanResourceMasterResourceNotPromotable(CliReportMessageCustom): _obj: messages.CannotBanResourceMasterResourceNotPromotable @property def message(self) -> str: return _resource_move_ban_clear_master_resource_not_promotable( self._obj.promotable_id ) class CannotMoveResourceMasterResourceNotPromotable(CliReportMessageCustom): _obj: messages.CannotMoveResourceMasterResourceNotPromotable @property def message(self) -> str: return _resource_move_ban_clear_master_resource_not_promotable( self._obj.promotable_id ) class CannotMoveResourceNotRunning(CliReportMessageCustom): _obj: messages.CannotMoveResourceNotRunning @property def message(self) -> str: return ( f"{self._obj.message}, use 'pcs resource move-with-constraint' or " "'pcs constraint location' commands instead" ) class SbdWatchdogTestMultipleDevices(CliReportMessageCustom): _obj: messages.SbdWatchdogTestMultipleDevices @property def message(self) -> str: return ( "Multiple watchdog devices available, therefore, watchdog which " "should be tested has to be specified. To list available watchdog " "devices use command 'pcs stonith sbd watchdog list'" ) class NodeUsedAsTieBreaker(CliReportMessageCustom): _obj: messages.NodeUsedAsTieBreaker @property def message(self) -> str: return ( self._obj.message + ", run 'pcs quorum device update model " "tie_breaker=' to change it" ) class NodesToRemoveUnreachable(CliReportMessageCustom): _obj: messages.NodesToRemoveUnreachable @property def message(self) -> str: return ( "Removed {node} {nodes} could not be reached and subsequently " "deconfigured. Run 'pcs cluster destroy' on the unreachable " "{node}." ).format( node=format_plural(self._obj.node_list, "node"), nodes=format_list(self._obj.node_list), ) class UnableToConnectToAllRemainingNodes(CliReportMessageCustom): _obj: messages.UnableToConnectToAllRemainingNodes @property def message(self) -> str: pluralize = partial(format_plural, self._obj.node_list) return ( "Remaining cluster {node} {nodes} could not be reached, run " "'pcs cluster sync' on any currently online node once the " "unreachable {one} become available" ).format( node=pluralize("node"), nodes=format_list(self._obj.node_list), one=pluralize("one"), ) class CannotRemoveAllClusterNodes(CliReportMessageCustom): _obj: messages.CannotRemoveAllClusterNodes @property def message(self) -> str: return ( "No nodes would be left in the cluster, if you intend to destroy " "the whole cluster, run 'pcs cluster destroy --all' instead" ) class WaitForNodeStartupWithoutStart(CliReportMessageCustom): _obj: messages.WaitForNodeStartupWithoutStart @property def message(self) -> str: return "Cannot specify '--wait' without specifying '--start'" class HostNotFound(CliReportMessageCustom): _obj: messages.HostNotFound @property def message(self) -> str: pluralize = partial(format_plural, self._obj.host_list) return ( ( "{host} {hosts_comma} {_is} not known to pcs, try to " "authenticate the {host} using 'pcs host auth {hosts_space}' " "command" ) .format( host=pluralize("host"), hosts_comma=format_list(self._obj.host_list), _is=pluralize("is"), hosts_space=" ".join(sorted(self._obj.host_list)), ) .capitalize() ) class UseCommandNodeRemoveRemote(CliReportMessageCustom): _obj: messages.UseCommandNodeRemoveRemote @property def message(self) -> str: return self._obj.message + ", use 'pcs cluster node remove-remote'" class UseCommandNodeRemoveGuest(CliReportMessageCustom): _obj: messages.UseCommandNodeRemoveGuest @property def message(self) -> str: return self._obj.message + ", use 'pcs cluster node remove-guest'" class UseCommandNodeAddGuest(CliReportMessageCustom): _obj: messages.UseCommandNodeAddGuest @property def message(self) -> str: return ( "this command is not sufficient for creating a guest node, use" " 'pcs cluster node add-guest'" ) class UseCommandNodeAddRemote(CliReportMessageCustom): _obj: messages.UseCommandNodeAddRemote @property def message(self) -> str: return ( "this command is not sufficient for creating a remote connection," " use 'pcs cluster node add-remote'" ) class CorosyncNodeConflictCheckSkipped(CliReportMessageCustom): _obj: messages.CorosyncNodeConflictCheckSkipped @property def message(self) -> str: return ( "Unable to check if there is a conflict with nodes set in corosync " "because {reason}" ).format(reason=_skip_reason_to_string(self._obj.reason_type)) class CorosyncNotRunningCheckFinishedRunning(CliReportMessageCustom): _obj: messages.CorosyncNotRunningCheckFinishedRunning @property def message(self) -> str: return self._obj.message + ( """ Run "pcs cluster stop {node_list}" to stop the {node} or """ """"pcs cluster stop --all" to stop the whole cluster.""" ).format( node=format_plural(self._obj.node_list, "node"), node_list=format_list(self._obj.node_list, separator=" "), ) class CorosyncQuorumAtbWillBeEnabledDueToSbdClusterIsRunning( CliReportMessageCustom ): _obj: messages.CorosyncQuorumAtbWillBeEnabledDueToSbdClusterIsRunning @property def message(self) -> str: return self._obj.message + ( " Use commands 'pcs cluster stop --all', 'pcs quorum update " "auto_tie_breaker=1', 'pcs cluster start --all'." ) class LiveEnvironmentNotConsistent(CliReportMessageCustom): _obj: messages.LiveEnvironmentNotConsistent @property def message(self) -> str: return ( "When {given} {_is} specified, {missing} must be specified as well" ).format( given=format_list( transform( self._obj.mocked_files, _file_role_to_option_translation ) ), _is=format_plural(self._obj.mocked_files, "is"), missing=format_list( transform( self._obj.required_files, _file_role_to_option_translation ) ), ) class LiveEnvironmentRequired(CliReportMessageCustom): _obj: messages.LiveEnvironmentRequired @property def message(self) -> str: return "This command does not support {forbidden_options}".format( forbidden_options=format_list( transform( self._obj.forbidden_options, _file_role_to_option_translation, ) ), ) class LiveEnvironmentRequiredForLocalNode(CliReportMessageCustom): _obj: messages.LiveEnvironmentRequiredForLocalNode @property def message(self) -> str: return "Node(s) must be specified if -f is used" class ServiceCommandsOnNodesSkipped(CliReportMessageCustom): _obj: messages.ServiceCommandsOnNodesSkipped @property def message(self) -> str: return ( "Running action(s) {actions} on {nodes} was skipped because " "{reason}. Please, run the action(s) manually." ).format( actions=format_list(self._obj.action_list), nodes=format_list(self._obj.node_list), reason=_skip_reason_to_string(self._obj.reason_type), ) class FilesRemoveFromNodesSkipped(CliReportMessageCustom): _obj: messages.FilesRemoveFromNodesSkipped @property def message(self) -> str: return ( "Removing {files} from {nodes} was skipped because {reason}. " "Please, remove the file(s) manually." ).format( files=format_list(self._obj.file_list), nodes=format_list(self._obj.node_list), reason=_skip_reason_to_string(self._obj.reason_type), ) class FilesDistributionSkipped(CliReportMessageCustom): _obj: messages.FilesDistributionSkipped @property def message(self) -> str: return ( "Distribution of {files} to {nodes} was skipped because " "{reason}. Please, distribute the file(s) manually." ).format( files=format_list(self._obj.file_list), nodes=format_list(self._obj.node_list), reason=_skip_reason_to_string(self._obj.reason_type), ) class WaitForIdleNotLiveCluster(CliReportMessageCustom): _obj: messages.WaitForIdleNotLiveCluster @property def message(self) -> str: return "Cannot use '-f' together with '--wait'" class TagCannotRemoveReferencesWithoutRemovingTag(CliReportMessageCustom): _obj: messages.TagCannotRemoveReferencesWithoutRemovingTag @property def message(self) -> str: tag_id = self._obj.tag_id return ( f"There would be no references left in the tag '{tag_id}', please " f"remove the whole tag using the 'pcs tag remove {tag_id}' command" ) class RuleExpressionParseError(CliReportMessageCustom): _obj: messages.RuleExpressionParseError @property def message(self) -> str: # Messages coming from the parser are not very useful and readable, # they mostly contain one line grammar expression covering the whole # rule. No user would be able to parse that. Therefore we omit the # messages. marker = "-" * (self._obj.column_number - 1) + "^" return ( f"'{self._obj.rule_string}' is not a valid rule expression, parse " f"error near or after line {self._obj.line_number} column " f"{self._obj.column_number}\n" f" {self._obj.rule_line}\n" f" {marker}" ) class CibNvsetAmbiguousProvideNvsetId(CliReportMessageCustom): _obj: messages.CibNvsetAmbiguousProvideNvsetId @property def message(self) -> str: command_map = { const.PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE: ( "pcs resource defaults set update" ), const.PCS_COMMAND_OPERATION_DEFAULTS_UPDATE: ( "pcs resource op defaults set update" ), } command = command_map.get(self._obj.pcs_command, "") return ( f"Several options sets exist, please use the '{command}' command " "and specify an option set ID" ) class UnableToGetAgentMetadata(CliReportMessageCustom): _obj: messages.UnableToGetAgentMetadata @property def message(self) -> str: reason = ", ".join(self._obj.reason.splitlines()) return ( f"Agent '{self._obj.agent}' is not installed or does not provide " f"valid metadata: {reason}" ) class HostAlreadyInClusterConfig(CliReportMessageCustom): _obj: messages.HostAlreadyInClusterConfig @property def message(self) -> str: return ( f"{self._obj.host_name}: The host seems to be in a cluster already " "as cluster configuration files have been found on the host. If " "the host is not part of a cluster, run 'pcs cluster destroy' on " f"host '{self._obj.host_name}' to remove those configuration files" ) class CannotLeaveGroupEmptyAfterMove(CliReportMessageCustom): _obj: messages.CannotLeaveGroupEmptyAfterMove @property def message(self) -> str: return ( f"{self._obj.message} Please, use the 'pcs resource " f"ungroup {self._obj.group_id}' command first." ) class StonithRestartlessUpdateUnableToPerform(CliReportMessageCustom): _obj: messages.StonithRestartlessUpdateUnableToPerform @property def message(self) -> str: msg = self._obj.message if ( self._obj.reason_type == const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_NOT_RUNNING ): msg += ", please use command 'pcs stonith update' instead" return msg class InvalidResourceAgentName(CliReportMessageCustom): _obj: messages.InvalidResourceAgentName @property def message(self) -> str: return ( f"{self._obj.message} List of standards and providers can be " "obtained by using commands 'pcs resource standards' and " "'pcs resource providers'." ) class InvalidStonithAgentName(CliReportMessageCustom): _obj: messages.InvalidStonithAgentName @property def message(self) -> str: return ( f"{self._obj.message} List of agents can be obtained by using " "command 'pcs stonith list'." ) class CommandArgumentTypeMismatch(CliReportMessageCustom): _obj: messages.CommandArgumentTypeMismatch @property def message(self) -> str: cmd = ( {const.PCS_COMMAND_STONITH_CREATE: "pcs stonith create"}.get( self._obj.command_to_use_instead ) if self._obj.command_to_use_instead else None ) additional_msg = format_optional(cmd, " Use '{}' command instead.") return f"{self._obj.message}{additional_msg}" class AgentSelfValidationResult(CliReportMessageCustom): _obj: messages.AgentSelfValidationResult _base_msg = "Validation result from agent" @property def _formatted_result(self) -> str: return "\n".join(indent(self._obj.result.splitlines())) @property def message(self) -> str: return f"{self._base_msg}:\n{self._formatted_result}" def get_message_with_force_text( self, force_code: Optional[types.ForceCode] ) -> str: force_text = ( " (use --force to override)" if force_code == codes.FORCE else "" ) return f"{self._base_msg}{force_text}:\n{self._formatted_result}" class AgentSelfValidationAutoOnWithWarnings(CliReportMessageCustom): _obj: messages.AgentSelfValidationAutoOnWithWarnings @property def message(self) -> str: return ( "Validating resource options using the resource agent itself is " "enabled by default and produces warnings. In a future version, " "this might be changed to errors. Specify --agent-validation to " "switch to the future behavior." ) class BoothAuthfileNotUsed(CliReportMessageCustom): _obj: messages.BoothAuthfileNotUsed @property def message(self) -> str: return ( f"{self._obj.message}. Run 'pcs booth enable-authfile --name " f"{self._obj.instance}' to enable usage of authfile." ) class BoothUnsupportedOptionEnableAuthfile(CliReportMessageCustom): _obj: messages.BoothUnsupportedOptionEnableAuthfile @property def message(self) -> str: return ( f"{self._obj.message}. Run 'pcs booth clean-enable-authfile --name " f"{self._obj.instance}' to remove the option." ) class ResourceMoveAutocleanSimulationFailure(CliReportMessageCustom): _obj: messages.ResourceMoveAutocleanSimulationFailure @property def message(self) -> str: if not self._obj.move_constraint_left_in_cib: return self._obj.message node = format_optional(self._obj.node, " {}") return ( f"{self._obj.message} Run 'pcs resource clear " f"{self._obj.resource_id}{node}' to remove the constraint." ) class ResourceWaitDeprecated(CliReportMessageCustom): _obj: messages.ResourceWaitDeprecated @property def message(self) -> str: return ( "Using '--wait' is deprecated. Instead, use the 'pcs status wait' " "command to wait for the cluster to settle into stable state. Use " "the 'pcs status query resource' commands to verify that the " "resource is in the expected state after the wait." ) def _create_report_msg_map() -> Dict[str, type]: result: Dict[str, type] = {} for report_msg_cls in get_all_subclasses(CliReportMessageCustom): # pylint: disable=protected-access code = ( get_type_hints(report_msg_cls) .get("_obj", item.ReportItemMessage) ._code ) if code: if code in result: raise AssertionError() result[code] = report_msg_cls return result REPORT_MSG_MAP = _create_report_msg_map() def report_item_msg_from_dto(obj: dto.ReportItemMessageDto) -> CliReportMessage: return REPORT_MSG_MAP.get(obj.code, CliReportMessage)(obj) _file_role_to_option_translation: Mapping[file_type_codes.FileTypeCode, str] = { file_type_codes.BOOTH_CONFIG: "--booth-conf", file_type_codes.BOOTH_KEY: "--booth-key", file_type_codes.CIB: "-f", file_type_codes.COROSYNC_CONF: "--corosync_conf", } def _resource_move_ban_clear_master_resource_not_promotable( promotable_id: str, ) -> str: return ( "when specifying --promoted you must use the promotable clone id{_id}" ).format( _id=format_optional(promotable_id, " ({})"), ) def _skip_reason_to_string(reason: types.ReasonType) -> str: return { const.REASON_NOT_LIVE_CIB: ( "the command does not run on a live cluster (e.g. -f was used)" ), const.REASON_UNREACHABLE: "pcs is unable to connect to the node(s)", }.get(reason, reason) pcs-0.12.0.2/pcs/cli/reports/output.py000066400000000000000000000035501500417470700174730ustar00rootroot00000000000000import sys from typing import Optional from pcs.cli.common.tools import print_to_stderr from pcs.common.reports import ( ReportItemList, ReportItemSeverity, ) from pcs.common.reports.dto import ReportItemContextDto from .messages import report_item_msg_from_dto def warn(message: str) -> None: print_to_stderr(f"Warning: {message}") def deprecation_warning(message: str) -> None: print_to_stderr(f"Deprecation Warning: {message}") def error(message: str) -> SystemExit: print_to_stderr(f"Error: {message}") return SystemExit(1) def add_context_to_message( msg: str, context: Optional[ReportItemContextDto] ) -> str: if context: msg = f"{context.node}: {msg}" return msg def process_library_reports( report_item_list: ReportItemList, exit_on_error: bool = True ) -> None: if not report_item_list: raise error("Errors have occurred, therefore pcs is unable to continue") critical_error = False for report_item in report_item_list: report_dto = report_item.to_dto() cli_report_msg = report_item_msg_from_dto(report_dto.message) msg = add_context_to_message(cli_report_msg.message, report_dto.context) severity = report_dto.severity.level if severity == ReportItemSeverity.WARNING: warn(msg) continue if severity == ReportItemSeverity.DEPRECATION: deprecation_warning(msg) continue if severity != ReportItemSeverity.ERROR: print_to_stderr(msg) continue error( add_context_to_message( cli_report_msg.get_message_with_force_text( report_item.severity.force_code ), report_dto.context, ) ) critical_error = True if critical_error and exit_on_error: sys.exit(1) pcs-0.12.0.2/pcs/cli/reports/preprocessor.py000066400000000000000000000123721500417470700206630ustar00rootroot00000000000000from typing import ( Any, Optional, cast, ) from pcs.cli.common.output import INDENT_STEP from pcs.cli.common.tools import print_to_stderr from pcs.cli.constraint import output from pcs.common import reports from pcs.common.pacemaker.constraint import CibConstraintsDto from pcs.common.str_tools import ( format_list, indent, ) from pcs.common.types import StringIterable from pcs.lib.errors import LibraryError from .output import process_library_reports from .processor import ReportItemPreprocessor def get_duplicate_constraint_exists_preprocessor( lib: Any, ) -> ReportItemPreprocessor: constraints_dto: Optional[CibConstraintsDto] = None def _report_item_preprocessor( report_item: reports.ReportItem, ) -> Optional[reports.ReportItem]: """ Provide additional info based on DuplicateConstraintsExist message Drop deprecated DuplicateConstraintsList message. This message contained structured info about duplicate constraints. Intercept DuplicateConstraintsExist message and extract constraint IDs from it. Load constraints from CIB using library and print those matching IDs from the message. """ # pylint: disable=too-many-branches nonlocal constraints_dto def my_print(lines: StringIterable) -> None: print_to_stderr("\n".join(indent(lines, INDENT_STEP))) if ( report_item.message.code == reports.deprecated_codes.DUPLICATE_CONSTRAINTS_LIST ): return None if isinstance( report_item.message, reports.messages.DuplicateConstraintsExist ): duplicate_id_list = report_item.message.constraint_ids try: if constraints_dto is None: constraints_dto = cast( CibConstraintsDto, lib.constraint.get_config(evaluate_rules=False), ) print_to_stderr("Duplicate constraints:") for dto_lp in constraints_dto.location: if dto_lp.attributes.constraint_id in duplicate_id_list: my_print( output.location.plain_constraint_to_text( dto_lp, True ) ) for dto_ls in constraints_dto.location_set: if dto_ls.attributes.constraint_id in duplicate_id_list: my_print( output.location.set_constraint_to_text(dto_ls, True) ) for dto_cp in constraints_dto.colocation: if dto_cp.attributes.constraint_id in duplicate_id_list: my_print( output.colocation.plain_constraint_to_text( dto_cp, True ) ) for dto_cs in constraints_dto.colocation_set: if dto_cs.attributes.constraint_id in duplicate_id_list: my_print( output.colocation.set_constraint_to_text( dto_cs, True ) ) for dto_op in constraints_dto.order: if dto_op.attributes.constraint_id in duplicate_id_list: my_print( output.order.plain_constraint_to_text(dto_op, True) ) for dto_os in constraints_dto.order_set: if dto_os.attributes.constraint_id in duplicate_id_list: my_print( output.order.set_constraint_to_text(dto_os, True) ) for dto_tp in constraints_dto.ticket: if dto_tp.attributes.constraint_id in duplicate_id_list: my_print( output.ticket.plain_constraint_to_text(dto_tp, True) ) for dto_ts in constraints_dto.ticket_set: if dto_ts.attributes.constraint_id in duplicate_id_list: my_print( output.ticket.set_constraint_to_text(dto_ts, True) ) except LibraryError as e: # If reading constraints failed, print their IDs and ignore the # exception. We want to print remaining reports from the # originally called command. Raising the exception would # prevent that. Also, it is not correct to exit with an error # just because we were unable to get optional additional info # for printing a report. if e.output: print_to_stderr(e.output) if e.args: process_library_reports( cast(reports.ReportItemList, e.args), exit_on_error=False, ) print_to_stderr( "Duplicate constraints: " + format_list(duplicate_id_list) ) return report_item return _report_item_preprocessor pcs-0.12.0.2/pcs/cli/reports/processor.py000066400000000000000000000060001500417470700201430ustar00rootroot00000000000000from typing import ( Callable, Iterable, List, Optional, Set, ) from pcs.cli.common.tools import print_to_stderr from pcs.common.reports import ( ReportItem, ReportItemSeverity, ReportProcessor, ) from pcs.common.reports.dto import ReportItemDto from pcs.common.reports.types import SeverityLevel from .messages import report_item_msg_from_dto from .output import ( add_context_to_message, deprecation_warning, error, warn, ) ReportItemPreprocessor = Callable[[ReportItem], Optional[ReportItem]] class ReportProcessorToConsole(ReportProcessor): def __init__(self, debug: bool = False) -> None: super().__init__() self.debug = debug self._ignore_severities = self._get_ignored_severities([]) self._report_item_preprocessor: ReportItemPreprocessor = lambda x: x def _do_report(self, report_item: ReportItem) -> None: filtered_report_item = self._report_item_preprocessor(report_item) if not filtered_report_item: return report_item_dto = filtered_report_item.to_dto() if report_item_dto.severity.level not in self._ignore_severities: print_report(report_item_dto) def _get_ignored_severities( self, suppressed_severity_list: Iterable[SeverityLevel] ) -> Set[SeverityLevel]: ignore_severities = set(suppressed_severity_list) if self.debug: ignore_severities -= {ReportItemSeverity.DEBUG} else: ignore_severities |= {ReportItemSeverity.DEBUG} return ignore_severities def suppress_reports_of_severity( self, severity_list: Iterable[SeverityLevel] ) -> None: self._ignore_severities = self._get_ignored_severities(severity_list) def set_report_item_preprocessor( self, report_item_preprocessor: ReportItemPreprocessor, ) -> None: self._report_item_preprocessor = report_item_preprocessor def print_report(report_item_dto: ReportItemDto) -> None: cli_report_msg = report_item_msg_from_dto(report_item_dto.message) msg = cli_report_msg.message if not msg: return msg = add_context_to_message(msg, report_item_dto.context) severity = report_item_dto.severity.level if severity == ReportItemSeverity.ERROR: error( add_context_to_message( cli_report_msg.get_message_with_force_text( report_item_dto.severity.force_code ), report_item_dto.context, ) ) elif severity == ReportItemSeverity.WARNING: warn(msg) elif severity == ReportItemSeverity.DEPRECATION: deprecation_warning(msg) else: print_to_stderr(msg) def has_errors(report_list_dto: List[ReportItemDto]) -> bool: for report_item_dto in report_list_dto: if _is_error(report_item_dto): return True return False def _is_error(report_item_dto: ReportItemDto) -> bool: return report_item_dto.severity.level == ReportItemSeverity.ERROR pcs-0.12.0.2/pcs/cli/resource/000077500000000000000000000000001500417470700157075ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/resource/__init__.py000066400000000000000000000000001500417470700200060ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/resource/command.py000066400000000000000000000036271500417470700177070ustar00rootroot00000000000000import json from typing import Any from pcs.cli.common.output import ( format_cmd_list, lines_to_str, smart_wrap_text, ) from pcs.cli.common.parse_args import ( OUTPUT_FORMAT_VALUE_CMD, OUTPUT_FORMAT_VALUE_JSON, Argv, InputModifiers, ) from pcs.cli.resource.output import ( ResourcesConfigurationFacade, resources_to_cmd, resources_to_text, ) from pcs.common.interface import dto from pcs.common.pacemaker.resource.list import CibResourcesDto def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: output = config_common(lib, argv, modifiers, stonith=False) if output: print(output) def config_common( lib: Any, argv: Argv, modifiers: InputModifiers, stonith: bool ) -> str: """ Also used by stonith commands. Options: * -f - CIB file * --output-format - supported formats: text, cmd, json """ modifiers.ensure_only_supported("-f", output_format_supported=True) resources_facade = ( ResourcesConfigurationFacade.from_resources_dto( lib.resource.get_configured_resources() ) .filter_stonith(stonith) .filter_resources(argv) ) output_format = modifiers.get_output_format() if output_format == OUTPUT_FORMAT_VALUE_CMD: output = format_cmd_list( [" \\\n".join(cmd) for cmd in resources_to_cmd(resources_facade)] ) elif output_format == OUTPUT_FORMAT_VALUE_JSON: output = json.dumps( dto.to_dict( CibResourcesDto( primitives=resources_facade.primitives, clones=resources_facade.clones, groups=resources_facade.groups, bundles=resources_facade.bundles, ) ) ) else: output = lines_to_str( smart_wrap_text(resources_to_text(resources_facade)) ) return output pcs-0.12.0.2/pcs/cli/resource/output.py000066400000000000000000001050441500417470700176250ustar00rootroot00000000000000import shlex from collections import defaultdict from collections.abc import ( Container, Sequence, ) from typing import ( Dict, List, Optional, Tuple, Union, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.output import ( INDENT_STEP, bool_to_cli_value, format_wrap_for_terminal, options_to_cmd, pairs_to_cmd, ) from pcs.cli.nvset import nvset_dto_to_lines from pcs.cli.reports.output import warn from pcs.cli.resource_agent import is_stonith from pcs.common import resource_agent from pcs.common.pacemaker.defaults import CibDefaultsDto from pcs.common.pacemaker.nvset import CibNvsetDto from pcs.common.pacemaker.resource.bundle import ( CibResourceBundleContainerRuntimeOptionsDto, CibResourceBundleDto, CibResourceBundleNetworkOptionsDto, CibResourceBundlePortMappingDto, CibResourceBundleStorageMappingDto, ) from pcs.common.pacemaker.resource.clone import CibResourceCloneDto from pcs.common.pacemaker.resource.group import CibResourceGroupDto from pcs.common.pacemaker.resource.list import CibResourcesDto from pcs.common.pacemaker.resource.operations import ( OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME, CibResourceOperationDto, ) from pcs.common.pacemaker.resource.primitive import CibResourcePrimitiveDto from pcs.common.resource_agent.dto import ( ResourceAgentNameDto, get_resource_agent_full_name, ) from pcs.common.str_tools import ( format_list, format_name_value_list, format_optional, format_plural, indent, ) from pcs.common.types import StringIterable def _get_ocf_check_level_from_operation( operation_dto: CibResourceOperationDto, ) -> Optional[str]: for nvset in operation_dto.instance_attributes: for nvpair in nvset.nvpairs: if nvpair.name == OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME: return nvpair.value return None def _resource_operation_to_pairs( operation_dto: CibResourceOperationDto, ) -> List[Tuple[str, str]]: pairs = [("interval", operation_dto.interval)] if operation_dto.id: pairs.append(("id", operation_dto.id)) if operation_dto.start_delay: pairs.append(("start-delay", operation_dto.start_delay)) elif operation_dto.interval_origin: pairs.append(("interval-origin", operation_dto.interval_origin)) if operation_dto.timeout: pairs.append(("timeout", operation_dto.timeout)) if operation_dto.enabled is not None: pairs.append(("enabled", bool_to_cli_value(operation_dto.enabled))) if operation_dto.record_pending is not None: pairs.append( ("record-pending", bool_to_cli_value(operation_dto.record_pending)) ) if operation_dto.role: pairs.append(("role", operation_dto.role)) if operation_dto.on_fail: pairs.append(("on-fail", operation_dto.on_fail)) ocf_check_level = _get_ocf_check_level_from_operation(operation_dto) if ocf_check_level: pairs.append((OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME, ocf_check_level)) return pairs def _resource_operation_to_str( operation_dto: CibResourceOperationDto, ) -> List[str]: lines = [] op_pairs = [ pair for pair in _resource_operation_to_pairs(operation_dto) if pair[0] != "id" ] if op_pairs: lines.append(" ".join(format_name_value_list(op_pairs))) # TODO: add support for meta and instance attributes once it is supported # by pcs return [ "{name}:{id}".format( name=operation_dto.name, id=format_optional(operation_dto.id, " {}"), ) ] + indent(lines, indent_step=INDENT_STEP) def resource_agent_parameter_metadata_to_text( parameter: resource_agent.dto.ResourceAgentParameterDto, ) -> list[str]: # pylint: disable=too-many-branches # title line param_title = [parameter.name] if parameter.deprecated_by: param_title.append( "(deprecated by {})".format(", ".join(parameter.deprecated_by)) ) elif parameter.deprecated: param_title.append("(deprecated)") if parameter.required: param_title.append("(required)") if parameter.unique_group: if parameter.unique_group.startswith( resource_agent.const.DEFAULT_UNIQUE_GROUP_PREFIX ): param_title.append("(unique)") else: param_title.append(f"(unique group: {parameter.unique_group})") if parameter.advanced: param_title.append("(advanced use only)") # description lines text: list[str] = [] if parameter.deprecated_desc: text.append("DEPRECATED: {parameter.deprecated_desc}") desc = "" if parameter.longdesc: desc = parameter.longdesc.replace("\n", " ") elif parameter.shortdesc: desc = parameter.shortdesc.replace("\n", " ") else: desc = "No description available" text.append(f"Description: {desc}") if parameter.enum_values: text.append( "Allowed values: {}".format(format_list(parameter.enum_values)) ) elif parameter.type: text.append(f"Type: {parameter.type}") if parameter.default: text.append(f"Default: {parameter.default}") return [" ".join(param_title)] + indent(text) def resource_agent_metadata_to_text( metadata: resource_agent.dto.ResourceAgentMetadataDto, default_operations: List[CibResourceOperationDto], verbose: bool = False, ) -> List[str]: output = [] _is_stonith = is_stonith(metadata.name) agent_name = ( metadata.name.type if _is_stonith else get_resource_agent_full_name(metadata.name) ) if metadata.shortdesc: output.extend( format_wrap_for_terminal( "{agent_name} - {shortdesc}".format( agent_name=agent_name, shortdesc=metadata.shortdesc.replace("\n", " "), ), ) ) else: output.append(agent_name) if metadata.longdesc: output.append("") output.extend( format_wrap_for_terminal( metadata.longdesc.replace("\n", " "), subsequent_indent=0 ) ) params = [] for param in metadata.parameters: if not verbose and (param.advanced or param.deprecated): continue params.extend(resource_agent_parameter_metadata_to_text(param)) if params: output.append("") if _is_stonith: output.append("Stonith options:") else: output.append("Resource options:") output.extend(indent(params, indent_step=INDENT_STEP)) operations = [] for operation in default_operations: operations.extend(_resource_operation_to_str(operation)) if operations: output.append("") output.append("Default operations:") output.extend(indent(operations, indent_step=INDENT_STEP)) return output class ResourcesConfigurationFacade: # pylint: disable=too-many-instance-attributes def __init__( self, primitives: Sequence[CibResourcePrimitiveDto], groups: Sequence[CibResourceGroupDto], clones: Sequence[CibResourceCloneDto], bundles: Sequence[CibResourceBundleDto], filtered_ids: StringIterable = (), only_stonith: Optional[bool] = None, ) -> None: self._primitives = primitives self._groups = groups self._clones = clones self._bundles = bundles self._filtered_ids = frozenset(filtered_ids) self._only_stonith = only_stonith self._primitives_map = {res.id: res for res in self._primitives} self._bundles_map = {res.id: res for res in self._bundles} self._clones_map = {res.id: res for res in self._clones} self._groups_map = {res.id: res for res in self._groups} self._child_parent_map: Dict[str, str] = {} self._parent_child_map: Dict[str, List[str]] = defaultdict(list) for bundle_dto in self._bundles: if bundle_dto.member_id: self._set_parent(bundle_dto.member_id, bundle_dto.id) for clone_dto in self._clones: self._set_parent(clone_dto.member_id, clone_dto.id) for group_dto in self._groups: for primitive_id in group_dto.member_ids: self._set_parent(primitive_id, group_dto.id) @classmethod def from_resources_dto( cls, resources_dto: CibResourcesDto ) -> "ResourcesConfigurationFacade": return cls( resources_dto.primitives, resources_dto.groups, resources_dto.clones, resources_dto.bundles, ) def get_parent_id(self, res_id: str) -> Optional[str]: return self._child_parent_map.get(res_id) def _get_children_ids(self, res_id: str) -> List[str]: return self._parent_child_map[res_id] def _set_parent(self, child_id: str, parent_id: str) -> None: if self.get_parent_id(child_id): raise AssertionError("invalid data") self._child_parent_map[child_id] = parent_id self._parent_child_map[parent_id].append(child_id) def get_primitive_dto( self, obj_id: str ) -> Optional[CibResourcePrimitiveDto]: return self._primitives_map.get(obj_id) def get_group_dto(self, obj_id: str) -> Optional[CibResourceGroupDto]: return self._groups_map.get(obj_id) def _get_any_resource_dto(self, obj_id: str) -> Optional[ Union[ CibResourcePrimitiveDto, CibResourceGroupDto, CibResourceCloneDto, CibResourceBundleDto, ] ]: return ( self._primitives_map.get(obj_id) or self._bundles_map.get(obj_id) or self._clones_map.get(obj_id) or self._groups_map.get(obj_id) ) @property def filtered_ids(self) -> Container[str]: return self._filtered_ids @property def primitives(self) -> Sequence[CibResourcePrimitiveDto]: return self._primitives @property def clones(self) -> Sequence[CibResourceCloneDto]: return self._clones @property def groups(self) -> Sequence[CibResourceGroupDto]: return self._groups @property def bundles(self) -> Sequence[CibResourceBundleDto]: return self._bundles def filter_stonith( self, allow_stonith: bool ) -> "ResourcesConfigurationFacade": primitives = [ primitive for primitive in self._primitives if is_stonith(primitive.agent_name) == allow_stonith or (not allow_stonith and self.get_parent_id(primitive.id)) ] if allow_stonith: return self.__class__( primitives=primitives, clones=[], bundles=[], groups=[], filtered_ids=self._filtered_ids, only_stonith=True, ) return self.__class__( primitives=primitives, clones=self._clones, bundles=self._bundles, groups=self._groups, filtered_ids=self._filtered_ids, only_stonith=False, ) def filter_resources( self, resource_ids_to_find: StringIterable ) -> "ResourcesConfigurationFacade": if not resource_ids_to_find: return self primitives = set() groups = set() clones = set() bundles = set() label = "resource/stonith device" if self._only_stonith is True: label = "stonith device" elif self._only_stonith is False: label = "resource" ids_to_process = set(resource_ids_to_find) processed_ids = set() while ids_to_process: resource_id = ids_to_process.pop() if resource_id in processed_ids: continue resource_dto = self._get_any_resource_dto(resource_id) if resource_dto is None: warn(f"Unable to find {label} '{resource_id}'") continue processed_ids.add(resource_id) ids_to_process.update(self._get_children_ids(resource_id)) if isinstance(resource_dto, CibResourcePrimitiveDto): primitives.add(resource_id) elif isinstance(resource_dto, CibResourceGroupDto): groups.add(resource_id) elif isinstance(resource_dto, CibResourceCloneDto): clones.add(resource_id) elif isinstance(resource_dto, CibResourceBundleDto): bundles.add(resource_id) else: raise AssertionError() if not processed_ids: raise CmdLineInputError(f"No {label} found") return self.__class__( primitives=sorted( (self._primitives_map[res_id] for res_id in primitives), key=lambda obj: obj.id, ), clones=sorted( (self._clones_map[res_id] for res_id in clones), key=lambda obj: obj.id, ), groups=sorted( (self._groups_map[res_id] for res_id in groups), key=lambda obj: obj.id, ), bundles=sorted( (self._bundles_map[res_id] for res_id in bundles), key=lambda obj: obj.id, ), filtered_ids=set(resource_ids_to_find) & set(processed_ids), only_stonith=self._only_stonith, ) def _resource_agent_name_to_text( resource_agent_name_dto: ResourceAgentNameDto, ) -> str: output = f"class={resource_agent_name_dto.standard}" if resource_agent_name_dto.provider: output += f" provider={resource_agent_name_dto.provider}" output += f" type={resource_agent_name_dto.type}" return output def _nvset_to_text(label: str, nvsets: Sequence[CibNvsetDto]) -> List[str]: if nvsets and nvsets[0].nvpairs: return nvset_dto_to_lines(nvset=nvsets[0], nvset_label=label) return [] def _resource_description_to_text(desc: Optional[str]) -> List[str]: if desc: return [f"Description: {desc}"] return [] def _resource_primitive_to_text( primitive_dto: CibResourcePrimitiveDto, ) -> List[str]: output = ( _resource_description_to_text(primitive_dto.description) + _nvset_to_text("Attributes", primitive_dto.instance_attributes) + _nvset_to_text("Meta Attributes", primitive_dto.meta_attributes) + _nvset_to_text("Utilization", primitive_dto.utilization) ) if primitive_dto.operations: operation_lines: List[str] = [] for operation_dto in primitive_dto.operations: operation_lines.extend(_resource_operation_to_str(operation_dto)) output.extend( ["Operations:"] + indent(operation_lines, indent_step=INDENT_STEP) ) return [ "Resource: {res_id} ({res_type})".format( res_id=primitive_dto.id, res_type=_resource_agent_name_to_text(primitive_dto.agent_name), ) ] + indent(output, indent_step=INDENT_STEP) def _resource_group_to_text( group_dto: CibResourceGroupDto, resources_facade: ResourcesConfigurationFacade, ) -> List[str]: output = ( _resource_description_to_text(group_dto.description) + _nvset_to_text("Attributes", group_dto.instance_attributes) + _nvset_to_text("Meta Attributes", group_dto.meta_attributes) ) for primitive_id in group_dto.member_ids: primitive_dto = resources_facade.get_primitive_dto(primitive_id) if primitive_dto is None: raise CmdLineInputError( f"Invalid data: group {group_dto.id} has no children" ) output.extend(_resource_primitive_to_text(primitive_dto)) return [f"Group: {group_dto.id}"] + indent(output, indent_step=INDENT_STEP) def _resource_clone_to_text( clone_dto: CibResourceCloneDto, resources_facade: ResourcesConfigurationFacade, ) -> List[str]: output = ( _resource_description_to_text(clone_dto.description) + _nvset_to_text("Attributes", clone_dto.instance_attributes) + _nvset_to_text("Meta Attributes", clone_dto.meta_attributes) ) primitive_dto = resources_facade.get_primitive_dto(clone_dto.member_id) group_dto = resources_facade.get_group_dto(clone_dto.member_id) if primitive_dto is not None: output.extend(_resource_primitive_to_text(primitive_dto)) elif group_dto is not None: output.extend(_resource_group_to_text(group_dto, resources_facade)) else: raise CmdLineInputError( f"Invalid data: clone {clone_dto.id} has no children" ) return [f"Clone: {clone_dto.id}"] + indent(output, indent_step=INDENT_STEP) def _resource_bundle_container_options_to_pairs( options: CibResourceBundleContainerRuntimeOptionsDto, ) -> List[Tuple[str, str]]: option_list = [("image", options.image)] if options.replicas is not None: option_list.append(("replicas", str(options.replicas))) if options.replicas_per_host is not None: option_list.append( ("replicas-per-host", str(options.replicas_per_host)) ) if options.promoted_max is not None: option_list.append(("promoted-max", str(options.promoted_max))) if options.run_command: option_list.append(("run-command", options.run_command)) if options.network: option_list.append(("network", options.network)) if options.options: option_list.append(("options", options.options)) return option_list def _resource_bundle_network_options_to_pairs( bundle_network_dto: Optional[CibResourceBundleNetworkOptionsDto], ) -> List[Tuple[str, str]]: network_options: List[Tuple[str, str]] = [] if not bundle_network_dto: return network_options if bundle_network_dto.ip_range_start: network_options.append( ("ip-range-start", bundle_network_dto.ip_range_start) ) if bundle_network_dto.control_port is not None: network_options.append( ("control-port", str(bundle_network_dto.control_port)) ) if bundle_network_dto.host_interface: network_options.append( ("host-interface", bundle_network_dto.host_interface) ) if bundle_network_dto.host_netmask is not None: network_options.append( ("host-netmask", str(bundle_network_dto.host_netmask)) ) if bundle_network_dto.add_host is not None: network_options.append( ("add-host", bool_to_cli_value(bundle_network_dto.add_host)) ) return network_options def _resource_bundle_port_mapping_to_pairs( bundle_net_port_mapping_dto: CibResourceBundlePortMappingDto, ) -> List[Tuple[str, str]]: mapping = [] if bundle_net_port_mapping_dto.port is not None: mapping.append(("port", str(bundle_net_port_mapping_dto.port))) if bundle_net_port_mapping_dto.internal_port is not None: mapping.append( ("internal-port", str(bundle_net_port_mapping_dto.internal_port)) ) if bundle_net_port_mapping_dto.range: mapping.append(("range", bundle_net_port_mapping_dto.range)) return mapping def _resource_bundle_network_port_mapping_to_str( bundle_net_port_mapping_dto: CibResourceBundlePortMappingDto, ) -> List[str]: output = format_name_value_list( _resource_bundle_port_mapping_to_pairs(bundle_net_port_mapping_dto) ) if output and bundle_net_port_mapping_dto.id: output.append(f"({bundle_net_port_mapping_dto.id})") return output def _resource_bundle_network_to_text( bundle_network_dto: Optional[CibResourceBundleNetworkOptionsDto], ) -> List[str]: network_options = _resource_bundle_network_options_to_pairs( bundle_network_dto ) if network_options: return [ " ".join(["Network:"] + format_name_value_list(network_options)) ] return [] def _resource_bundle_port_mappings_to_text( bundle_port_mappings: Sequence[CibResourceBundlePortMappingDto], ) -> List[str]: port_mappings = [ " ".join(_resource_bundle_network_port_mapping_to_str(port_mapping_dto)) for port_mapping_dto in bundle_port_mappings ] if port_mappings: return ["Port Mapping:"] + indent( port_mappings, indent_step=INDENT_STEP ) return [] def _resource_bundle_storage_mapping_to_pairs( storage_mapping: CibResourceBundleStorageMappingDto, ) -> List[Tuple[str, str]]: mapping = [] if storage_mapping.source_dir: mapping.append(("source-dir", storage_mapping.source_dir)) if storage_mapping.source_dir_root: mapping.append(("source-dir-root", storage_mapping.source_dir_root)) mapping.append(("target-dir", storage_mapping.target_dir)) if storage_mapping.options: mapping.append(("options", storage_mapping.options)) return mapping def _resource_bundle_storage_mapping_to_str( storage_mapping: CibResourceBundleStorageMappingDto, ) -> List[str]: return format_name_value_list( _resource_bundle_storage_mapping_to_pairs(storage_mapping) ) + [f"({storage_mapping.id})"] def _resource_bundle_storage_to_text( storage_mappings: Sequence[CibResourceBundleStorageMappingDto], ) -> List[str]: if not storage_mappings: return [] output = [] for storage_mapping in storage_mappings: output.append( " ".join(_resource_bundle_storage_mapping_to_str(storage_mapping)) ) return ["Storage Mapping:"] + indent(output, indent_step=INDENT_STEP) def _resource_bundle_to_text( bundle_dto: CibResourceBundleDto, resources_facade: ResourcesConfigurationFacade, ) -> List[str]: container_options = [] if bundle_dto.container_type and bundle_dto.container_options: container_options.append( " ".join( ["{}:".format(str(bundle_dto.container_type).capitalize())] + format_name_value_list( _resource_bundle_container_options_to_pairs( bundle_dto.container_options ) ) ) ) output = ( _resource_description_to_text(bundle_dto.description) + container_options + _resource_bundle_network_to_text(bundle_dto.network) + _resource_bundle_port_mappings_to_text(bundle_dto.port_mappings) + _resource_bundle_storage_to_text(bundle_dto.storage_mappings) + _nvset_to_text("Meta Attributes", bundle_dto.meta_attributes) ) if bundle_dto.member_id: primitive_dto = resources_facade.get_primitive_dto(bundle_dto.member_id) if primitive_dto is None: raise CmdLineInputError( f"Invalid data: bundle '{bundle_dto.id}' has inner primitive " f"resource with id '{bundle_dto.member_id}' which was not found" ) output.extend(_resource_primitive_to_text(primitive_dto)) return [f"Bundle: {bundle_dto.id}"] + indent( output, indent_step=INDENT_STEP ) def resources_to_text( resources_facade: ResourcesConfigurationFacade, ) -> List[str]: def _is_allowed_to_display_fn(res_id: str) -> bool: if resources_facade.filtered_ids: return res_id in resources_facade.filtered_ids return resources_facade.get_parent_id(res_id) is None output = [] for primitive_dto in resources_facade.primitives: if _is_allowed_to_display_fn(primitive_dto.id): output.extend(_resource_primitive_to_text(primitive_dto)) for group_dto in resources_facade.groups: if _is_allowed_to_display_fn(group_dto.id): output.extend(_resource_group_to_text(group_dto, resources_facade)) for clone_dto in resources_facade.clones: if _is_allowed_to_display_fn(clone_dto.id): output.extend(_resource_clone_to_text(clone_dto, resources_facade)) for bundle_dto in resources_facade.bundles: if _is_allowed_to_display_fn(bundle_dto.id): output.extend( _resource_bundle_to_text(bundle_dto, resources_facade) ) return output def _nvset_to_cmd( label: Optional[str], nvsets: Sequence[CibNvsetDto], ) -> List[str]: if nvsets and nvsets[0].nvpairs: options = pairs_to_cmd( (nvpair.name, nvpair.value) for nvpair in nvsets[0].nvpairs ) if label: options = f"{label} {options}" return [options] return [] def _resource_operation_to_cmd( operations: Sequence[CibResourceOperationDto], ) -> List[str]: if not operations: return [] cmd = [] for op in operations: cmd.append( "{name} {options}".format( name=op.name, options=pairs_to_cmd(_resource_operation_to_pairs(op)), ) ) return ["op"] + indent(cmd, indent_step=INDENT_STEP) def _resource_primitive_to_cmd( primitive_dto: CibResourcePrimitiveDto, bundle_id: Optional[str], ) -> List[List[str]]: _is_stonith = is_stonith(primitive_dto.agent_name) options = ( _nvset_to_cmd(None, primitive_dto.instance_attributes) + _resource_operation_to_cmd(primitive_dto.operations) + _nvset_to_cmd("meta", primitive_dto.meta_attributes) ) if bundle_id: options.append(f"bundle {bundle_id}") output = [ [ options_to_cmd( [ "pcs", "stonith" if _is_stonith else "resource", "create", "--no-default-ops", "--force", "--", primitive_dto.id, ( primitive_dto.agent_name.type if _is_stonith else get_resource_agent_full_name( primitive_dto.agent_name ) ), ] ) ] + indent(options, indent_step=INDENT_STEP) ] utilization_cmd_params = _nvset_to_cmd(None, primitive_dto.utilization) if utilization_cmd_params: output.append( [ options_to_cmd( ["pcs", "resource", "utilization", primitive_dto.id] ) ] + indent(utilization_cmd_params, indent_step=INDENT_STEP) ) return output def _resource_bundle_to_cmd( bundle_dto: CibResourceBundleDto, ) -> List[List[str]]: if not (bundle_dto.container_type and bundle_dto.container_options): return [] options = [ options_to_cmd(["container", str(bundle_dto.container_type)]) ] + indent( [ pairs_to_cmd( _resource_bundle_container_options_to_pairs( bundle_dto.container_options ) ) ], indent_step=INDENT_STEP, ) network_options = pairs_to_cmd( _resource_bundle_network_options_to_pairs(bundle_dto.network) ) if network_options: options.append(f"network {network_options}") for port_mapping in bundle_dto.port_mappings: options.append( "port-map {}".format( pairs_to_cmd( _resource_bundle_port_mapping_to_pairs(port_mapping) ) ) ) for storage_mapping in bundle_dto.storage_mappings: options.append( "storage-map {}".format( pairs_to_cmd( _resource_bundle_storage_mapping_to_pairs(storage_mapping) ) ) ) options.extend(_nvset_to_cmd("meta", bundle_dto.meta_attributes)) return [ [options_to_cmd(["pcs", "resource", "bundle", "create", bundle_dto.id])] + indent(options, indent_step=INDENT_STEP) ] def _resource_group_to_cmd( group_dto: CibResourceGroupDto, resources_facade: ResourcesConfigurationFacade, ) -> List[List[str]]: stonith_ids = _get_stonith_ids_from_group_dto(group_dto, resources_facade) if stonith_ids: _warn_stonith_unsupported(group_dto, stonith_ids) group_ids = [_id for _id in group_dto.member_ids if _id not in stonith_ids] if not group_ids: return [] output = [] output.append( [options_to_cmd(["pcs", "resource", "group", "add", group_dto.id])] + indent( [options_to_cmd(group_ids)], indent_step=INDENT_STEP, ) ) meta_options = _nvset_to_cmd(None, group_dto.meta_attributes) if meta_options: output.append( [options_to_cmd(["pcs", "resource", "meta", group_dto.id])] + indent(meta_options, indent_step=INDENT_STEP) ) return output def _resource_clone_to_cmd( clone_dto: CibResourceCloneDto, resources_facade: ResourcesConfigurationFacade, ) -> List[List[str]]: primitive_dto = resources_facade.get_primitive_dto(clone_dto.member_id) group_dto = resources_facade.get_group_dto(clone_dto.member_id) stonith_ids = [] if primitive_dto is not None: if is_stonith(primitive_dto.agent_name): stonith_ids = [primitive_dto.id] elif group_dto is not None: stonith_ids = _get_stonith_ids_from_group_dto( group_dto, resources_facade ) if len(group_dto.member_ids) != len(stonith_ids): stonith_ids = [] else: raise CmdLineInputError( f"Invalid data: clone {clone_dto.id} has no children" ) if stonith_ids: _warn_stonith_unsupported(clone_dto, stonith_ids) return [] return [ [ options_to_cmd( ["pcs", "resource", "clone", clone_dto.member_id, clone_dto.id] ) ] + indent( _nvset_to_cmd("meta", clone_dto.meta_attributes), indent_step=INDENT_STEP, ) ] def _get_stonith_ids_from_group_dto( group_dto: CibResourceGroupDto, resources_facade: ResourcesConfigurationFacade, ) -> list[str]: stonith_ids = [] for member_id in group_dto.member_ids: primitive_dto = resources_facade.get_primitive_dto(member_id) if primitive_dto is None: raise CmdLineInputError( f"Invalid data: group {group_dto.id} has no children" ) if is_stonith(primitive_dto.agent_name): stonith_ids.append(primitive_dto.id) return stonith_ids def _warn_stonith_unsupported( dto: Union[CibResourceBundleDto, CibResourceGroupDto, CibResourceCloneDto], stonith_ids: list[str], ) -> None: resource_pl = format_plural(sorted(stonith_ids), "resource") stonith_id_list = format_list(sorted(stonith_ids)) omitted_element = None if isinstance(dto, CibResourceBundleDto): element = "bundle resource" elif isinstance(dto, CibResourceGroupDto): element = "group" if len(dto.member_ids) != len(stonith_ids): omitted_element = f"stonith {resource_pl}" elif isinstance(dto, CibResourceCloneDto): element = "clone" else: raise AssertionError(f"unexpected cib resource dto: {dto}") if not omitted_element: omitted_element = element warn( f"{element.capitalize()} '{dto.id}' contains stonith {resource_pl}: " f"{stonith_id_list}, which is unsupported. The {omitted_element} " "will be omitted." ) def resources_to_cmd( resources_facade: ResourcesConfigurationFacade, ) -> List[List[str]]: # pylint: disable=too-many-branches output: List[List[str]] = [] primitives_created_with_bundle = set() for bundle_dto in resources_facade.bundles: if not (bundle_dto.container_type and bundle_dto.container_options): warn( f"Bundle resource '{bundle_dto.id}' uses unsupported container " "type, therefore pcs is unable to create it. The resource will be omitted." ) continue primitive_dto = None if bundle_dto.member_id: primitive_dto = resources_facade.get_primitive_dto( bundle_dto.member_id ) if primitive_dto is None: raise CmdLineInputError( f"Invalid data: bundle '{bundle_dto.id}' has inner " f"primitive resource with id '{bundle_dto.member_id}' " "which was not found" ) if is_stonith(primitive_dto.agent_name): _warn_stonith_unsupported(bundle_dto, [bundle_dto.member_id]) continue output.extend(_resource_bundle_to_cmd(bundle_dto)) if primitive_dto: output.extend( _resource_primitive_to_cmd(primitive_dto, bundle_dto.id) ) primitives_created_with_bundle.add(bundle_dto.member_id) for primitive_dto in resources_facade.primitives: # stonith in bundle, clone, group is not filtered out for resource # config if is_stonith( primitive_dto.agent_name ) and resources_facade.get_parent_id(primitive_dto.id): continue if primitive_dto.id not in primitives_created_with_bundle: output.extend(_resource_primitive_to_cmd(primitive_dto, None)) for group_dto in resources_facade.groups: output.extend(_resource_group_to_cmd(group_dto, resources_facade)) for clone_dto in resources_facade.clones: output.extend(_resource_clone_to_cmd(clone_dto, resources_facade)) return output def _nvset_options_to_pairs(nvset_dto: CibNvsetDto) -> list[tuple[str, str]]: pairs = list(nvset_dto.options.items()) pairs.append(("id", nvset_dto.id)) return pairs def _nvset_rule_to_cmd(nvset_dto: CibNvsetDto) -> list[str]: if not nvset_dto.rule: return [] rule_str = shlex.quote(nvset_dto.rule.as_string) return [f"rule {rule_str}"] def _defaults_to_cmd( defaults_command: str, cib_defaults_dto: CibDefaultsDto, ) -> list[list[str]]: command_list: list[list[str]] = [] for meta_attributes in cib_defaults_dto.meta_attributes: nvset_options = pairs_to_cmd( sorted(_nvset_options_to_pairs(meta_attributes)) ) command_list.append( [f"{defaults_command} {nvset_options}"] + indent( ( _nvset_to_cmd("meta", [meta_attributes]) + _nvset_rule_to_cmd(meta_attributes) ), indent_step=INDENT_STEP, ) ) return command_list def operation_defaults_to_cmd( cib_defaults_dto: CibDefaultsDto, ) -> list[list[str]]: return _defaults_to_cmd( "pcs -- resource op defaults set create", cib_defaults_dto ) def resource_defaults_to_cmd( cib_defaults_dto: CibDefaultsDto, ) -> list[list[str]]: return _defaults_to_cmd( "pcs -- resource defaults set create", cib_defaults_dto ) pcs-0.12.0.2/pcs/cli/resource/parse_args.py000066400000000000000000000460721500417470700204200ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( FUTURE_OPTION, ArgsByKeywords, Argv, InputModifiers, KeyValueParser, group_by_keywords, ) from pcs.cli.reports.output import deprecation_warning @dataclass(frozen=True) class PrimitiveOptions: instance_attrs: dict[str, str] meta_attrs: dict[str, str] operations: list[dict[str, str]] @dataclass(frozen=True) class CloneOptions: clone_id: Optional[str] meta_attrs: dict[str, str] @dataclass(frozen=True) class GroupOptions: group_id: str after_resource: Optional[str] before_resource: Optional[str] @dataclass(frozen=True) class ComplexResourceOptions: primitive: PrimitiveOptions group: Optional[GroupOptions] clone: Optional[CloneOptions] promotable: Optional[CloneOptions] bundle_id: Optional[str] @dataclass(frozen=True) class BundleCreateOptions: container_type: str container: dict[str, str] network: dict[str, str] port_map: list[dict[str, str]] storage_map: list[dict[str, str]] meta_attrs: dict[str, str] @dataclass(frozen=True) class BundleUpdateOptions: container: dict[str, str] network: dict[str, str] port_map_add: list[dict[str, str]] port_map_remove: list[str] storage_map_add: list[dict[str, str]] storage_map_remove: list[str] meta_attrs: dict[str, str] @dataclass(frozen=True) class AddRemoveOptions: add: list[dict[str, str]] remove: list[str] def parse_primitive(arg_list: Argv) -> PrimitiveOptions: groups = group_by_keywords( arg_list, set(["op", "meta"]), implicit_first_keyword="instance" ) parts = PrimitiveOptions( instance_attrs=KeyValueParser( groups.get_args_flat("instance") ).get_unique(), meta_attrs=KeyValueParser(groups.get_args_flat("meta")).get_unique(), operations=[ KeyValueParser(op).get_unique() for op in build_operations(groups.get_args_groups("op")) ], ) return parts def parse_clone(arg_list: Argv, promotable: bool = False) -> CloneOptions: clone_id = None allowed_keywords = set(["op", "meta"]) if ( arg_list and arg_list[0] not in allowed_keywords and "=" not in arg_list[0] ): clone_id = arg_list.pop(0) groups = group_by_keywords( arg_list, allowed_keywords, implicit_first_keyword="options" ) if groups.has_keyword("op"): raise CmdLineInputError( "op settings must be changed on base resource, not the clone", ) if groups.has_keyword("options"): # deprecated since 0.11.6 deprecation_warning( "configuring meta attributes without specifying the 'meta' keyword " "is deprecated and will be removed in a future release" ) meta = KeyValueParser( groups.get_args_flat("options") + groups.get_args_flat("meta") ).get_unique() if promotable: if "promotable" in meta: raise CmdLineInputError( "you cannot specify both promotable option and promotable " "keyword" ) meta["promotable"] = "true" return CloneOptions(clone_id=clone_id, meta_attrs=meta) def parse_create_new(arg_list: Argv) -> ComplexResourceOptions: # pylint: disable=too-many-branches # pylint: disable=too-many-locals top_groups = group_by_keywords( arg_list, set(["clone", "promotable", "bundle", "group"]), implicit_first_keyword="primitive", ) top_groups.ensure_unique_keywords() primitive_groups = group_by_keywords( top_groups.get_args_flat("primitive"), set(["op", "meta"]), implicit_first_keyword="instance", ) primitive_options = PrimitiveOptions( instance_attrs=KeyValueParser( primitive_groups.get_args_flat("instance") ).get_unique(), meta_attrs=KeyValueParser( primitive_groups.get_args_flat("meta") ).get_unique(), operations=[ KeyValueParser(op).get_unique() for op in build_operations(primitive_groups.get_args_groups("op")) ], ) group_options = None if top_groups.has_keyword("group"): group_groups = group_by_keywords( top_groups.get_args_flat("group"), set(["before", "after", "op", "meta"]), implicit_first_keyword="group_id", ) if group_groups.has_keyword("meta"): raise CmdLineInputError( "meta options must be defined on the base resource, " "not the group" ) if group_groups.has_keyword("op"): raise CmdLineInputError( "op settings must be defined on the base resource, " "not the group" ) if len(group_groups.get_args_flat("group_id")) != 1: raise CmdLineInputError( "You have to specify exactly one group after 'group'" ) position: dict[str, Optional[str]] = {"after": None, "before": None} for where in position: if group_groups.has_keyword(where): if len(group_groups.get_args_flat(where)) != 1: raise CmdLineInputError( f"You have to specify exactly one resource after '{where}'" ) position[where] = group_groups.get_args_flat(where)[0] group_options = GroupOptions( group_id=group_groups.get_args_flat("group_id")[0], before_resource=position["before"], after_resource=position["after"], ) clone_options: dict[str, Optional[CloneOptions]] = { "clone": None, "promotable": None, } for clone_type in clone_options: if not top_groups.has_keyword(clone_type): continue clone_groups = group_by_keywords( top_groups.get_args_flat(clone_type), set(["op", "meta"]), implicit_first_keyword="options", ) clone_id = None options = clone_groups.get_args_flat("options") if options and "=" not in options[0]: clone_id = options.pop(0) if options: raise CmdLineInputError( f"Specifying instance attributes for a {clone_type} " f"is not supported. Use 'meta' after '{clone_type}' " "if you want to specify meta attributes." ) if clone_groups.has_keyword("op"): raise CmdLineInputError( "op settings must be defined on the base resource, " f"not the {clone_type}" ) clone_options[clone_type] = CloneOptions( clone_id=clone_id, meta_attrs=KeyValueParser( clone_groups.get_args_flat("meta") ).get_unique(), ) bundle_id = None if top_groups.has_keyword("bundle"): bundle_groups = group_by_keywords( top_groups.get_args_flat("bundle"), set(["op", "meta"]), implicit_first_keyword="options", ) if bundle_groups.has_keyword("meta"): raise CmdLineInputError( "meta options must be defined on the base resource, " "not the bundle" ) if bundle_groups.has_keyword("op"): raise CmdLineInputError( "op settings must be defined on the base resource, " "not the bundle" ) if len(bundle_groups.get_args_flat("options")) != 1: raise CmdLineInputError("you have to specify exactly one bundle") bundle_id = bundle_groups.get_args_flat("options")[0] return ComplexResourceOptions( primitive=primitive_options, group=group_options, clone=clone_options["clone"], promotable=clone_options["promotable"], bundle_id=bundle_id, ) # deprecated since 0.11.6 def parse_create_old( arg_list: Argv, modifiers: InputModifiers ) -> ComplexResourceOptions: # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements top_groups = group_by_keywords( arg_list, set(["clone", "promotable", "bundle"]), implicit_first_keyword="primitive", ) primitive_groups = group_by_keywords( top_groups.get_args_flat("primitive"), set(["op", "meta"]), implicit_first_keyword="instance", ) primitive_instance_attrs = primitive_groups.get_args_flat("instance") primitive_meta_attrs = primitive_groups.get_args_flat("meta") primitive_operations = primitive_groups.get_args_groups("op") group_options = None if modifiers.is_specified("--group"): dash_deprecation = ( "Using '--{option}' is deprecated and will be replaced with " "'{option}' in a future release. " f"Specify {FUTURE_OPTION} to switch to the future behavior." ) deprecation_warning(dash_deprecation.format(option="group")) before_resource = None if modifiers.get("--before"): before_resource = str(modifiers.get("--before")) deprecation_warning(dash_deprecation.format(option="before")) after_resource = None if modifiers.get("--after"): after_resource = str(modifiers.get("--after")) deprecation_warning(dash_deprecation.format(option="after")) group_options = GroupOptions( group_id=str(modifiers.get("--group")), before_resource=before_resource, after_resource=after_resource, ) else: for option in ("--before", "--after"): if modifiers.is_specified(option): raise CmdLineInputError( f"you cannot use {option} without --group" ) clone_options: dict[str, Optional[CloneOptions]] = { "clone": None, "promotable": None, } for clone_type in clone_options: if not top_groups.has_keyword(clone_type): continue clone_groups = group_by_keywords( top_groups.get_args_flat(clone_type), set(["op", "meta"]), implicit_first_keyword="options", ) clone_id = None options = clone_groups.get_args_flat("options") if options and "=" not in options[0]: clone_id = options.pop(0) if options: deprecation_warning( f"Configuring {clone_type} meta attributes without specifying " f"the 'meta' keyword after the '{clone_type}' keyword " "is deprecated and will be removed in a future release. " f"Specify {FUTURE_OPTION} to switch to the future behavior." ) if clone_groups.has_keyword("op"): deprecation_warning( f"Specifying 'op' after '{clone_type}' now defines " "operations for the base resource. In a future release, this " f"will be removed and operations will have to be specified " f"before '{clone_type}'. " f"Specify {FUTURE_OPTION} to switch to the future behavior." ) primitive_operations += clone_groups.get_args_groups("op") if clone_groups.has_keyword("meta"): deprecation_warning( f"Specifying 'meta' after '{clone_type}' now defines " "meta attributes for the base resource. In a future release, this " f"will define meta attributes for the {clone_type}. " f"Specify {FUTURE_OPTION} to switch to the future behavior." ) primitive_meta_attrs += clone_groups.get_args_flat("meta") clone_options[clone_type] = CloneOptions( clone_id=clone_id, meta_attrs=KeyValueParser(options).get_unique(), ) bundle_id = None if top_groups.has_keyword("bundle"): bundle_groups = group_by_keywords( top_groups.get_args_flat("bundle"), set(["op", "meta"]), implicit_first_keyword="options", ) if bundle_groups.has_keyword("meta"): deprecation_warning( "Specifying 'meta' after 'bundle' now defines meta options for " "the base resource. In a future release, this will be removed and meta " "options will have to be specified before 'bundle'. " f"Specify {FUTURE_OPTION} to switch to the future behavior." ) primitive_meta_attrs += bundle_groups.get_args_flat("meta") if bundle_groups.has_keyword("op"): deprecation_warning( "Specifying 'op' after 'bundle' now defines operations for the " "base resource. In a future release, this will be removed and operations " "will have to be specified before 'bundle'. " f"Specify {FUTURE_OPTION} to switch to the future behavior." ) primitive_operations += bundle_groups.get_args_groups("op") if len(bundle_groups.get_args_flat("options")) != 1: raise CmdLineInputError("you have to specify exactly one bundle") bundle_id = bundle_groups.get_args_flat("options")[0] return ComplexResourceOptions( primitive=PrimitiveOptions( KeyValueParser(primitive_instance_attrs).get_unique(), KeyValueParser(primitive_meta_attrs).get_unique(), [ KeyValueParser(op).get_unique() for op in build_operations(primitive_operations) ], ), group=group_options, clone=clone_options["clone"], promotable=clone_options["promotable"], bundle_id=bundle_id, ) def _parse_bundle_groups(arg_list: Argv) -> ArgsByKeywords: """ Commandline options: no options """ repeatable_keyword_list = ["port-map", "storage-map"] keyword_list = ["meta", "container", "network"] + repeatable_keyword_list groups = group_by_keywords(arg_list, set(keyword_list)) for keyword in keyword_list: if not groups.has_keyword(keyword): continue if keyword in repeatable_keyword_list: for repeated_section in groups.get_args_groups(keyword): if not repeated_section: raise CmdLineInputError(f"No {keyword} options specified") else: if not groups.get_args_flat(keyword): raise CmdLineInputError(f"No {keyword} options specified") return groups def _parse_bundle_create_or_reset( arg_list: Argv, reset: bool ) -> BundleCreateOptions: """ Commandline options: no options """ groups = _parse_bundle_groups(arg_list) container_options = groups.get_args_flat("container") container_type = "" if not reset and container_options and "=" not in container_options[0]: container_type = container_options.pop(0) return BundleCreateOptions( container_type=container_type, container=KeyValueParser(container_options).get_unique(), network=KeyValueParser(groups.get_args_flat("network")).get_unique(), port_map=[ KeyValueParser(port_map).get_unique() for port_map in groups.get_args_groups("port-map") ], storage_map=[ KeyValueParser(storage_map).get_unique() for storage_map in groups.get_args_groups("storage-map") ], meta_attrs=KeyValueParser(groups.get_args_flat("meta")).get_unique(), ) def parse_bundle_create_options(arg_list: Argv) -> BundleCreateOptions: """ Commandline options: no options """ return _parse_bundle_create_or_reset(arg_list, reset=False) def parse_bundle_reset_options(arg_list: Argv) -> BundleCreateOptions: """ Commandline options: no options """ return _parse_bundle_create_or_reset(arg_list, reset=True) def _split_bundle_map_update_op_and_options( map_arg_list: Argv, result_parts: AddRemoveOptions, map_name: str ) -> None: if len(map_arg_list) < 2: raise _bundle_map_update_not_valid(map_name) op, options = map_arg_list[0], map_arg_list[1:] if op == "add": result_parts.add.append(KeyValueParser(options).get_unique()) elif op in {"delete", "remove"}: result_parts.remove.extend(options) else: raise _bundle_map_update_not_valid(map_name) def _bundle_map_update_not_valid(map_name: str) -> CmdLineInputError: return CmdLineInputError( f"When using '{map_name}' you must specify either 'add' and options or " "either of 'delete' or 'remove' and id(s)" ) def parse_bundle_update_options(arg_list: Argv) -> BundleUpdateOptions: """ Commandline options: no options """ groups = _parse_bundle_groups(arg_list) port_map = AddRemoveOptions(add=[], remove=[]) for map_group in groups.get_args_groups("port-map"): _split_bundle_map_update_op_and_options(map_group, port_map, "port-map") storage_map = AddRemoveOptions(add=[], remove=[]) for map_group in groups.get_args_groups("storage-map"): _split_bundle_map_update_op_and_options( map_group, storage_map, "storage-map" ) return BundleUpdateOptions( container=KeyValueParser( groups.get_args_flat("container") ).get_unique(), network=KeyValueParser(groups.get_args_flat("network")).get_unique(), port_map_add=port_map.add, port_map_remove=port_map.remove, storage_map_add=storage_map.add, storage_map_remove=storage_map.remove, meta_attrs=KeyValueParser(groups.get_args_flat("meta")).get_unique(), ) def build_operations(op_group_list: list[Argv]) -> list[Argv]: """ Return a list of dicts. Each dict represents one operation. op_group_list -- contains items that have parameters after "op" (so item can contain multiple operations) for example: [ [monitor timeout=1 start timeout=2], [monitor timeout=3 interval=10], ] """ operation_list = [] for op_group in op_group_list: # empty operation is not allowed if not op_group: raise __not_enough_parts_in_operation() # every operation group needs to start with operation name if "=" in op_group[0]: raise __every_operation_needs_name() for arg in op_group: if "=" not in arg: operation_list.append([f"name={arg}"]) else: operation_list[-1].append(arg) # every operation needs at least name and one option # there can be more than one operation in op_group: check is after # processing if any(len(operation) < 2 for operation in operation_list): raise __not_enough_parts_in_operation() return operation_list def __not_enough_parts_in_operation() -> CmdLineInputError: return CmdLineInputError( "When using 'op' you must specify an operation name" " and at least one option" ) def __every_operation_needs_name() -> CmdLineInputError: return CmdLineInputError( "When using 'op' you must specify an operation name after 'op'" ) pcs-0.12.0.2/pcs/cli/resource/relations.py000066400000000000000000000166611500417470700202730ustar00rootroot00000000000000from typing import ( Any, List, Mapping, Sequence, ) from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import InputModifiers from pcs.cli.common.printable_tree import ( PrintableTreeNode, tree_to_lines, ) from pcs.common.interface import dto from pcs.common.pacemaker.resource.relations import ( RelationEntityDto, ResourceRelationDto, ResourceRelationType, ) from pcs.common.str_tools import format_optional from pcs.common.types import ( StringCollection, StringSequence, ) def show_resource_relations_cmd( lib: Any, argv: StringSequence, modifiers: InputModifiers, ) -> None: """ Options: * -f - CIB file * --full - show constraint ids and resource types """ modifiers.ensure_only_supported("-f", "--full") if len(argv) != 1: raise CmdLineInputError() tree = ResourcePrintableNode.from_dto( dto.from_dict( ResourceRelationDto, lib.resource.get_resource_relations_tree(argv[0]), ) ) for line in tree_to_lines(tree, verbose=bool(modifiers.get("--full"))): print(line) class ResourceRelationBase(PrintableTreeNode): def __init__( self, relation_entity: RelationEntityDto, members: Sequence["ResourceRelationBase"], is_leaf: bool, ): self._relation_entity = relation_entity self._members = members self._is_leaf = is_leaf @property def is_leaf(self) -> bool: return self._is_leaf @property def relation_entity(self) -> RelationEntityDto: return self._relation_entity @property def members(self) -> Sequence["ResourceRelationBase"]: return self._members @property def detail(self) -> list[str]: raise NotImplementedError() def get_title(self, verbose: bool) -> str: raise NotImplementedError() class ResourcePrintableNode(ResourceRelationBase): @classmethod def from_dto( cls, resource_dto: ResourceRelationDto ) -> "ResourcePrintableNode": def _relation_comparator(item: ResourceRelationBase) -> str: type_priorities = ( ResourceRelationType.INNER_RESOURCES, ResourceRelationType.OUTER_RESOURCE, ResourceRelationType.ORDER, ResourceRelationType.ORDER_SET, ) priority_map = { _type: value for value, _type in enumerate(type_priorities) } return "{_type}_{_id}".format( _type=priority_map.get( # Hardcoded number 9 is intentional. If there is more than # 10 items, it would be required to also prepend zeros for # lower numbers. E.g: if there is 100 options, it should # starts as 000, 001, ... item.relation_entity.type, # type: ignore 9, ), _id=item.relation_entity.id, ) return cls( resource_dto.relation_entity, sorted( [ RelationPrintableNode.from_dto(member_dto) for member_dto in resource_dto.members ], key=_relation_comparator, ), resource_dto.is_leaf, ) def get_title(self, verbose: bool) -> str: metadata = self._relation_entity.metadata rsc_type = self._relation_entity.type type_str = { ResourceRelationType.RSC_GROUP: "group", ResourceRelationType.RSC_BUNDLE: "bundle", ResourceRelationType.RSC_CLONE: "clone", }.get(rsc_type, "") if rsc_type == ResourceRelationType.RSC_PRIMITIVE: type_str = "{_class}{_provider}{_type}".format( _class=format_optional(metadata.get("class"), "{}:"), _provider=format_optional(metadata.get("provider"), "{}:"), _type=metadata.get("type"), ) detail = f" (resource: {type_str})" if verbose else "" return f"{self._relation_entity.id}{detail}" @property def detail(self) -> list[str]: return [] class RelationPrintableNode(ResourceRelationBase): @classmethod def from_dto( cls, relation_dto: ResourceRelationDto ) -> "RelationPrintableNode": return cls( relation_dto.relation_entity, sorted( [ ResourcePrintableNode.from_dto(member_dto) for member_dto in relation_dto.members ], key=lambda item: item.relation_entity.id, ), relation_dto.is_leaf, ) def get_title(self, verbose: bool) -> str: rel_type_map: Mapping[ResourceRelationType, str] = { ResourceRelationType.ORDER: "order", ResourceRelationType.ORDER_SET: "order set", ResourceRelationType.INNER_RESOURCES: "inner resource(s)", ResourceRelationType.OUTER_RESOURCE: "outer resource", } detail = ( " ({})".format(self._relation_entity.metadata.get("id")) if verbose else "" ) return "{type}{detail}".format( type=rel_type_map.get(self._relation_entity.type, ""), detail=detail, ) @property def detail(self) -> list[str]: ent = self._relation_entity if ent.type is ResourceRelationType.ORDER: return _order_metadata_to_str(ent.metadata) if ent.type is ResourceRelationType.ORDER_SET: return _order_set_metadata_to_str(ent.metadata) if ( ent.type is ResourceRelationType.INNER_RESOURCES and len(ent.members) > 1 ): return ["members: {}".format(" ".join(ent.members))] return [] def _order_metadata_to_str(metadata: Mapping[str, Any]) -> list[str]: return [ "{action1} {resource1} then {action2} {resource2}".format( action1=metadata["first-action"], resource1=metadata["first"], action2=metadata["then-action"], resource2=metadata["then"], ) ] + _order_common_metadata_to_str(metadata) def _order_set_metadata_to_str(metadata: Mapping[str, Any]) -> list[str]: result = [] for res_set in metadata["sets"]: result.append( " set {resources}{options}".format( resources=" ".join(res_set["members"]), options=_resource_set_options_to_str(res_set["metadata"]), ) ) return _order_common_metadata_to_str(metadata) + result def _resource_set_options_to_str(metadata: Mapping[str, Any]) -> str: supported_keys = ( "sequential", "require-all", "ordering", "action", "role", "kind", "score", ) result = _filter_supported_keys(metadata, supported_keys) return f" ({result})" if result else "" def _filter_supported_keys( data: Mapping[str, Any], supported_keys: StringCollection ) -> str: return " ".join( [ f"{key}={value}" for key, value in sorted(data.items()) if key in supported_keys ] ) def _order_common_metadata_to_str(metadata: Mapping[str, Any]) -> List[str]: result = _filter_supported_keys( metadata, ("symmetrical", "kind", "require-all", "score") ) return [result] if result else [] pcs-0.12.0.2/pcs/cli/resource_agent.py000066400000000000000000000023051500417470700174370ustar00rootroot00000000000000from typing import List from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.tools import print_to_stderr from pcs.common import reports from pcs.common.resource_agent.dto import ( ResourceAgentNameDto, get_resource_agent_full_name, ) def is_stonith(agent_name: ResourceAgentNameDto) -> bool: return agent_name.standard == "stonith" def find_single_agent( agent_names: List[ResourceAgentNameDto], to_find: str ) -> ResourceAgentNameDto: to_find_normalized = to_find.lower() matches = [ agent_name for agent_name in agent_names if agent_name.type.lower() == to_find_normalized ] if len(matches) == 1: print_to_stderr( reports.messages.AgentNameGuessed( to_find, get_resource_agent_full_name(matches[0]) ).message ) return matches[0] report_msg: reports.item.ReportItemMessage if matches: report_msg = reports.messages.AgentNameGuessFoundMoreThanOne( to_find, sorted(map(get_resource_agent_full_name, matches)) ) else: report_msg = reports.messages.AgentNameGuessFoundNone(to_find) raise CmdLineInputError(report_msg.message) pcs-0.12.0.2/pcs/cli/routing/000077500000000000000000000000001500417470700155475ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/routing/__init__.py000066400000000000000000000000001500417470700176460ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/routing/acl.py000066400000000000000000000030451500417470700166620ustar00rootroot00000000000000from pcs import ( acl, usage, ) from pcs.cli.common.errors import raise_command_replaced from pcs.cli.common.routing import create_router acl_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.acl(argv)), "show": lambda lib, argv, modifiers: raise_command_replaced( ["pcs acl config"], pcs_version="0.12" ), "config": acl.acl_config, "enable": acl.acl_enable, "disable": acl.acl_disable, "role": create_router( { "create": acl.role_create, "delete": acl.role_delete, "remove": acl.role_delete, "assign": acl.role_assign, "unassign": acl.role_unassign, }, ["acl", "role"], ), "user": create_router( { "create": acl.user_create, "delete": acl.user_delete, "remove": acl.user_delete, }, ["acl", "user"], ), "group": create_router( { "create": acl.group_create, "delete": acl.group_delete, "remove": acl.group_delete, }, ["acl", "group"], ), "permission": create_router( { "add": acl.permission_add, "delete": acl.run_permission_delete, "remove": acl.run_permission_delete, }, ["acl", "permission"], ), }, ["acl"], default_cmd="config", ) pcs-0.12.0.2/pcs/cli/routing/alert.py000066400000000000000000000021331500417470700172270ustar00rootroot00000000000000from pcs import ( alert, usage, ) from pcs.cli.common.errors import raise_command_replaced from pcs.cli.common.routing import create_router alert_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.alert(argv)), "create": alert.alert_add, "update": alert.alert_update, "delete": alert.alert_remove, "remove": alert.alert_remove, "config": alert.print_alert_config, "show": lambda lib, argv, modifiers: raise_command_replaced( ["pcs alert config"], pcs_version="0.12" ), "recipient": create_router( { "help": lambda lib, argv, modifiers: print( usage.alert(["recipient"]) ), "add": alert.recipient_add, "update": alert.recipient_update, "delete": alert.recipient_remove, "remove": alert.recipient_remove, }, ["alert", "recipient"], ), "get_all_alerts": alert.print_alerts_in_json, }, ["alert"], default_cmd="config", ) pcs-0.12.0.2/pcs/cli/routing/booth.py000066400000000000000000000027311500417470700172370ustar00rootroot00000000000000from pcs import ( settings, usage, ) from pcs.cli.booth import command from pcs.cli.common.routing import create_router mapping = { "help": lambda lib, argv, modifiers: print(usage.booth(argv)), "config": command.config_show, "setup": command.config_setup, "destroy": command.config_destroy, "ticket": create_router( { "help": lambda lib, argv, modifiers: print(usage.booth(["ticket"])), "add": command.config_ticket_add, "cleanup": command.ticket_cleanup, "delete": command.config_ticket_remove, "grant": command.ticket_grant, "remove": command.config_ticket_remove, "revoke": command.ticket_revoke, "standby": command.ticket_standby, "unstandby": command.ticket_unstandby, }, ["booth", "ticket"], ), "create": command.create_in_cluster, "delete": command.remove_from_cluster, "remove": command.remove_from_cluster, "restart": command.restart, "sync": command.sync, "pull": command.pull, "enable": command.enable, "disable": command.disable, "start": command.start, "stop": command.stop, "status": command.status, } if settings.booth_enable_authfile_set_enabled: mapping["enable-authfile"] = command.enable_authfile if settings.booth_enable_authfile_unset_enabled: mapping["clean-enable-authfile"] = command.enable_authfile_clean booth_cmd = create_router(mapping, ["booth"]) pcs-0.12.0.2/pcs/cli/routing/client.py000066400000000000000000000004151500417470700173770ustar00rootroot00000000000000from pcs import ( client, usage, ) from pcs.cli.common.routing import create_router client_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.client(argv)), "local-auth": client.local_auth_cmd, }, ["client"], ) pcs-0.12.0.2/pcs/cli/routing/cluster.py000066400000000000000000000063301500417470700176040ustar00rootroot00000000000000import pcs.cli.cluster.command as cluster_command from pcs import ( cluster, status, usage, ) from pcs.cli.common.errors import raise_command_replaced from pcs.cli.common.routing import create_router cluster_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.cluster(argv)), "setup": cluster.cluster_setup, "config": create_router( { "show": cluster.config_show, "update": cluster.config_update, "uuid": create_router( { "generate": cluster.generate_uuid, }, ["cluster", "config", "uuid"], ), }, ["cluster", "config"], default_cmd="show", ), "authkey": create_router( {"corosync": cluster.authkey_corosync}, ["cluster", "authkey"], ), "sync": create_router( { "corosync": cluster.sync_nodes, }, ["cluster", "sync"], default_cmd="corosync", ), "status": status.cluster_status, "pcsd-status": lambda lib, argv, modifiers: raise_command_replaced( ["pcs pcsd status", "pcs status pcsd"], pcs_version="0.12" ), "certkey": lambda lib, argv, modifiers: raise_command_replaced( ["pcs pcsd certkey"], pcs_version="0.12" ), "auth": cluster.cluster_auth_cmd, "start": cluster.cluster_start_cmd, "stop": cluster.cluster_stop_cmd, "kill": cluster.kill_cluster, "enable": cluster.cluster_enable_cmd, "disable": cluster.cluster_disable_cmd, "cib": cluster.get_cib, "cib-push": cluster.cluster_push, "cib-upgrade": cluster.cluster_cib_upgrade_cmd, "edit": cluster.cluster_edit, "link": create_router( { "add": cluster.link_add, "delete": cluster.link_remove, "remove": cluster.link_remove, "update": cluster.link_update, }, ["cluster", "link"], ), "node": create_router( { "add": cluster.node_add, "add-guest": cluster_command.node_add_guest, "add-outside": cluster.node_add_outside_cluster, "add-remote": cluster_command.node_add_remote, "clear": cluster_command.node_clear, "delete": cluster.node_remove, "delete-guest": cluster_command.node_remove_guest, "delete-remote": cluster_command.node_remove_remote, "remove": cluster.node_remove, "remove-guest": cluster_command.node_remove_guest, "remove-remote": cluster_command.node_remove_remote, }, ["cluster", "node"], ), "uidgid": cluster.cluster_uidgid, "corosync": cluster.cluster_get_corosync_conf, "reload": cluster.cluster_reload, "destroy": cluster.cluster_destroy, "verify": cluster.cluster_verify, "report": cluster.cluster_report, "remove_nodes_from_cib": cluster.remove_nodes_from_cib, }, ["cluster"], ) pcs-0.12.0.2/pcs/cli/routing/config.py000066400000000000000000000013431500417470700173670ustar00rootroot00000000000000from pcs import ( config, usage, ) from pcs.cli.common.routing import create_router config_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.config(argv)), "show": config.config_show, "backup": config.config_backup, "restore": config.config_restore, "checkpoint": create_router( { "list": config.config_checkpoint_list, "view": config.config_checkpoint_view, "restore": config.config_checkpoint_restore, "diff": config.config_checkpoint_diff, }, ["config", "checkpoint"], default_cmd="list", ), }, ["config"], default_cmd="show", ) pcs-0.12.0.2/pcs/cli/routing/constraint.py000066400000000000000000000071231500417470700203100ustar00rootroot00000000000000from typing import Any import pcs.cli.constraint_colocation.command as colocation_command from pcs import ( constraint, usage, ) from pcs.cli.common.errors import ( CmdLineInputError, raise_command_removed, raise_command_replaced, ) from pcs.cli.common.parse_args import ( Argv, InputModifiers, ) from pcs.cli.common.routing import create_router from pcs.cli.constraint import command as constraint_command from pcs.cli.constraint.location import command as location_command from pcs.cli.constraint_ticket import command as ticket_command from pcs.utils import exit_on_cmdline_input_error def constraint_location_cmd( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: if not argv: sub_cmd = "config" else: sub_cmd = argv.pop(0) try: if sub_cmd == "add": constraint.location_add(lib, argv, modifiers) elif sub_cmd in ["remove", "delete"]: location_command.remove(lib, argv, modifiers) elif sub_cmd == "show": raise_command_replaced( ["pcs constraint location config"], pcs_version="0.12" ) elif sub_cmd == "config": constraint.location_config_cmd(lib, argv, modifiers) elif len(argv) >= 2: if argv[0] == "rule": location_command.create_with_rule( lib, [sub_cmd] + argv, modifiers ) else: constraint.location_prefer(lib, [sub_cmd] + argv, modifiers) else: raise CmdLineInputError() except CmdLineInputError as e: exit_on_cmdline_input_error(e, "constraint", ["location", sub_cmd]) constraint_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.constraint(argv)), "location": constraint_location_cmd, "order": constraint.constraint_order_cmd, "ticket": create_router( { "set": ticket_command.create_with_set, "add": ticket_command.add, "delete": ticket_command.remove, "remove": ticket_command.remove, "show": lambda lib, argv, modifiers: raise_command_replaced( ["pcs constraint ticket config"], pcs_version="0.12" ), "config": ticket_command.config_cmd, }, ["constraint", "ticket"], default_cmd="config", ), "colocation": create_router( { "add": constraint.colocation_add, "remove": colocation_command.remove, "delete": colocation_command.remove, "set": colocation_command.create_with_set, "show": lambda lib, argv, modifiers: raise_command_replaced( ["pcs constraint colocation config"], pcs_version="0.12" ), "config": colocation_command.config_cmd, }, ["constraint", "colocation"], default_cmd="config", ), "remove": constraint_command.remove, "delete": constraint_command.remove, "show": lambda lib, argv, modifiers: raise_command_replaced( ["pcs constraint config"], pcs_version="0.12" ), "list": lambda lib, argv, modifiers: raise_command_replaced( ["pcs constraint config"], pcs_version="0.12" ), "config": constraint.config_cmd, "ref": constraint.ref, "rule": lambda lib, argv, modifiers: raise_command_removed( pcs_version="0.12" ), }, ["constraint"], default_cmd="config", ) pcs-0.12.0.2/pcs/cli/routing/dr.py000066400000000000000000000005421500417470700165270ustar00rootroot00000000000000from pcs import usage from pcs.cli import dr from pcs.cli.common.routing import create_router dr_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.dr(argv)), "config": dr.config, "destroy": dr.destroy, "set-recovery-site": dr.set_recovery_site, "status": dr.status, }, ["dr"], ) pcs-0.12.0.2/pcs/cli/routing/host.py000066400000000000000000000004321500417470700170750ustar00rootroot00000000000000from pcs import ( host, usage, ) from pcs.cli.common.routing import create_router host_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.host(argv)), "auth": host.auth_cmd, "deauth": host.deauth_cmd, }, ["host"], ) pcs-0.12.0.2/pcs/cli/routing/node.py000066400000000000000000000013041500417470700170440ustar00rootroot00000000000000from functools import partial from pcs import ( node, usage, ) from pcs.cli.common.routing import create_router node_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.node(argv)), "maintenance": partial(node.node_maintenance_cmd, enable=True), "unmaintenance": partial(node.node_maintenance_cmd, enable=False), "standby": partial(node.node_standby_cmd, enable=True), "unstandby": partial(node.node_standby_cmd, enable=False), "attribute": node.node_attribute_cmd, "utilization": node.node_utilization_cmd, # pcs-to-pcsd use only "pacemaker-status": node.node_pacemaker_status, }, ["node"], ) pcs-0.12.0.2/pcs/cli/routing/pcsd.py000066400000000000000000000006601500417470700170540ustar00rootroot00000000000000from pcs import ( pcsd, usage, ) from pcs.cli.common.routing import create_router pcsd_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.pcsd(argv)), "accept_token": pcsd.accept_token_cmd, "deauth": pcsd.pcsd_deauth, "certkey": pcsd.pcsd_certkey_cmd, "status": pcsd.pcsd_status_cmd, "sync-certificates": pcsd.pcsd_sync_certs, }, ["pcsd"], ) pcs-0.12.0.2/pcs/cli/routing/prop.py000066400000000000000000000017441500417470700171070ustar00rootroot00000000000000from pcs import usage from pcs.cli.cluster_property import command as cluster_property from pcs.cli.common.errors import raise_command_replaced from pcs.cli.common.routing import create_router property_cmd = create_router( { "help": lambda _lib, _argv, _modifiers: print(usage.property(_argv)), "set": cluster_property.set_property, "unset": cluster_property.unset_property, "list": lambda lib, argv, modifiers: raise_command_replaced( ["pcs property config"], pcs_version="0.12" ), "show": lambda lib, argv, modifiers: raise_command_replaced( ["pcs property config"], pcs_version="0.12" ), "config": cluster_property.config, "defaults": cluster_property.defaults, "describe": cluster_property.describe, "get_cluster_properties_definition": ( cluster_property.print_cluster_properties_definition_legacy ), }, ["property"], default_cmd="config", ) pcs-0.12.0.2/pcs/cli/routing/qdevice.py000066400000000000000000000011141500417470700175360ustar00rootroot00000000000000from pcs import ( qdevice, usage, ) from pcs.cli.common.routing import create_router qdevice_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.qdevice(argv)), "status": qdevice.qdevice_status_cmd, "setup": qdevice.qdevice_setup_cmd, "destroy": qdevice.qdevice_destroy_cmd, "start": qdevice.qdevice_start_cmd, "stop": qdevice.qdevice_stop_cmd, "kill": qdevice.qdevice_kill_cmd, "enable": qdevice.qdevice_enable_cmd, "disable": qdevice.qdevice_disable_cmd, }, ["qdevice"], ) pcs-0.12.0.2/pcs/cli/routing/quorum.py000066400000000000000000000026561500417470700174620ustar00rootroot00000000000000from pcs import ( quorum, usage, ) from pcs.cli.common.routing import create_router quorum_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.quorum(argv)), "config": quorum.quorum_config_cmd, "expected-votes": quorum.quorum_expected_votes_cmd, "status": quorum.quorum_status_cmd, "device": create_router( { "add": quorum.quorum_device_add_cmd, "heuristics": create_router( { "delete": quorum.quorum_device_heuristics_remove_cmd, "remove": quorum.quorum_device_heuristics_remove_cmd, }, ["quorum", "device", "heuristics"], ), "delete": quorum.quorum_device_remove_cmd, "remove": quorum.quorum_device_remove_cmd, "status": quorum.quorum_device_status_cmd, "update": quorum.quorum_device_update_cmd, # used by ha_cluster system role "check_local_qnetd_certs": quorum.check_local_qnetd_certs_cmd, "setup_local_qnetd_certs": quorum.setup_local_qnetd_certs_cmd, }, ["quorum", "device"], ), # TODO switch to new architecture "unblock": quorum.quorum_unblock_cmd, "update": quorum.quorum_update_cmd, }, ["quorum"], default_cmd="config", ) pcs-0.12.0.2/pcs/cli/routing/resource.py000066400000000000000000000102411500417470700177460ustar00rootroot00000000000000from functools import partial import pcs.cli.resource.command as resource_cli from pcs import ( resource, usage, ) from pcs.cli.common.routing import create_router from pcs.cli.resource.relations import show_resource_relations_cmd from .resource_stonith_common import ( resource_defaults_cmd, resource_op_defaults_cmd, ) resource_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.resource(argv)), "list": resource.resource_list_available, "describe": resource.resource_list_options, "create": resource.resource_create, "move": resource.resource_move, "move-with-constraint": resource.resource_move_with_constraint, "ban": resource.resource_ban, "clear": resource.resource_unmove_unban, "standards": resource.resource_standards, "providers": resource.resource_providers, "agents": resource.resource_agents, "update": resource.update_cmd, "meta": resource.meta_cmd, "delete": resource.resource_remove_cmd, "remove": resource.resource_remove_cmd, "status": resource.resource_status, "config": resource_cli.config, "group": create_router( { "add": resource.resource_group_add_cmd, "list": resource.resource_group_list, "remove": resource.resource_group_rm_cmd, "delete": resource.resource_group_rm_cmd, }, ["resource", "group"], ), "ungroup": resource.resource_group_rm_cmd, "clone": resource.resource_clone, "promotable": partial(resource.resource_clone, promotable=True), "unclone": resource.resource_clone_master_remove, "enable": resource.resource_enable_cmd, "disable": resource.resource_disable_cmd, "safe-disable": resource.resource_safe_disable_cmd, "restart": resource.resource_restart_cmd, "debug-start": partial( resource.resource_force_action, action="debug-start" ), "debug-stop": partial( resource.resource_force_action, action="debug-stop" ), "debug-promote": partial( resource.resource_force_action, action="debug-promote" ), "debug-demote": partial( resource.resource_force_action, action="debug-demote" ), "debug-monitor": partial( resource.resource_force_action, action="debug-monitor" ), "manage": resource.resource_manage_cmd, "unmanage": resource.resource_unmanage_cmd, "failcount": create_router( { "show": resource.resource_failcount_show, }, ["resource", "failcount"], default_cmd="show", ), "op": create_router( { "defaults": resource_op_defaults_cmd( ["resource", "op", "defaults"] ), "add": resource.op_add_cmd, "remove": resource.op_delete_cmd, "delete": resource.op_delete_cmd, }, ["resource", "op"], ), "defaults": resource_defaults_cmd(["resource", "defaults"]), "cleanup": resource.resource_cleanup, "refresh": resource.resource_refresh, "relocate": create_router( { "show": resource.resource_relocate_show_cmd, "dry-run": resource.resource_relocate_dry_run_cmd, "run": resource.resource_relocate_run_cmd, "clear": resource.resource_relocate_clear_cmd, }, ["resource", "relocate"], ), "utilization": resource.resource_utilization_cmd, "bundle": create_router( { "create": resource.resource_bundle_create_cmd, "reset": resource.resource_bundle_reset_cmd, "update": resource.resource_bundle_update_cmd, }, ["resource", "bundle"], ), # internal use only "get_resource_agent_info": resource.get_resource_agent_info, "relations": show_resource_relations_cmd, }, ["resource"], default_cmd="status", ) pcs-0.12.0.2/pcs/cli/routing/resource_stonith_common.py000066400000000000000000000052171500417470700230750ustar00rootroot00000000000000from typing import ( Any, List, ) from pcs import resource from pcs.cli.common.errors import command_replaced from pcs.cli.common.parse_args import InputModifiers from pcs.cli.common.routing import ( CliCmdInterface, create_router, ) def resource_defaults_cmd(parent_cmd: List[str]) -> CliCmdInterface: def _get_router( lib: Any, argv: List[str], modifiers: InputModifiers ) -> None: """ Options: * -f - CIB file * --force - allow unknown options """ if argv and "=" in argv[0]: raise command_replaced( ["pcs resource defaults update"], pcs_version="0.12" ) router = create_router( { "config": resource.resource_defaults_config_cmd, "set": create_router( { "create": resource.resource_defaults_set_create_cmd, "delete": resource.resource_defaults_set_remove_cmd, "remove": resource.resource_defaults_set_remove_cmd, "update": resource.resource_defaults_set_update_cmd, }, parent_cmd + ["set"], ), "update": resource.resource_defaults_update_cmd, }, parent_cmd, default_cmd="config", ) return router(lib, argv, modifiers) return _get_router def resource_op_defaults_cmd(parent_cmd: List[str]) -> CliCmdInterface: def _get_router( lib: Any, argv: List[str], modifiers: InputModifiers ) -> None: """ Options: * -f - CIB file * --force - allow unknown options """ if argv and "=" in argv[0]: raise command_replaced( ["pcs resource op defaults update"], pcs_version="0.12" ) router = create_router( { "config": resource.resource_op_defaults_config_cmd, "set": create_router( { "create": resource.resource_op_defaults_set_create_cmd, "delete": resource.resource_op_defaults_set_remove_cmd, "remove": resource.resource_op_defaults_set_remove_cmd, "update": resource.resource_op_defaults_set_update_cmd, }, parent_cmd + ["set"], ), "update": resource.resource_op_defaults_update_cmd, }, parent_cmd, default_cmd="config", ) return router(lib, argv, modifiers) return _get_router pcs-0.12.0.2/pcs/cli/routing/status.py000066400000000000000000000043561500417470700174540ustar00rootroot00000000000000from typing import Any from pcs import ( status, usage, ) from pcs.cli.booth.command import status as booth_status_cmd from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, ) from pcs.cli.common.routing import create_router from pcs.cli.query import resource from pcs.cli.status import command as status_command from pcs.pcsd import pcsd_status_cmd from pcs.qdevice import qdevice_status_cmd from pcs.quorum import quorum_status_cmd from pcs.resource import resource_status def _query_resource_router( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: if not argv: raise CmdLineInputError() if len(argv) < 2: # show correct usage when resource_id is missing in the command argv.insert(0, "") # swap resource_id with next keyword in the command to be able to use router argv[0], argv[1] = argv[1], argv[0] create_router( { "exists": resource.exists, "is-in-bundle": resource.is_in_bundle, "is-in-clone": resource.is_in_clone, "is-in-group": resource.is_in_group, "is-state": resource.is_state, "is-stonith": resource.is_stonith, "is-type": resource.is_type, "get-type": resource.get_type, "get-members": resource.get_members, "get-nodes": resource.get_nodes, "get-index-in-group": resource.get_index_in_group, }, ["status", "query", "resource", ""], )(lib, argv, modifiers) status_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.status(argv)), "booth": booth_status_cmd, "corosync": status.corosync_status, "cluster": status.cluster_status, "nodes": status.nodes_status, "pcsd": pcsd_status_cmd, "qdevice": qdevice_status_cmd, "quorum": quorum_status_cmd, "resources": resource_status, "xml": status.xml_status, "status": status.full_status, "query": create_router( {"resource": _query_resource_router}, ["status", "query"] ), "wait": status_command.wait_for_pcmk_idle, }, ["status"], default_cmd="status", ) pcs-0.12.0.2/pcs/cli/routing/stonith.py000066400000000000000000000072421500417470700176160ustar00rootroot00000000000000import pcs.cli.stonith.command as stonith_cli import pcs.cli.stonith.levels.command as levels_cli from pcs import ( resource, stonith, usage, ) from pcs.cli.common.routing import create_router from .resource_stonith_common import ( resource_defaults_cmd, resource_op_defaults_cmd, ) stonith_cmd = create_router( { "help": lambda lib, argv, modifiers: print(usage.stonith(argv)), "list": stonith.stonith_list_available, "describe": stonith.stonith_list_options, "config": stonith_cli.config, "create": stonith.stonith_create, "update": stonith.update_cmd, "update-scsi-devices": stonith.stonith_update_scsi_devices, "delete": stonith.delete_cmd, "remove": stonith.delete_cmd, "status": stonith.stonith_status_cmd, "meta": stonith.meta_cmd, "op": create_router( { "defaults": resource_op_defaults_cmd( ["resource", "op", "defaults"] ), "add": stonith.op_add_cmd, "remove": stonith.op_delete_cmd, "delete": stonith.op_delete_cmd, }, ["stonith", "op"], ), "defaults": resource_defaults_cmd(["resource", "defaults"]), "level": create_router( { "add": stonith.stonith_level_add_cmd, "clear": stonith.stonith_level_clear_cmd, "config": levels_cli.config, "remove": stonith.stonith_level_remove_cmd, "delete": stonith.stonith_level_remove_cmd, "verify": stonith.stonith_level_verify_cmd, }, ["stonith", "level"], default_cmd="config", ), "failcount": create_router( { "show": resource.resource_failcount_show, }, ["stonith", "failcount"], default_cmd="show", ), "fence": stonith.stonith_fence, "cleanup": resource.resource_cleanup, "refresh": resource.resource_refresh, "confirm": stonith.stonith_confirm, "sbd": create_router( { "enable": stonith.sbd_enable, "disable": stonith.sbd_disable, "status": stonith.sbd_status, "config": stonith.sbd_config, "device": create_router( { "setup": stonith.sbd_setup_block_device, "message": stonith.sbd_message, }, ["stonith", "sbd", "device"], ), "watchdog": create_router( { "list": stonith.sbd_watchdog_list, "test": stonith.sbd_watchdog_test, # internal use only "list_json": stonith.sbd_watchdog_list_json, }, ["stonith", "sbd", "watchdog"], ), # internal use only "local_config_in_json": stonith.local_sbd_config, }, ["stonith", "sbd"], ), "enable": stonith.enable_cmd, "disable": stonith.disable_cmd, "history": create_router( { "show": stonith.stonith_history_show_cmd, "cleanup": stonith.stonith_history_cleanup_cmd, "update": stonith.stonith_history_update_cmd, }, ["stonith", "history"], default_cmd="show", ), # internal use only "get_fence_agent_info": stonith.get_fence_agent_info, }, ["stonith"], default_cmd="status", ) pcs-0.12.0.2/pcs/cli/routing/tag.py000066400000000000000000000011511500417470700166720ustar00rootroot00000000000000from pcs import usage from pcs.cli.common.errors import raise_command_replaced from pcs.cli.common.routing import create_router from pcs.cli.tag import command as tag tag_cmd = create_router( { "config": tag.tag_config, "create": tag.tag_create, "delete": tag.tag_remove, "help": lambda lib, argv, modifiers: print(usage.tag(argv)), "list": lambda lib, argv, modifiers: raise_command_replaced( ["pcs tag config"], pcs_version="0.12" ), "remove": tag.tag_remove, "update": tag.tag_update, }, ["tag"], default_cmd="config", ) pcs-0.12.0.2/pcs/cli/rule.py000066400000000000000000000063561500417470700154130ustar00rootroot00000000000000from typing import ( List, Optional, ) from pcs.common.pacemaker.rule import CibRuleExpressionDto from pcs.common.str_tools import ( format_name_value_list, indent, ) from pcs.common.types import ( CibRuleExpressionType, CibRuleInEffectStatus, ) _in_effect_label_map = { CibRuleInEffectStatus.NOT_YET_IN_EFFECT: "not yet in effect", CibRuleInEffectStatus.IN_EFFECT: None, CibRuleInEffectStatus.EXPIRED: "expired", } def get_in_effect_label(rule: CibRuleExpressionDto) -> Optional[str]: return _in_effect_label_map.get(rule.in_effect, None) def rule_expression_dto_to_lines( rule_expr: CibRuleExpressionDto, with_ids: bool = False ) -> List[str]: if rule_expr.type == CibRuleExpressionType.RULE: return _rule_dto_to_lines(rule_expr, with_ids) if rule_expr.type == CibRuleExpressionType.DATE_EXPRESSION: return _date_dto_to_lines(rule_expr, with_ids) return _simple_expr_to_lines(rule_expr, with_ids) def _rule_dto_to_lines( rule_expr: CibRuleExpressionDto, with_ids: bool = False ) -> List[str]: in_effect_label = get_in_effect_label(rule_expr) heading_parts = [ "Rule{0}:".format(f" ({in_effect_label})" if in_effect_label else "") ] heading_parts.extend( format_name_value_list(sorted(rule_expr.options.items())) ) if with_ids: heading_parts.append(f"(id: {rule_expr.id})") lines = [] for child in rule_expr.expressions: lines.extend(rule_expression_dto_to_lines(child, with_ids)) return [" ".join(heading_parts)] + indent(lines) def _date_dto_to_lines( rule_expr: CibRuleExpressionDto, with_ids: bool = False ) -> List[str]: operation = rule_expr.options.get("operation", None) if operation == "date_spec": heading_parts = ["Expression:"] if with_ids: heading_parts.append(f"(id: {rule_expr.id})") line_parts = ["Date Spec:"] if rule_expr.date_spec: line_parts.extend( format_name_value_list( sorted(rule_expr.date_spec.options.items()) ) ) if with_ids: line_parts.append(f"(id: {rule_expr.date_spec.id})") return [" ".join(heading_parts)] + indent([" ".join(line_parts)]) if operation == "in_range" and rule_expr.duration: heading_parts = ["Expression:", "date", "in_range"] if "start" in rule_expr.options: heading_parts.append(rule_expr.options["start"]) heading_parts.extend(["to", "duration"]) if with_ids: heading_parts.append(f"(id: {rule_expr.id})") lines = [" ".join(heading_parts)] line_parts = ["Duration:"] line_parts.extend( format_name_value_list(sorted(rule_expr.duration.options.items())) ) if with_ids: line_parts.append(f"(id: {rule_expr.duration.id})") lines.extend(indent([" ".join(line_parts)])) return lines return _simple_expr_to_lines(rule_expr, with_ids=with_ids) def _simple_expr_to_lines( rule_expr: CibRuleExpressionDto, with_ids: bool = False ) -> List[str]: parts = ["Expression:", rule_expr.as_string] if with_ids: parts.append(f"(id: {rule_expr.id})") return [" ".join(parts)] pcs-0.12.0.2/pcs/cli/status/000077500000000000000000000000001500417470700154035ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/status/__init__.py000066400000000000000000000000001500417470700175020ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/status/command.py000066400000000000000000000006551500417470700174010ustar00rootroot00000000000000from typing import Any from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, ) def wait_for_pcmk_idle(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: no options """ modifiers.ensure_only_supported() if len(argv) > 1: raise CmdLineInputError() lib.cluster.wait_for_pcmk_idle(argv[0] if argv else None) pcs-0.12.0.2/pcs/cli/stonith/000077500000000000000000000000001500417470700155505ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/stonith/__init__.py000066400000000000000000000000001500417470700176470ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/stonith/command.py000066400000000000000000000036331500417470700175450ustar00rootroot00000000000000from typing import Any from pcs.cli.common.output import ( format_cmd_list, lines_to_str, smart_wrap_text, ) from pcs.cli.common.parse_args import ( OUTPUT_FORMAT_VALUE_CMD, OUTPUT_FORMAT_VALUE_JSON, Argv, InputModifiers, ) from pcs.cli.reports.output import warn from pcs.cli.resource import command as resource_cmd from pcs.cli.stonith.levels.output import ( stonith_level_config_to_cmd, stonith_level_config_to_text, ) from pcs.common.str_tools import indent def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --output-format - supported formats: text, cmd, json * -f CIB file """ output_format = modifiers.get_output_format() output = resource_cmd.config_common(lib, argv, modifiers, stonith=True) if output_format == OUTPUT_FORMAT_VALUE_JSON: # JSON output format does not include fencing levels because it would # change the current JSON structure and break existing user tooling warn( "Fencing levels are not included because this command could only " "export stonith configuration previously. This cannot be changed " "to avoid breaking existing tooling. To export fencing levels, run " "'pcs stonith level config --output-format=json'" ) print(output) return fencing_topology_dto = lib.fencing_topology.get_config_dto() if output_format == OUTPUT_FORMAT_VALUE_CMD: # we can look at the output of config_common as one command output = format_cmd_list( [output, *stonith_level_config_to_cmd(fencing_topology_dto)] ) else: text_output = stonith_level_config_to_text(fencing_topology_dto) if text_output: output += "\n\nFencing Levels:\n" + lines_to_str( smart_wrap_text(indent(text_output)) ) if output: print(output) pcs-0.12.0.2/pcs/cli/stonith/levels/000077500000000000000000000000001500417470700170425ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/stonith/levels/__init__.py000066400000000000000000000000001500417470700211410ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/stonith/levels/command.py000066400000000000000000000023571500417470700210410ustar00rootroot00000000000000import json from typing import Any from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.output import ( format_cmd_list, lines_to_str, ) from pcs.cli.common.parse_args import ( OUTPUT_FORMAT_VALUE_CMD, OUTPUT_FORMAT_VALUE_JSON, Argv, InputModifiers, ) from pcs.cli.stonith.levels import output as levels_output from pcs.common.interface.dto import to_dict def config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --output-format - supported formats: text, cmd, json * -f - CIB file """ modifiers.ensure_only_supported("-f", output_format_supported=True) output_format = modifiers.get_output_format() if argv: raise CmdLineInputError fencing_topology_dto = lib.fencing_topology.get_config_dto() if output_format == OUTPUT_FORMAT_VALUE_JSON: output = json.dumps(to_dict(fencing_topology_dto)) elif output_format == OUTPUT_FORMAT_VALUE_CMD: output = format_cmd_list( levels_output.stonith_level_config_to_cmd(fencing_topology_dto) ) else: output = lines_to_str( levels_output.stonith_level_config_to_text(fencing_topology_dto) ) if output: print(output) pcs-0.12.0.2/pcs/cli/stonith/levels/output.py000066400000000000000000000063511500417470700207610ustar00rootroot00000000000000from collections.abc import Sequence from pcs.common.pacemaker.fencing_topology import ( CibFencingLevel, CibFencingLevelAttributeDto, CibFencingLevelRegexDto, CibFencingTopologyDto, ) from pcs.common.str_tools import indent from pcs.common.types import ( StringCollection, StringSequence, ) def _get_targets_with_levels_str( levels: Sequence[CibFencingLevel], ) -> list[str]: lines = [] last_target_value = "" for level in levels: if isinstance(level, CibFencingLevelAttributeDto): target_label = "attribute" target_value = f"{level.target_attribute}={level.target_value}" elif isinstance(level, CibFencingLevelRegexDto): target_label = "regexp" target_value = level.target_pattern else: target_label = "node" target_value = level.target if target_value != last_target_value: lines.append(f"Target ({target_label}): {target_value}") last_target_value = target_value lines.extend( indent( [ "Level {level}: {devices}".format( level=level.index, devices=" ".join(level.devices) ) ] ) ) return lines def stonith_level_config_to_text( fencing_topology: CibFencingTopologyDto, ) -> StringSequence: target_node_levels = sorted( fencing_topology.target_node, key=lambda level: (level.target, level.index), ) target_regex_levels = sorted( fencing_topology.target_regex, key=lambda level: (level.target_pattern, level.index), ) target_attr_levels = sorted( fencing_topology.target_attribute, key=lambda level: ( level.target_value, level.target_attribute, level.index, ), ) return ( _get_targets_with_levels_str(target_node_levels) + _get_targets_with_levels_str(target_regex_levels) + _get_targets_with_levels_str(target_attr_levels) ) def _get_level_add_cmd( index: int, target: str, device_list: StringCollection, level_id: str, ) -> str: devices = " ".join(device_list) return ( f"pcs stonith level add --force -- {index} {target} {devices} " f"id={level_id}" ) def stonith_level_config_to_cmd( fencing_topology: CibFencingTopologyDto, ) -> StringSequence: lines: list[str] = [] level = None for level in fencing_topology.target_node: lines.append( _get_level_add_cmd( level.index, level.target, level.devices, level.id ) ) for level_regex in fencing_topology.target_regex: target = f"regexp%{level_regex.target_pattern}" lines.append( _get_level_add_cmd( level_regex.index, target, level_regex.devices, level_regex.id ) ) for level_attr in fencing_topology.target_attribute: target = ( f"attrib%{level_attr.target_attribute}={level_attr.target_value}" ) lines.append( _get_level_add_cmd( level_attr.index, target, level_attr.devices, level_attr.id ) ) return lines pcs-0.12.0.2/pcs/cli/tag/000077500000000000000000000000001500417470700146335ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/tag/__init__.py000066400000000000000000000000001500417470700167320ustar00rootroot00000000000000pcs-0.12.0.2/pcs/cli/tag/command.py000066400000000000000000000053551500417470700166330ustar00rootroot00000000000000from typing import Any from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.parse_args import ( Argv, InputModifiers, group_by_keywords, ) from pcs.cli.tag.output import print_config def tag_create(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) < 2: raise CmdLineInputError() tag_id, idref_list = argv[0], argv[1:] lib.tag.create(tag_id, idref_list) def tag_config(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --output-format - supported formats: text, cmd, json """ modifiers.ensure_only_supported("-f", "--output-format") tag_dto = lib.tag.get_config_dto(argv) print_config(tag_dto, modifiers) def tag_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) < 1: raise CmdLineInputError() lib.tag.remove(argv) def tag_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --after - place a reference id in a tag after the specified reference id in the tag * --before - place a reference id in a tag before the specified reference id in the tag """ modifiers.ensure_only_supported("-f", "--after", "--before") if not argv: raise CmdLineInputError() tag_id = argv[0] parsed_args = group_by_keywords(argv[1:], ["add", "remove"]) parsed_args.ensure_unique_keywords() no_add_remove_arguments = not ( parsed_args.has_keyword("add") or parsed_args.has_keyword("remove") ) no_add_id = parsed_args.has_empty_keyword("add") no_remove_id = parsed_args.has_empty_keyword("remove") if no_add_remove_arguments or no_add_id or no_remove_id: raise CmdLineInputError( show_both_usage_and_message=True, hint=("Specify at least one id for 'add' or 'remove' arguments."), ) adjacent_idref = None after_adjacent = True if modifiers.is_specified("--after") and modifiers.is_specified("--before"): raise CmdLineInputError("Cannot specify both --before and --after") if modifiers.is_specified("--after"): adjacent_idref = modifiers.get("--after") after_adjacent = True elif modifiers.is_specified("--before"): adjacent_idref = modifiers.get("--before") after_adjacent = False lib.tag.update( tag_id, parsed_args.get_args_flat("add"), parsed_args.get_args_flat("remove"), adjacent_idref=adjacent_idref, put_after_adjacent=after_adjacent, ) pcs-0.12.0.2/pcs/cli/tag/output.py000066400000000000000000000024001500417470700165410ustar00rootroot00000000000000import json import shlex from pcs.cli.common.output import lines_to_str from pcs.cli.common.parse_args import ( OUTPUT_FORMAT_VALUE_CMD, OUTPUT_FORMAT_VALUE_JSON, InputModifiers, ) from pcs.common.interface import dto from pcs.common.pacemaker.tag import CibTagListDto from pcs.common.str_tools import indent def tags_to_text(tags_dto: CibTagListDto) -> list[str]: result = [] for tag in tags_dto.tags: result.append(tag.id) result.extend(indent(list(tag.idref_list))) return result def tags_to_cmd(tags_dto: CibTagListDto) -> list[str]: return [ "pcs -- tag create {tag_id} {idref_list}".format( tag_id=shlex.quote(tag.id), idref_list=" ".join(shlex.quote(idref) for idref in tag.idref_list), ) for tag in tags_dto.tags ] def print_config(tags_dto: CibTagListDto, modifiers: InputModifiers) -> None: output_format = modifiers.get_output_format() if output_format == OUTPUT_FORMAT_VALUE_JSON: print(json.dumps(dto.to_dict(tags_dto), indent=2)) return if output_format == OUTPUT_FORMAT_VALUE_CMD: print(";\n".join(tags_to_cmd(tags_dto))) return result = lines_to_str(tags_to_text(tags_dto)) if result: print(result) pcs-0.12.0.2/pcs/client.py000066400000000000000000000013541500417470700151440ustar00rootroot00000000000000from pcs import ( settings, utils, ) from pcs.cli.common.errors import CmdLineInputError def local_auth_cmd(lib, argv, modifiers): """ Options: * -u - username * -p - password * --request-timeout - timeout for HTTP requests """ del lib modifiers.ensure_only_supported("-u", "-p", "--request-timeout") if len(argv) > 1: raise CmdLineInputError() port = argv[0] if argv else settings.pcsd_default_port username, password = utils.get_user_and_pass() utils.auth_hosts( { "localhost": { "username": username, "password": password, "dest_list": [{"addr": "localhost", "port": port}], } } ) pcs-0.12.0.2/pcs/cluster.py000066400000000000000000002137141500417470700153540ustar00rootroot00000000000000# pylint: disable=too-many-lines import datetime import json import math import os import subprocess import sys import tempfile import time import xml.dom.minidom from typing import ( Any, Callable, Iterable, Mapping, Optional, Union, cast, ) import pcs.lib.pacemaker.live as lib_pacemaker from pcs import ( settings, utils, ) from pcs.cli.common import parse_args from pcs.cli.common.errors import ( ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE, CmdLineInputError, ) from pcs.cli.common.parse_args import ( OUTPUT_FORMAT_VALUE_CMD, OUTPUT_FORMAT_VALUE_JSON, Argv, InputModifiers, KeyValueParser, ) from pcs.cli.common.tools import print_to_stderr from pcs.cli.file import metadata as file_metadata from pcs.cli.reports import process_library_reports from pcs.cli.reports.messages import report_item_msg_from_dto from pcs.cli.reports.output import ( deprecation_warning, warn, ) from pcs.common import file as pcs_file from pcs.common import ( file_type_codes, reports, ) from pcs.common.corosync_conf import ( CorosyncConfDto, CorosyncNodeDto, ) from pcs.common.interface import dto from pcs.common.node_communicator import ( HostNotFound, Request, RequestData, ) from pcs.common.str_tools import ( format_list, indent, ) from pcs.common.tools import format_os_error from pcs.common.types import ( StringCollection, StringIterable, ) from pcs.lib import sbd as lib_sbd from pcs.lib.commands.remote_node import _destroy_pcmk_remote_env from pcs.lib.communication.nodes import CheckAuth from pcs.lib.communication.tools import RunRemotelyBase from pcs.lib.communication.tools import run as run_com_cmd from pcs.lib.communication.tools import run_and_raise from pcs.lib.corosync import qdevice_net from pcs.lib.corosync.live import ( QuorumStatusException, QuorumStatusFacade, ) from pcs.lib.errors import LibraryError from pcs.lib.node import get_existing_nodes_names from pcs.utils import parallel_for_nodes def _corosync_conf_local_cmd_call( corosync_conf_path: parse_args.ModifierValueType, lib_cmd: Callable[[bytes], bytes], ) -> None: """ Call a library command that requires modifications of a corosync.conf file supplied as an argument The lib command needs to take the corosync.conf file content as its first argument lib_cmd -- the lib command to be called """ corosync_conf_file = pcs_file.RawFile( file_metadata.for_file_type( file_type_codes.COROSYNC_CONF, corosync_conf_path ) ) try: corosync_conf_file.write( lib_cmd( corosync_conf_file.read(), ), can_overwrite=True, ) except pcs_file.RawFileError as e: raise CmdLineInputError( reports.messages.FileIoError( e.metadata.file_type_code, e.action, e.reason, file_path=e.metadata.path, ).message ) from e def cluster_cib_upgrade_cmd( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: """ Options: * -f - CIB file """ del lib modifiers.ensure_only_supported("-f") if argv: raise CmdLineInputError() utils.cluster_upgrade() def cluster_disable_cmd( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: """ Options: * --all - disable all cluster nodes * --request-timeout - timeout for HTTP requests - effective only when at least one node has been specified or --all has been used """ del lib modifiers.ensure_only_supported("--all", "--request-timeout") if modifiers.get("--all"): if argv: utils.err(ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE) disable_cluster_all() else: disable_cluster(argv) def cluster_enable_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --all - enable all cluster nodes * --request-timeout - timeout for HTTP requests - effective only when at least one node has been specified or --all has been used """ del lib modifiers.ensure_only_supported("--all", "--request-timeout") if modifiers.get("--all"): if argv: utils.err(ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE) enable_cluster_all() else: enable_cluster(argv) def cluster_stop_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - no error when possible quorum loss * --request-timeout - timeout for HTTP requests - effective only when at least one node has been specified * --pacemaker - stop pacemaker, only effective when no node has been specified * --corosync - stop corosync, only effective when no node has been specified * --all - stop all cluster nodes """ del lib modifiers.ensure_only_supported( "--wait", "--request-timeout", "--pacemaker", "--corosync", "--all", "--force", ) if modifiers.get("--all"): if argv: utils.err(ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE) stop_cluster_all() else: stop_cluster(argv) def cluster_start_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --wait * --request-timeout - timeout for HTTP requests, have effect only if at least one node have been specified * --all - start all cluster nodes """ del lib modifiers.ensure_only_supported( "--wait", "--request-timeout", "--all", "--corosync_conf" ) if modifiers.get("--all"): if argv: utils.err(ERR_NODE_LIST_AND_ALL_MUTUALLY_EXCLUSIVE) start_cluster_all() else: start_cluster(argv) def authkey_corosync(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - skip check for authkey length * --request-timeout - timeout for HTTP requests * --skip-offline - skip unreachable nodes """ modifiers.ensure_only_supported( "--force", "--skip-offline", "--request-timeout" ) if len(argv) > 1: raise CmdLineInputError() force_flags = [] if modifiers.get("--force"): force_flags.append(reports.codes.FORCE) if modifiers.get("--skip-offline"): force_flags.append(reports.codes.SKIP_OFFLINE_NODES) corosync_authkey = None if argv: try: with open(argv[0], "rb") as file: corosync_authkey = file.read() except OSError as e: utils.err(f"Unable to read file '{argv[0]}': {format_os_error(e)}") lib.cluster.corosync_authkey_change( corosync_authkey=corosync_authkey, force_flags=force_flags, ) def sync_nodes(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --request-timeout - timeout for HTTP requests """ del lib modifiers.ensure_only_supported("--request-timeout") if argv: raise CmdLineInputError() config = utils.getCorosyncConf() nodes, report_list = get_existing_nodes_names( utils.get_corosync_conf_facade(conf_text=config) ) if not nodes: report_list.append( reports.ReportItem.error( reports.messages.CorosyncConfigNoNodesDefined() ) ) if report_list: process_library_reports(report_list) for node in nodes: utils.setCorosyncConfig(node, config) warn( "Corosync configuration has been synchronized, please reload corosync " "daemon using 'pcs cluster reload corosync' command." ) def start_cluster(argv: Argv) -> None: """ Commandline options: * --wait * --request-timeout - timeout for HTTP requests, have effect only if at least one node have been specified """ wait = False wait_timeout = None if "--wait" in utils.pcs_options: wait_timeout = utils.validate_wait_get_timeout(False) wait = True if argv: nodes = set(argv) # unique start_cluster_nodes(nodes) if wait: wait_for_nodes_started(nodes, wait_timeout) return if not utils.hasCorosyncConf(): utils.err("cluster is not currently configured on this node") print_to_stderr("Starting Cluster...") service_list = ["corosync"] if utils.need_to_handle_qdevice_service(): service_list.append("corosync-qdevice") service_list.append("pacemaker") for service in service_list: utils.start_service(service) if wait: wait_for_nodes_started([], wait_timeout) def start_cluster_all() -> None: """ Commandline options: * --wait * --request-timeout - timeout for HTTP requests """ wait = False wait_timeout = None if "--wait" in utils.pcs_options: wait_timeout = utils.validate_wait_get_timeout(False) wait = True all_nodes, report_list = get_existing_nodes_names( utils.get_corosync_conf_facade() ) if not all_nodes: report_list.append( reports.ReportItem.error( reports.messages.CorosyncConfigNoNodesDefined() ) ) if report_list: process_library_reports(report_list) start_cluster_nodes(all_nodes) if wait: wait_for_nodes_started(all_nodes, wait_timeout) def start_cluster_nodes(nodes: StringCollection) -> None: """ Commandline options: * --request-timeout - timeout for HTTP requests """ # Large clusters take longer time to start up. So we make the timeout longer # for each 8 nodes: # 1 - 8 nodes: 1 * timeout # 9 - 16 nodes: 2 * timeout # 17 - 24 nodes: 3 * timeout # and so on # Users can override this and set their own timeout by specifying # the --request-timeout option (see utils.sendHTTPRequest). timeout = int( settings.default_request_timeout * math.ceil(len(nodes) / 8.0) ) utils.read_known_hosts_file() # cache known hosts node_errors = parallel_for_nodes( utils.startCluster, nodes, quiet=True, timeout=timeout ) if node_errors: utils.err( "unable to start all nodes\n" + "\n".join(node_errors.values()) ) def is_node_fully_started(node_status) -> bool: """ Commandline options: no options """ return ( "online" in node_status and "pending" in node_status and node_status["online"] and not node_status["pending"] ) def wait_for_local_node_started( stop_at: datetime.datetime, interval: float ) -> tuple[int, str]: """ Commandline options: no options """ try: while True: time.sleep(interval) node_status = lib_pacemaker.get_local_node_status( utils.cmd_runner() ) if is_node_fully_started(node_status): return 0, "Started" if datetime.datetime.now() > stop_at: return 1, "Waiting timeout" except LibraryError as e: return ( 1, "Unable to get node status: {0}".format( "\n".join( report_item_msg_from_dto( cast(reports.ReportItemDto, item).message ).message for item in e.args ) ), ) def wait_for_remote_node_started( node: str, stop_at: datetime.datetime, interval: float ) -> tuple[int, str]: """ Commandline options: * --request-timeout - timeout for HTTP requests """ while True: time.sleep(interval) code, output = utils.getPacemakerNodeStatus(node) # HTTP error, permission denied or unable to auth # there is no point in trying again as it won't get magically fixed if code in [1, 3, 4]: return 1, output if code == 0: try: node_status = json.loads(output) if is_node_fully_started(node_status): return 0, "Started" except (ValueError, KeyError): # this won't get fixed either return 1, "Unable to get node status" if datetime.datetime.now() > stop_at: return 1, "Waiting timeout" def wait_for_nodes_started( node_list: StringIterable, timeout: Optional[int] = None ) -> None: """ Commandline options: * --request-timeout - timeout for HTTP request, effective only if node_list is not empty list """ timeout = 60 * 15 if timeout is None else timeout interval = 2 stop_at = datetime.datetime.now() + datetime.timedelta(seconds=timeout) print_to_stderr("Waiting for node(s) to start...") if not node_list: code, output = wait_for_local_node_started(stop_at, interval) if code != 0: utils.err(output) else: print_to_stderr(output) else: utils.read_known_hosts_file() # cache known hosts node_errors = parallel_for_nodes( wait_for_remote_node_started, node_list, stop_at, interval ) if node_errors: utils.err("unable to verify all nodes have started") def stop_cluster_all() -> None: """ Commandline options: * --force - no error when possible quorum loss * --request-timeout - timeout for HTTP requests """ all_nodes, report_list = get_existing_nodes_names( utils.get_corosync_conf_facade() ) if not all_nodes: report_list.append( reports.ReportItem.error( reports.messages.CorosyncConfigNoNodesDefined() ) ) if report_list: process_library_reports(report_list) stop_cluster_nodes(all_nodes) def stop_cluster_nodes(nodes: StringCollection) -> None: """ Commandline options: * --force - no error when possible quorum loss * --request-timeout - timeout for HTTP requests """ # pylint: disable=too-many-branches all_nodes, report_list = get_existing_nodes_names( utils.get_corosync_conf_facade() ) unknown_nodes = set(nodes) - set(all_nodes) if unknown_nodes: if report_list: process_library_reports(report_list) utils.err( "nodes '%s' do not appear to exist in configuration" % "', '".join(sorted(unknown_nodes)) ) utils.read_known_hosts_file() # cache known hosts stopping_all = set(nodes) >= set(all_nodes) if "--force" not in utils.pcs_options and not stopping_all: error_list = [] for node in nodes: retval, data = utils.get_remote_quorumtool_output(node) if retval != 0: error_list.append(node + ": " + data) continue try: quorum_status_facade = QuorumStatusFacade.from_string(data) if not quorum_status_facade.is_quorate: # Get quorum status from a quorate node, non-quorate nodes # may provide inaccurate info. If no node is quorate, there # is no quorum to be lost and therefore no error to be # reported. continue if quorum_status_facade.stopping_nodes_cause_quorum_loss(nodes): utils.err( "Stopping the node(s) will cause a loss of the quorum" + ", use --force to override" ) else: # We have the info, no need to print errors error_list = [] break except QuorumStatusException: if not utils.is_node_offline_by_quorumtool_output(data): error_list.append(node + ": Unable to get quorum status") # else the node seems to be stopped already if error_list: utils.err( "Unable to determine whether stopping the nodes will cause " + "a loss of the quorum, use --force to override\n" + "\n".join(error_list) ) was_error = False node_errors = parallel_for_nodes( utils.repeat_if_timeout(utils.stopPacemaker), nodes, quiet=True ) accessible_nodes = [node for node in nodes if node not in node_errors] if node_errors: utils.err( "unable to stop all nodes\n" + "\n".join(node_errors.values()), exit_after_error=not accessible_nodes, ) was_error = True for node in node_errors: print_to_stderr( "{0}: Not stopping cluster - node is unreachable".format(node) ) node_errors = parallel_for_nodes( utils.stopCorosync, accessible_nodes, quiet=True ) if node_errors: utils.err( "unable to stop all nodes\n" + "\n".join(node_errors.values()) ) if was_error: utils.err("unable to stop all nodes") def enable_cluster(argv: Argv) -> None: """ Commandline options: * --request-timeout - timeout for HTTP requests, effective only if at least one node has been specified """ if argv: enable_cluster_nodes(argv) return try: utils.enableServices() except LibraryError as e: process_library_reports(list(e.args)) def disable_cluster(argv: Argv) -> None: """ Commandline options: * --request-timeout - timeout for HTTP requests, effective only if at least one node has been specified """ if argv: disable_cluster_nodes(argv) return try: utils.disableServices() except LibraryError as e: process_library_reports(list(e.args)) def enable_cluster_all() -> None: """ Commandline options: * --request-timeout - timeout for HTTP requests """ all_nodes, report_list = get_existing_nodes_names( utils.get_corosync_conf_facade() ) if not all_nodes: report_list.append( reports.ReportItem.error( reports.messages.CorosyncConfigNoNodesDefined() ) ) if report_list: process_library_reports(report_list) enable_cluster_nodes(all_nodes) def disable_cluster_all() -> None: """ Commandline options: * --request-timeout - timeout for HTTP requests """ all_nodes, report_list = get_existing_nodes_names( utils.get_corosync_conf_facade() ) if not all_nodes: report_list.append( reports.ReportItem.error( reports.messages.CorosyncConfigNoNodesDefined() ) ) if report_list: process_library_reports(report_list) disable_cluster_nodes(all_nodes) def enable_cluster_nodes(nodes: StringIterable) -> None: """ Commandline options: * --request-timeout - timeout for HTTP requests """ error_list = utils.map_for_error_list(utils.enableCluster, nodes) if error_list: utils.err("unable to enable all nodes\n" + "\n".join(error_list)) def disable_cluster_nodes(nodes: StringIterable) -> None: """ Commandline options: * --request-timeout - timeout for HTTP requests """ error_list = utils.map_for_error_list(utils.disableCluster, nodes) if error_list: utils.err("unable to disable all nodes\n" + "\n".join(error_list)) def destroy_cluster(argv: Argv) -> None: """ Commandline options: * --request-timeout - timeout for HTTP requests """ if argv: utils.read_known_hosts_file() # cache known hosts # stop pacemaker and resources while cluster is still quorate nodes = argv node_errors = parallel_for_nodes( utils.repeat_if_timeout(utils.stopPacemaker), nodes, quiet=True ) # proceed with destroy regardless of errors # destroy will stop any remaining cluster daemons node_errors = parallel_for_nodes( utils.destroyCluster, nodes, quiet=True ) if node_errors: utils.err( "unable to destroy cluster\n" + "\n".join(node_errors.values()) ) def stop_cluster(argv: Argv) -> None: """ Commandline options: * --force - no error when possible quorum loss * --request-timeout - timeout for HTTP requests - effective only when at least one node has been specified * --pacemaker - stop pacemaker, only effective when no node has been specified """ if argv: stop_cluster_nodes(argv) return if "--force" not in utils.pcs_options: # corosync 3.0.1 and older: # - retval is 0 on success if a node is not in a partition with quorum # - retval is 1 on error OR on success if a node has quorum # corosync 3.0.2 and newer: # - retval is 0 on success if a node has quorum # - retval is 1 on error # - retval is 2 on success if a node is not in a partition with quorum output, dummy_retval = utils.run(["corosync-quorumtool", "-p", "-s"]) try: if QuorumStatusFacade.from_string( output ).stopping_local_node_cause_quorum_loss(): utils.err( "Stopping the node will cause a loss of the quorum" + ", use --force to override" ) except QuorumStatusException: if not utils.is_node_offline_by_quorumtool_output(output): utils.err( "Unable to determine whether stopping the node will cause " + "a loss of the quorum, use --force to override" ) # else the node seems to be stopped already, proceed to be sure stop_all = ( "--pacemaker" not in utils.pcs_options and "--corosync" not in utils.pcs_options ) if stop_all or "--pacemaker" in utils.pcs_options: stop_cluster_pacemaker() if stop_all or "--corosync" in utils.pcs_options: stop_cluster_corosync() def stop_cluster_pacemaker() -> None: """ Commandline options: no options """ print_to_stderr("Stopping Cluster (pacemaker)...") utils.stop_service("pacemaker") def stop_cluster_corosync() -> None: """ Commandline options: no options """ print_to_stderr("Stopping Cluster (corosync)...") service_list = [] if utils.need_to_handle_qdevice_service(): service_list.append("corosync-qdevice") service_list.append("corosync") for service in service_list: utils.stop_service(service) def kill_cluster(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: no options """ del lib if argv: raise CmdLineInputError() modifiers.ensure_only_supported() dummy_output, dummy_retval = kill_local_cluster_services() # if dummy_retval != 0: # print "Error: unable to execute killall -9" # print output # sys.exit(1) def kill_local_cluster_services() -> tuple[str, int]: """ Commandline options: no options """ all_cluster_daemons = [ # Daemons taken from cluster-clean script in pacemaker "pacemaker-attrd", "pacemaker-based", "pacemaker-controld", "pacemaker-execd", "pacemaker-fenced", "pacemaker-remoted", "pacemaker-schedulerd", "pacemakerd", "dlm_controld", "gfs_controld", # Corosync daemons "corosync-qdevice", "corosync", ] return utils.run([settings.killall_exec, "-9"] + all_cluster_daemons) def cluster_push(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --wait * --config - push only configuration section of CIB * -f - CIB file """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements del lib modifiers.ensure_only_supported("--wait", "--config", "-f") if len(argv) > 2: raise CmdLineInputError() filename = None scope = None timeout = None diff_against = None if modifiers.get("--wait"): timeout = utils.validate_wait_get_timeout() for arg in argv: if "=" not in arg: filename = arg else: arg_name, arg_value = arg.split("=", 1) if arg_name == "scope": if modifiers.get("--config"): utils.err("Cannot use both scope and --config") if not utils.is_valid_cib_scope(arg_value): utils.err("invalid CIB scope '%s'" % arg_value) else: scope = arg_value elif arg_name == "diff-against": diff_against = arg_value else: raise CmdLineInputError() if modifiers.get("--config"): scope = "configuration" if diff_against and scope: utils.err("Cannot use both scope and diff-against") if not filename: raise CmdLineInputError() try: new_cib_dom = xml.dom.minidom.parse(filename) if scope and not new_cib_dom.getElementsByTagName(scope): utils.err( "unable to push cib, scope '%s' not present in new cib" % scope ) except (EnvironmentError, xml.parsers.expat.ExpatError) as e: utils.err("unable to parse new cib: %s" % e) if diff_against: runner = utils.cmd_runner() command = [ settings.crm_diff_exec, "--original", diff_against, "--new", filename, "--no-version", ] patch, stderr, retval = runner.run(command) # 0 (CRM_EX_OK) - success with no difference # 1 (CRM_EX_ERROR) - success with difference # 64 (CRM_EX_USAGE) - usage error # 65 (CRM_EX_DATAERR) - XML fragments not parseable if retval > 1: utils.err("unable to diff the CIBs:\n" + stderr) if retval == 0: print_to_stderr( "The new CIB is the same as the original CIB, nothing to push." ) sys.exit(0) command = [ settings.cibadmin_exec, "--patch", "--xml-pipe", ] output, stderr, retval = runner.run(command, patch) if retval != 0: utils.err("unable to push cib\n" + stderr + output) else: command = ["cibadmin", "--replace", "--xml-file", filename] if scope: command.append("--scope=%s" % scope) output, retval = utils.run(command) # 103 (CRM_EX_OLD) - update older than existing config if retval == 103: utils.err( "Unable to push to the CIB because pushed configuration " "is older than existing one. If you are sure you want to " "push this configuration, try to use --config to replace only " "configuration part instead of whole CIB. Otherwise get current" " configuration by running command 'pcs cluster cib' and update" " that." ) elif retval != 0: utils.err("unable to push cib\n" + output) print_to_stderr("CIB updated") if not modifiers.is_specified("--wait"): return cmd = ["crm_resource", "--wait"] if timeout: cmd.extend(["--timeout", str(timeout)]) output, retval = utils.run(cmd) if retval != 0: msg = [] if retval == settings.pacemaker_wait_timeout_status: msg.append("waiting timeout") if output: msg.append("\n" + output) utils.err("\n".join(msg).strip()) def cluster_edit(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --config - edit configuration section of CIB * -f - CIB file * --wait """ # pylint: disable=too-many-branches modifiers.ensure_only_supported("--config", "--wait", "-f") if "EDITOR" in os.environ: if len(argv) > 1: raise CmdLineInputError() scope = None scope_arg = "" for arg in argv: if "=" not in arg: raise CmdLineInputError() arg_name, arg_value = arg.split("=", 1) if arg_name == "scope" and not modifiers.get("--config"): if not utils.is_valid_cib_scope(arg_value): utils.err("invalid CIB scope '%s'" % arg_value) else: scope_arg = arg scope = arg_value else: raise CmdLineInputError() if modifiers.get("--config"): scope = "configuration" # Leave scope_arg empty as cluster_push will pick up a --config # option from utils.pcs_options scope_arg = "" editor = os.environ["EDITOR"] cib = utils.get_cib(scope) with tempfile.NamedTemporaryFile(mode="w+", suffix=".pcs") as tempcib: tempcib.write(cib) tempcib.flush() try: subprocess.call([editor, tempcib.name]) except OSError: utils.err("unable to open file with $EDITOR: " + editor) tempcib.seek(0) newcib = "".join(tempcib.readlines()) if newcib == cib: print_to_stderr("CIB not updated, no changes detected") else: cluster_push( lib, [arg for arg in [tempcib.name, scope_arg] if arg], modifiers.get_subset("--wait", "--config", "-f"), ) else: utils.err("$EDITOR environment variable is not set") def get_cib(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --config show configuration section of CIB * -f - CIB file """ # pylint: disable=too-many-branches del lib modifiers.ensure_only_supported("--config", "-f") if len(argv) > 2: raise CmdLineInputError() filename = None scope = None for arg in argv: if "=" not in arg: filename = arg else: arg_name, arg_value = arg.split("=", 1) if arg_name == "scope" and not modifiers.get("--config"): if not utils.is_valid_cib_scope(arg_value): utils.err("invalid CIB scope '%s'" % arg_value) else: scope = arg_value else: raise CmdLineInputError() if modifiers.get("--config"): scope = "configuration" if not filename: print(utils.get_cib(scope).rstrip()) else: output = utils.get_cib(scope) if not output: utils.err("No data in the CIB") try: with open(filename, "w") as cib_file: cib_file.write(output) except EnvironmentError as e: utils.err( "Unable to write to file '%s', %s" % (filename, e.strerror) ) class RemoteAddNodes(RunRemotelyBase): def __init__(self, report_processor, target, data): super().__init__(report_processor) self._target = target self._data = data self._success = False def get_initial_request_list(self): return [ Request( self._target, RequestData( "remote/cluster_add_nodes", [("data_json", json.dumps(self._data))], ), ) ] def _process_response(self, response): node_label = response.request.target.label report_item = self._get_response_report(response) if report_item is not None: self._report(report_item) return try: output = json.loads(response.data) for report_dict in output["report_list"]: self._report( reports.ReportItem( severity=reports.ReportItemSeverity( report_dict["severity"], report_dict["forceable"], ), message=reports.messages.LegacyCommonMessage( report_dict["code"], report_dict["info"], report_dict["report_text"], ), ) ) if output["status"] == "success": self._success = True elif output["status"] != "error": print_to_stderr("Error: {}".format(output["status_msg"])) except (KeyError, json.JSONDecodeError): self._report( reports.ReportItem.warning( reports.messages.InvalidResponseFormat(node_label) ) ) def on_complete(self): return self._success def node_add_outside_cluster( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: """ Options: * --wait - wait until new node will start up, effective only when --start is specified * --start - start new node * --enable - enable new node * --force - treat validation issues and not resolvable addresses as warnings instead of errors * --skip-offline - skip unreachable nodes * --no-watchdog-validation - do not validatate watchdogs * --request-timeout - HTTP request timeout """ del lib modifiers.ensure_only_supported( "--wait", "--start", "--enable", "--force", "--skip-offline", "--no-watchdog-validation", "--request-timeout", ) if len(argv) < 2: raise CmdLineInputError( "Usage: pcs cluster node add-outside " "[addr=]... [watchdog=] " "[device=]... [--start [--wait[=]]] [--enable] " "[--no-watchdog-validation]" ) cluster_node, *argv = argv node_dict = _parse_add_node(argv) force_flags = [] if modifiers.get("--force"): force_flags.append(reports.codes.FORCE) if modifiers.get("--skip-offline"): force_flags.append(reports.codes.SKIP_OFFLINE_NODES) cmd_data = dict( nodes=[node_dict], wait=modifiers.get("--wait"), start=modifiers.get("--start"), enable=modifiers.get("--enable"), no_watchdog_validation=modifiers.get("--no-watchdog-validation"), force_flags=force_flags, ) lib_env = utils.get_lib_env() report_processor = lib_env.report_processor target_factory = lib_env.get_node_target_factory() report_list, target_list = target_factory.get_target_list_with_reports( [cluster_node], skip_non_existing=False, allow_skip=False, ) report_processor.report_list(report_list) if report_processor.has_errors: raise LibraryError() com_cmd = RemoteAddNodes(report_processor, target_list[0], cmd_data) was_successful = run_com_cmd(lib_env.get_node_communicator(), com_cmd) if not was_successful: raise LibraryError() def node_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - continue even though the action may cause qourum loss * --skip-offline - skip unreachable nodes * --request-timeout - HTTP request timeout """ modifiers.ensure_only_supported( "--force", "--skip-offline", "--request-timeout", ) if not argv: raise CmdLineInputError() force_flags = [] if modifiers.get("--force"): force_flags.append(reports.codes.FORCE) if modifiers.get("--skip-offline"): force_flags.append(reports.codes.SKIP_OFFLINE_NODES) lib.cluster.remove_nodes(argv, force_flags=force_flags) def cluster_uidgid( lib: Any, argv: Argv, modifiers: InputModifiers, silent_list: bool = False ) -> None: """ Options: no options """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals del lib modifiers.ensure_only_supported() if not argv: uid_gid_files = os.listdir(settings.corosync_uidgid_dir) uid_gid_lines: list[str] = [] for ug_file in uid_gid_files: uid_gid_dict = utils.read_uid_gid_file(ug_file) if "uid" in uid_gid_dict or "gid" in uid_gid_dict: line = "UID/GID: uid=" if "uid" in uid_gid_dict: line += uid_gid_dict["uid"] line += " gid=" if "gid" in uid_gid_dict: line += uid_gid_dict["gid"] uid_gid_lines.append(line) if uid_gid_lines: print("\n".join(sorted(uid_gid_lines))) elif not silent_list: print_to_stderr("No uidgids configured") return command = argv.pop(0) uid = "" gid = "" if command in {"add", "delete", "remove"} and argv: for arg in argv: if arg.find("=") == -1: utils.err( "uidgid options must be of the form uid= gid=" ) (key, value) = arg.split("=", 1) if key not in {"uid", "gid"}: utils.err( "%s is not a valid key, you must use uid or gid" % key ) if key == "uid": uid = value if key == "gid": gid = value if uid == "" and gid == "": utils.err("you must set either uid or gid") if command == "add": utils.write_uid_gid_file(uid, gid) elif command in {"delete", "remove"}: file_removed = utils.remove_uid_gid_file(uid, gid) if not file_removed: utils.err( "no uidgid files with uid=%s and gid=%s found" % (uid, gid) ) else: raise CmdLineInputError() def cluster_get_corosync_conf( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: """ Options: * --request-timeout - timeout for HTTP requests, effetive only when at least one node has been specified """ del lib modifiers.ensure_only_supported("--request-timeout") if len(argv) > 1: raise CmdLineInputError() if not argv: print(utils.getCorosyncConf().rstrip()) return node = argv[0] retval, output = utils.getCorosyncConfig(node) if retval != 0: utils.err(output) else: print(output.rstrip()) def cluster_reload(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: no options """ del lib modifiers.ensure_only_supported() if len(argv) != 1 or argv[0] != "corosync": raise CmdLineInputError() output, retval = utils.reloadCorosync() if retval != 0 or "invalid option" in output: utils.err(output.rstrip()) print_to_stderr("Corosync reloaded") # Completely tear down the cluster & remove config files # Code taken from cluster-clean script in pacemaker def cluster_destroy(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --all - destroy cluster on all cluster nodes => destroy whole cluster * --force - required for destroying the cluster - DEPRECATED * --request-timeout - timeout of HTTP requests, effective only with --all * --yes - required for destroying the cluster """ # pylint: disable=too-many-branches # pylint: disable=too-many-statements del lib modifiers.ensure_only_supported( "--all", "--force", "--request-timeout", "--yes" ) if argv: raise CmdLineInputError() if utils.is_run_interactive(): warn( "It is recommended to run 'pcs cluster stop' before " "destroying the cluster." ) if not utils.get_continue_confirmation( "This would kill all cluster processes and then PERMANENTLY remove " "cluster state and configuration", bool(modifiers.get("--yes")), bool(modifiers.get("--force")), ): return if modifiers.get("--all"): # load data cib = None lib_env = utils.get_lib_env() try: cib = lib_env.get_cib() except LibraryError: warn( "Unable to load CIB to get guest and remote nodes from it, " "those nodes will not be deconfigured." ) corosync_nodes, report_list = get_existing_nodes_names( utils.get_corosync_conf_facade() ) if not corosync_nodes: report_list.append( reports.ReportItem.error( reports.messages.CorosyncConfigNoNodesDefined() ) ) if report_list: process_library_reports(report_list) # destroy remote and guest nodes if cib is not None: try: all_remote_nodes, report_list = get_existing_nodes_names( cib=cib ) if report_list: process_library_reports(report_list) if all_remote_nodes: _destroy_pcmk_remote_env( lib_env, all_remote_nodes, skip_offline_nodes=True, allow_fails=True, ) except LibraryError as e: process_library_reports(list(e.args)) # destroy full-stack nodes destroy_cluster(corosync_nodes) else: print_to_stderr("Shutting down pacemaker/corosync services...") for service in ["pacemaker", "corosync-qdevice", "corosync"]: try: utils.stop_service(service) except LibraryError: # It is safe to ignore error since we want it not to be running # anyways. pass print_to_stderr("Killing any remaining services...") kill_local_cluster_services() try: utils.disableServices() # pylint: disable=bare-except except: # previously errors were suppressed in here, let's keep it that way # for now pass try: service_manager = utils.get_service_manager() service_manager.disable( lib_sbd.get_sbd_service_name(service_manager) ) # pylint: disable=bare-except except: # it's not a big deal if sbd disable fails pass print_to_stderr("Removing all cluster configuration files...") dummy_output, dummy_retval = utils.run( [ settings.rm_exec, "-f", settings.corosync_conf_file, settings.corosync_authkey_file, settings.pacemaker_authkey_file, settings.pcsd_dr_config_location, ] ) state_files = [ "cib-*", "cib.*", "cib.xml*", "core.*", "cts.*", "hostcache", "pe*.bz2", ] for name in state_files: dummy_output, dummy_retval = utils.run( [ settings.find_exec, settings.pacemaker_local_state_dir, "-name", name, "-exec", settings.rm_exec, "-f", "{}", ";", ] ) try: qdevice_net.client_destroy() # pylint: disable=bare-except except: # errors from deleting other files are suppressed as well # we do not want to fail if qdevice was not set up pass def cluster_verify(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * -f - CIB file * --full - more verbose output """ modifiers.ensure_only_supported("-f", "--full") if argv: raise CmdLineInputError() lib.cluster.verify(verbose=modifiers.get("--full")) def cluster_report(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - allow overwriting existing files - DEPRECATED * --from - timestamp * --to - timestamp * --overwrite - allow overwriting existing files The resulting file should be stored on the machine where pcs cli is running, not on the machine where pcs daemon is running. Therefore we want to use --overwrite and not --force. """ # pylint: disable=too-many-branches del lib modifiers.ensure_only_supported("--force", "--from", "--overwrite", "--to") if len(argv) != 1: raise CmdLineInputError() outfile = argv[0] dest_outfile = outfile + ".tar.bz2" if os.path.exists(dest_outfile): if not (modifiers.get("--overwrite") or modifiers.get("--force")): utils.err( dest_outfile + " already exists, use --overwrite to overwrite" ) return if modifiers.get("--force"): # deprecated in the first pcs-0.12 version, replaced by --overwrite deprecation_warning( "Using --force to confirm this action is deprecated and might " "be removed in a future release, use --overwrite instead" ) try: os.remove(dest_outfile) except OSError as e: utils.err("Unable to remove " + dest_outfile + ": " + e.strerror) crm_report_opts = [] crm_report_opts.append("-f") if modifiers.is_specified("--from"): crm_report_opts.append(str(modifiers.get("--from"))) if modifiers.is_specified("--to"): crm_report_opts.append("-t") crm_report_opts.append(str(modifiers.get("--to"))) else: yesterday = datetime.datetime.now() - datetime.timedelta(1) crm_report_opts.append(yesterday.strftime("%Y-%m-%d %H:%M")) crm_report_opts.append(outfile) output, retval = utils.run([settings.crm_report_exec] + crm_report_opts) if retval != 0 and ( "ERROR: Cannot determine nodes; specify --nodes or --single-node" in output ): utils.err("cluster is not configured on this node") newoutput = "" for line in output.split("\n"): if ( line.startswith("cat:") or line.startswith("grep") or line.startswith("tail") ): continue if "We will attempt to remove" in line: continue if "-p option" in line: continue if "However, doing" in line: continue if "to diagnose" in line: continue if "--dest" in line: line = line.replace("--dest", "") newoutput = newoutput + line + "\n" if retval != 0: utils.err(newoutput) print_to_stderr(newoutput) def send_local_configs( node_name_list: StringIterable, clear_local_cluster_permissions: bool = False, force: bool = False, ) -> list[str]: """ Commandline options: * --request-timeout - timeout of HTTP requests """ pcsd_data = { "nodes": node_name_list, "force": force, "clear_local_cluster_permissions": clear_local_cluster_permissions, } err_msgs = [] output, retval = utils.run_pcsdcli("send_local_configs", pcsd_data) if retval == 0 and output["status"] == "ok" and output["data"]: try: for node_name in node_name_list: node_response = output["data"][node_name] if node_response["status"] == "notauthorized": err_msgs.append( ( "Unable to authenticate to {0}, try running 'pcs " "host auth {0}'" ).format(node_name) ) if node_response["status"] not in ["ok", "not_supported"]: err_msgs.append( "Unable to set pcsd configs on {0}".format(node_name) ) # pylint: disable=bare-except except: err_msgs.append("Unable to communicate with pcsd") else: err_msgs.append("Unable to set pcsd configs") return err_msgs def cluster_auth_cmd(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --corosync_conf - corosync.conf file * --request-timeout - timeout of HTTP requests * -u - username * -p - password """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals del lib modifiers.ensure_only_supported( "--corosync_conf", "--request-timeout", "-u", "-p" ) if argv: raise CmdLineInputError() lib_env = utils.get_lib_env() target_factory = lib_env.get_node_target_factory() cluster_node_list = lib_env.get_corosync_conf().get_nodes() cluster_node_names = [] missing_name = False for node in cluster_node_list: if node.name: cluster_node_names.append(node.name) else: missing_name = True if missing_name: warn( "Skipping nodes which do not have their name defined in " "corosync.conf, use the 'pcs host auth' command to authenticate " "them" ) target_list = [] not_authorized_node_name_list = [] for node_name in cluster_node_names: try: target_list.append(target_factory.get_target(node_name)) except HostNotFound: print_to_stderr("{}: Not authorized".format(node_name)) not_authorized_node_name_list.append(node_name) com_cmd = CheckAuth(lib_env.report_processor) com_cmd.set_targets(target_list) not_authorized_node_name_list.extend( run_and_raise(lib_env.get_node_communicator(), com_cmd) ) if not_authorized_node_name_list: print( "Nodes to authorize: {}".format( ", ".join(not_authorized_node_name_list) ) ) username, password = utils.get_user_and_pass() not_auth_node_list = [] for node_name in not_authorized_node_name_list: for node in cluster_node_list: if node.name == node_name: if node.addrs_plain(): not_auth_node_list.append(node) else: print_to_stderr( f"{node.name}: No addresses defined in " "corosync.conf, use the 'pcs host auth' command to " "authenticate the node" ) nodes_to_auth_data = { node.name: dict( username=username, password=password, dest_list=[ dict( addr=node.addrs_plain()[0], port=settings.pcsd_default_port, ) ], ) for node in not_auth_node_list } utils.auth_hosts(nodes_to_auth_data) else: print_to_stderr("Sending cluster config files to the nodes...") msgs = send_local_configs(cluster_node_names, force=True) for msg in msgs: warn(msg) def _parse_node_options( node: str, options: Argv, additional_options: StringCollection = (), additional_repeatable_options: StringCollection = (), ) -> dict[str, Union[str, list[str]]]: """ Commandline options: no options """ ADDR_OPT_KEYWORD = "addr" # pylint: disable=invalid-name supported_options = {ADDR_OPT_KEYWORD} | set(additional_options) repeatable_options = {ADDR_OPT_KEYWORD} | set(additional_repeatable_options) parser = KeyValueParser(options, repeatable_options) parsed_unique = parser.get_unique() parsed_repeatable = parser.get_repeatable() unknown_options = ( set(parsed_unique.keys()) | set(parsed_repeatable) ) - supported_options if unknown_options: raise CmdLineInputError( f"Unknown options {format_list(unknown_options)} for node '{node}'" ) parsed_unique["name"] = node if ADDR_OPT_KEYWORD in parsed_repeatable: parsed_repeatable["addrs"] = parsed_repeatable[ADDR_OPT_KEYWORD] del parsed_repeatable[ADDR_OPT_KEYWORD] return parsed_unique | parsed_repeatable TRANSPORT_KEYWORD = "transport" TRANSPORT_DEFAULT_SECTION = "__default__" LINK_KEYWORD = "link" def _parse_transport( transport_args: Argv, ) -> tuple[str, dict[str, Union[dict[str, str], list[dict[str, str]]]]]: """ Commandline options: no options """ if not transport_args: raise CmdLineInputError( f"{TRANSPORT_KEYWORD.capitalize()} type not defined" ) transport_type, *transport_options = transport_args keywords = {"compression", "crypto", LINK_KEYWORD} parsed_options = parse_args.group_by_keywords( transport_options, keywords, implicit_first_keyword=TRANSPORT_DEFAULT_SECTION, ) options: dict[str, Union[dict[str, str], list[dict[str, str]]]] = { section: KeyValueParser( parsed_options.get_args_flat(section) ).get_unique() for section in keywords | {TRANSPORT_DEFAULT_SECTION} if section != LINK_KEYWORD } options[LINK_KEYWORD] = [ KeyValueParser(link_options).get_unique() for link_options in parsed_options.get_args_groups(LINK_KEYWORD) ] return transport_type, options def cluster_setup(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --wait - only effective when used with --start * --start - start cluster * --enable - enable cluster * --force - some validation issues and unresolvable addresses are treated as warnings * --no-keys-sync - do not create and distribute pcsd ssl cert and key, corosync and pacemaker authkeys * --no-cluster-uuid - do not generate a cluster UUID during setup * --corosync_conf - corosync.conf file path, do not talk to cluster nodes * --overwrite - allow overwriting existing files """ # pylint: disable=too-many-locals is_local = modifiers.is_specified("--corosync_conf") allowed_options_common = ["--force", "--no-cluster-uuid"] allowed_options_live = [ "--wait", "--start", "--enable", "--no-keys-sync", ] allowed_options_local = ["--corosync_conf", "--overwrite"] modifiers.ensure_only_supported( *( allowed_options_common + allowed_options_live + allowed_options_local ), ) if is_local and modifiers.is_specified_any(allowed_options_live): raise CmdLineInputError( f"Cannot specify any of {format_list(allowed_options_live)} " "when '--corosync_conf' is specified" ) if not is_local and modifiers.is_specified("--overwrite"): raise CmdLineInputError( "Cannot specify '--overwrite' when '--corosync_conf' is not " "specified" ) if len(argv) < 2: raise CmdLineInputError() cluster_name, *argv = argv keywords = [TRANSPORT_KEYWORD, "totem", "quorum"] parsed_args = parse_args.group_by_keywords( argv, keywords, implicit_first_keyword="nodes" ) parsed_args.ensure_unique_keywords() nodes = [ _parse_node_options(node, options) for node, options in parse_args.split_list_by_any_keywords( parsed_args.get_args_flat("nodes"), "node name" ).items() ] transport_type = None transport_options: dict[ str, Union[dict[str, str], list[dict[str, str]]] ] = {} if parsed_args.has_keyword(TRANSPORT_KEYWORD): transport_type, transport_options = _parse_transport( parsed_args.get_args_flat(TRANSPORT_KEYWORD) ) force_flags = [] if modifiers.get("--force"): force_flags.append(reports.codes.FORCE) totem_options = KeyValueParser( parsed_args.get_args_flat("totem") ).get_unique() quorum_options = KeyValueParser( parsed_args.get_args_flat("quorum") ).get_unique() if not is_local: lib.cluster.setup( cluster_name, nodes, transport_type=transport_type, transport_options=transport_options.get( TRANSPORT_DEFAULT_SECTION, {} ), link_list=transport_options.get(LINK_KEYWORD, []), compression_options=transport_options.get("compression", {}), crypto_options=transport_options.get("crypto", {}), totem_options=totem_options, quorum_options=quorum_options, wait=modifiers.get("--wait"), start=modifiers.get("--start"), enable=modifiers.get("--enable"), no_keys_sync=modifiers.get("--no-keys-sync"), no_cluster_uuid=modifiers.is_specified("--no-cluster-uuid"), force_flags=force_flags, ) return corosync_conf_data = lib.cluster.setup_local( cluster_name, nodes, transport_type=transport_type, transport_options=transport_options.get(TRANSPORT_DEFAULT_SECTION, {}), link_list=transport_options.get(LINK_KEYWORD, []), compression_options=transport_options.get("compression", {}), crypto_options=transport_options.get("crypto", {}), totem_options=totem_options, quorum_options=quorum_options, no_cluster_uuid=modifiers.is_specified("--no-cluster-uuid"), force_flags=force_flags, ) corosync_conf_file = pcs_file.RawFile( file_metadata.for_file_type( file_type_codes.COROSYNC_CONF, modifiers.get("--corosync_conf") ) ) overwrite = modifiers.is_specified("--overwrite") try: corosync_conf_file.write(corosync_conf_data, can_overwrite=overwrite) except pcs_file.FileAlreadyExists as e: utils.err( reports.messages.FileAlreadyExists( e.metadata.file_type_code, e.metadata.path, ).message + ", use --overwrite to overwrite existing file(s)" ) except pcs_file.RawFileError as e: utils.err( reports.messages.FileIoError( e.metadata.file_type_code, e.action, e.reason, file_path=e.metadata.path, ).message ) def config_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --corosync_conf - corosync.conf file path, do not talk to cluster nodes """ modifiers.ensure_only_supported("--corosync_conf") parsed_args = parse_args.group_by_keywords( argv, ["transport", "compression", "crypto", "totem"], ) transport_options = KeyValueParser( parsed_args.get_args_flat("transport") ).get_unique() compression_options = KeyValueParser( parsed_args.get_args_flat("compression") ).get_unique() crypto_options = KeyValueParser( parsed_args.get_args_flat("crypto") ).get_unique() totem_options = KeyValueParser( parsed_args.get_args_flat("totem") ).get_unique() if not modifiers.is_specified("--corosync_conf"): lib.cluster.config_update( transport_options, compression_options, crypto_options, totem_options, ) return _corosync_conf_local_cmd_call( modifiers.get("--corosync_conf"), lambda corosync_conf_content: lib.cluster.config_update_local( corosync_conf_content, transport_options, compression_options, crypto_options, totem_options, ), ) def _format_options(label: str, options: Mapping[str, str]) -> list[str]: output = [] if options: output.append(f"{label}:") output.extend( indent([f"{opt}: {val}" for opt, val in sorted(options.items())]) ) return output def _format_nodes(nodes: Iterable[CorosyncNodeDto]) -> list[str]: output = ["Nodes:"] for node in sorted(nodes, key=lambda node: node.name): node_attrs = [ f"Link {addr.link} address: {addr.addr}" for addr in sorted(node.addrs, key=lambda addr: addr.link) ] + [f"nodeid: {node.nodeid}"] output.extend(indent([f"{node.name}:"] + indent(node_attrs))) return output def config_show( lib: Any, argv: Argv, modifiers: parse_args.InputModifiers ) -> None: """ Options: * --corosync_conf - corosync.conf file path, do not talk to cluster nodes * --output-format - supported formats: text, cmd, json """ modifiers.ensure_only_supported( "--corosync_conf", output_format_supported=True ) if argv: raise CmdLineInputError() output_format = modifiers.get_output_format() corosync_conf_dto = lib.cluster.get_corosync_conf_struct() if output_format == OUTPUT_FORMAT_VALUE_CMD: if corosync_conf_dto.quorum_device is not None: warn( "Quorum device configuration detected but not yet supported by " "this command." ) output = " \\\n".join(_config_get_cmd(corosync_conf_dto)) elif output_format == OUTPUT_FORMAT_VALUE_JSON: output = json.dumps(dto.to_dict(corosync_conf_dto)) else: output = "\n".join(_config_get_text(corosync_conf_dto)) print(output) def _config_get_text(corosync_conf: CorosyncConfDto) -> list[str]: lines = [f"Cluster Name: {corosync_conf.cluster_name}"] if corosync_conf.cluster_uuid: lines.append(f"Cluster UUID: {corosync_conf.cluster_uuid}") lines.append(f"Transport: {corosync_conf.transport.lower()}") lines.extend(_format_nodes(corosync_conf.nodes)) if corosync_conf.links_options: lines.append("Links:") for linknum, link_options in sorted( corosync_conf.links_options.items() ): lines.extend( indent(_format_options(f"Link {linknum}", link_options)) ) lines.extend( _format_options("Transport Options", corosync_conf.transport_options) ) lines.extend( _format_options( "Compression Options", corosync_conf.compression_options ) ) lines.extend( _format_options("Crypto Options", corosync_conf.crypto_options) ) lines.extend(_format_options("Totem Options", corosync_conf.totem_options)) lines.extend( _format_options("Quorum Options", corosync_conf.quorum_options) ) if corosync_conf.quorum_device: lines.append(f"Quorum Device: {corosync_conf.quorum_device.model}") lines.extend( indent( _format_options( "Options", corosync_conf.quorum_device.generic_options ) ) ) lines.extend( indent( _format_options( "Model Options", corosync_conf.quorum_device.model_options, ) ) ) lines.extend( indent( _format_options( "Heuristics", corosync_conf.quorum_device.heuristics_options, ) ) ) return lines def _corosync_node_to_cmd_line(node: CorosyncNodeDto) -> str: return " ".join( [node.name] + [ f"addr={addr.addr}" for addr in sorted(node.addrs, key=lambda addr: addr.link) ] ) def _section_to_lines( options: Mapping[str, str], keyword: Optional[str] = None ) -> list[str]: output: list[str] = [] if options: if keyword: output.append(keyword) output.extend( indent([f"{key}={val}" for key, val in sorted(options.items())]) ) return indent(output) def _config_get_cmd(corosync_conf: CorosyncConfDto) -> list[str]: lines = [f"pcs cluster setup {corosync_conf.cluster_name}"] lines += indent( [ _corosync_node_to_cmd_line(node) for node in sorted( corosync_conf.nodes, key=lambda node: node.nodeid ) ] ) transport = [ "transport", str(corosync_conf.transport.value).lower(), ] + _section_to_lines(corosync_conf.transport_options) for _, link in sorted(corosync_conf.links_options.items()): transport.extend(_section_to_lines(link, "link")) transport.extend( _section_to_lines(corosync_conf.compression_options, "compression") ) transport.extend(_section_to_lines(corosync_conf.crypto_options, "crypto")) lines.extend(indent(transport)) lines.extend(_section_to_lines(corosync_conf.totem_options, "totem")) lines.extend(_section_to_lines(corosync_conf.quorum_options, "quorum")) if not corosync_conf.cluster_uuid: lines.extend(indent(["--no-cluster-uuid"])) return lines def _parse_add_node(argv: Argv) -> dict[str, Union[str, list[str]]]: DEVICE_KEYWORD = "device" # pylint: disable=invalid-name WATCHDOG_KEYWORD = "watchdog" # pylint: disable=invalid-name hostname, *argv = argv node_dict = _parse_node_options( hostname, argv, additional_options={DEVICE_KEYWORD, WATCHDOG_KEYWORD}, additional_repeatable_options={DEVICE_KEYWORD}, ) if DEVICE_KEYWORD in node_dict: node_dict[f"{DEVICE_KEYWORD}s"] = node_dict[DEVICE_KEYWORD] del node_dict[DEVICE_KEYWORD] return node_dict def node_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --wait - wait until new node will start up, effective only when --start is specified * --start - start new node * --enable - enable new node * --force - treat validation issues and not resolvable addresses as warnings instead of errors * --skip-offline - skip unreachable nodes * --no-watchdog-validation - do not validatate watchdogs * --request-timeout - HTTP request timeout """ modifiers.ensure_only_supported( "--wait", "--start", "--enable", "--force", "--skip-offline", "--no-watchdog-validation", "--request-timeout", ) if not argv: raise CmdLineInputError() node_dict = _parse_add_node(argv) force_flags = [] if modifiers.get("--force"): force_flags.append(reports.codes.FORCE) if modifiers.get("--skip-offline"): force_flags.append(reports.codes.SKIP_OFFLINE_NODES) lib.cluster.add_nodes( nodes=[node_dict], wait=modifiers.get("--wait"), start=modifiers.get("--start"), enable=modifiers.get("--enable"), no_watchdog_validation=modifiers.get("--no-watchdog-validation"), force_flags=force_flags, ) def remove_nodes_from_cib( lib: Any, argv: Argv, modifiers: InputModifiers ) -> None: """ Options: no options """ modifiers.ensure_only_supported() if not argv: raise CmdLineInputError("No nodes specified") lib.cluster.remove_nodes_from_cib(argv) def link_add(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - treat validation issues and not resolvable addresses as warnings instead of errors * --skip-offline - skip unreachable nodes * --request-timeout - HTTP request timeout """ modifiers.ensure_only_supported( "--force", "--request-timeout", "--skip-offline" ) if not argv: raise CmdLineInputError() force_flags = [] if modifiers.get("--force"): force_flags.append(reports.codes.FORCE) if modifiers.get("--skip-offline"): force_flags.append(reports.codes.SKIP_OFFLINE_NODES) parsed = parse_args.group_by_keywords( argv, {"options"}, implicit_first_keyword="nodes" ) parsed.ensure_unique_keywords() lib.cluster.add_link( KeyValueParser(parsed.get_args_flat("nodes")).get_unique(), KeyValueParser(parsed.get_args_flat("options")).get_unique(), force_flags=force_flags, ) def link_remove(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --skip-offline - skip unreachable nodes * --request-timeout - HTTP request timeout """ modifiers.ensure_only_supported("--request-timeout", "--skip-offline") if not argv: raise CmdLineInputError() force_flags = [] if modifiers.get("--skip-offline"): force_flags.append(reports.codes.SKIP_OFFLINE_NODES) lib.cluster.remove_links(argv, force_flags=force_flags) def link_update(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - treat validation issues and not resolvable addresses as warnings instead of errors * --skip-offline - skip unreachable nodes * --request-timeout - HTTP request timeout """ modifiers.ensure_only_supported( "--force", "--request-timeout", "--skip-offline" ) if len(argv) < 2: raise CmdLineInputError() force_flags = [] if modifiers.get("--force"): force_flags.append(reports.codes.FORCE) if modifiers.get("--skip-offline"): force_flags.append(reports.codes.SKIP_OFFLINE_NODES) linknumber = argv[0] parsed = parse_args.group_by_keywords( argv[1:], {"options"}, implicit_first_keyword="nodes" ) parsed.ensure_unique_keywords() lib.cluster.update_link( linknumber, KeyValueParser(parsed.get_args_flat("nodes")).get_unique(), KeyValueParser(parsed.get_args_flat("options")).get_unique(), force_flags=force_flags, ) def generate_uuid(lib: Any, argv: Argv, modifiers: InputModifiers) -> None: """ Options: * --force - allow to rewrite an existing UUID in corosync.conf * --corosync_conf - corosync.conf file path, do not talk to cluster nodes """ modifiers.ensure_only_supported("--force", "--corosync_conf") if argv: raise CmdLineInputError() force_flags = [] if modifiers.get("--force"): force_flags.append(reports.codes.FORCE) if not modifiers.is_specified("--corosync_conf"): lib.cluster.generate_cluster_uuid(force_flags=force_flags) return _corosync_conf_local_cmd_call( modifiers.get("--corosync_conf"), lambda corosync_conf_content: lib.cluster.generate_cluster_uuid_local( corosync_conf_content, force_flags=force_flags ), ) pcs-0.12.0.2/pcs/common/000077500000000000000000000000001500417470700146015ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/__init__.py000066400000000000000000000000001500417470700167000ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/async_tasks/000077500000000000000000000000001500417470700171235ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/async_tasks/__init__.py000066400000000000000000000000001500417470700212220ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/async_tasks/dto.py000066400000000000000000000017261500417470700202710ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Any, Dict, List, Optional, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.reports.dto import ReportItemDto from .types import ( TaskFinishType, TaskKillReason, TaskState, ) @dataclass(frozen=True) class CommandOptionsDto(DataTransferObject): request_timeout: Optional[int] = None effective_username: Optional[str] = None effective_groups: Optional[List[str]] = None @dataclass(frozen=True) class CommandDto(DataTransferObject): command_name: str params: Dict[str, Any] options: CommandOptionsDto @dataclass(frozen=True) class TaskIdentDto(DataTransferObject): task_ident: str @dataclass(frozen=True) class TaskResultDto(DataTransferObject): task_ident: str command: CommandDto reports: List[ReportItemDto] state: TaskState task_finish_type: TaskFinishType kill_reason: Optional[TaskKillReason] result: Any pcs-0.12.0.2/pcs/common/async_tasks/types.py000066400000000000000000000007151500417470700206440ustar00rootroot00000000000000from enum import auto from pcs.common.types import AutoNameEnum class TaskFinishType(AutoNameEnum): UNFINISHED = auto() UNHANDLED_EXCEPTION = auto() FAIL = auto() SUCCESS = auto() KILL = auto() class TaskState(AutoNameEnum): CREATED = auto() QUEUED = auto() EXECUTED = auto() FINISHED = auto() class TaskKillReason(AutoNameEnum): USER = auto() COMPLETION_TIMEOUT = auto() INTERNAL_MESSAGING_ERROR = auto() pcs-0.12.0.2/pcs/common/capabilities.py000066400000000000000000000041331500417470700176050ustar00rootroot00000000000000from dataclasses import dataclass from textwrap import dedent from typing import Iterable from lxml import etree from pcs import settings from pcs.common.tools import xml_fromstring @dataclass(frozen=True) class Capability: code: str description: str in_pcs: bool in_pcsd: bool class CapabilitiesError(Exception): def __init__(self, msg: str): super().__init__() self.msg = msg def __str__(self) -> str: return self.msg def get_capabilities_definition() -> list[Capability]: """ Read and parse capabilities file The point is to return all data in python structures for further processing. """ filename = settings.pcs_capabilities try: with open(filename, mode="r") as xml_file: capabilities_xml = xml_fromstring(xml_file.read()) except (OSError, etree.XMLSyntaxError, etree.DocumentInvalid) as e: raise CapabilitiesError( f"Cannot read capabilities definition file '{filename}': '{e}'" ) from e capabilities = [] for feat_xml in capabilities_xml.findall(".//capability"): desc_elem = feat_xml.find("./description") # dedent and strip remove indentation in the XML file desc = "" if desc_elem is None else dedent(desc_elem.text or "").strip() capabilities.append( Capability( code=str(feat_xml.attrib["id"]), description=desc, in_pcs=feat_xml.attrib["in-pcs"] == "1", in_pcsd=feat_xml.attrib["in-pcsd"] == "1", ) ) return capabilities def get_pcs_capabilities() -> list[Capability]: """ Get pcs capabilities from the capabilities file """ return [feat for feat in get_capabilities_definition() if feat.in_pcs] def get_pcsd_capabilities() -> list[Capability]: """ Get pcsd capabilities from the capabilities file """ return [feat for feat in get_capabilities_definition() if feat.in_pcsd] def capabilities_to_codes_str(capabilities: Iterable[Capability]) -> str: return " ".join(sorted([feat.code for feat in capabilities])) pcs-0.12.0.2/pcs/common/communication/000077500000000000000000000000001500417470700174465ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/communication/__init__.py000066400000000000000000000000611500417470700215540ustar00rootroot00000000000000from . import ( const, dto, types, ) pcs-0.12.0.2/pcs/common/communication/const.py000066400000000000000000000005001500417470700211410ustar00rootroot00000000000000from .types import CommunicationResultStatus as Status COM_STATUS_SUCCESS = Status("success") COM_STATUS_INPUT_ERROR = Status("input_error") COM_STATUS_UNKNOWN_CMD = Status("unknown_cmd") COM_STATUS_ERROR = Status("error") COM_STATUS_EXCEPTION = Status("exception") COM_STATUS_NOT_AUTHORIZED = Status("not_authorized") pcs-0.12.0.2/pcs/common/communication/dto.py000066400000000000000000000013761500417470700206150ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Any, List, Mapping, Optional, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.reports.dto import ReportItemDto from .types import CommunicationResultStatus as StatusType @dataclass(frozen=True) class InternalCommunicationResultDto(DataTransferObject): status: StatusType status_msg: Optional[str] report_list: List[ReportItemDto] data: Any @dataclass(frozen=True) class InternalCommunicationRequestOptionsDto(DataTransferObject): request_timeout: Optional[int] @dataclass(frozen=True) class InternalCommunicationRequestDto(DataTransferObject): options: InternalCommunicationRequestOptionsDto cmd: str cmd_data: Mapping[str, Any] pcs-0.12.0.2/pcs/common/communication/types.py000066400000000000000000000001421500417470700211610ustar00rootroot00000000000000from typing import NewType CommunicationResultStatus = NewType("CommunicationResultStatus", str) pcs-0.12.0.2/pcs/common/const.py000066400000000000000000000061021500417470700163000ustar00rootroot00000000000000from typing import NewType from pcs.common.tools import Version PcmkRoleType = NewType("PcmkRoleType", str) PcmkStatusRoleType = NewType("PcmkStatusRoleType", str) PcmkOnFailAction = NewType("PcmkOnFailAction", str) PcmkAction = NewType("PcmkAction", str) ResourceIdType = NewType("ResourceIdType", str) INFINITY = "INFINITY" PCMK_ROLE_STARTED = PcmkRoleType("Started") PCMK_ROLE_STOPPED = PcmkRoleType("Stopped") PCMK_ROLE_PROMOTED = PcmkRoleType("Promoted") PCMK_ROLE_UNPROMOTED = PcmkRoleType("Unpromoted") PCMK_ROLE_PROMOTED_LEGACY = PcmkRoleType("Master") PCMK_ROLE_UNPROMOTED_LEGACY = PcmkRoleType("Slave") PCMK_STATUS_ROLE_STARTED = PcmkStatusRoleType("Started") PCMK_STATUS_ROLE_STOPPED = PcmkStatusRoleType("Stopped") PCMK_STATUS_ROLE_PROMOTED = PcmkStatusRoleType("Promoted") PCMK_STATUS_ROLE_UNPROMOTED = PcmkStatusRoleType("Unpromoted") PCMK_STATUS_ROLE_STARTING = PcmkStatusRoleType("Starting") PCMK_STATUS_ROLE_STOPPING = PcmkStatusRoleType("Stopping") PCMK_STATUS_ROLE_MIGRATING = PcmkStatusRoleType("Migrating") PCMK_STATUS_ROLE_PROMOTING = PcmkStatusRoleType("Promoting") PCMK_STATUS_ROLE_DEMOTING = PcmkStatusRoleType("Demoting") PCMK_ON_FAIL_ACTION_IGNORE = PcmkOnFailAction("ignore") PCMK_ON_FAIL_ACTION_BLOCK = PcmkOnFailAction("block") PCMK_ON_FAIL_ACTION_DEMOTE = PcmkOnFailAction("demote") PCMK_ON_FAIL_ACTION_STOP = PcmkOnFailAction("stop") PCMK_ON_FAIL_ACTION_RESTART = PcmkOnFailAction("restart") PCMK_ON_FAIL_ACTION_STANDBY = PcmkOnFailAction("standby") PCMK_ON_FAIL_ACTION_FENCE = PcmkOnFailAction("fence") PCMK_ON_FAIL_ACTION_RESTART_CONTAINER = PcmkOnFailAction("restart-container") PCMK_ROLES_RUNNING = ( PCMK_ROLE_STARTED, PCMK_ROLE_PROMOTED, PCMK_ROLE_UNPROMOTED, ) PCMK_ROLES_RUNNING_WITH_LEGACY = PCMK_ROLES_RUNNING + ( PCMK_ROLE_PROMOTED_LEGACY, PCMK_ROLE_UNPROMOTED_LEGACY, ) PCMK_ROLES = (PCMK_ROLE_STOPPED,) + PCMK_ROLES_RUNNING PCMK_ROLES_WITH_LEGACY = (PCMK_ROLE_STOPPED,) + PCMK_ROLES_RUNNING_WITH_LEGACY PCMK_STATUS_ROLES_RUNNING = ( PCMK_STATUS_ROLE_STARTED, PCMK_STATUS_ROLE_PROMOTED, PCMK_STATUS_ROLE_UNPROMOTED, ) PCMK_STATUS_ROLES_PENDING = ( PCMK_STATUS_ROLE_STARTING, PCMK_STATUS_ROLE_STOPPING, PCMK_STATUS_ROLE_MIGRATING, PCMK_STATUS_ROLE_PROMOTING, PCMK_STATUS_ROLE_DEMOTING, ) PCMK_STATUS_ROLES = ( PCMK_STATUS_ROLES_RUNNING + PCMK_STATUS_ROLES_PENDING + (PCMK_STATUS_ROLE_STOPPED,) ) PCMK_ACTION_START = PcmkAction("start") PCMK_ACTION_STOP = PcmkAction("stop") PCMK_ACTION_PROMOTE = PcmkAction("promote") PCMK_ACTION_DEMOTE = PcmkAction("demote") PCMK_ACTIONS = ( PCMK_ACTION_START, PCMK_ACTION_STOP, PCMK_ACTION_PROMOTE, PCMK_ACTION_DEMOTE, ) PCMK_NEW_ROLES_CIB_VERSION = Version(3, 7, 0) # CIB schema which supports new rules syntax and options defined in Pacemaker 3. # Lower schema is not supported for rules by pcs, as we support Pacemaker 3+ # only in pcs-0.12. PCMK_RULES_PCMK3_SYNTAX_CIB_VERSION = Version(3, 9, 0) PCMK_ON_FAIL_DEMOTE_CIB_VERSION = Version(3, 4, 0) RESOURCE_ID_TYPE_PLAIN = ResourceIdType("resource_id_plain") RESOURCE_ID_TYPE_REGEXP = ResourceIdType("resource_id_regexp") pcs-0.12.0.2/pcs/common/corosync_conf.py000066400000000000000000000024241500417470700200210ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Mapping, Optional, Sequence, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.types import ( CorosyncNodeAddressType, CorosyncTransportType, ) @dataclass(frozen=True) class CorosyncNodeAddressDto(DataTransferObject): addr: str link: str type: CorosyncNodeAddressType @dataclass(frozen=True) class CorosyncNodeDto(DataTransferObject): name: str nodeid: str addrs: Sequence[CorosyncNodeAddressDto] @dataclass(frozen=True) class CorosyncQuorumDeviceSettingsDto(DataTransferObject): model: str model_options: Mapping[str, str] generic_options: Mapping[str, str] heuristics_options: Mapping[str, str] @dataclass(frozen=True) class CorosyncConfDto(DataTransferObject): # pylint: disable=too-many-instance-attributes cluster_name: str cluster_uuid: Optional[str] transport: CorosyncTransportType totem_options: Mapping[str, str] transport_options: Mapping[str, str] compression_options: Mapping[str, str] crypto_options: Mapping[str, str] nodes: Sequence[CorosyncNodeDto] links_options: Mapping[str, Mapping[str, str]] quorum_options: Mapping[str, str] quorum_device: Optional[CorosyncQuorumDeviceSettingsDto] pcs-0.12.0.2/pcs/common/dr.py000066400000000000000000000012641500417470700155630ustar00rootroot00000000000000from dataclasses import dataclass from typing import Sequence from pcs.common.interface.dto import DataTransferObject from pcs.common.types import DrRole @dataclass(frozen=True) class DrConfigNodeDto(DataTransferObject): name: str @dataclass(frozen=True) class DrConfigSiteDto(DataTransferObject): site_role: DrRole node_list: Sequence[DrConfigNodeDto] @dataclass(frozen=True) class DrConfigDto(DataTransferObject): local_site: DrConfigSiteDto remote_site_list: Sequence[DrConfigSiteDto] @dataclass(frozen=True) class DrSiteStatusDto(DataTransferObject): local_site: bool site_role: DrRole status_plaintext: str status_successfully_obtained: bool pcs-0.12.0.2/pcs/common/fencing_topology.py000066400000000000000000000005151500417470700205210ustar00rootroot00000000000000from typing import ( Final, NewType, Union, ) FencingTargetType = NewType("FencingTargetType", str) FencingTargetValue = Union[str, tuple[str, str]] TARGET_TYPE_NODE: Final = FencingTargetType("node") TARGET_TYPE_REGEXP: Final = FencingTargetType("regexp") TARGET_TYPE_ATTRIBUTE: Final = FencingTargetType("attribute") pcs-0.12.0.2/pcs/common/file.py000066400000000000000000000210751500417470700160770ustar00rootroot00000000000000import fcntl import os import shutil from contextlib import ( AbstractContextManager, contextmanager, ) from dataclasses import dataclass from io import BytesIO from typing import ( IO, Any, Iterator, NewType, Optional, ) from pcs.common.file_type_codes import FileTypeCode from pcs.common.tools import format_os_error # TODO add logging (logger / debug reports ?) to the RawFile class; be aware # the class is used both in pcs.cli and pcs.lib packages @dataclass(frozen=True) class FileMetadata: file_type_code: FileTypeCode path: str owner_user_name: Optional[str] owner_group_name: Optional[str] permissions: Optional[int] is_binary: bool FileAction = NewType("FileAction", str) class RawFileError(Exception): # So far there has been no need to have a separate exception for each # action. Actions must be passed in a report and we certainely do not want # a separate report for each action. ACTION_CHMOD = FileAction("chmod") ACTION_CHOWN = FileAction("chown") ACTION_READ = FileAction("read") ACTION_REMOVE = FileAction("remove") ACTION_UPDATE = FileAction("update") ACTION_WRITE = FileAction("write") def __init__( self, metadata: FileMetadata, action: FileAction, reason: str = "" ): """ metadata -- describes the file involved in the error action -- possible values enumerated in RawFileError reason -- plain text error details """ super().__init__() self.metadata = metadata self.action = action self.reason = reason class FileAlreadyExists(RawFileError): def __init__(self, metadata: FileMetadata): """ metadata -- describes the file involved in the error """ super().__init__(metadata, RawFileError.ACTION_WRITE) class RawFileInterface: def __init__(self, metadata: FileMetadata): """ metadata -- describes the file and provides its metadata """ self.__metadata = metadata @property def metadata(self) -> FileMetadata: return self.__metadata def exists(self) -> bool: """ Return True if file exists, False otherwise """ raise NotImplementedError() def read(self) -> bytes: """ Return content of the file as bytes """ raise NotImplementedError() def write(self, file_data: bytes, can_overwrite: bool = False) -> None: """ Write file_data to the file file_data -- data to be written can_overwrite -- raise if False and the file already exists """ raise NotImplementedError() def update(self) -> AbstractContextManager[BytesIO]: """ Returns a context manager which __enter__ method returns a buffer filled with file data and stores data from the returned buffer to the same file once __exit__ method is called. Context manager holds an exclusive lock on the file between __enter__ and __exit__ calls. """ raise NotImplementedError() class RawFile(RawFileInterface): def exists(self) -> bool: # Returns False if the file is not accessible, does not raise. return os.path.exists(self.metadata.path) def read(self) -> bytes: try: mode = "rb" if self.metadata.is_binary else "r" with open(self.metadata.path, mode) as my_file: # the lock is released when the file gets closed on leaving the # with statement fcntl.flock(my_file.fileno(), fcntl.LOCK_SH) content = my_file.read() return ( content if self.metadata.is_binary else content.encode("utf-8") ) except OSError as e: # Specific exception if the file does not exist is not needed, # anyone can and should check that using the exists method. raise RawFileError( self.metadata, RawFileError.ACTION_READ, format_os_error(e) ) from e def _chown(self) -> None: try: # Need to split to two conditions and check owner and group # separately due to mypy. if self.metadata.owner_user_name is not None: shutil.chown( self.metadata.path, user=self.metadata.owner_user_name, ) if self.metadata.owner_group_name is not None: shutil.chown( self.metadata.path, group=self.metadata.owner_group_name, ) except LookupError as e: raise RawFileError( self.metadata, RawFileError.ACTION_CHOWN, str(e) ) from e except OSError as e: raise RawFileError( self.metadata, RawFileError.ACTION_CHOWN, format_os_error(e), ) from e def _chmod(self, file_obj: IO[Any]) -> None: if self.metadata.permissions is not None: try: os.chmod(file_obj.fileno(), self.metadata.permissions) except OSError as e: raise RawFileError( self.metadata, RawFileError.ACTION_CHMOD, format_os_error(e), ) from e def write(self, file_data: bytes, can_overwrite: bool = False) -> None: try: mode = "{write_mode}{binary_mode}".format( write_mode="w" if can_overwrite else "x", binary_mode="b" if self.metadata.is_binary else "", ) with open(self.metadata.path, mode) as my_file: # the lock is released when the file gets closed on leaving the # with statement fcntl.flock(my_file.fileno(), fcntl.LOCK_EX) # Set the ownership and permissions to cover the case when we # just created the file. If the file already existed, make sure # the ownership and permissions are correct before writing any # data into it. self._chown() self._chmod(my_file) # Write file data my_file.write( file_data if self.metadata.is_binary else file_data.decode("utf-8") ) except FileExistsError as e: raise FileAlreadyExists(self.metadata) from e except OSError as e: raise RawFileError( self.metadata, RawFileError.ACTION_WRITE, format_os_error(e) ) from e @contextmanager def update(self) -> Iterator[BytesIO]: mode = "a+" if self.metadata.is_binary: mode += "b" try: with open(self.metadata.path, mode) as file_obj: # the lock is released when the file gets closed on leaving the # with statement fcntl.flock(file_obj.fileno(), fcntl.LOCK_EX) file_obj.seek(0) content = file_obj.read() stream = BytesIO( content if self.metadata.is_binary else content.encode("utf-8") ) stream.seek(0) yield stream self._chown() self._chmod(file_obj) file_obj.seek(0) file_obj.truncate() new_content = stream.getvalue() file_obj.write( new_content if self.metadata.is_binary else new_content.decode("utf-8") ) except OSError as e: # Specific exception if the file does not exist is not needed, # anyone can and should check that using the exists method. raise RawFileError( self.metadata, RawFileError.ACTION_UPDATE, format_os_error(e) ) from e def remove(self, fail_if_file_not_found: bool = True) -> None: try: os.remove(self.metadata.path) except FileNotFoundError as e: if fail_if_file_not_found: raise self.__get_raw_file_error(e) from e except OSError as e: raise self.__get_raw_file_error(e) from e def backup(self) -> None: # TODO implement raise NotImplementedError() def __get_raw_file_error(self, e: OSError) -> RawFileError: return RawFileError( self.metadata, RawFileError.ACTION_REMOVE, format_os_error(e) ) pcs-0.12.0.2/pcs/common/file_type_codes.py000066400000000000000000000015531500417470700203140ustar00rootroot00000000000000from typing import NewType FileTypeCode = NewType("FileTypeCode", str) BOOTH_CONFIG = FileTypeCode("BOOTH_CONFIG") BOOTH_KEY = FileTypeCode("BOOTH_KEY") CIB = FileTypeCode("CIB") COROSYNC_AUTHKEY = FileTypeCode("COROSYNC_AUTHKEY") COROSYNC_CONF = FileTypeCode("COROSYNC_CONF") COROSYNC_QDEVICE_NSSDB = FileTypeCode("COROSYNC_QDEVICE_NSSDB") COROSYNC_QNETD_NSSDB = FileTypeCode("COROSYNC_QNETD_NSSDB") COROSYNC_QNETD_CA_CERT = FileTypeCode("COROSYNC_QNETD_CA_CERT") PACEMAKER_AUTHKEY = FileTypeCode("PACEMAKER_AUTHKEY") PCSD_ENVIRONMENT_CONFIG = FileTypeCode("PCSD_ENVIRONMENT_CONFIG") PCSD_SSL_CERT = FileTypeCode("PCSD_SSL_CERT") PCSD_SSL_KEY = FileTypeCode("PCSD_SSL_KEY") PCS_KNOWN_HOSTS = FileTypeCode("PCS_KNOWN_HOSTS") PCS_SETTINGS_CONF = FileTypeCode("PCS_SETTINGS_CONF") PCS_DR_CONFIG = FileTypeCode("PCS_DR_CONFIG") PCS_USERS_CONF = FileTypeCode("PCS_USERS_CONF") pcs-0.12.0.2/pcs/common/host.py000066400000000000000000000024251500417470700161330ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Any, Mapping, ) from pcs import settings @dataclass(frozen=True) class Destination: addr: str port: int @dataclass(frozen=True) class PcsKnownHost: name: str token: str dest_list: list[Destination] @classmethod def from_known_host_file_dict( cls, name: str, known_host_dict: Mapping[str, Any] ) -> "PcsKnownHost": dest_list = [ Destination(conn["addr"], conn["port"]) for conn in known_host_dict["dest_list"] ] if not dest_list: raise KeyError("no destination defined") return cls(name, token=known_host_dict["token"], dest_list=dest_list) def to_known_host_dict(self) -> tuple[str, dict[str, Any]]: return ( self.name, dict( token=self.token, dest_list=[ dict( addr=dest.addr, port=dest.port, ) for dest in self.dest_list ], ), ) @property def dest(self) -> Destination: if self.dest_list: return self.dest_list[0] return Destination(self.name, settings.pcsd_default_port) pcs-0.12.0.2/pcs/common/interface/000077500000000000000000000000001500417470700165415ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/interface/__init__.py000066400000000000000000000000001500417470700206400ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/interface/dto.py000066400000000000000000000120071500417470700177010ustar00rootroot00000000000000from dataclasses import ( asdict, fields, is_dataclass, ) from enum import Enum from typing import ( TYPE_CHECKING, Any, Dict, Iterable, NewType, TypeVar, Union, ) import dacite import pcs.common.async_tasks.types as async_tasks_types import pcs.common.permissions.types as permissions_types from pcs.common import types if TYPE_CHECKING: from _typeshed import DataclassInstance # pylint: disable=import-error else: class DataclassInstance: pass PrimitiveType = Union[str, int, float, bool, None] DtoPayload = Dict[str, "SerializableType"] # type: ignore SerializableType = Union[ # type: ignore PrimitiveType, DtoPayload, # type: ignore Iterable["SerializableType"], # type: ignore ] T = TypeVar("T") ToDictMetaKey = NewType("ToDictMetaKey", str) META_NAME = ToDictMetaKey("META_NAME") class PayloadConversionError(Exception): pass class DataTransferObject(DataclassInstance): pass def meta(name: str) -> Dict[str, str]: metadata: Dict[str, str] = {} if name: metadata[META_NAME] = name return metadata # _type is Any, since based on static code analysis it can be either of # type[Any], str, None - depending on the step in dataclass instance # initialization def _is_compatible_type(_type: Any, arg_index: int) -> bool: return ( hasattr(_type, "__args__") and len(_type.__args__) >= arg_index and is_dataclass(_type.__args__[arg_index]) ) def _convert_dict( klass: type[DataTransferObject], obj_dict: DtoPayload ) -> DtoPayload: new_dict = {} for _field in fields(klass): value = obj_dict[_field.name] if is_dataclass(_field.type): value = _convert_dict(_field.type, value) # type: ignore elif isinstance(value, list) and _is_compatible_type(_field.type, 0): value = [ # ignore _field.type may not have __args__ # this is prevented by _is_compatible_type _convert_dict(_field.type.__args__[0], item) # type: ignore for item in value ] elif isinstance(value, dict) and _is_compatible_type(_field.type, 1): value = { item_key: _convert_dict( _field.type.__args__[1], item_val # type: ignore ) for item_key, item_val in value.items() } elif isinstance(value, Enum): value = value.value new_dict[_field.metadata.get(META_NAME, _field.name)] = value return new_dict def to_dict(obj: DataTransferObject) -> DtoPayload: return _convert_dict(obj.__class__, asdict(obj)) DTOTYPE = TypeVar("DTOTYPE", bound=DataTransferObject) def _convert_payload(klass: type[DTOTYPE], data: DtoPayload) -> DtoPayload: try: new_dict = dict(data) except ValueError as e: raise PayloadConversionError() from e for _field in fields(klass): new_name = _field.metadata.get(META_NAME, _field.name) if new_name not in data: continue value = data[new_name] if is_dataclass(_field.type): value = _convert_payload(_field.type, value) # type: ignore elif isinstance(value, list) and _is_compatible_type(_field.type, 0): value = [ # ignore _field.type may not have __args__ # this is prevented by _is_compatible_type _convert_payload(_field.type.__args__[0], item) # type: ignore for item in value ] elif isinstance(value, dict) and _is_compatible_type(_field.type, 1): value = { item_key: _convert_payload( _field.type.__args__[1], item_val # type: ignore ) for item_key, item_val in value.items() } del new_dict[new_name] new_dict[_field.name] = value return new_dict def from_dict( cls: type[DTOTYPE], data: DtoPayload, strict: bool = False ) -> DTOTYPE: return dacite.from_dict( data_class=cls, data=_convert_payload(cls, data), # NOTE: all enum types has to be listed here in key cast # see: https://github.com/konradhalas/dacite#casting config=dacite.Config( cast=[ types.CibRuleExpressionType, types.CibRuleInEffectStatus, types.CorosyncNodeAddressType, types.CorosyncTransportType, types.DrRole, types.ResourceRelationType, async_tasks_types.TaskFinishType, async_tasks_types.TaskState, async_tasks_types.TaskKillReason, permissions_types.PermissionAccessType, permissions_types.PermissionTargetType, ], strict=strict, ), ) class ImplementsToDto: def to_dto(self) -> Any: raise NotImplementedError() class ImplementsFromDto: @classmethod def from_dto(cls: type[T], dto_obj: Any) -> T: raise NotImplementedError() pcs-0.12.0.2/pcs/common/node_communicator.py000066400000000000000000000464071500417470700206730ustar00rootroot00000000000000import base64 import io import re from dataclasses import ( dataclass, field, ) from typing import ( Generator, Iterable, Mapping, Optional, Sequence, Union, ) from urllib.parse import urlencode # We should ignore SIGPIPE when using pycurl.NOSIGNAL - see the libcurl tutorial # for more info. try: import signal signal.signal(signal.SIGPIPE, signal.SIG_IGN) except ImportError: pass from pcs import settings from pcs.common import pcs_pycurl as pycurl from pcs.common.host import ( Destination, PcsKnownHost, ) from pcs.common.types import StringIterable class HostNotFound(Exception): def __init__(self, name: str): super().__init__() self.name = name @dataclass(frozen=True) class RequestTarget: """ This class represents target (host) for request to be performed on """ label: str token: Optional[str] = None dest_list: list[Destination] = field(default_factory=list) def __post_init__(self) -> None: if not self.dest_list: object.__setattr__( self, "dest_list", [Destination(self.label, settings.pcsd_default_port)], ) @classmethod def from_known_host(cls, known_host: PcsKnownHost) -> "RequestTarget": return cls( known_host.name, token=known_host.token, dest_list=known_host.dest_list, ) @property def first_addr(self) -> str: # __post_init__ ensures there is always at least one item in # self.dest_list return self.dest_list[0].addr class NodeTargetFactory: def __init__(self, known_hosts: Mapping[str, PcsKnownHost]): self._known_hosts = known_hosts def get_target(self, host_name: str) -> RequestTarget: known_host = self._known_hosts.get(host_name) if known_host is None: raise HostNotFound(host_name) return RequestTarget.from_known_host(known_host) def get_target_from_hostname(self, hostname: str) -> RequestTarget: try: return self.get_target(hostname) except HostNotFound: return RequestTarget(hostname) @dataclass(frozen=True) class RequestData: """ This class represents action and data associated with action which will be send in request action -- action to perform structured_data -- list of tuples, data to send with specified action data -- raw data to send in request's body """ action: str structured_data: Union[ Sequence[tuple[Union[str, bytes], Union[str, bytes]]], Sequence[tuple[Union[str, bytes], Sequence[Union[str, bytes]]]], ] = () data: str = "" def __post_init__(self) -> None: if not self.data: object.__setattr__(self, "data", urlencode(self.structured_data)) else: object.__setattr__(self, "structured_data", self.data) class Request: """ This class represents request. With usage of RequestTarget it provides interface for getting next available host to make request on. """ def __init__( self, request_target: RequestTarget, request_data: RequestData ) -> None: self._target = request_target self._data = request_data self._current_dest_iterator = iter(self._target.dest_list) self.next_dest() def next_dest(self) -> None: """ Move to the next available host connection. Raises StopIteration when there is no connection to use. """ self._current_dest = next(self._current_dest_iterator) @property def url(self) -> str: """ URL representing request using current host. """ addr = self.dest.addr port = self.dest.port return "https://{host}:{port}/{request}".format( host="[{0}]".format(addr) if ":" in addr else addr, port=(port if port else settings.pcsd_default_port), request=self._data.action, ) @property def dest(self) -> Destination: return self._current_dest @property def host_label(self) -> str: return self._target.label @property def target(self) -> RequestTarget: return self._target @property def data(self) -> str: return self._data.data @property def action(self) -> str: return self._data.action @property def cookies(self) -> dict[str, str]: cookies = {} if self._target.token: cookies["token"] = self._target.token return cookies def __repr__(self) -> str: return str("Request({0}, {1})").format(self._target, self._data) class Response: """ This class represents response for request which is available as instance property. """ def __init__( self, handle: pycurl.Curl, was_connected: bool, errno: Optional[int] = None, error_msg: Optional[str] = None, ) -> None: self._handle = handle self._was_connected = was_connected self._errno = errno self._error_msg = error_msg self._data = None self._debug = None @classmethod def connection_successful(cls, handle: pycurl.Curl) -> "Response": """ Returns Response instance that is marked as successfully connected. handle -- curl easy handle, which connection was successful """ return cls(handle, True) @classmethod def connection_failure( cls, handle: pycurl.Curl, errno: int, error_msg: str ) -> "Response": """ Returns Response instance that is marked as not successfully connected. handle -- curl easy handle, which was not connected errno -- error number error_msg -- text description of error """ return cls(handle, False, errno, error_msg) @property def request(self) -> Request: return self._handle.request_obj # type: ignore[attr-defined] @property def handle(self) -> pycurl.Curl: return self._handle @property def was_connected(self) -> bool: return self._was_connected @property def errno(self) -> Optional[int]: return self._errno @property def error_msg(self) -> Optional[str]: return self._error_msg @property def data(self) -> str: if self._data is None: self._data = self._handle.output_buffer.getvalue().decode("utf-8") # type: ignore[attr-defined] return str(self._data) @property def debug(self) -> str: if self._debug is None: self._debug = self._handle.debug_buffer.getvalue().decode("utf-8") # type: ignore[attr-defined] return str(self._debug) @property def response_code(self) -> Optional[int]: if not self.was_connected: return None return self._handle.getinfo(pycurl.RESPONSE_CODE) def __repr__(self) -> str: return str( "Response({0} data='{1}' was_connected={2}) errno='{3}'" " error_msg='{4}' response_code='{5}')" ).format( self.request, self.data, self.was_connected, self.errno, self.error_msg, self.response_code, ) class CommunicatorLoggerInterface: def log_request_start(self, request: Request) -> None: raise NotImplementedError() def log_response(self, response: Response) -> None: raise NotImplementedError() def log_retry(self, response: Response, previous_dest: Destination) -> None: raise NotImplementedError() def log_no_more_addresses(self, response: Response) -> None: raise NotImplementedError() class Communicator: """ This class provides simple interface for making parallel requests. The instances of this class are not thread-safe! It is intended to use it only in a single thread. Use an unique instance for each thread. """ curl_multi_select_timeout_default = 0.8 # in seconds def __init__( self, communicator_logger: CommunicatorLoggerInterface, user: Optional[str], groups: Optional[StringIterable], request_timeout: Optional[int] = None, ) -> None: self._logger = communicator_logger self._auth_cookies = _get_auth_cookies(user, groups) self._request_timeout = ( request_timeout if request_timeout is not None else settings.default_request_timeout ) self._multi_handle = pycurl.CurlMulti() self._is_running = False # This is used just for storing references of curl easy handles. # We need to have references for all the handles, so they don't be # cleaned up by the garbage collector. self._easy_handle_list: list[pycurl.Curl] = [] def add_requests(self, request_list: Iterable[Request]) -> None: """ Add requests to queue to be processed. It is possible to call this method before getting generator using start_loop method and also during getting responses from generator. Requests are not performed after calling this method, but only when generator returned by start_loop method is in progress (returned at least one response and not raised StopIteration exception). request_list -- Request objects to add to the queue """ for request in request_list: handle = _create_request_handle( request, self._auth_cookies, self._request_timeout, ) self._easy_handle_list.append(handle) self._multi_handle.add_handle(handle) if self._is_running: self._logger.log_request_start(request) def start_loop(self) -> Generator[Response, None, None]: """ Returns generator. When generator is invoked, all requests in queue (added by method add_requests) will be invoked in parallel, and generator will then return responses for these requests. It is possible to add new request to the queue while the generator is in progress. Generator will stop (raise StopIteration) after all requests (also those added after creation of generator) are processed. WARNING: do not use multiple instances of generator (of one Communicator instance) when there is one which didn't finish (raised StopIteration). It will cause AssertionError. USAGE: com = Communicator(...) com.add_requests([ Request(...), ... ]) for response in communicator.start_loop(): # do something with response # if needed, add some new requests to the queue com.add_requests([Request(...)]) """ if self._is_running: raise AssertionError("Method start_loop already running") self._is_running = True for handle in self._easy_handle_list: self._logger.log_request_start(handle.request_obj) # type: ignore[attr-defined] finished_count = 0 while finished_count < len(self._easy_handle_list): self.__multi_perform() self.__wait_for_multi_handle() response_list = self.__get_all_ready_responses() for response in response_list: # free up memory for next usage of this Communicator instance self._multi_handle.remove_handle(response.handle) self._logger.log_response(response) yield response # if something was added to the queue in the meantime, run it # immediately, so we don't need to wait until all responses will # be processed self.__multi_perform() finished_count += len(response_list) self._easy_handle_list = [] self._is_running = False def __get_all_ready_responses(self) -> list[Response]: response_list = [] repeat = True while repeat: num_queued, ok_list, err_list = self._multi_handle.info_read() response_list.extend( [Response.connection_successful(handle) for handle in ok_list] + [ Response.connection_failure(handle, errno, error_msg) for handle, errno, error_msg in err_list ] ) repeat = num_queued > 0 return response_list def __multi_perform(self) -> int: # run all internal operation required by libcurl status, num_to_process = self._multi_handle.perform() # if perform returns E_CALL_MULTI_PERFORM it requires to call perform # once again right away while status == pycurl.E_CALL_MULTI_PERFORM: status, num_to_process = self._multi_handle.perform() return num_to_process def __wait_for_multi_handle(self) -> None: # try to wait until there is something to do for us need_to_wait = True while need_to_wait: timeout = self._multi_handle.timeout() if timeout == 0: # if timeout == 0 then there is something to precess already return select_timeout = ( timeout / 1000.0 if timeout > 0 # curl don't have timeout set, so we can use our default else self.curl_multi_select_timeout_default ) # when value returned from select is -1, it timed out, so we can # wait need_to_wait = self._multi_handle.select(select_timeout) == -1 class MultiaddressCommunicator(Communicator): """ Class with same interface as Communicator. In difference with Communicator, it takes advantage of multiple hosts in RequestTarget. So if it is not possible to connect to target using first hostname, it will use next one until connection will be successful or there is no host left. """ def start_loop(self) -> Generator[Response, None, None]: for response in super().start_loop(): if response.was_connected: yield response continue try: previous_dest = response.request.dest response.request.next_dest() if previous_dest is not None: self._logger.log_retry(response, previous_dest) self.add_requests([response.request]) except StopIteration: self._logger.log_no_more_addresses(response) yield response class NodeCommunicatorFactory: def __init__( self, communicator_logger: CommunicatorLoggerInterface, user: Optional[str], groups: Optional[StringIterable], request_timeout: Optional[int], ) -> None: self._logger = communicator_logger self._user = user self._groups = groups self._request_timeout = request_timeout def get_communicator( self, request_timeout: Optional[int] = None ) -> Communicator: return self.get_simple_communicator(request_timeout=request_timeout) def get_simple_communicator( self, request_timeout: Optional[int] = None ) -> Communicator: timeout = request_timeout if request_timeout else self._request_timeout return Communicator( self._logger, self._user, self._groups, request_timeout=timeout ) def get_multiaddress_communicator( self, request_timeout: Optional[int] = None ) -> MultiaddressCommunicator: timeout = request_timeout if request_timeout else self._request_timeout return MultiaddressCommunicator( self._logger, self._user, self._groups, request_timeout=timeout ) def _get_auth_cookies( user: Optional[str], group_list: Optional[StringIterable] ) -> dict[str, str]: """ Returns input parameters in a dictionary which is prepared to be converted to cookie string. user -- CIB user group_list -- CIB user groups """ # Let's be safe about characters in variables (they can come from env) # and do base64. We cannot do it for CIB_user however to be backward # compatible so we at least remove disallowed characters. cookies = {} if user: cookies["CIB_user"] = re.sub(r"[^!-~]", "", user).replace(";", "") if group_list: # cookies require string but base64encode returns bytes, so decode it... cookies["CIB_user_groups"] = base64.b64encode( # python3 requires the value to be bytes not str " ".join(group_list).encode("utf-8") ).decode("utf-8") return cookies def _create_request_handle( request: Request, cookies: Mapping[str, str], timeout: int ) -> pycurl.Curl: """ Returns Curl object (easy handle) which is set up with specified parameters. request -- request specification cookies -- cookies to add to request timeout -- request timeout """ # it is not possible to take this callback out of this function, because of # curl API def __debug_callback(data_type: int, debug_data: bytes) -> None: # pylint: disable=no-member prefixes = { # Dynamically added attributes in pcs/common/pcs_pycurl.py pycurl.DEBUG_TEXT: b"* ", # type: ignore[attr-defined] pycurl.DEBUG_HEADER_IN: b"< ", # type: ignore[attr-defined] pycurl.DEBUG_HEADER_OUT: b"> ", # type: ignore[attr-defined] pycurl.DEBUG_DATA_IN: b"<< ", # type: ignore[attr-defined] pycurl.DEBUG_DATA_OUT: b">> ", # type: ignore[attr-defined] } if data_type in prefixes: debug_output.write(prefixes[data_type]) debug_output.write(debug_data) if not debug_data.endswith(b"\n"): debug_output.write(b"\n") output = io.BytesIO() debug_output = io.BytesIO() handle_cookies = dict(cookies.items()) handle_cookies.update(request.cookies) handle = pycurl.Curl() handle.setopt(pycurl.PROTOCOLS, pycurl.PROTO_HTTPS) handle.setopt(pycurl.TIMEOUT, timeout) handle.setopt(pycurl.URL, request.url.encode("utf-8")) handle.setopt(pycurl.WRITEFUNCTION, output.write) handle.setopt(pycurl.VERBOSE, 1) handle.setopt(pycurl.DEBUGFUNCTION, __debug_callback) handle.setopt(pycurl.SSL_VERIFYHOST, 0) handle.setopt(pycurl.SSL_VERIFYPEER, 0) handle.setopt(pycurl.NOSIGNAL, 1) # required for multi-threading handle.setopt(pycurl.HTTPHEADER, ["Expect: "]) if handle_cookies: handle.setopt( pycurl.COOKIE, _dict_to_cookies(handle_cookies).encode("utf-8") ) if request.data: handle.setopt(pycurl.COPYPOSTFIELDS, request.data.encode("utf-8")) # add reference for request object and output buffers to handle, so later # we don't need to match these objects when they are returned from # pycurl after they've been processed # similar usage is in pycurl example: # https://github.com/pycurl/pycurl/blob/REL_7_19_0_3/examples/retriever-multi.py handle.request_obj = request # type: ignore[attr-defined] handle.output_buffer = output # type: ignore[attr-defined] handle.debug_buffer = debug_output # type: ignore[attr-defined] return handle def _dict_to_cookies(cookies_dict: Mapping[str, str]) -> str: return ";".join( [f"{key}={value}" for key, value in sorted(cookies_dict.items())] ) pcs-0.12.0.2/pcs/common/pacemaker/000077500000000000000000000000001500417470700165315ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/pacemaker/__init__.py000066400000000000000000000000771500417470700206460ustar00rootroot00000000000000from . import ( nvset, resource, role, rule, ) pcs-0.12.0.2/pcs/common/pacemaker/cluster_property.py000066400000000000000000000006401500417470700225300ustar00rootroot00000000000000from dataclasses import dataclass from typing import Sequence from pcs.common.interface.dto import DataTransferObject from pcs.common.resource_agent.dto import ResourceAgentParameterDto from pcs.common.types import StringCollection @dataclass(frozen=True) class ClusterPropertyMetadataDto(DataTransferObject): properties_metadata: Sequence[ResourceAgentParameterDto] readonly_properties: StringCollection pcs-0.12.0.2/pcs/common/pacemaker/constraint/000077500000000000000000000000001500417470700207155ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/pacemaker/constraint/__init__.py000066400000000000000000000012521500417470700230260ustar00rootroot00000000000000from .all import ( CibConstraintsDto, get_all_constraints_ids, get_all_location_constraints_ids, get_all_location_rules_ids, ) from .colocation import ( CibConstraintColocationAttributesDto, CibConstraintColocationDto, CibConstraintColocationSetDto, ) from .location import ( CibConstraintLocationAttributesDto, CibConstraintLocationDto, CibConstraintLocationSetDto, ) from .order import ( CibConstraintOrderAttributesDto, CibConstraintOrderDto, CibConstraintOrderSetDto, ) from .set import CibResourceSetDto from .ticket import ( CibConstraintTicketAttributesDto, CibConstraintTicketDto, CibConstraintTicketSetDto, ) pcs-0.12.0.2/pcs/common/pacemaker/constraint/all.py000066400000000000000000000056521500417470700220470ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Iterable, Sequence, Union, ) from pcs.common.interface.dto import DataTransferObject from .colocation import ( CibConstraintColocationDto, CibConstraintColocationSetDto, ) from .location import ( CibConstraintLocationDto, CibConstraintLocationSetDto, ) from .order import ( CibConstraintOrderDto, CibConstraintOrderSetDto, ) from .ticket import ( CibConstraintTicketDto, CibConstraintTicketSetDto, ) @dataclass(frozen=True) class CibConstraintsDto(DataTransferObject): # pylint: disable=too-many-instance-attributes location: Sequence[CibConstraintLocationDto] = tuple() location_set: Sequence[CibConstraintLocationSetDto] = tuple() colocation: Sequence[CibConstraintColocationDto] = tuple() colocation_set: Sequence[CibConstraintColocationSetDto] = tuple() order: Sequence[CibConstraintOrderDto] = tuple() order_set: Sequence[CibConstraintOrderSetDto] = tuple() ticket: Sequence[CibConstraintTicketDto] = tuple() ticket_set: Sequence[CibConstraintTicketSetDto] = tuple() def _get_constraint_ids( constraint_dtos: Iterable[ Union[ CibConstraintLocationDto, CibConstraintLocationSetDto, CibConstraintColocationDto, CibConstraintColocationSetDto, CibConstraintOrderDto, CibConstraintOrderSetDto, CibConstraintTicketDto, CibConstraintTicketSetDto, ] ] ) -> list[str]: return [ constraint_dto.attributes.constraint_id for constraint_dto in constraint_dtos ] def _get_location_rule_ids( constraint_dtos: Iterable[ Union[ CibConstraintLocationDto, CibConstraintLocationSetDto, ] ] ) -> list[str]: return [ rule_dto.id for constraint_dto in constraint_dtos for rule_dto in constraint_dto.attributes.rules ] def get_all_constraints_ids(constraints_dto: CibConstraintsDto) -> set[str]: return set( _get_constraint_ids(constraints_dto.location) + _get_constraint_ids(constraints_dto.location_set) + _get_constraint_ids(constraints_dto.colocation) + _get_constraint_ids(constraints_dto.colocation_set) + _get_constraint_ids(constraints_dto.order) + _get_constraint_ids(constraints_dto.order_set) + _get_constraint_ids(constraints_dto.ticket) + _get_constraint_ids(constraints_dto.ticket_set) ) def get_all_location_rules_ids( constraints_dto: CibConstraintsDto, ) -> set[str]: return set( _get_location_rule_ids(constraints_dto.location) + _get_location_rule_ids(constraints_dto.location_set) ) def get_all_location_constraints_ids( constraints_dto: CibConstraintsDto, ) -> set[str]: return set( _get_constraint_ids(constraints_dto.location) + _get_constraint_ids(constraints_dto.location_set) ) pcs-0.12.0.2/pcs/common/pacemaker/constraint/colocation.py000066400000000000000000000021001500417470700234120ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, ) from pcs.common.const import PcmkRoleType from pcs.common.interface.dto import DataTransferObject from ..rule import CibRuleExpressionDto from .set import CibResourceSetDto @dataclass(frozen=True) class CibConstraintColocationAttributesDto(DataTransferObject): constraint_id: str score: Optional[str] influence: Optional[str] lifetime: Sequence[CibRuleExpressionDto] @dataclass(frozen=True) class CibConstraintColocationDto(DataTransferObject): # pylint: disable=too-many-instance-attributes resource_id: str with_resource_id: str node_attribute: Optional[str] resource_role: Optional[PcmkRoleType] with_resource_role: Optional[PcmkRoleType] resource_instance: Optional[int] with_resource_instance: Optional[int] attributes: CibConstraintColocationAttributesDto @dataclass(frozen=True) class CibConstraintColocationSetDto(DataTransferObject): resource_sets: Sequence[CibResourceSetDto] attributes: CibConstraintColocationAttributesDto pcs-0.12.0.2/pcs/common/pacemaker/constraint/location.py000066400000000000000000000020061500417470700230750ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, ) from pcs.common.const import PcmkRoleType from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.types import CibResourceDiscovery from ..rule import CibRuleExpressionDto from .set import CibResourceSetDto @dataclass(frozen=True) class CibConstraintLocationAttributesDto(DataTransferObject): constraint_id: str score: Optional[str] node: Optional[str] rules: Sequence[CibRuleExpressionDto] lifetime: Sequence[CibRuleExpressionDto] resource_discovery: Optional[CibResourceDiscovery] @dataclass(frozen=True) class CibConstraintLocationDto(DataTransferObject): resource_id: Optional[str] resource_pattern: Optional[str] role: Optional[PcmkRoleType] attributes: CibConstraintLocationAttributesDto @dataclass(frozen=True) class CibConstraintLocationSetDto(DataTransferObject): resource_sets: Sequence[CibResourceSetDto] attributes: CibConstraintLocationAttributesDto pcs-0.12.0.2/pcs/common/pacemaker/constraint/order.py000066400000000000000000000020111500417470700223740ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, ) from pcs.common.const import PcmkAction from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.types import CibResourceSetOrderType from .set import CibResourceSetDto @dataclass(frozen=True) class CibConstraintOrderAttributesDto(DataTransferObject): constraint_id: str symmetrical: Optional[bool] require_all: Optional[bool] score: Optional[str] kind: Optional[CibResourceSetOrderType] @dataclass(frozen=True) class CibConstraintOrderDto(DataTransferObject): first_resource_id: str then_resource_id: str first_action: Optional[PcmkAction] then_action: Optional[PcmkAction] first_resource_instance: Optional[int] then_resource_instance: Optional[int] attributes: CibConstraintOrderAttributesDto @dataclass(frozen=True) class CibConstraintOrderSetDto(DataTransferObject): resource_sets: Sequence[CibResourceSetDto] attributes: CibConstraintOrderAttributesDto pcs-0.12.0.2/pcs/common/pacemaker/constraint/set.py000066400000000000000000000013531500417470700220640ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional from pcs.common.const import ( PcmkAction, PcmkRoleType, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.types import ( CibResourceSetOrdering, CibResourceSetOrderType, ) from pcs.common.types import StringSequence @dataclass(frozen=True) class CibResourceSetDto(DataTransferObject): # pylint: disable=too-many-instance-attributes set_id: str sequential: Optional[bool] require_all: Optional[bool] ordering: Optional[CibResourceSetOrdering] action: Optional[PcmkAction] role: Optional[PcmkRoleType] score: Optional[str] kind: Optional[CibResourceSetOrderType] resources_ids: StringSequence pcs-0.12.0.2/pcs/common/pacemaker/constraint/ticket.py000066400000000000000000000014451500417470700225560ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, ) from pcs.common.const import PcmkRoleType from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.types import CibTicketLossPolicy from .set import CibResourceSetDto @dataclass(frozen=True) class CibConstraintTicketAttributesDto(DataTransferObject): constraint_id: str ticket: str loss_policy: Optional[CibTicketLossPolicy] @dataclass(frozen=True) class CibConstraintTicketDto(DataTransferObject): resource_id: str role: Optional[PcmkRoleType] attributes: CibConstraintTicketAttributesDto @dataclass(frozen=True) class CibConstraintTicketSetDto(DataTransferObject): resource_sets: Sequence[CibResourceSetDto] attributes: CibConstraintTicketAttributesDto pcs-0.12.0.2/pcs/common/pacemaker/defaults.py000066400000000000000000000004651500417470700207170ustar00rootroot00000000000000from dataclasses import dataclass from typing import Sequence from pcs.common.interface.dto import DataTransferObject from .nvset import CibNvsetDto @dataclass(frozen=True) class CibDefaultsDto(DataTransferObject): instance_attributes: Sequence[CibNvsetDto] meta_attributes: Sequence[CibNvsetDto] pcs-0.12.0.2/pcs/common/pacemaker/fencing_topology.py000066400000000000000000000017141500417470700224530ustar00rootroot00000000000000from collections.abc import Sequence from dataclasses import dataclass from typing import Union from pcs.common.interface.dto import DataTransferObject @dataclass(frozen=True) class CibFencingLevelNodeDto(DataTransferObject): id: str target: str index: int devices: list[str] @dataclass(frozen=True) class CibFencingLevelRegexDto(DataTransferObject): id: str target_pattern: str index: int devices: list[str] @dataclass(frozen=True) class CibFencingLevelAttributeDto(DataTransferObject): id: str target_attribute: str target_value: str index: int devices: list[str] CibFencingLevel = Union[ CibFencingLevelNodeDto, CibFencingLevelRegexDto, CibFencingLevelAttributeDto ] @dataclass(frozen=True) class CibFencingTopologyDto(DataTransferObject): target_node: Sequence[CibFencingLevelNodeDto] target_regex: Sequence[CibFencingLevelRegexDto] target_attribute: Sequence[CibFencingLevelAttributeDto] pcs-0.12.0.2/pcs/common/pacemaker/nvset.py000066400000000000000000000012411500417470700202400ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Mapping, Optional, Sequence, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.rule import CibRuleExpressionDto @dataclass(frozen=True) class CibNvpairDto(DataTransferObject): id: str # pylint: disable=invalid-name name: str value: str @dataclass(frozen=True) class CibNvsetDto(DataTransferObject): id: str # pylint: disable=invalid-name options: Mapping[str, str] rule: Optional[CibRuleExpressionDto] nvpairs: Sequence[CibNvpairDto] @dataclass(frozen=True) class ListCibNvsetDto(DataTransferObject): nvsets: Sequence[CibNvsetDto] pcs-0.12.0.2/pcs/common/pacemaker/resource/000077500000000000000000000000001500417470700203605ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/pacemaker/resource/__init__.py000066400000000000000000000000001500417470700224570ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/pacemaker/resource/bundle.py000066400000000000000000000036661500417470700222160ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( NewType, Optional, Sequence, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.nvset import CibNvsetDto ContainerType = NewType("ContainerType", str) CONTAINER_TYPE_DOCKER = ContainerType("docker") CONTAINER_TYPE_PODMAN = ContainerType("podman") @dataclass(frozen=True) class CibResourceBundleContainerRuntimeOptionsDto(DataTransferObject): image: str replicas: Optional[int] replicas_per_host: Optional[int] promoted_max: Optional[int] run_command: Optional[str] network: Optional[str] options: Optional[str] @dataclass(frozen=True) class CibResourceBundlePortMappingDto(DataTransferObject): id: str # pylint: disable=invalid-name port: Optional[int] internal_port: Optional[int] range: Optional[str] @dataclass(frozen=True) class CibResourceBundleNetworkOptionsDto(DataTransferObject): ip_range_start: Optional[str] control_port: Optional[int] host_interface: Optional[str] host_netmask: Optional[int] add_host: Optional[bool] @dataclass(frozen=True) class CibResourceBundleStorageMappingDto(DataTransferObject): id: str # pylint: disable=invalid-name source_dir: Optional[str] source_dir_root: Optional[str] target_dir: str options: Optional[str] @dataclass(frozen=True) class CibResourceBundleDto(DataTransferObject): # pylint: disable=too-many-instance-attributes id: str # pylint: disable=invalid-name description: Optional[str] member_id: Optional[str] container_type: Optional[ContainerType] container_options: Optional[CibResourceBundleContainerRuntimeOptionsDto] network: Optional[CibResourceBundleNetworkOptionsDto] port_mappings: Sequence[CibResourceBundlePortMappingDto] storage_mappings: Sequence[CibResourceBundleStorageMappingDto] meta_attributes: Sequence[CibNvsetDto] instance_attributes: Sequence[CibNvsetDto] pcs-0.12.0.2/pcs/common/pacemaker/resource/clone.py000066400000000000000000000007021500417470700220310ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.nvset import CibNvsetDto @dataclass(frozen=True) class CibResourceCloneDto(DataTransferObject): id: str # pylint: disable=invalid-name description: Optional[str] member_id: str meta_attributes: Sequence[CibNvsetDto] instance_attributes: Sequence[CibNvsetDto] pcs-0.12.0.2/pcs/common/pacemaker/resource/group.py000066400000000000000000000007721500417470700220740ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.nvset import CibNvsetDto from pcs.common.types import StringSequence @dataclass(frozen=True) class CibResourceGroupDto(DataTransferObject): id: str # pylint: disable=invalid-name description: Optional[str] member_ids: StringSequence meta_attributes: Sequence[CibNvsetDto] instance_attributes: Sequence[CibNvsetDto] pcs-0.12.0.2/pcs/common/pacemaker/resource/list.py000066400000000000000000000022001500417470700216770ustar00rootroot00000000000000from dataclasses import dataclass from itertools import chain from typing import Sequence from pcs.common.interface.dto import DataTransferObject from .bundle import CibResourceBundleDto from .clone import CibResourceCloneDto from .group import CibResourceGroupDto from .primitive import CibResourcePrimitiveDto @dataclass(frozen=True) class CibResourcesDto(DataTransferObject): primitives: Sequence[CibResourcePrimitiveDto] clones: Sequence[CibResourceCloneDto] groups: Sequence[CibResourceGroupDto] bundles: Sequence[CibResourceBundleDto] def get_all_resources_ids(resources_dto: CibResourcesDto) -> set[str]: return set( chain( (primitive.id for primitive in resources_dto.primitives), (group.id for group in resources_dto.groups), (clone.id for clone in resources_dto.clones), (bundle.id for bundle in resources_dto.bundles), ) ) def get_stonith_resources_ids(resources_dto: CibResourcesDto) -> set[str]: return set( primitive.id for primitive in resources_dto.primitives if primitive.agent_name.standard == "stonith" ) pcs-0.12.0.2/pcs/common/pacemaker/resource/operations.py000066400000000000000000000020731500417470700231170ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, ) from pcs.common.const import ( PcmkOnFailAction, PcmkRoleType, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.nvset import CibNvsetDto OCF_CHECK_LEVEL_INSTANCE_ATTRIBUTE_NAME = "OCF_CHECK_LEVEL" @dataclass(frozen=True) class CibResourceOperationDto(DataTransferObject): # pylint: disable=too-many-instance-attributes id: str # pylint: disable=invalid-name name: str interval: str description: Optional[str] # exactly one of start_delay and interval_origin should be defined start_delay: Optional[str] interval_origin: Optional[str] timeout: Optional[str] enabled: Optional[bool] record_pending: Optional[bool] role: Optional[PcmkRoleType] on_fail: Optional[PcmkOnFailAction] meta_attributes: Sequence[CibNvsetDto] instance_attributes: Sequence[CibNvsetDto] @dataclass(frozen=True) class ListCibResourceOperationDto(DataTransferObject): operations: Sequence[CibResourceOperationDto] pcs-0.12.0.2/pcs/common/pacemaker/resource/primitive.py000066400000000000000000000012411500417470700227400ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.pacemaker.nvset import CibNvsetDto from pcs.common.resource_agent.dto import ResourceAgentNameDto from .operations import CibResourceOperationDto @dataclass(frozen=True) class CibResourcePrimitiveDto(DataTransferObject): id: str # pylint: disable=invalid-name agent_name: ResourceAgentNameDto description: Optional[str] operations: Sequence[CibResourceOperationDto] meta_attributes: Sequence[CibNvsetDto] instance_attributes: Sequence[CibNvsetDto] utilization: Sequence[CibNvsetDto] pcs-0.12.0.2/pcs/common/pacemaker/resource/relations.py000066400000000000000000000011421500417470700227300ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Any, Mapping, Sequence, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.types import ( ResourceRelationType, StringSequence, ) @dataclass(frozen=True) class RelationEntityDto(DataTransferObject): id: str # pylint: disable=invalid-name type: ResourceRelationType members: StringSequence metadata: Mapping[str, Any] @dataclass(frozen=True) class ResourceRelationDto(DataTransferObject): relation_entity: RelationEntityDto members: Sequence["ResourceRelationDto"] is_leaf: bool pcs-0.12.0.2/pcs/common/pacemaker/role.py000066400000000000000000000012231500417470700200420ustar00rootroot00000000000000from .. import const def get_value_for_cib( role: const.PcmkRoleType, is_latest_supported: bool ) -> const.PcmkRoleType: if is_latest_supported: return get_value_primary(role) if role == const.PCMK_ROLE_PROMOTED: return const.PCMK_ROLE_PROMOTED_LEGACY if role == const.PCMK_ROLE_UNPROMOTED: return const.PCMK_ROLE_UNPROMOTED_LEGACY return role def get_value_primary(role: const.PcmkRoleType) -> const.PcmkRoleType: if role == const.PCMK_ROLE_PROMOTED_LEGACY: return const.PCMK_ROLE_PROMOTED if role == const.PCMK_ROLE_UNPROMOTED_LEGACY: return const.PCMK_ROLE_UNPROMOTED return role pcs-0.12.0.2/pcs/common/pacemaker/rule.py000066400000000000000000000015211500417470700200510ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Mapping, Optional, Sequence, ) from pcs.common.interface.dto import DataTransferObject from pcs.common.types import ( CibRuleExpressionType, CibRuleInEffectStatus, ) @dataclass(frozen=True) class CibRuleDateCommonDto(DataTransferObject): id: str # pylint: disable=invalid-name options: Mapping[str, str] @dataclass(frozen=True) class CibRuleExpressionDto(DataTransferObject): # pylint: disable=too-many-instance-attributes id: str # pylint: disable=invalid-name type: CibRuleExpressionType in_effect: CibRuleInEffectStatus # only valid for type==rule options: Mapping[str, str] date_spec: Optional[CibRuleDateCommonDto] duration: Optional[CibRuleDateCommonDto] expressions: Sequence["CibRuleExpressionDto"] as_string: str pcs-0.12.0.2/pcs/common/pacemaker/tag.py000066400000000000000000000005561500417470700176640ustar00rootroot00000000000000from dataclasses import dataclass from typing import Sequence from pcs.common.interface.dto import DataTransferObject from pcs.common.types import StringSequence @dataclass(frozen=True) class CibTagDto(DataTransferObject): id: str idref_list: StringSequence @dataclass(frozen=True) class CibTagListDto(DataTransferObject): tags: Sequence[CibTagDto] pcs-0.12.0.2/pcs/common/pacemaker/tools.py000066400000000000000000000003561500417470700202470ustar00rootroot00000000000000def is_negative_score(score: str) -> bool: return score.startswith("-") def abs_score(score: str) -> str: """ return absolute value of score """ if is_negative_score(score): return score[1:] return score pcs-0.12.0.2/pcs/common/pacemaker/types.py000066400000000000000000000014551500417470700202540ustar00rootroot00000000000000from typing import cast class CibResourceDiscovery(str): ALWAYS = cast("CibResourceDiscovery", "always") NEVER = cast("CibResourceDiscovery", "never") EXCLUSIVE = cast("CibResourceDiscovery", "exclusive") class CibResourceSetOrdering(str): GROUP = cast("CibResourceSetOrdering", "group") LISTED = cast("CibResourceSetOrdering", "listed") class CibResourceSetOrderType(str): OPTIONAL = cast("CibResourceSetOrderType", "Optional") MANDATORY = cast("CibResourceSetOrderType", "Mandatory") SERIALIZE = cast("CibResourceSetOrderType", "Serialize") class CibTicketLossPolicy(str): STOP = cast("CibTicketLossPolicy", "stop") DEMOTE = cast("CibTicketLossPolicy", "demote") FENCE = cast("CibTicketLossPolicy", "fence") FREEZE = cast("CibTicketLossPolicy", "freeze") pcs-0.12.0.2/pcs/common/pcs_pycurl.py000066400000000000000000000014741500417470700173440ustar00rootroot00000000000000import sys # pylint: disable=unused-wildcard-import # pylint: disable=wildcard-import from pycurl import * # This package defines constants which are not present in some older versions # of pycurl but pcs needs to use them required_constants = { "PROTOCOLS": 181, "PROTO_HTTPS": 2, "E_OPERATION_TIMEDOUT": 28, # these are types of debug messages # see https://curl.haxx.se/libcurl/c/CURLOPT_DEBUGFUNCTION.html "DEBUG_TEXT": 0, "DEBUG_HEADER_IN": 1, "DEBUG_HEADER_OUT": 2, "DEBUG_DATA_IN": 3, "DEBUG_DATA_OUT": 4, "DEBUG_SSL_DATA_IN": 5, "DEBUG_SSL_DATA_OUT": 6, "DEBUG_END": 7, } __current_module = sys.modules[__name__] for constant, value in required_constants.items(): if not hasattr(__current_module, constant): setattr(__current_module, constant, value) pcs-0.12.0.2/pcs/common/permissions/000077500000000000000000000000001500417470700171545ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/permissions/__init__.py000066400000000000000000000000001500417470700212530ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/permissions/types.py000066400000000000000000000003661500417470700206770ustar00rootroot00000000000000from enum import Enum class PermissionTargetType(str, Enum): USER = "user" GROUP = "group" class PermissionAccessType(str, Enum): READ = "read" WRITE = "write" GRANT = "grant" FULL = "full" SUPERUSER = "superuser" pcs-0.12.0.2/pcs/common/reports/000077500000000000000000000000001500417470700162775ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/reports/__init__.py000066400000000000000000000005501500417470700204100ustar00rootroot00000000000000from . import ( codes, const, item, messages, types, ) from .conversions import report_dto_to_item from .dto import ReportItemDto from .item import ( ReportItem, ReportItemContext, ReportItemList, ReportItemMessage, ReportItemSeverity, get_severity, ) from .processor import ( ReportProcessor, has_errors, ) pcs-0.12.0.2/pcs/common/reports/codes.py000066400000000000000000000723241500417470700177560ustar00rootroot00000000000000# pylint: disable=unused-wildcard-import # pylint: disable=wildcard-import # Wildcard import of deprecated report codes will prevent creation of a new # report with the code of deprecated report from .deprecated_codes import * from .types import ForceCode as F from .types import MessageCode as M # force categories FORCE = F("FORCE") SKIP_OFFLINE_NODES = F("SKIP_OFFLINE_NODES") # messages ADD_REMOVE_ITEMS_NOT_SPECIFIED = M("ADD_REMOVE_ITEMS_NOT_SPECIFIED") ADD_REMOVE_ITEMS_DUPLICATION = M("ADD_REMOVE_ITEMS_DUPLICATION") ADD_REMOVE_CANNOT_ADD_ITEMS_ALREADY_IN_THE_CONTAINER = M( "ADD_REMOVE_CANNOT_ADD_ITEMS_ALREADY_IN_THE_CONTAINER" ) ADD_REMOVE_CANNOT_REMOVE_ITEMS_NOT_IN_THE_CONTAINER = M( "ADD_REMOVE_CANNOT_REMOVE_ITEMS_NOT_IN_THE_CONTAINER" ) ADD_REMOVE_CANNOT_ADD_AND_REMOVE_ITEMS_AT_THE_SAME_TIME = M( "ADD_REMOVE_CANNOT_ADD_AND_REMOVE_ITEMS_AT_THE_SAME_TIME" ) ADD_REMOVE_CANNOT_REMOVE_ALL_ITEMS_FROM_THE_CONTAINER = M( "ADD_REMOVE_CANNOT_REMOVE_ALL_ITEMS_FROM_THE_CONTAINER" ) ADD_REMOVE_ADJACENT_ITEM_NOT_IN_THE_CONTAINER = M( "ADD_REMOVE_ADJACENT_ITEM_NOT_IN_THE_CONTAINER" ) ADD_REMOVE_CANNOT_PUT_ITEM_NEXT_TO_ITSELF = M( "ADD_REMOVE_CANNOT_PUT_ITEM_NEXT_TO_ITSELF" ) ADD_REMOVE_CANNOT_SPECIFY_ADJACENT_ITEM_WITHOUT_ITEMS_TO_ADD = M( "ADD_REMOVE_CANNOT_SPECIFY_ADJACENT_ITEM_WITHOUT_ITEMS_TO_ADD" ) AGENT_GENERIC_ERROR = M("AGENT_GENERIC_ERROR") AGENT_IMPLEMENTS_UNSUPPORTED_OCF_VERSION = M( "AGENT_IMPLEMENTS_UNSUPPORTED_OCF_VERSION" ) AGENT_NAME_GUESS_FOUND_MORE_THAN_ONE = M("AGENT_NAME_GUESS_FOUND_MORE_THAN_ONE") AGENT_NAME_GUESS_FOUND_NONE = M("AGENT_NAME_GUESS_FOUND_NONE") AGENT_NAME_GUESSED = M("AGENT_NAME_GUESSED") AGENT_SELF_VALIDATION_AUTO_ON_WITH_WARNINGS = M( "AGENT_SELF_VALIDATION_AUTO_ON_WITH_WARNINGS" ) AGENT_SELF_VALIDATION_INVALID_DATA = M("AGENT_SELF_VALIDATION_INVALID_DATA") AGENT_SELF_VALIDATION_SKIPPED_UPDATED_RESOURCE_MISCONFIGURED = M( "AGENT_SELF_VALIDATION_SKIPPED_UPDATED_RESOURCE_MISCONFIGURED" ) AGENT_SELF_VALIDATION_RESULT = M("AGENT_SELF_VALIDATION_RESULT") BAD_CLUSTER_STATE_FORMAT = M("BAD_CLUSTER_STATE_FORMAT") BAD_CLUSTER_STATE_DATA = M("BAD_CLUSTER_STATE_DATA") BOOTH_ADDRESS_DUPLICATION = M("BOOTH_ADDRESS_DUPLICATION") BOOTH_ALREADY_IN_CIB = M("BOOTH_ALREADY_IN_CIB") BOOTH_AUTHFILE_NOT_USED = M("BOOTH_AUTHFILE_NOT_USED") BOOTH_UNSUPPORTED_OPTION_ENABLE_AUTHFILE = M( "BOOTH_UNSUPPORTED_OPTION_ENABLE_AUTHFILE" ) BOOTH_CANNOT_DETERMINE_LOCAL_SITE_IP = M("BOOTH_CANNOT_DETERMINE_LOCAL_SITE_IP") BOOTH_CONFIG_ACCEPTED_BY_NODE = M("BOOTH_CONFIG_ACCEPTED_BY_NODE") BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR = M("BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR") BOOTH_CONFIG_DISTRIBUTION_STARTED = M("BOOTH_CONFIG_DISTRIBUTION_STARTED") BOOTH_CONFIG_IS_USED = M("BOOTH_CONFIG_IS_USED") BOOTH_CONFIG_UNEXPECTED_LINES = M("BOOTH_CONFIG_UNEXPECTED_LINES") BOOTH_DAEMON_STATUS_ERROR = M("BOOTH_DAEMON_STATUS_ERROR") BOOTH_EVEN_PEERS_NUM = M("BOOTH_EVEN_PEERS_NUM") BOOTH_FETCHING_CONFIG_FROM_NODE = M("BOOTH_FETCHING_CONFIG_FROM_NODE") BOOTH_INVALID_NAME = M("BOOTH_INVALID_NAME") BOOTH_LACK_OF_SITES = M("BOOTH_LACK_OF_SITES") BOOTH_MULTIPLE_TIMES_IN_CIB = M("BOOTH_MULTIPLE_TIMES_IN_CIB") BOOTH_NOT_EXISTS_IN_CIB = M("BOOTH_NOT_EXISTS_IN_CIB") BOOTH_PATH_NOT_EXISTS = M("BOOTH_PATH_NOT_EXISTS") BOOTH_PEERS_STATUS_ERROR = M("BOOTH_PEERS_STATUS_ERROR") BOOTH_TICKET_CHANGING_STATE = M("BOOTH_TICKET_CHANGING_STATE") BOOTH_TICKET_CLEANUP = M("BOOTH_TICKET_CLEANUP") BOOTH_TICKET_DOES_NOT_EXIST = M("BOOTH_TICKET_DOES_NOT_EXIST") BOOTH_TICKET_DUPLICATE = M("BOOTH_TICKET_DUPLICATE") BOOTH_TICKET_NAME_INVALID = M("BOOTH_TICKET_NAME_INVALID") BOOTH_TICKET_NOT_IN_CIB = M("BOOTH_TICKET_NOT_IN_CIB") BOOTH_TICKET_OPERATION_FAILED = M("BOOTH_TICKET_OPERATION_FAILED") BOOTH_TICKET_STATUS_ERROR = M("BOOTH_TICKET_STATUS_ERROR") BOOTH_UNSUPPORTED_FILE_LOCATION = M("BOOTH_UNSUPPORTED_FILE_LOCATION") CANNOT_BAN_RESOURCE_BUNDLE_INNER = M("CANNOT_BAN_RESOURCE_BUNDLE_INNER") CANNOT_BAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE = M( "CANNOT_BAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE" ) CANNOT_BAN_RESOURCE_MULTIPLE_INSTANCES_NO_NODE_SPECIFIED = M( "CANNOT_BAN_RESOURCE_MULTIPLE_INSTANCES_NO_NODE_SPECIFIED" ) CANNOT_BAN_RESOURCE_STOPPED_NO_NODE_SPECIFIED = M( "CANNOT_BAN_RESOURCE_STOPPED_NO_NODE_SPECIFIED" ) STOPPING_RESOURCES_BEFORE_DELETING = M("STOPPING_RESOURCES_BEFORE_DELETING") STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED = M( "STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED" ) CANNOT_STOP_RESOURCES_BEFORE_DELETING = M( "CANNOT_STOP_RESOURCES_BEFORE_DELETING" ) CANNOT_CREATE_DEFAULT_CLUSTER_PROPERTY_SET = M( "CANNOT_CREATE_DEFAULT_CLUSTER_PROPERTY_SET" ) CANNOT_GROUP_RESOURCE_WRONG_TYPE = M("CANNOT_GROUP_RESOURCE_WRONG_TYPE") CANNOT_LEAVE_GROUP_EMPTY_AFTER_MOVE = M("CANNOT_LEAVE_GROUP_EMPTY_AFTER_MOVE") CANNOT_MOVE_RESOURCE_BUNDLE_INNER = M("CANNOT_MOVE_RESOURCE_BUNDLE_INNER") CANNOT_MOVE_RESOURCE_CLONE_INNER = M("CANNOT_MOVE_RESOURCE_CLONE_INNER") CANNOT_MOVE_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE = M( "CANNOT_MOVE_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE" ) CANNOT_MOVE_RESOURCE_MULTIPLE_INSTANCES = M( "CANNOT_MOVE_RESOURCE_MULTIPLE_INSTANCES" ) CANNOT_MOVE_RESOURCE_MULTIPLE_INSTANCES_NO_NODE_SPECIFIED = M( "CANNOT_MOVE_RESOURCE_MULTIPLE_INSTANCES_NO_NODE_SPECIFIED" ) CANNOT_MOVE_RESOURCE_PROMOTABLE_INNER = M( "CANNOT_MOVE_RESOURCE_PROMOTABLE_INNER" ) CANNOT_MOVE_RESOURCE_NOT_RUNNING = M("CANNOT_MOVE_RESOURCE_NOT_RUNNING") CANNOT_MOVE_RESOURCE_STOPPED_NO_NODE_SPECIFIED = M( "CANNOT_MOVE_RESOURCE_STOPPED_NO_NODE_SPECIFIED" ) CANNOT_REMOVE_ALL_CLUSTER_NODES = M("CANNOT_REMOVE_ALL_CLUSTER_NODES") CANNOT_SET_ORDER_CONSTRAINTS_FOR_RESOURCES_IN_THE_SAME_GROUP = M( "CANNOT_SET_ORDER_CONSTRAINTS_FOR_RESOURCES_IN_THE_SAME_GROUP" ) CANNOT_UNMOVE_UNBAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE = M( "CANNOT_UNMOVE_UNBAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE" ) CIB_ACL_ROLE_IS_ALREADY_ASSIGNED_TO_TARGET = M( "CIB_ACL_ROLE_IS_ALREADY_ASSIGNED_TO_TARGET" ) CIB_ACL_ROLE_IS_NOT_ASSIGNED_TO_TARGET = M( "CIB_ACL_ROLE_IS_NOT_ASSIGNED_TO_TARGET" ) CIB_ACL_TARGET_ALREADY_EXISTS = M("CIB_ACL_TARGET_ALREADY_EXISTS") CIB_ALERT_RECIPIENT_ALREADY_EXISTS = M("CIB_ALERT_RECIPIENT_ALREADY_EXISTS") CIB_ALERT_RECIPIENT_VALUE_INVALID = M("CIB_ALERT_RECIPIENT_VALUE_INVALID") CIB_CANNOT_FIND_MANDATORY_SECTION = M("CIB_CANNOT_FIND_MANDATORY_SECTION") CIB_DIFF_ERROR = M("CIB_DIFF_ERROR") CIB_FENCING_LEVEL_ALREADY_EXISTS = M("CIB_FENCING_LEVEL_ALREADY_EXISTS") CIB_FENCING_LEVEL_DOES_NOT_EXIST = M("CIB_FENCING_LEVEL_DOES_NOT_EXIST") CIB_LOAD_ERROR_BAD_FORMAT = M("CIB_LOAD_ERROR_BAD_FORMAT") CIB_LOAD_ERROR = M("CIB_LOAD_ERROR") CIB_LOAD_ERROR_GET_NODES_FOR_VALIDATION = M( "CIB_LOAD_ERROR_GET_NODES_FOR_VALIDATION" ) CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID = M("CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID") CIB_LOAD_ERROR_SCOPE_MISSING = M("CIB_LOAD_ERROR_SCOPE_MISSING") CIB_PUSH_ERROR = M("CIB_PUSH_ERROR") CIB_REMOVE_REFERENCES = M("CIB_REMOVE_REFERENCES") CIB_REMOVE_RESOURCES = M("CIB_REMOVE_RESOURCES") CIB_REMOVE_DEPENDANT_ELEMENTS = M("CIB_REMOVE_DEPENDANT_ELEMENTS") CIB_SAVE_TMP_ERROR = M("CIB_SAVE_TMP_ERROR") CIB_SIMULATE_ERROR = M("CIB_SIMULATE_ERROR") CIB_UPGRADE_FAILED = M("CIB_UPGRADE_FAILED") CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION = M( "CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION" ) CIB_UPGRADE_SUCCESSFUL = M("CIB_UPGRADE_SUCCESSFUL") CLUSTER_DESTROY_STARTED = M("CLUSTER_DESTROY_STARTED") CLUSTER_DESTROY_SUCCESS = M("CLUSTER_DESTROY_SUCCESS") CLUSTER_ENABLE_STARTED = M("CLUSTER_ENABLE_STARTED") CLUSTER_ENABLE_SUCCESS = M("CLUSTER_ENABLE_SUCCESS") CLUSTER_OPTIONS_METADATA_NOT_SUPPORTED = M( "CLUSTER_OPTIONS_METADATA_NOT_SUPPORTED" ) CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES = M( "CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES" ) CLUSTER_SETUP_SUCCESS = M("CLUSTER_SETUP_SUCCESS") CLUSTER_START_STARTED = M("CLUSTER_START_STARTED") CLUSTER_START_SUCCESS = M("CLUSTER_START_SUCCESS") CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT = M( "CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT" ) CLUSTER_UUID_ALREADY_SET = M("CLUSTER_UUID_ALREADY_SET") CLUSTER_WILL_BE_DESTROYED = M("CLUSTER_WILL_BE_DESTROYED") COMMAND_INVALID_PAYLOAD = M("COMMAND_INVALID_PAYLOAD") COMMAND_UNKNOWN = M("COMMAND_UNKNOWN") CONFIGURED_RESOURCE_MISSING_IN_STATUS = M( "CONFIGURED_RESOURCE_MISSING_IN_STATUS" ) LIVE_ENVIRONMENT_NOT_CONSISTENT = M("LIVE_ENVIRONMENT_NOT_CONSISTENT") LIVE_ENVIRONMENT_REQUIRED = M("LIVE_ENVIRONMENT_REQUIRED") LIVE_ENVIRONMENT_REQUIRED_FOR_LOCAL_NODE = M( "LIVE_ENVIRONMENT_REQUIRED_FOR_LOCAL_NODE" ) COMMAND_ARGUMENT_TYPE_MISMATCH = M("COMMAND_ARGUMENT_TYPE_MISMATCH") COROSYNC_ADDRESS_IP_VERSION_WRONG_FOR_LINK = M( "COROSYNC_ADDRESS_IP_VERSION_WRONG_FOR_LINK" ) COROSYNC_AUTHKEY_WRONG_LENGTH = M("COROSYNC_AUTHKEY_WRONG_LENGTH") COROSYNC_BAD_NODE_ADDRESSES_COUNT = M("COROSYNC_BAD_NODE_ADDRESSES_COUNT") COROSYNC_CLUSTER_NAME_INVALID_FOR_GFS2 = M( "COROSYNC_CLUSTER_NAME_INVALID_FOR_GFS2" ) COROSYNC_CONFIG_ACCEPTED_BY_NODE = M("COROSYNC_CONFIG_ACCEPTED_BY_NODE") COROSYNC_CONFIG_CANNOT_SAVE_INVALID_NAMES_VALUES = M( "COROSYNC_CONFIG_CANNOT_SAVE_INVALID_NAMES_VALUES" ) COROSYNC_CONFIG_DISTRIBUTION_NODE_ERROR = M( "COROSYNC_CONFIG_DISTRIBUTION_NODE_ERROR" ) COROSYNC_CONFIG_DISTRIBUTION_STARTED = M("COROSYNC_CONFIG_DISTRIBUTION_STARTED") COROSYNC_CONFIG_MISSING_IDS_OF_NODES = M("COROSYNC_CONFIG_MISSING_IDS_OF_NODES") COROSYNC_CONFIG_MISSING_NAMES_OF_NODES = M( "COROSYNC_CONFIG_MISSING_NAMES_OF_NODES" ) COROSYNC_CONFIG_NO_NODES_DEFINED = M("COROSYNC_CONFIG_NO_NODES_DEFINED") COROSYNC_CONFIG_RELOADED = M("COROSYNC_CONFIG_RELOADED") COROSYNC_CONFIG_RELOAD_ERROR = M("COROSYNC_CONFIG_RELOAD_ERROR") COROSYNC_CONFIG_RELOAD_NOT_POSSIBLE = M("COROSYNC_CONFIG_RELOAD_NOT_POSSIBLE") COROSYNC_CONFIG_UNSUPPORTED_TRANSPORT = M( "COROSYNC_CONFIG_UNSUPPORTED_TRANSPORT" ) COROSYNC_IP_VERSION_MISMATCH_IN_LINKS = M( "COROSYNC_IP_VERSION_MISMATCH_IN_LINKS" ) COROSYNC_CANNOT_ADD_REMOVE_LINKS_BAD_TRANSPORT = M( "COROSYNC_CANNOT_ADD_REMOVE_LINKS_BAD_TRANSPORT" ) COROSYNC_CANNOT_ADD_REMOVE_LINKS_NO_LINKS_SPECIFIED = M( "COROSYNC_CANNOT_ADD_REMOVE_LINKS_NO_LINKS_SPECIFIED" ) COROSYNC_CANNOT_ADD_REMOVE_LINKS_TOO_MANY_FEW_LINKS = M( "COROSYNC_CANNOT_ADD_REMOVE_LINKS_TOO_MANY_FEW_LINKS" ) COROSYNC_LINK_ALREADY_EXISTS_CANNOT_ADD = M( "COROSYNC_LINK_ALREADY_EXISTS_CANNOT_ADD" ) COROSYNC_LINK_DOES_NOT_EXIST_CANNOT_REMOVE = M( "COROSYNC_LINK_DOES_NOT_EXIST_CANNOT_REMOVE" ) COROSYNC_LINK_DOES_NOT_EXIST_CANNOT_UPDATE = M( "COROSYNC_LINK_DOES_NOT_EXIST_CANNOT_UPDATE" ) COROSYNC_LINK_NUMBER_DUPLICATION = M("COROSYNC_LINK_NUMBER_DUPLICATION") COROSYNC_NODE_ADDRESS_COUNT_MISMATCH = M("COROSYNC_NODE_ADDRESS_COUNT_MISMATCH") COROSYNC_NODE_CONFLICT_CHECK_SKIPPED = M("COROSYNC_NODE_CONFLICT_CHECK_SKIPPED") COROSYNC_NODES_MISSING = M("COROSYNC_NODES_MISSING") COROSYNC_NOT_RUNNING_CHECK_FINISHED_RUNNING = M( "COROSYNC_NOT_RUNNING_CHECK_FINISHED_RUNNING" ) COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR = M( "COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR" ) COROSYNC_NOT_RUNNING_CHECK_NODE_RUNNING = M( "COROSYNC_NOT_RUNNING_CHECK_NODE_RUNNING" ) COROSYNC_NOT_RUNNING_CHECK_NODE_STOPPED = M( "COROSYNC_NOT_RUNNING_CHECK_NODE_STOPPED" ) COROSYNC_NOT_RUNNING_CHECK_STARTED = M("COROSYNC_NOT_RUNNING_CHECK_STARTED") COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE = M( "COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE" ) COROSYNC_QUORUM_ATB_CANNOT_BE_DISABLED_DUE_TO_SBD = M( "COROSYNC_QUORUM_ATB_CANNOT_BE_DISABLED_DUE_TO_SBD" ) COROSYNC_QUORUM_ATB_WILL_BE_ENABLED_DUE_TO_SBD = M( "COROSYNC_QUORUM_ATB_WILL_BE_ENABLED_DUE_TO_SBD" ) COROSYNC_QUORUM_ATB_WILL_BE_ENABLED_DUE_TO_SBD_CLUSTER_IS_RUNNING = M( "COROSYNC_QUORUM_ATB_WILL_BE_ENABLED_DUE_TO_SBD_CLUSTER_IS_RUNNING" ) COROSYNC_QUORUM_GET_STATUS_ERROR = M("COROSYNC_QUORUM_GET_STATUS_ERROR") COROSYNC_QUORUM_HEURISTICS_ENABLED_WITH_NO_EXEC = M( "COROSYNC_QUORUM_HEURISTICS_ENABLED_WITH_NO_EXEC" ) COROSYNC_QUORUM_LOSS_UNABLE_TO_CHECK = M("COROSYNC_QUORUM_LOSS_UNABLE_TO_CHECK") COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR = M( "COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR" ) COROSYNC_QUORUM_WILL_BE_LOST = M("COROSYNC_QUORUM_WILL_BE_LOST") COROSYNC_TOO_MANY_LINKS_OPTIONS = M("COROSYNC_TOO_MANY_LINKS_OPTIONS") COROSYNC_TRANSPORT_UNSUPPORTED_OPTIONS = M( "COROSYNC_TRANSPORT_UNSUPPORTED_OPTIONS" ) CRM_MON_ERROR = M("CRM_MON_ERROR") DEFAULTS_CAN_BE_OVERRIDDEN = M("DEFAULTS_CAN_BE_OVERRIDDEN") DEPRECATED_OPTION = M("DEPRECATED_OPTION") DEPRECATED_OPTION_VALUE = M("DEPRECATED_OPTION_VALUE") DR_CONFIG_ALREADY_EXIST = M("DR_CONFIG_ALREADY_EXIST") DR_CONFIG_DOES_NOT_EXIST = M("DR_CONFIG_DOES_NOT_EXIST") DUPLICATE_CONSTRAINTS_EXIST = M("DUPLICATE_CONSTRAINTS_EXIST") EMPTY_RESOURCE_SET_LIST = M("EMPTY_RESOURCE_SET_LIST") FENCE_HISTORY_COMMAND_ERROR = M("FENCE_HISTORY_COMMAND_ERROR") FENCE_HISTORY_NOT_SUPPORTED = M("FENCE_HISTORY_NOT_SUPPORTED") FILES_DISTRIBUTION_SKIPPED = M("FILES_DISTRIBUTION_SKIPPED") FILES_DISTRIBUTION_STARTED = M("FILES_DISTRIBUTION_STARTED") FILES_REMOVE_FROM_NODES_STARTED = M("FILES_REMOVE_FROM_NODES_STARTED") FILES_REMOVE_FROM_NODES_SKIPPED = M("FILES_REMOVE_FROM_NODES_SKIPPED") FILE_ALREADY_EXISTS = M("FILE_ALREADY_EXISTS") FILE_DISTRIBUTION_ERROR = M("FILE_DISTRIBUTION_ERROR") FILE_DISTRIBUTION_SUCCESS = M("FILE_DISTRIBUTION_SUCCESS") FILE_IO_ERROR = M("FILE_IO_ERROR") FILE_REMOVE_FROM_NODE_ERROR = M("FILE_REMOVE_FROM_NODE_ERROR") FILE_REMOVE_FROM_NODE_SUCCESS = M("FILE_REMOVE_FROM_NODE_SUCCESS") HOST_NOT_FOUND = M("HOST_NOT_FOUND") HOST_ALREADY_AUTHORIZED = M("HOST_ALREADY_AUTHORIZED") HOST_ALREADY_IN_CLUSTER_CONFIG = M("HOST_ALREADY_IN_CLUSTER_CONFIG") HOST_ALREADY_IN_CLUSTER_SERVICES = M("HOST_ALREADY_IN_CLUSTER_SERVICES") ID_ALREADY_EXISTS = M("ID_ALREADY_EXISTS") ID_BELONGS_TO_UNEXPECTED_TYPE = M("ID_BELONGS_TO_UNEXPECTED_TYPE") ID_NOT_FOUND = M("ID_NOT_FOUND") INVALID_CIB_CONTENT = M("INVALID_CIB_CONTENT") INVALID_ID_BAD_CHAR = M("INVALID_ID_BAD_CHAR") INVALID_ID_IS_EMPTY = M("INVALID_ID_IS_EMPTY") INVALID_ID_TYPE = M("INVALID_ID_TYPE") INVALID_OPTIONS = M("INVALID_OPTIONS") INVALID_USERDEFINED_OPTIONS = M("INVALID_USERDEFINED_OPTIONS") INVALID_OPTION_TYPE = M("INVALID_OPTION_TYPE") INVALID_OPTION_VALUE = M("INVALID_OPTION_VALUE") INVALID_RESOURCE_AGENT_NAME = M("INVALID_RESOURCE_AGENT_NAME") INVALID_RESPONSE_FORMAT = M("INVALID_RESPONSE_FORMAT") INVALID_SCORE = M("INVALID_SCORE") INVALID_STONITH_AGENT_NAME = M("INVALID_STONITH_AGENT_NAME") INVALID_TIMEOUT_VALUE = M("INVALID_TIMEOUT_VALUE") MULTIPLE_RESULTS_FOUND = M("MULTIPLE_RESULTS_FOUND") MUTUALLY_EXCLUSIVE_OPTIONS = M("MUTUALLY_EXCLUSIVE_OPTIONS") NO_ACTION_NECESSARY = M("NO_ACTION_NECESSARY") NODE_ADDRESSES_ALREADY_EXIST = M("NODE_ADDRESSES_ALREADY_EXIST") NODE_ADDRESSES_CANNOT_BE_EMPTY = M("NODE_ADDRESSES_CANNOT_BE_EMPTY") NODE_ADDRESSES_DUPLICATION = M("NODE_ADDRESSES_DUPLICATION") NODE_ADDRESSES_UNRESOLVABLE = M("NODE_ADDRESSES_UNRESOLVABLE") NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL = M( "NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL" ) NODE_COMMUNICATION_DEBUG_INFO = M("NODE_COMMUNICATION_DEBUG_INFO") NODE_COMMUNICATION_ERROR = M("NODE_COMMUNICATION_ERROR") NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED = M( "NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED" ) NODE_COMMUNICATION_ERROR_PERMISSION_DENIED = M( "NODE_COMMUNICATION_ERROR_PERMISSION_DENIED" ) NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT = M( "NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT" ) NODE_COMMUNICATION_ERROR_UNSUPPORTED_COMMAND = M( "NODE_COMMUNICATION_ERROR_UNSUPPORTED_COMMAND" ) NODE_COMMUNICATION_ERROR_TIMED_OUT = M("NODE_COMMUNICATION_ERROR_TIMED_OUT") NODE_COMMUNICATION_FINISHED = M("NODE_COMMUNICATION_FINISHED") NODE_COMMUNICATION_NOT_CONNECTED = M("NODE_COMMUNICATION_NOT_CONNECTED") NODE_COMMUNICATION_NO_MORE_ADDRESSES = M("NODE_COMMUNICATION_NO_MORE_ADDRESSES") NODE_COMMUNICATION_PROXY_IS_SET = M("NODE_COMMUNICATION_PROXY_IS_SET") NODE_COMMUNICATION_RETRYING = M("NODE_COMMUNICATION_RETRYING") NODE_COMMUNICATION_STARTED = M("NODE_COMMUNICATION_STARTED") NODE_NAMES_ALREADY_EXIST = M("NODE_NAMES_ALREADY_EXIST") NODE_NAMES_DUPLICATION = M("NODE_NAMES_DUPLICATION") NODE_NOT_FOUND = M("NODE_NOT_FOUND") NODE_REMOVE_IN_PACEMAKER_FAILED = M("NODE_REMOVE_IN_PACEMAKER_FAILED") NONE_HOST_FOUND = M("NONE_HOST_FOUND") NODE_USED_AS_TIE_BREAKER = M("NODE_USED_AS_TIE_BREAKER") NODES_TO_REMOVE_UNREACHABLE = M("NODES_TO_REMOVE_UNREACHABLE") NODE_TO_CLEAR_IS_STILL_IN_CLUSTER = M("NODE_TO_CLEAR_IS_STILL_IN_CLUSTER") NODE_IN_LOCAL_CLUSTER = M("NODE_IN_LOCAL_CLUSTER") NOT_AUTHORIZED = M("NOT_AUTHORIZED") OBJECT_WITH_ID_IN_UNEXPECTED_CONTEXT = M("OBJECT_WITH_ID_IN_UNEXPECTED_CONTEXT") OMITTING_NODE = M("OMITTING_NODE") PACEMAKER_SIMULATION_RESULT = M("PACEMAKER_SIMULATION_RESULT") PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND = M("PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND") PARSE_ERROR_COROSYNC_CONF = M("PARSE_ERROR_COROSYNC_CONF") PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_AFTER_OPENING_BRACE = M( "PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_AFTER_OPENING_BRACE" ) PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_BEFORE_OR_AFTER_CLOSING_BRACE = M( "PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_BEFORE_OR_AFTER_CLOSING_BRACE" ) PARSE_ERROR_COROSYNC_CONF_LINE_IS_NOT_SECTION_NOR_KEY_VALUE = M( "PARSE_ERROR_COROSYNC_CONF_LINE_IS_NOT_SECTION_NOR_KEY_VALUE" ) PARSE_ERROR_COROSYNC_CONF_MISSING_CLOSING_BRACE = M( "PARSE_ERROR_COROSYNC_CONF_MISSING_CLOSING_BRACE" ) PARSE_ERROR_COROSYNC_CONF_MISSING_SECTION_NAME_BEFORE_OPENING_BRACE = M( "PARSE_ERROR_COROSYNC_CONF_MISSING_SECTION_NAME_BEFORE_OPENING_BRACE" ) PARSE_ERROR_COROSYNC_CONF_UNEXPECTED_CLOSING_BRACE = M( "PARSE_ERROR_COROSYNC_CONF_UNEXPECTED_CLOSING_BRACE" ) PARSE_ERROR_JSON_FILE = M("PARSE_ERROR_JSON_FILE") PCSD_VERSION_TOO_OLD = M("PCSD_VERSION_TOO_OLD") PCSD_SSL_CERT_AND_KEY_DISTRIBUTION_STARTED = M( "PCSD_SSL_CERT_AND_KEY_DISTRIBUTION_STARTED" ) PCSD_SSL_CERT_AND_KEY_SET_SUCCESS = M("PCSD_SSL_CERT_AND_KEY_SET_SUCCESS") PREREQUISITE_OPTION_MUST_BE_ENABLED_AS_WELL = M( "PREREQUISITE_OPTION_MUST_BE_ENABLED_AS_WELL" ) PREREQUISITE_OPTION_MUST_BE_DISABLED = M("PREREQUISITE_OPTION_MUST_BE_DISABLED") PREREQUISITE_OPTION_MUST_NOT_BE_SET = M("PREREQUISITE_OPTION_MUST_NOT_BE_SET") PREREQUISITE_OPTION_IS_MISSING = M("PREREQUISITE_OPTION_IS_MISSING") QDEVICE_ALREADY_DEFINED = M("QDEVICE_ALREADY_DEFINED") QDEVICE_ALREADY_INITIALIZED = M("QDEVICE_ALREADY_INITIALIZED") QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE = M("QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE") QDEVICE_CERTIFICATE_BAD_FORMAT = M("QDEVICE_CERTIFICATE_BAD_FORMAT") QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED = M( "QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED" ) QDEVICE_CERTIFICATE_IMPORT_ERROR = M("QDEVICE_CERTIFICATE_IMPORT_ERROR") QDEVICE_CERTIFICATE_READ_ERROR = M("QDEVICE_CERTIFICATE_READ_ERROR") QDEVICE_CERTIFICATE_REMOVAL_STARTED = M("QDEVICE_CERTIFICATE_REMOVAL_STARTED") QDEVICE_CERTIFICATE_REMOVED_FROM_NODE = M( "QDEVICE_CERTIFICATE_REMOVED_FROM_NODE" ) QDEVICE_CERTIFICATE_SIGN_ERROR = M("QDEVICE_CERTIFICATE_SIGN_ERROR") QDEVICE_DESTROY_ERROR = M("QDEVICE_DESTROY_ERROR") QDEVICE_DESTROY_SUCCESS = M("QDEVICE_DESTROY_SUCCESS") QDEVICE_GET_STATUS_ERROR = M("QDEVICE_GET_STATUS_ERROR") QDEVICE_INITIALIZATION_ERROR = M("QDEVICE_INITIALIZATION_ERROR") QDEVICE_INITIALIZATION_SUCCESS = M("QDEVICE_INITIALIZATION_SUCCESS") QDEVICE_NOT_DEFINED = M("QDEVICE_NOT_DEFINED") QDEVICE_NOT_INITIALIZED = M("QDEVICE_NOT_INITIALIZED") QDEVICE_NOT_RUNNING = M("QDEVICE_NOT_RUNNING") QDEVICE_CLIENT_RELOAD_STARTED = M("QDEVICE_CLIENT_RELOAD_STARTED") QDEVICE_REMOVE_OR_CLUSTER_STOP_NEEDED = M( "QDEVICE_REMOVE_OR_CLUSTER_STOP_NEEDED" ) QDEVICE_USED_BY_CLUSTERS = M("QDEVICE_USED_BY_CLUSTERS") CLONING_STONITH_RESOURCES_HAS_NO_EFFECT = M( "CLONING_STONITH_RESOURCES_HAS_NO_EFFECT" ) REQUIRED_OPTIONS_ARE_MISSING = M("REQUIRED_OPTIONS_ARE_MISSING") REQUIRED_OPTION_OF_ALTERNATIVES_IS_MISSING = M( "REQUIRED_OPTION_OF_ALTERNATIVES_IS_MISSING" ) RESOURCE_BAN_PCMK_ERROR = M("RESOURCE_BAN_PCMK_ERROR") RESOURCE_BAN_PCMK_SUCCESS = M("RESOURCE_BAN_PCMK_SUCCESS") RESOURCE_BUNDLE_ALREADY_CONTAINS_A_RESOURCE = M( "RESOURCE_BUNDLE_ALREADY_CONTAINS_A_RESOURCE" ) RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE = M( "RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE" ) RESOURCE_CLEANUP_ERROR = M("RESOURCE_CLEANUP_ERROR") RESOURCE_CLONE_INCOMPATIBLE_META_ATTRIBUTES = M( "RESOURCE_CLONE_INCOMPATIBLE_META_ATTRIBUTES" ) RESOURCE_DOES_NOT_RUN = M("RESOURCE_DOES_NOT_RUN") RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES = M( "RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES" ) RESOURCE_FOR_CONSTRAINT_IS_MULTIINSTANCE = M( "RESOURCE_FOR_CONSTRAINT_IS_MULTIINSTANCE" ) RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE = M("RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE") RESOURCE_INSTANCE_ATTR_VALUE_NOT_UNIQUE = M( "RESOURCE_INSTANCE_ATTR_VALUE_NOT_UNIQUE" ) RESOURCE_INSTANCE_ATTR_GROUP_VALUE_NOT_UNIQUE = M( "RESOURCE_INSTANCE_ATTR_GROUP_VALUE_NOT_UNIQUE" ) RESOURCE_IS_GUEST_NODE_ALREADY = M("RESOURCE_IS_GUEST_NODE_ALREADY") RESOURCE_IS_UNMANAGED = M("RESOURCE_IS_UNMANAGED") RESOURCE_MANAGED_NO_MONITOR_ENABLED = M("RESOURCE_MANAGED_NO_MONITOR_ENABLED") RESOURCE_MOVE_PCMK_ERROR = M("RESOURCE_MOVE_PCMK_ERROR") RESOURCE_MOVE_PCMK_SUCCESS = M("RESOURCE_MOVE_PCMK_SUCCESS") RESOURCE_OPERATION_INTERVAL_ADAPTED = M("RESOURCE_OPERATION_INTERVAL_ADAPTED") RESOURCE_OPERATION_INTERVAL_DUPLICATION = M( "RESOURCE_OPERATION_INTERVAL_DUPLICATION" ) RESOURCE_REFRESH_ERROR = M("RESOURCE_REFRESH_ERROR") RESOURCE_REFRESH_TOO_TIME_CONSUMING = M("RESOURCE_REFRESH_TOO_TIME_CONSUMING") RESOURCE_RESTART_ERROR = M("RESOURCE_RESTART_ERROR") RESOURCE_RESTART_NODE_IS_FOR_MULTIINSTANCE_ONLY = M( "RESOURCE_RESTART_NODE_IS_FOR_MULTIINSTANCE_ONLY" ) RESOURCE_RESTART_USING_PARENT_RESOURCE = M( "RESOURCE_RESTART_USING_PARENT_RESOURCE" ) RESOURCE_RUNNING_ON_NODES = M("RESOURCE_RUNNING_ON_NODES") RESOURCE_UNMOVE_UNBAN_PCMK_ERROR = M("RESOURCE_UNMOVE_UNBAN_PCMK_ERROR") RESOURCE_UNMOVE_UNBAN_PCMK_SUCCESS = M("RESOURCE_UNMOVE_UNBAN_PCMK_SUCCESS") RESOURCE_UNMOVE_UNBAN_PCMK_EXPIRED_NOT_SUPPORTED = M( "RESOURCE_UNMOVE_UNBAN_PCMK_EXPIRED_NOT_SUPPORTED" ) RESOURCE_MAY_OR_MAY_NOT_MOVE = M("RESOURCE_MAY_OR_MAY_NOT_MOVE") RESOURCE_MOVE_CONSTRAINT_CREATED = M("RESOURCE_MOVE_CONSTRAINT_CREATED") RESOURCE_MOVE_CONSTRAINT_REMOVED = M("RESOURCE_MOVE_CONSTRAINT_REMOVED") RESOURCE_MOVE_NOT_AFFECTING_RESOURCE = M("RESOURCE_MOVE_NOT_AFFECTING_RESOURCE") RESOURCE_MOVE_AFFECTS_OTRHER_RESOURCES = M( "RESOURCE_MOVE_AFFECTS_OTRHER_RESOURCES" ) RESOURCE_MOVE_AUTOCLEAN_SIMULATION_FAILURE = M( "RESOURCE_MOVE_AUTOCLEAN_SIMULATION_FAILURE" ) RESOURCE_WAIT_DEPRECATED = M("RESOURCE_WAIT_DEPRECATED") RULE_IN_EFFECT_STATUS_DETECTION_NOT_SUPPORTED = M( "RULE_IN_EFFECT_STATUS_DETECTION_NOT_SUPPORTED" ) RULE_EXPRESSION_NOT_ALLOWED = M("RULE_EXPRESSION_NOT_ALLOWED") RULE_EXPRESSION_OPTIONS_DUPLICATION = M("RULE_EXPRESSION_OPTIONS_DUPLICATION") RULE_EXPRESSION_PARSE_ERROR = M("RULE_EXPRESSION_PARSE_ERROR") RULE_EXPRESSION_SINCE_GREATER_THAN_UNTIL = M( "RULE_EXPRESSION_SINCE_GREATER_THAN_UNTIL" ) RULE_NO_EXPRESSION_SPECIFIED = M("RULE_NO_EXPRESSION_SPECIFIED") RUN_EXTERNAL_PROCESS_ERROR = M("RUN_EXTERNAL_PROCESS_ERROR") RUN_EXTERNAL_PROCESS_FINISHED = M("RUN_EXTERNAL_PROCESS_FINISHED") RUN_EXTERNAL_PROCESS_STARTED = M("RUN_EXTERNAL_PROCESS_STARTED") SBD_CHECK_STARTED = M("SBD_CHECK_STARTED") SBD_CHECK_SUCCESS = M("SBD_CHECK_SUCCESS") SBD_CONFIG_ACCEPTED_BY_NODE = M("SBD_CONFIG_ACCEPTED_BY_NODE") SBD_CONFIG_DISTRIBUTION_STARTED = M("SBD_CONFIG_DISTRIBUTION_STARTED") SBD_DEVICE_DOES_NOT_EXIST = M("SBD_DEVICE_DOES_NOT_EXIST") SBD_DEVICE_DUMP_ERROR = M("SBD_DEVICE_DUMP_ERROR") SBD_DEVICE_INITIALIZATION_ERROR = M("SBD_DEVICE_INITIALIZATION_ERROR") SBD_DEVICE_INITIALIZATION_STARTED = M("SBD_DEVICE_INITIALIZATION_STARTED") SBD_DEVICE_INITIALIZATION_SUCCESS = M("SBD_DEVICE_INITIALIZATION_SUCCESS") SBD_DEVICE_IS_NOT_BLOCK_DEVICE = M("SBD_DEVICE_IS_NOT_BLOCK_DEVICE") SBD_DEVICE_LIST_ERROR = M("SBD_DEVICE_LIST_ERROR") SBD_DEVICE_MESSAGE_ERROR = M("SBD_DEVICE_MESSAGE_ERROR") SBD_DEVICE_PATH_NOT_ABSOLUTE = M("SBD_DEVICE_PATH_NOT_ABSOLUTE") SBD_LIST_WATCHDOG_ERROR = M("SBD_LIST_WATCHDOG_ERROR") SBD_NO_DEVICE_FOR_NODE = M("SBD_NO_DEVICE_FOR_NODE") SBD_NOT_USED_CANNOT_SET_SBD_OPTIONS = M("SBD_NOT_USED_CANNOT_SET_SBD_OPTIONS") SBD_TOO_MANY_DEVICES_FOR_NODE = M("SBD_TOO_MANY_DEVICES_FOR_NODE") SBD_WITH_DEVICES_NOT_USED_CANNOT_SET_DEVICE = M( "SBD_WITH_DEVICES_NOT_USED_CANNOT_SET_DEVICE" ) SBD_WATCHDOG_NOT_SUPPORTED = M("SBD_WATCHDOG_NOT_SUPPORTED") SBD_WATCHDOG_VALIDATION_INACTIVE = M("SBD_WATCHDOG_VALIDATION_INACTIVE") SBD_WATCHDOG_TEST_ERROR = M("SBD_WATCHDOG_TEST_ERROR") SBD_WATCHDOG_TEST_MULTIPLE_DEVICES = M("SBD_WATCHDOG_TEST_MULTIPLE_DEVICES") SBD_WATCHDOG_TEST_FAILED = M("SBD_WATCHDOG_TEST_FAILED") SERVICE_ACTION_STARTED = M("SERVICE_ACTION_STARTED") SERVICE_ACTION_FAILED = M("SERVICE_ACTION_FAILED") SERVICE_ACTION_SUCCEEDED = M("SERVICE_ACTION_SUCCEEDED") SERVICE_ACTION_SKIPPED = M("SERVICE_ACTION_SKIPPED") SERVICE_NOT_INSTALLED = M("SERVICE_NOT_INSTALLED") SERVICE_VERSION_MISMATCH = M("SERVICE_VERSION_MISMATCH") UNABLE_TO_GET_RESOURCE_OPERATION_DIGESTS = M( "UNABLE_TO_GET_RESOURCE_OPERATION_DIGESTS" ) STONITH_WATCHDOG_TIMEOUT_CANNOT_BE_SET = M( "STONITH_WATCHDOG_TIMEOUT_CANNOT_BE_SET" ) STONITH_WATCHDOG_TIMEOUT_CANNOT_BE_UNSET = M( "STONITH_WATCHDOG_TIMEOUT_CANNOT_BE_UNSET" ) STONITH_WATCHDOG_TIMEOUT_TOO_SMALL = M("STONITH_WATCHDOG_TIMEOUT_TOO_SMALL") STONITH_RESOURCES_DO_NOT_EXIST = M("STONITH_RESOURCES_DO_NOT_EXIST") STONITH_RESTARTLESS_UPDATE_MISSING_MPATH_KEYS = M( "STONITH_RESTARTLESS_UPDATE_MISSING_MPATH_KEYS" ) STONITH_RESTARTLESS_UPDATE_OF_SCSI_DEVICES_NOT_SUPPORTED = M( "STONITH_RESTARTLESS_UPDATE_OF_SCSI_DEVICES_NOT_SUPPORTED" ) STONITH_RESTARTLESS_UPDATE_UNSUPPORTED_AGENT = M( "STONITH_RESTARTLESS_UPDATE_UNSUPPORTED_AGENT" ) STONITH_UNFENCING_FAILED = M("STONITH_UNFENCING_FAILED") STONITH_UNFENCING_DEVICE_STATUS_FAILED = M( "STONITH_UNFENCING_DEVICE_STATUS_FAILED" ) STONITH_UNFENCING_SKIPPED_DEVICES_FENCED = M( "STONITH_UNFENCING_SKIPPED_DEVICES_FENCED" ) STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM = M( "STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM" ) SERVICE_COMMANDS_ON_NODES_STARTED = M("SERVICE_COMMANDS_ON_NODES_STARTED") SERVICE_COMMANDS_ON_NODES_SKIPPED = M("SERVICE_COMMANDS_ON_NODES_SKIPPED") SERVICE_COMMAND_ON_NODE_ERROR = M("SERVICE_COMMAND_ON_NODE_ERROR") SERVICE_COMMAND_ON_NODE_SUCCESS = M("SERVICE_COMMAND_ON_NODE_SUCCESS") SERVICE_UNABLE_TO_DETECT_INIT_SYSTEM = M("SERVICE_UNABLE_TO_DETECT_INIT_SYSTEM") SYSTEM_WILL_RESET = M("SYSTEM_WILL_RESET") # TODO: remove, use ADD_REMOVE reports TAG_ADD_REMOVE_IDS_DUPLICATION = M("TAG_ADD_REMOVE_IDS_DUPLICATION") # TODO: remove, use ADD_REMOVE reports TAG_ADJACENT_REFERENCE_ID_NOT_IN_THE_TAG = M( "TAG_ADJACENT_REFERENCE_ID_NOT_IN_THE_TAG" ) # TODO: remove, use ADD_REMOVE reports TAG_CANNOT_ADD_AND_REMOVE_IDS_AT_THE_SAME_TIME = M( "TAG_CANNOT_ADD_AND_REMOVE_IDS_AT_THE_SAME_TIME" ) # TODO: remove, use ADD_REMOVE reports TAG_CANNOT_ADD_REFERENCE_IDS_ALREADY_IN_THE_TAG = M( "TAG_CANNOT_ADD_REFERENCE_IDS_ALREADY_IN_THE_TAG" ) TAG_CANNOT_CONTAIN_ITSELF = M("TAG_CANNOT_CONTAIN_ITSELF") TAG_CANNOT_CREATE_EMPTY_TAG_NO_IDS_SPECIFIED = M( "TAG_CANNOT_CREATE_EMPTY_TAG_NO_IDS_SPECIFIED" ) # TODO: remove, use ADD_REMOVE reports TAG_CANNOT_PUT_ID_NEXT_TO_ITSELF = M("TAG_CANNOT_PUT_ID_NEXT_TO_ITSELF") # TODO: remove, use ADD_REMOVE reports TAG_CANNOT_REMOVE_ADJACENT_ID = M("TAG_CANNOT_REMOVE_ADJACENT_ID") # TODO: remove, use ADD_REMOVE reports TAG_CANNOT_REMOVE_REFERENCES_WITHOUT_REMOVING_TAG = M( "TAG_CANNOT_REMOVE_REFERENCES_WITHOUT_REMOVING_TAG" ) TAG_CANNOT_REMOVE_TAG_REFERENCED_IN_CONSTRAINTS = M( "TAG_CANNOT_REMOVE_TAG_REFERENCED_IN_CONSTRAINTS" ) TAG_CANNOT_REMOVE_TAGS_NO_TAGS_SPECIFIED = M( "TAG_CANNOT_REMOVE_TAGS_NO_TAGS_SPECIFIED" ) # TODO: remove, use ADD_REMOVE reports TAG_CANNOT_SPECIFY_ADJACENT_ID_WITHOUT_IDS_TO_ADD = M( "TAG_CANNOT_SPECIFY_ADJACENT_ID_WITHOUT_IDS_TO_ADD" ) # TODO: remove, use ADD_REMOVE reports TAG_CANNOT_UPDATE_TAG_NO_IDS_SPECIFIED = M( "TAG_CANNOT_UPDATE_TAG_NO_IDS_SPECIFIED" ) # TODO: remove, use ADD_REMOVE reports TAG_IDS_NOT_IN_THE_TAG = M("TAG_IDS_NOT_IN_THE_TAG") TMP_FILE_WRITE = M("TMP_FILE_WRITE") UNABLE_TO_CONNECT_TO_ANY_REMAINING_NODE = M( "UNABLE_TO_CONNECT_TO_ANY_REMAINING_NODE" ) UNABLE_TO_CONNECT_TO_ALL_REMAINING_NODE = M( "UNABLE_TO_CONNECT_TO_ALL_REMAINING_NODE" ) UNABLE_TO_GET_AGENT_METADATA = M("UNABLE_TO_GET_AGENT_METADATA") UNABLE_TO_GET_SBD_CONFIG = M("UNABLE_TO_GET_SBD_CONFIG") UNABLE_TO_GET_SBD_STATUS = M("UNABLE_TO_GET_SBD_STATUS") UNABLE_TO_PERFORM_OPERATION_ON_ANY_NODE = M( "UNABLE_TO_PERFORM_OPERATION_ON_ANY_NODE" ) WATCHDOG_INVALID = M("WATCHDOG_INVALID") UNSUPPORTED_OPERATION_ON_NON_SYSTEMD_SYSTEMS = M( "UNSUPPORTED_OPERATION_ON_NON_SYSTEMD_SYSTEMS" ) USE_COMMAND_NODE_ADD_REMOTE = M("USE_COMMAND_NODE_ADD_REMOTE") USE_COMMAND_NODE_ADD_GUEST = M("USE_COMMAND_NODE_ADD_GUEST") USE_COMMAND_NODE_REMOVE_REMOTE = M("USE_COMMAND_NODE_REMOVE_REMOTE") USE_COMMAND_NODE_REMOVE_GUEST = M("USE_COMMAND_NODE_REMOVE_GUEST") USING_DEFAULT_ADDRESS_FOR_HOST = M("USING_DEFAULT_ADDRESS_FOR_HOST") USING_DEFAULT_WATCHDOG = M("USING_DEFAULT_WATCHDOG") WAIT_FOR_IDLE_STARTED = M("WAIT_FOR_IDLE_STARTED") WAIT_FOR_IDLE_ERROR = M("WAIT_FOR_IDLE_ERROR") WAIT_FOR_IDLE_NOT_LIVE_CLUSTER = M("WAIT_FOR_IDLE_NOT_LIVE_CLUSTER") WAIT_FOR_IDLE_TIMED_OUT = M("WAIT_FOR_IDLE_TIMED_OUT") WAIT_FOR_NODE_STARTUP_ERROR = M("WAIT_FOR_NODE_STARTUP_ERROR") WAIT_FOR_NODE_STARTUP_STARTED = M("WAIT_FOR_NODE_STARTUP_STARTED") WAIT_FOR_NODE_STARTUP_TIMED_OUT = M("WAIT_FOR_NODE_STARTUP_TIMED_OUT") WAIT_FOR_NODE_STARTUP_WITHOUT_START = M("WAIT_FOR_NODE_STARTUP_WITHOUT_START") WATCHDOG_NOT_FOUND = M("WATCHDOG_NOT_FOUND") pcs-0.12.0.2/pcs/common/reports/const.py000066400000000000000000000046451500417470700200100ustar00rootroot00000000000000from .types import ( AddRemoveContainerType, AddRemoveItemType, BoothConfigUsedWhere, DefaultAddressSource, FenceHistoryCommandType, PcsCommand, ReasonType, ServiceAction, StonithRestartlessUpdateUnableToPerformReason, StonithWatchdogTimeoutCannotBeSetReason, ) ADD_REMOVE_CONTAINER_TYPE_PROPERTY_SET = AddRemoveContainerType("property_set") ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE = AddRemoveContainerType("stonith") ADD_REMOVE_CONTAINER_TYPE_GROUP = AddRemoveContainerType("group") ADD_REMOVE_ITEM_TYPE_DEVICE = AddRemoveItemType("device") ADD_REMOVE_ITEM_TYPE_PROPERTY = AddRemoveItemType("property") ADD_REMOVE_ITEM_TYPE_RESOURCE = AddRemoveItemType("resource") BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE = BoothConfigUsedWhere( "in a cluster resource" ) BOOTH_CONFIG_USED_ENABLED_IN_SYSTEMD = BoothConfigUsedWhere( "enabled in systemd" ) BOOTH_CONFIG_USED_RUNNING_IN_SYSTEMD = BoothConfigUsedWhere( "running in systemd" ) FENCE_HISTORY_COMMAND_CLEANUP = FenceHistoryCommandType("cleanup") FENCE_HISTORY_COMMAND_SHOW = FenceHistoryCommandType("show") FENCE_HISTORY_COMMAND_UPDATE = FenceHistoryCommandType("update") PCS_COMMAND_OPERATION_DEFAULTS_UPDATE = PcsCommand( "resource op defaults update" ) PCS_COMMAND_RESOURCE_CREATE = PcsCommand("resource create") PCS_COMMAND_RESOURCE_DEFAULTS_UPDATE = PcsCommand("resource defaults update") PCS_COMMAND_STONITH_CREATE = PcsCommand("stonith create") SERVICE_ACTION_START = ServiceAction("START") SERVICE_ACTION_STOP = ServiceAction("STOP") SERVICE_ACTION_ENABLE = ServiceAction("ENABLE") SERVICE_ACTION_DISABLE = ServiceAction("DISABLE") SERVICE_ACTION_KILL = ServiceAction("KILL") REASON_UNREACHABLE = ReasonType("unreachable") REASON_NOT_LIVE_CIB = ReasonType("not_live_cib") DEFAULT_ADDRESS_SOURCE_KNOWN_HOSTS = DefaultAddressSource("known_hosts") DEFAULT_ADDRESS_SOURCE_HOST_NAME = DefaultAddressSource("host_name") SBD_NOT_SET_UP = StonithWatchdogTimeoutCannotBeSetReason("sbd_not_set_up") SBD_SET_UP_WITH_DEVICES = StonithWatchdogTimeoutCannotBeSetReason( "sbd_set_up_with_devices" ) SBD_SET_UP_WITHOUT_DEVICES = StonithWatchdogTimeoutCannotBeSetReason( "sbd_set_up_without_devices" ) STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_NOT_RUNNING = ( StonithRestartlessUpdateUnableToPerformReason("not_running") ) STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER = ( StonithRestartlessUpdateUnableToPerformReason("other") ) pcs-0.12.0.2/pcs/common/reports/conversions.py000066400000000000000000000026531500417470700212270ustar00rootroot00000000000000from typing import ( Dict, Optional, ) from pcs.common.tools import get_all_subclasses from . import messages from .dto import ( ReportItemDto, ReportItemMessageDto, ) from .item import ( ReportItem, ReportItemContext, ReportItemSeverity, ) def report_dto_to_item( dto_obj: ReportItemDto, context: Optional[ReportItemContext] = None, ) -> ReportItem: return ReportItem( severity=ReportItemSeverity.from_dto(dto_obj.severity), message=report_item_msg_from_dto(dto_obj.message), context=( context if context else ( ReportItemContext.from_dto(dto_obj.context) if dto_obj.context else None ) ), ) def _create_report_msg_map() -> Dict[str, type]: result: Dict[str, type] = {} for report_msg_cls in get_all_subclasses(messages.ReportItemMessage): code = report_msg_cls._code # pylint: disable=protected-access if code: if code in result: raise AssertionError() result[code] = report_msg_cls return result REPORT_MSG_MAP = _create_report_msg_map() def report_item_msg_from_dto( obj: ReportItemMessageDto, ) -> messages.ReportItemMessage: try: return REPORT_MSG_MAP[obj.code](**obj.payload) except KeyError: return messages.LegacyCommonMessage(obj.code, obj.payload, obj.message) pcs-0.12.0.2/pcs/common/reports/deprecated_codes.py000066400000000000000000000060631500417470700221330ustar00rootroot00000000000000from .types import DeprecatedMessageCode as _D # DEPRECATED REPORT CODES # Move the unused report codes to the top of this file and remove # the ReportItemMessage and CliReportMessageCustom definitions with associated # report building tests # Comment structure: # Removed after [, unused after ] # Use known version number of last release. "Removed" means when was the report # code deprecated. "Unused" means that the report is no longer produced by PCS # but the report code and message are still defined. # Optionally, if the report is being replaced by a different report, a comment # with the new report code can be added # Removed after 0.11.6 CANNOT_MOVE_RESOURCE_BUNDLE = _D("CANNOT_MOVE_RESOURCE_BUNDLE") CANNOT_MOVE_RESOURCE_CLONE = _D("CANNOT_MOVE_RESOURCE_CLONE") # Removed after 0.11.3 SBD_NOT_INSTALLED = _D("SBD_NOT_INSTALLED") UNABLE_TO_DETERMINE_USER_UID = _D("UNABLE_TO_DETERMINE_USER_UID") UNABLE_TO_DETERMINE_GROUP_GID = _D("UNABLE_TO_DETERMINE_GROUP_GID") # Removed after 0.11.3, unused after 0.11.2 # Replaced by DEFAULTS_CAN_BE_OVERRIDDEN # Fixed a typo in report code DEFAULTS_CAN_BE_OVERRIDEN = _D("DEFAULTS_CAN_BE_OVERRIDEN") # Removed after 0.11.3, unused after 0.10.10 # Replaced by ADD_REMOVE_* reports from new add/remove validator CANNOT_GROUP_RESOURCE_ADJACENT_RESOURCE_FOR_NEW_GROUP = _D( "CANNOT_GROUP_RESOURCE_ADJACENT_RESOURCE_FOR_NEW_GROUP" ) CANNOT_GROUP_RESOURCE_ADJACENT_RESOURCE_NOT_IN_GROUP = _D( "CANNOT_GROUP_RESOURCE_ADJACENT_RESOURCE_NOT_IN_GROUP" ) CANNOT_GROUP_RESOURCE_ALREADY_IN_THE_GROUP = _D( "CANNOT_GROUP_RESOURCE_ALREADY_IN_THE_GROUP" ) CANNOT_GROUP_RESOURCE_MORE_THAN_ONCE = _D( "CANNOT_GROUP_RESOURCE_MORE_THAN_ONCE" ) CANNOT_GROUP_RESOURCE_NO_RESOURCES = _D("CANNOT_GROUP_RESOURCE_NO_RESOURCES") CANNOT_GROUP_RESOURCE_NEXT_TO_ITSELF = _D( "CANNOT_GROUP_RESOURCE_NEXT_TO_ITSELF" ) # Removed after 0.11.3, unused after 0.10.8 # Produced only with Pacemaker 1.x MULTIPLE_SCORE_OPTIONS = _D("MULTIPLE_SCORE_OPTIONS") # Removed after 0.11.6, unused after 0.11.6 # Replaced by COROSYNC_NOT_RUNNING_CHECK_NODE_STOPPED # and COROSYNC_NOT_RUNNING_CHECK_NODE_RUNNING # These reports were replaced as they were to generic (both the codes and the # messages) and thus didn't convey the required specific information and use # case COROSYNC_NOT_RUNNING_ON_NODE = _D("COROSYNC_NOT_RUNNING_ON_NODE") COROSYNC_RUNNING_ON_NODE = _D("COROSYNC_RUNNING_ON_NODE") # Removed after 0.11.7, unused after 0.11.7 # Replaced by DUPLICATE_CONSTRAINTS_EXIST and # pcs.cli.reports.preprocessor.get_duplicate_constraint_exists_preprocessor DUPLICATE_CONSTRAINTS_LIST = _D("DUPLICATE_CONSTRAINTS_LIST") # Removed in the first pcs-0.12.x version # Replaced by COMMAND_MISMATCH_ARGUMENT_TYPE RESOURCE_STONITH_COMMANDS_MISMATCH = _D("RESOURCE_STONITH_COMMANDS_MISMATCH") # Removed in the first pcs-0.12.x version # Pacemaker 3 no longer allows several rules in one constraint. Related code # was removed from pcs and the report is no longer produced. CANNOT_ADD_RULE_TO_CONSTRAINT_WRONG_TYPE = _D( "CANNOT_ADD_RULE_TO_CONSTRAINT_WRONG_TYPE" ) pcs-0.12.0.2/pcs/common/reports/dto.py000066400000000000000000000014061500417470700174400ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Any, Mapping, Optional, ) from pcs.common.interface.dto import DataTransferObject from .types import ( ForceCode, MessageCode, SeverityLevel, ) @dataclass(frozen=True) class ReportItemSeverityDto(DataTransferObject): level: SeverityLevel force_code: Optional[ForceCode] @dataclass(frozen=True) class ReportItemMessageDto(DataTransferObject): code: MessageCode message: str payload: Mapping[str, Any] @dataclass(frozen=True) class ReportItemContextDto(DataTransferObject): node: str @dataclass(frozen=True) class ReportItemDto(DataTransferObject): severity: ReportItemSeverityDto message: ReportItemMessageDto context: Optional[ReportItemContextDto] pcs-0.12.0.2/pcs/common/reports/item.py000066400000000000000000000123161500417470700176120ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Any, Dict, List, Optional, ) from pcs.common.interface.dto import ( ImplementsFromDto, ImplementsToDto, ) from .dto import ( ReportItemContextDto, ReportItemDto, ReportItemMessageDto, ReportItemSeverityDto, ) from .types import ( ForceCode, MessageCode, SeverityLevel, ) @dataclass(frozen=True) class ReportItemSeverity(ImplementsToDto, ImplementsFromDto): ERROR = SeverityLevel("ERROR") WARNING = SeverityLevel("WARNING") DEPRECATION = SeverityLevel("DEPRECATION") INFO = SeverityLevel("INFO") DEBUG = SeverityLevel("DEBUG") level: SeverityLevel force_code: Optional[ForceCode] = None def to_dto(self) -> ReportItemSeverityDto: return ReportItemSeverityDto( level=self.level, force_code=self.force_code, ) @classmethod def from_dto(cls, dto_obj: ReportItemSeverityDto) -> "ReportItemSeverity": return cls( level=dto_obj.level, force_code=dto_obj.force_code, ) @classmethod def error( cls, force_code: Optional[ForceCode] = None ) -> "ReportItemSeverity": return cls(level=cls.ERROR, force_code=force_code) @classmethod def warning(cls) -> "ReportItemSeverity": return cls(level=cls.WARNING) @classmethod def deprecation(cls) -> "ReportItemSeverity": return cls(level=cls.DEPRECATION) @classmethod def info(cls) -> "ReportItemSeverity": return cls(level=cls.INFO) @classmethod def debug(cls) -> "ReportItemSeverity": return cls(level=cls.DEBUG) def get_severity( force_code: Optional[ForceCode], is_forced: bool ) -> ReportItemSeverity: if is_forced: return ReportItemSeverity(ReportItemSeverity.WARNING) return ReportItemSeverity(ReportItemSeverity.ERROR, force_code) @dataclass(frozen=True, init=False) class ReportItemMessage(ImplementsToDto): _code = MessageCode("") @property def message(self) -> str: raise NotImplementedError() @property def code(self) -> MessageCode: return self._code def to_dto(self) -> ReportItemMessageDto: payload: Dict[str, Any] = {} if hasattr(self.__class__, "__annotations__"): try: annotations = self.__class__.__annotations__ except AttributeError as e: raise AssertionError() from e for attr_name in annotations.keys(): if attr_name.startswith("_") or attr_name in ("message",): continue attr_val = getattr(self, attr_name) if hasattr(attr_val, "to_dto"): payload[attr_name] = attr_val.to_dto() else: payload[attr_name] = attr_val return ReportItemMessageDto( code=self.code, message=self.message, payload=payload, ) @dataclass(frozen=True) class ReportItemContext(ImplementsToDto, ImplementsFromDto): node: str @classmethod def from_dto(cls, dto_obj: ReportItemContextDto) -> "ReportItemContext": return cls(node=dto_obj.node) def to_dto(self) -> ReportItemContextDto: return ReportItemContextDto(node=self.node) @dataclass class ReportItem(ImplementsToDto): severity: ReportItemSeverity message: ReportItemMessage context: Optional[ReportItemContext] = None @classmethod def error( cls, message: ReportItemMessage, force_code: Optional[ForceCode] = None, context: Optional[ReportItemContext] = None, ) -> "ReportItem": return cls( severity=ReportItemSeverity.error(force_code), message=message, context=context, ) @classmethod def warning( cls, message: ReportItemMessage, context: Optional[ReportItemContext] = None, ) -> "ReportItem": return cls( severity=ReportItemSeverity.warning(), message=message, context=context, ) @classmethod def deprecation( cls, message: ReportItemMessage, context: Optional[ReportItemContext] = None, ) -> "ReportItem": return cls( severity=ReportItemSeverity.deprecation(), message=message, context=context, ) @classmethod def info( cls, message: ReportItemMessage, context: Optional[ReportItemContext] = None, ) -> "ReportItem": return cls( severity=ReportItemSeverity.info(), message=message, context=context, ) @classmethod def debug( cls, message: ReportItemMessage, context: Optional[ReportItemContext] = None, ) -> "ReportItem": return cls( severity=ReportItemSeverity.debug(), message=message, context=context, ) def to_dto(self) -> ReportItemDto: return ReportItemDto( severity=self.severity.to_dto(), context=self.context.to_dto() if self.context else None, message=self.message.to_dto(), ) ReportItemList = List[ReportItem] pcs-0.12.0.2/pcs/common/reports/messages.py000066400000000000000000007041361500417470700204730ustar00rootroot00000000000000# pylint: disable=too-many-lines from collections import defaultdict from dataclasses import ( dataclass, field, ) from functools import partial from typing import ( Any, Dict, List, Literal, Mapping, Optional, Tuple, Union, cast, ) from pcs.common import file_type_codes from pcs.common.fencing_topology import ( TARGET_TYPE_ATTRIBUTE, FencingTargetType, FencingTargetValue, ) from pcs.common.file import ( FileAction, RawFileError, ) from pcs.common.resource_agent.dto import ( ResourceAgentNameDto, get_resource_agent_full_name, ) from pcs.common.resource_status import ResourceState from pcs.common.str_tools import ( format_list, format_list_custom_last_separator, format_list_dont_sort, format_optional, format_plural, get_plural, indent, is_iterable_not_str, ) from pcs.common.types import ( CibRuleExpressionType, StringIterable, ) from . import ( codes, const, types, ) from .dto import ReportItemMessageDto from .item import ReportItemMessage INSTANCE_SUFFIX = "@{0}" NODE_PREFIX = "{0}: " def _stdout_stderr_to_string(stdout: str, stderr: str, prefix: str = "") -> str: new_lines = [prefix] if prefix else [] for line in stdout.splitlines() + stderr.splitlines(): if line.strip(): new_lines.append(line) return "\n".join(new_lines) def _resource_move_ban_clear_master_resource_not_promotable( promotable_id: str, ) -> str: return ( "when specifying promoted you must use the promotable clone id{_id}" ).format( _id=format_optional(promotable_id, " ({})"), ) def _resource_move_ban_pcmk_success(stdout: str, stderr: str) -> str: new_lines = [] for line in stdout.splitlines() + stderr.splitlines(): if not line.strip(): continue line = line.replace( "WARNING: Creating rsc_location constraint", "Warning: Creating location constraint", ) line = line.replace( " using the clear option or by editing the CIB with an " "appropriate tool", "", ) new_lines.append(line) return "\n".join(new_lines) def _format_fencing_level_target( target_type: FencingTargetType, target_value: FencingTargetValue, ) -> str: if target_type == TARGET_TYPE_ATTRIBUTE: return f"{target_value[0]}={target_value[1]}" # Other target types guarantee values to be only strings return str(target_value) def _format_booth_default(value: Optional[str], template: str) -> str: return "" if value in ("booth", "", None) else template.format(value) def _key_numeric(item: str) -> Tuple[int, str]: return (int(item), item) if item.isdigit() else (-1, item) _add_remove_container_translation = { const.ADD_REMOVE_CONTAINER_TYPE_PROPERTY_SET: "property set", const.ADD_REMOVE_CONTAINER_TYPE_STONITH_RESOURCE: "stonith resource", } _add_remove_item_translation = { const.ADD_REMOVE_ITEM_TYPE_DEVICE: "device", const.ADD_REMOVE_ITEM_TYPE_PROPERTY: "property", } _file_role_translation = { file_type_codes.BOOTH_CONFIG: "Booth configuration", file_type_codes.BOOTH_KEY: "Booth key", file_type_codes.COROSYNC_AUTHKEY: "Corosync authkey", file_type_codes.COROSYNC_CONF: "Corosync configuration", file_type_codes.COROSYNC_QDEVICE_NSSDB: "QDevice certificate database", file_type_codes.COROSYNC_QNETD_CA_CERT: "QNetd CA certificate", file_type_codes.COROSYNC_QNETD_NSSDB: "QNetd certificate database", file_type_codes.PCS_DR_CONFIG: "disaster-recovery configuration", file_type_codes.PACEMAKER_AUTHKEY: "Pacemaker authkey", file_type_codes.PCSD_ENVIRONMENT_CONFIG: "pcsd configuration", file_type_codes.PCSD_SSL_CERT: "pcsd SSL certificate", file_type_codes.PCSD_SSL_KEY: "pcsd SSL key", file_type_codes.PCS_KNOWN_HOSTS: "known-hosts", file_type_codes.PCS_SETTINGS_CONF: "pcs configuration", } _type_translation = { "acl_group": "ACL group", "acl_permission": "ACL permission", "acl_role": "ACL role", "acl_target": "ACL user", "fencing-level": "fencing level", # Pacemaker-2.0 deprecated masters. Masters are now called promotable # clones. We treat masters as clones. Do not report we were doing something # with a master, say we were doing it with a clone instead. "master": "clone", "primitive": "resource", "resource_set": "resource set", "rsc_colocation": "colocation constraint", "rsc_location": "location constraint", "rsc_order": "order constraint", "rsc_ticket": "ticket constraint", } _type_articles = { "ACL group": "an", "ACL user": "an", "ACL role": "an", "ACL permission": "an", "options set": "an", } def _add_remove_container_str( container: types.AddRemoveContainerType, ) -> str: return _add_remove_container_translation.get(container, container) def _add_remove_item_str(item: types.AddRemoveItemType) -> str: return _add_remove_item_translation.get(item, item) def _format_file_role(role: file_type_codes.FileTypeCode) -> str: return _file_role_translation.get(role, role) def _format_file_action(action: FileAction) -> str: return _file_operation_translation.get(action, str(action)) _file_operation_translation = { RawFileError.ACTION_CHMOD: "change permissions of", RawFileError.ACTION_CHOWN: "change ownership of", RawFileError.ACTION_READ: "read", RawFileError.ACTION_REMOVE: "remove", RawFileError.ACTION_UPDATE: "update", RawFileError.ACTION_WRITE: "write", } def _service_action_str(action: types.ServiceAction, suffix: str = "") -> str: base = action.lower() if not suffix: return base base = { const.SERVICE_ACTION_STOP: "stopp", const.SERVICE_ACTION_ENABLE: "enabl", const.SERVICE_ACTION_DISABLE: "disabl", }.get(action, base) return f"{base}{suffix}" def _skip_reason_to_string(reason: types.ReasonType) -> str: return { const.REASON_NOT_LIVE_CIB: "the command does not run on a live cluster", const.REASON_UNREACHABLE: "pcs is unable to connect to the node(s)", }.get(reason, reason) def _typelist_to_string( type_list: StringIterable, article: bool = False ) -> str: if not type_list: return "" # use set to drop duplicate items: # * master is translated to clone # * i.e. "clone, master" is translated to "clone, clone" # * so we want to drop the second clone new_list = sorted( { # get a translation or make a type_name a string _type_translation.get(type_name, f"{type_name}") for type_name in type_list } ) res_types = "/".join(new_list) if not article: return res_types return "{article} {types}".format( article=_type_articles.get(new_list[0], "a"), types=res_types, ) def _type_to_string(type_name: str, article: bool = False) -> str: if not type_name: return "" # get a translation or make a type_name a string translated = _type_translation.get(type_name, f"{type_name}") if not article: return translated return "{article} {type}".format( article=_type_articles.get(translated, "a"), type=translated, ) def _build_node_description(node_types: List[str]) -> str: if not node_types: return "Node" label = "{0} node".format if len(node_types) == 1: return label(node_types[0]) return "nor " + " or ".join([label(ntype) for ntype in node_types]) def _stonith_watchdog_timeout_reason_to_str( reason: types.StonithWatchdogTimeoutCannotBeSetReason, ) -> str: return { const.SBD_NOT_SET_UP: "SBD is disabled", const.SBD_SET_UP_WITH_DEVICES: "SBD is enabled with devices", const.SBD_SET_UP_WITHOUT_DEVICES: "SBD is enabled without devices", }.get(reason, reason) @dataclass(frozen=True) class LegacyCommonMessage(ReportItemMessage): """ This class is used for legacy report transport protocol from 'pcs_internal.py' and is used in 'pcs.cluster.RemoteAddNodes'. This method should be replaced with transporting DTOs of reports in the future. """ legacy_code: types.MessageCode legacy_info: Mapping[str, Any] legacy_message: str @property def message(self) -> str: return self.legacy_message def to_dto(self) -> ReportItemMessageDto: return ReportItemMessageDto( code=self.legacy_code, message=self.message, payload=dict(self.legacy_info), ) @dataclass(frozen=True) class ResourceForConstraintIsMultiinstance(ReportItemMessage): """ When setting up a constraint a resource in a type of a clone was specified resource_id -- specified resource parent_type -- type of a clone (clone, bundle...) parent_id -- clone resource id """ resource_id: str parent_type: str parent_id: str _code = codes.RESOURCE_FOR_CONSTRAINT_IS_MULTIINSTANCE @property def message(self) -> str: parent_type = _type_to_string(self.parent_type) return ( f"{self.resource_id} is a {parent_type} resource, you should " f"use the {parent_type} id: {self.parent_id} when adding " "constraints" ) @dataclass(frozen=True) class DuplicateConstraintsExist(ReportItemMessage): """ When creating a constraint pcs detected a similar constraint already exists constraint_ids -- ids of similar constraints """ constraint_ids: list[str] _code = codes.DUPLICATE_CONSTRAINTS_EXIST @property def message(self) -> str: pluralize = partial(format_plural, self.constraint_ids) constraint = pluralize("constraint") exists = pluralize("exists", "exist") return f"Duplicate {constraint} already {exists}" @dataclass(frozen=True) class EmptyResourceSetList(ReportItemMessage): """ An empty resource set has been specified, which is not allowed by cib schema """ _code = codes.EMPTY_RESOURCE_SET_LIST @property def message(self) -> str: return "Resource set list is empty" @dataclass(frozen=True) class CannotSetOrderConstraintsForResourcesInTheSameGroup(ReportItemMessage): """ Can't set order constraint for resources in one group because the start sequence of the resources is determined by their location in the group """ _code = codes.CANNOT_SET_ORDER_CONSTRAINTS_FOR_RESOURCES_IN_THE_SAME_GROUP @property def message(self) -> str: return ( "Cannot create an order constraint for resources in the same group" ) @dataclass(frozen=True) class RequiredOptionsAreMissing(ReportItemMessage): """ Required option has not been specified, command cannot continue option_names -- are required but was not entered option_type -- describes the option """ option_names: List[str] option_type: Optional[str] = None _code = codes.REQUIRED_OPTIONS_ARE_MISSING @property def message(self) -> str: return ( "required {desc}{_option} {option_names_list} {_is} missing" ).format( desc=format_optional(self.option_type), option_names_list=format_list(self.option_names), _option=format_plural(self.option_names, "option"), _is=format_plural(self.option_names, "is"), ) @dataclass(frozen=True) class PrerequisiteOptionIsMissing(ReportItemMessage): """ If the option_name is specified, the prerequisite_option must be specified option_name -- an option which depends on the prerequisite_option prerequisite_name -- the prerequisite option option_type -- describes the option prerequisite_type -- describes the prerequisite_option """ option_name: str prerequisite_name: str option_type: Optional[str] = None prerequisite_type: Optional[str] = None _code = codes.PREREQUISITE_OPTION_IS_MISSING @property def message(self) -> str: return ( "If {opt_desc}option '{option_name}' is specified, " "{pre_desc}option '{prerequisite_name}' must be specified as well" ).format( opt_desc=format_optional(self.option_type), pre_desc=format_optional(self.prerequisite_type), option_name=self.option_name, prerequisite_name=self.prerequisite_name, ) @dataclass(frozen=True) class PrerequisiteOptionMustBeEnabledAsWell(ReportItemMessage): """ If the option_name is enabled, the prerequisite_option must be also enabled option_name -- an option which depends on the prerequisite_option prerequisite_name -- the prerequisite option option_type -- describes the option prerequisite_type -- describes the prerequisite_option """ option_name: str prerequisite_name: str option_type: str = "" prerequisite_type: str = "" _code = codes.PREREQUISITE_OPTION_MUST_BE_ENABLED_AS_WELL @property def message(self) -> str: return ( "If {opt_desc}option '{option_name}' is enabled, " "{pre_desc}option '{prerequisite_name}' must be enabled as well" ).format( opt_desc=format_optional(self.option_type), pre_desc=format_optional(self.prerequisite_type), option_name=self.option_name, prerequisite_name=self.prerequisite_name, ) @dataclass(frozen=True) class PrerequisiteOptionMustBeDisabled(ReportItemMessage): """ If the option_name is enabled, the prerequisite_option must be disabled option_name -- an option which depends on the prerequisite_option prerequisite_name -- the prerequisite option option_type -- describes the option prerequisite_type -- describes the prerequisite_option """ option_name: str prerequisite_name: str option_type: str = "" prerequisite_type: str = "" _code = codes.PREREQUISITE_OPTION_MUST_BE_DISABLED @property def message(self) -> str: return ( "If {opt_desc}option '{option_name}' is enabled, " "{pre_desc}option '{prerequisite_name}' must be disabled" ).format( opt_desc=format_optional(self.option_type), pre_desc=format_optional(self.prerequisite_type), option_name=self.option_name, prerequisite_name=self.prerequisite_name, ) @dataclass(frozen=True) class PrerequisiteOptionMustNotBeSet(ReportItemMessage): """ The option_name cannot be set because the prerequisite_name is already set option_name -- an option which depends on the prerequisite_option prerequisite_name -- the prerequisite option option_type -- describes the option prerequisite_type -- describes the prerequisite_option """ option_name: str prerequisite_name: str option_type: str = "" prerequisite_type: str = "" _code = codes.PREREQUISITE_OPTION_MUST_NOT_BE_SET @property def message(self) -> str: return ( "Cannot set {opt_desc}option '{option_name}' because " "{pre_desc}option '{prerequisite_name}' is already set" ).format( opt_desc=format_optional(self.option_type), pre_desc=format_optional(self.prerequisite_type), option_name=self.option_name, prerequisite_name=self.prerequisite_name, ) @dataclass(frozen=True) class RequiredOptionOfAlternativesIsMissing(ReportItemMessage): """ At least one option has to be specified option_names -- options from which at least one has to be specified option_type -- describes the option """ option_names: List[str] deprecated_names: List[str] = field(default_factory=list) option_type: Optional[str] = None _code = codes.REQUIRED_OPTION_OF_ALTERNATIVES_IS_MISSING @property def message(self) -> str: flag_name_list = [ (name in self.deprecated_names, name) for name in self.option_names ] str_list = [ f"'{item[1]}' (deprecated)" if item[0] else f"'{item[1]}'" for item in sorted(flag_name_list) ] if not str_list: options_str = "" elif len(str_list) == 1: options_str = str_list[0] else: options_str = "{} or {}".format( ", ".join(str_list[:-1]), str_list[-1] ) desc = format_optional(self.option_type) return f"{desc}option {options_str} has to be specified" @dataclass(frozen=True) class InvalidOptions(ReportItemMessage): """ Specified option names are not valid, usually an error or a warning option_names -- specified invalid option names allowed -- possible allowed option names option_type -- describes the option allowed_patterns -- allowed user defined options patterns """ option_names: List[str] allowed: List[str] option_type: Optional[str] = None allowed_patterns: List[str] = field(default_factory=list) _code = codes.INVALID_OPTIONS @property def message(self) -> str: template = "invalid {desc}option{plural_options} {option_names_list}," if not self.allowed and not self.allowed_patterns: template += " there are no options allowed" elif not self.allowed_patterns: template += " allowed option{plural_allowed} {allowed_values}" elif not self.allowed: template += ( " allowed are options matching patterns: " "{allowed_patterns_values}" ) else: template += ( " allowed option{plural_allowed} {allowed_values}" " and" " options matching patterns: {allowed_patterns_values}" ) return template.format( desc=format_optional(self.option_type), allowed_values=format_list(self.allowed), allowed_patterns_values=format_list(self.allowed_patterns), option_names_list=format_list(self.option_names), plural_options=format_plural(self.option_names, "", "s:"), plural_allowed=format_plural(self.allowed, " is", "s are:"), ) @dataclass(frozen=True) class InvalidUserdefinedOptions(ReportItemMessage): """ Specified option names defined by a user are not valid This is different than invalid_options. In this case, the options are supposed to be defined by a user. This report carries information that the option names do not meet requirements, i.e. contain not allowed characters. Invalid_options is used when the options are predefined by pcs (or underlying tools). option_names -- specified invalid option names allowed_characters -- which characters are allowed in the names option_type -- describes the option """ option_names: List[str] allowed_characters: str option_type: Optional[str] = None _code = codes.INVALID_USERDEFINED_OPTIONS @property def message(self) -> str: return ( "invalid {desc}option{plural_options} {option_names_list}, " "{desc}options may contain {allowed_characters} characters only" ).format( desc=format_optional(self.option_type), option_names_list=format_list(self.option_names), plural_options=format_plural(self.option_names, "", "s:"), allowed_characters=self.allowed_characters, ) @dataclass(frozen=True) class InvalidOptionType(ReportItemMessage): """ Specified value is not of a valid type for the option option_name -- option name whose value is not of a valid type allowed_types -- list of allowed types or string description """ option_name: str allowed_types: Union[List[str], str] _code = codes.INVALID_OPTION_TYPE @property def message(self) -> str: return "specified {option_name} is not valid, use {hint}".format( hint=( format_list(cast(List[str], self.allowed_types)) if is_iterable_not_str(self.allowed_types) else self.allowed_types ), option_name=self.option_name, ) @dataclass(frozen=True) class InvalidOptionValue(ReportItemMessage): """ Specified value is not valid for the option, usually an error or a warning option_name -- specified option name whose value is not valid option_value -- specified value which is not valid allowed_values -- a list or description of allowed values, may be undefined cannot_be_empty -- the value is empty and that is not allowed forbidden_characters -- characters the value cannot contain """ option_name: str option_value: str allowed_values: Union[List[str], str, None] cannot_be_empty: bool = False forbidden_characters: Optional[str] = None _code = codes.INVALID_OPTION_VALUE @property def message(self) -> str: if self.cannot_be_empty: template = "{option_name} cannot be empty" elif self.forbidden_characters: template = ( "{option_name} cannot contain {forbidden_characters} characters" ) else: template = "'{option_value}' is not a valid {option_name} value" if self.allowed_values: template += ", use {hint}" return template.format( hint=( format_list(cast(List[str], self.allowed_values)) if ( self.allowed_values and is_iterable_not_str(self.allowed_values) ) else self.allowed_values ), option_name=self.option_name, option_value=self.option_value, forbidden_characters=self.forbidden_characters, ) @dataclass(frozen=True) class DeprecatedOption(ReportItemMessage): """ Specified option name is deprecated and has been replaced by other option(s) option_name -- the deprecated option replaced_by -- new option(s) to be used instead option_type -- option description """ option_name: str replaced_by: List[str] option_type: Optional[str] = None _code = codes.DEPRECATED_OPTION @property def message(self) -> str: return ( "{desc}option '{option_name}' is deprecated and might be removed " "in a future release, therefore it should not be used{hint}" ).format( option_name=self.option_name, desc=format_optional(self.option_type), hint=format_optional( format_list(self.replaced_by), ", use {} instead" ), ) @dataclass(frozen=True) class DeprecatedOptionValue(ReportItemMessage): """ Specified option value is deprecated and has been replaced by other value option_name -- option which value is deprecated deprecated_value -- value which should not be used anymore replaced_by -- new value to be used instead """ option_name: str deprecated_value: str replaced_by: Optional[str] = None _code = codes.DEPRECATED_OPTION_VALUE @property def message(self) -> str: return ( "Value '{deprecated_value}' of option {option_name} is deprecated " "and might be removed in a future release, therefore it should not " "be used{replaced_by}" ).format( deprecated_value=self.deprecated_value, option_name=self.option_name, replaced_by=format_optional( self.replaced_by, f", use '{self.replaced_by}' value instead" ), ) @dataclass(frozen=True) class MutuallyExclusiveOptions(ReportItemMessage): """ Entered options can not coexist option_names -- contain entered mutually exclusive options option_type -- describes the option """ option_names: List[str] option_type: Optional[str] = None _code = codes.MUTUALLY_EXCLUSIVE_OPTIONS @property def message(self) -> str: return "Only one of {desc}options {option_names} can be used".format( desc=format_optional(self.option_type), option_names=format_list_custom_last_separator( self.option_names, " and " ), ) @dataclass(frozen=True) class InvalidCibContent(ReportItemMessage): """ Given cib content is not valid report -- human readable explanation of a cib invalidity (a stderr of `crm_verify`) can_be_more_verbose -- can the user ask for a more verbose report """ report: str can_be_more_verbose: bool _code = codes.INVALID_CIB_CONTENT @property def message(self) -> str: return f"invalid cib:\n{self.report}" @dataclass(frozen=True) class InvalidIdIsEmpty(ReportItemMessage): """ Empty string was specified as an id, which is not valid id_description -- describe id's role """ id_description: str _code = codes.INVALID_ID_IS_EMPTY @property def message(self) -> str: return f"{self.id_description} cannot be empty" @dataclass(frozen=True) class InvalidIdBadChar(ReportItemMessage): """ specified id is not valid as it contains a forbidden character id -- specified id id_description -- describe id's role invalid_character -- forbidden character is_first_char -- is it the first character which is forbidden? """ id: str # pylint: disable=invalid-name id_description: str invalid_character: str is_first_char: bool _code = codes.INVALID_ID_BAD_CHAR @property def message(self) -> str: desc = "first " if self.is_first_char else "" return ( f"invalid {self.id_description} '{self.id}', " f"'{self.invalid_character}' is not a valid {desc}character for a " f"{self.id_description}" ) @dataclass(frozen=True) class InvalidIdType(ReportItemMessage): """ Specified type of id (plain, pattern, ...) is not valid id_type -- specified type of an id allowed_types -- list of allowed types """ id_type: str allowed_types: list[str] _code = codes.INVALID_ID_TYPE @property def message(self) -> str: hint = format_list(self.allowed_types) return ( f"'{self.id_type}' is not a valid type of ID specification, " f"use {hint}" ) @dataclass(frozen=True) class InvalidTimeoutValue(ReportItemMessage): """ Specified timeout is not valid (number or other format e.g. 2min) timeout -- specified invalid timeout """ timeout: str _code = codes.INVALID_TIMEOUT_VALUE @property def message(self) -> str: return f"'{self.timeout}' is not a valid number of seconds to wait" @dataclass(frozen=True) class InvalidScore(ReportItemMessage): """ Specified score value is not valid score -- specified score value """ score: str _code = codes.INVALID_SCORE @property def message(self) -> str: return ( f"invalid score '{self.score}', use integer or INFINITY or " "-INFINITY" ) @dataclass(frozen=True) class RunExternalProcessStarted(ReportItemMessage): """ Information about running an external process command -- the external process command stdin -- passed to the external process via its stdin environment -- environment variables for the command """ command: str stdin: Optional[str] environment: Mapping[str, str] _code = codes.RUN_EXTERNAL_PROCESS_STARTED @property def message(self) -> str: return ( "Running: {command}\nEnvironment:{env_part}\n{stdin_part}" ).format( command=self.command, stdin_part=format_optional( self.stdin, "--Debug Input Start--\n{}\n--Debug Input End--\n" ), env_part=( "" if not self.environment else "\n" + "\n".join( [ f" {key}={val}" for key, val in sorted(self.environment.items()) ] ) ), ) @dataclass(frozen=True) class RunExternalProcessFinished(ReportItemMessage): """ Information about result of running an external process command -- the external process command return_value -- external process's return (exit) code stdout -- external process's stdout stderr -- external process's stderr """ command: str return_value: int stdout: str stderr: str _code = codes.RUN_EXTERNAL_PROCESS_FINISHED @property def message(self) -> str: return ( f"Finished running: {self.command}\n" f"Return value: {self.return_value}\n" "--Debug Stdout Start--\n" f"{self.stdout}\n" "--Debug Stdout End--\n" "--Debug Stderr Start--\n" f"{self.stderr}\n" "--Debug Stderr End--\n" ) @dataclass(frozen=True) class RunExternalProcessError(ReportItemMessage): """ Attempt to run an external process failed command -- the external process command reason -- error description """ command: str reason: str _code = codes.RUN_EXTERNAL_PROCESS_ERROR @property def message(self) -> str: return f"unable to run command {self.command}: {self.reason}" @dataclass(frozen=True) class NoActionNecessary(ReportItemMessage): """ Configuration already satisfy change that was requested by the user, therefore no action/change of configuration is necessary. """ _code = codes.NO_ACTION_NECESSARY @property def message(self) -> str: return "No action necessary, requested change would have no effect" @dataclass(frozen=True) class NodeCommunicationStarted(ReportItemMessage): """ Request is about to be sent to a remote node, debug info target -- where the request is about to be sent to data -- request's data """ target: str data: str _code = codes.NODE_COMMUNICATION_STARTED @property def message(self) -> str: data = format_optional( self.data, "--Debug Input Start--\n{}\n--Debug Input End--\n" ) return f"Sending HTTP Request to: {self.target}\n{data}" @dataclass(frozen=True) class NodeCommunicationFinished(ReportItemMessage): """ Remote node request has been finished, debug info target -- where the request was sent to response_code -- response return code response_data -- response data """ target: str response_code: int response_data: str _code = codes.NODE_COMMUNICATION_FINISHED @property def message(self) -> str: return ( f"Finished calling: {self.target}\n" f"Response Code: {self.response_code}\n" "--Debug Response Start--\n" f"{self.response_data}\n" "--Debug Response End--\n" ) @dataclass(frozen=True) class NodeCommunicationDebugInfo(ReportItemMessage): """ Node communication debug info from pycurl target -- request target data -- pycurl communication data """ target: str data: str _code = codes.NODE_COMMUNICATION_DEBUG_INFO @property def message(self) -> str: return ( f"Communication debug info for calling: {self.target}\n" "--Debug Communication Info Start--\n" f"{self.data}\n" "--Debug Communication Info End--\n" ) @dataclass(frozen=True) class NodeCommunicationNotConnected(ReportItemMessage): """ An error occurred when connecting to a remote node, debug info node -- node address / name reason -- description of the error """ node: str reason: str _code = codes.NODE_COMMUNICATION_NOT_CONNECTED @property def message(self) -> str: return f"Unable to connect to {self.node} ({self.reason})" @dataclass(frozen=True) class NodeCommunicationNoMoreAddresses(ReportItemMessage): """ Request failed and there are no more addresses to try it again """ node: str request: str _code = codes.NODE_COMMUNICATION_NO_MORE_ADDRESSES @property def message(self) -> str: return f"Unable to connect to '{self.node}' via any of its addresses" @dataclass(frozen=True) class NodeCommunicationErrorNotAuthorized(ReportItemMessage): """ Node rejected a request as we are not authorized node -- node address / name command -- executed command reason -- description of the error """ node: str command: str reason: str _code = codes.NODE_COMMUNICATION_ERROR_NOT_AUTHORIZED @property def message(self) -> str: return f"Unable to authenticate to {self.node} ({self.reason})" @dataclass(frozen=True) class NodeCommunicationErrorPermissionDenied(ReportItemMessage): """ Node rejected a request as we do not have permissions to run the request node -- node address / name command -- executed command reason -- description of the error """ node: str command: str reason: str _code = codes.NODE_COMMUNICATION_ERROR_PERMISSION_DENIED @property def message(self) -> str: return f"{self.node}: Permission denied ({self.reason})" @dataclass(frozen=True) class NodeCommunicationErrorUnsupportedCommand(ReportItemMessage): """ Node rejected a request as it does not support the request node -- node address / name command -- executed command reason -- description of the error """ node: str command: str reason: str _code = codes.NODE_COMMUNICATION_ERROR_UNSUPPORTED_COMMAND @property def message(self) -> str: return ( f"{self.node}: Unsupported command ({self.reason}), try upgrading " "pcsd" ) @dataclass(frozen=True) class NodeCommunicationCommandUnsuccessful(ReportItemMessage): """ Node rejected a request for another reason with a plain text explanation node -- node address / name command -- executed command reason -- description of the error """ node: str command: str reason: str _code = codes.NODE_COMMUNICATION_COMMAND_UNSUCCESSFUL @property def message(self) -> str: return f"{self.node}: {self.reason}" @dataclass(frozen=True) class NodeCommunicationError(ReportItemMessage): """ Node rejected a request for another reason (may be faulty node) node -- node address / name command -- executed command reason -- description of the error """ node: str command: str reason: str _code = codes.NODE_COMMUNICATION_ERROR @property def message(self) -> str: return f"Error connecting to {self.node} ({self.reason})" @dataclass(frozen=True) class NodeCommunicationErrorUnableToConnect(ReportItemMessage): """ We were unable to connect to a node node -- node address / name command -- executed command reason -- description of the error """ node: str command: str reason: str _code = codes.NODE_COMMUNICATION_ERROR_UNABLE_TO_CONNECT @property def message(self) -> str: return f"Unable to connect to {self.node} ({self.reason})" @dataclass(frozen=True) class NodeCommunicationErrorTimedOut(ReportItemMessage): """ Communication with node timed out. node -- node address / name command -- executed command reason -- description of the error """ node: str command: str reason: str _code = codes.NODE_COMMUNICATION_ERROR_TIMED_OUT @property def message(self) -> str: return f"{self.node}: Connection timeout ({self.reason})" @dataclass(frozen=True) class NodeCommunicationProxyIsSet(ReportItemMessage): """ Warning when connection failed and there is proxy set in environment variables """ node: str = "" address: str = "" _code = codes.NODE_COMMUNICATION_PROXY_IS_SET @property def message(self) -> str: return "Proxy is set in environment variables, try disabling it" @dataclass(frozen=True) class NodeCommunicationRetrying(ReportItemMessage): """ Request failed due communication error connecting via specified address, therefore trying another address if there is any. """ node: str failed_address: str failed_port: str next_address: str next_port: str request: str _code = codes.NODE_COMMUNICATION_RETRYING @property def message(self) -> str: return ( f"Unable to connect to '{self.node}' via address " f"'{self.failed_address}' and port '{self.failed_port}'. Retrying " f"request '{self.request}' via address '{self.next_address}' and " f"port '{self.next_port}'" ) @dataclass(frozen=True) class DefaultsCanBeOverridden(ReportItemMessage): """ Warning when settings defaults (op_defaults, rsc_defaults...) """ _code = codes.DEFAULTS_CAN_BE_OVERRIDDEN @property def message(self) -> str: return ( "Defaults do not apply to resources which override them with their " "own defined values" ) @dataclass(frozen=True) class CorosyncAuthkeyWrongLength(ReportItemMessage): """ Wrong corosync authkey length. """ _code = codes.COROSYNC_AUTHKEY_WRONG_LENGTH actual_length: int min_length: int max_length: int @property def message(self) -> str: if self.min_length == self.max_length: template = ( "{max_length} {bytes_allowed} key must be provided for a " "corosync authkey, {actual_length} {bytes_provided} key " "provided" ) else: template = ( "At least {min_length} and at most {max_length} " "{bytes_allowed} key must be provided for a corosync " "authkey, {actual_length} {bytes_provided} key provided" ) return template.format( min_length=self.min_length, max_length=self.max_length, actual_length=self.actual_length, bytes_allowed=format_plural(self.max_length, "byte"), bytes_provided=format_plural(self.actual_length, "byte"), ) @dataclass(frozen=True) class CorosyncConfigDistributionStarted(ReportItemMessage): """ Corosync configuration is about to be sent to nodes """ _code = codes.COROSYNC_CONFIG_DISTRIBUTION_STARTED @property def message(self) -> str: return "Sending updated corosync.conf to nodes..." @dataclass(frozen=True) class CorosyncConfigAcceptedByNode(ReportItemMessage): """ corosync configuration has been accepted by a node node -- node address / name """ node: str _code = codes.COROSYNC_CONFIG_ACCEPTED_BY_NODE @property def message(self) -> str: return f"{self.node}: Succeeded" @dataclass(frozen=True) class CorosyncConfigDistributionNodeError(ReportItemMessage): """ Communication error occurred when saving corosync configuration to a node node -- faulty node address / name """ node: str _code = codes.COROSYNC_CONFIG_DISTRIBUTION_NODE_ERROR @property def message(self) -> str: return f"{self.node}: Unable to set corosync config" @dataclass(frozen=True) class CorosyncNotRunningCheckStarted(ReportItemMessage): """ We are about to make sure corosync is not running on nodes """ _code = codes.COROSYNC_NOT_RUNNING_CHECK_STARTED @property def message(self) -> str: return "Checking that corosync is not running on nodes..." @dataclass(frozen=True) class CorosyncNotRunningCheckNodeError(ReportItemMessage): """ Communication error occurred when checking corosync is not running on a node node -- faulty node address / name """ node: str _code = codes.COROSYNC_NOT_RUNNING_CHECK_NODE_ERROR @property def message(self) -> str: return ( f"Unable to check if corosync is not running on node '{self.node}'" ) @dataclass(frozen=True) class CorosyncNotRunningCheckNodeStopped(ReportItemMessage): """ Check that corosync is not running on a node passed, corosync is stopped node -- node address / name """ node: str _code = codes.COROSYNC_NOT_RUNNING_CHECK_NODE_STOPPED @property def message(self) -> str: return f"Corosync is not running on node '{self.node}'" @dataclass(frozen=True) class CorosyncNotRunningCheckNodeRunning(ReportItemMessage): """ Check that corosync is not running on a node passed, but corosync is running node -- node address / name """ node: str _code = codes.COROSYNC_NOT_RUNNING_CHECK_NODE_RUNNING @property def message(self) -> str: return f"Corosync is running on node '{self.node}'" @dataclass(frozen=True) class CorosyncNotRunningCheckFinishedRunning(ReportItemMessage): """ Check that corosync is not running revealed corosync is running on nodes """ node_list: list[str] _code = codes.COROSYNC_NOT_RUNNING_CHECK_FINISHED_RUNNING @property def message(self) -> str: return ( "Corosync is running on {node} {node_list}. Requested change can " "only be made if the cluster is stopped. In order to proceed, stop " "the cluster." ).format( node=format_plural(self.node_list, "node"), node_list=format_list(self.node_list), ) @dataclass(frozen=True) class CorosyncQuorumGetStatusError(ReportItemMessage): """ Unable to get runtime status of quorum reason -- an error message node -- a node where the error occurred, local node if not specified """ reason: str node: str = "" _code = codes.COROSYNC_QUORUM_GET_STATUS_ERROR @property def message(self) -> str: node = format_optional(self.node, "{}: ") return f"{node}Unable to get quorum status: {self.reason}" @dataclass(frozen=True) class CorosyncQuorumHeuristicsEnabledWithNoExec(ReportItemMessage): """ No exec_ is specified, therefore heuristics are effectively disabled """ _code = codes.COROSYNC_QUORUM_HEURISTICS_ENABLED_WITH_NO_EXEC @property def message(self) -> str: return ( "No exec_NAME options are specified, so heuristics are effectively " "disabled" ) @dataclass(frozen=True) class CorosyncQuorumSetExpectedVotesError(ReportItemMessage): """ Unable to set expected votes in a live cluster reason -- an error message """ reason: str _code = codes.COROSYNC_QUORUM_SET_EXPECTED_VOTES_ERROR @property def message(self) -> str: return f"Unable to set expected votes: {self.reason}" @dataclass(frozen=True) class CorosyncConfigReloaded(ReportItemMessage): """ Corosync configuration has been reloaded node -- node label on which operation has been executed """ node: str = "" _code = codes.COROSYNC_CONFIG_RELOADED @property def message(self) -> str: return "{node}Corosync configuration reloaded".format( node=format_optional(self.node, "{}: "), ) @dataclass(frozen=True) class CorosyncConfigReloadError(ReportItemMessage): """ An error occurred when reloading corosync configuration reason -- an error message node -- node label """ reason: str node: str = "" _code = codes.COROSYNC_CONFIG_RELOAD_ERROR @property def message(self) -> str: node = format_optional(self.node, "{}: ") return f"{node}Unable to reload corosync configuration: {self.reason}" @dataclass(frozen=True) class CorosyncConfigReloadNotPossible(ReportItemMessage): """ Corosync configuration cannot be reloaded because corosync is not running on the specified node node -- node label on which confi """ node: str _code = codes.COROSYNC_CONFIG_RELOAD_NOT_POSSIBLE @property def message(self) -> str: return ( f"{self.node}: Corosync is not running, therefore reload of the " "corosync configuration is not possible" ) @dataclass(frozen=True) class CorosyncConfigUnsupportedTransport(ReportItemMessage): """ Transport type defined in corosync.conf is unknown. """ actual_transport: str supported_transport_types: List[str] _code = codes.COROSYNC_CONFIG_UNSUPPORTED_TRANSPORT @property def message(self) -> str: return ( "Transport '{actual_transport}' currently configured in " "corosync.conf is unsupported. Supported transport types are: " "{supported_transport_types}" ).format( actual_transport=self.actual_transport, supported_transport_types=format_list( self.supported_transport_types ), ) @dataclass(frozen=True) class ParseErrorCorosyncConfMissingClosingBrace(ReportItemMessage): """ Corosync config cannot be parsed due to missing closing brace """ _code = codes.PARSE_ERROR_COROSYNC_CONF_MISSING_CLOSING_BRACE @property def message(self) -> str: return "Unable to parse corosync config: missing closing brace" @dataclass(frozen=True) class ParseErrorCorosyncConfUnexpectedClosingBrace(ReportItemMessage): """ Corosync config cannot be parsed due to unexpected closing brace """ _code = codes.PARSE_ERROR_COROSYNC_CONF_UNEXPECTED_CLOSING_BRACE @property def message(self) -> str: return "Unable to parse corosync config: unexpected closing brace" @dataclass(frozen=True) class ParseErrorCorosyncConfMissingSectionNameBeforeOpeningBrace( ReportItemMessage ): """ Corosync config cannot be parsed due to a section name missing before { """ _code = ( codes.PARSE_ERROR_COROSYNC_CONF_MISSING_SECTION_NAME_BEFORE_OPENING_BRACE ) @property def message(self) -> str: return ( "Unable to parse corosync config: missing a section name before {" ) @dataclass(frozen=True) class ParseErrorCorosyncConfExtraCharactersAfterOpeningBrace(ReportItemMessage): """ Corosync config cannot be parsed due to extra characters after { """ _code = codes.PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_AFTER_OPENING_BRACE @property def message(self) -> str: return "Unable to parse corosync config: extra characters after {" @dataclass(frozen=True) class ParseErrorCorosyncConfExtraCharactersBeforeOrAfterClosingBrace( ReportItemMessage ): """ Corosync config cannot be parsed due to extra characters before or after } """ _code = ( codes.PARSE_ERROR_COROSYNC_CONF_EXTRA_CHARACTERS_BEFORE_OR_AFTER_CLOSING_BRACE ) @property def message(self) -> str: return "Unable to parse corosync config: extra characters before or after }" @dataclass(frozen=True) class ParseErrorCorosyncConfLineIsNotSectionNorKeyValue(ReportItemMessage): """ Corosync config cannot be parsed due to a line is not a section nor key:val """ _code = codes.PARSE_ERROR_COROSYNC_CONF_LINE_IS_NOT_SECTION_NOR_KEY_VALUE @property def message(self) -> str: return ( "Unable to parse corosync config: a line is not opening or closing " "a section or key: value" ) @dataclass(frozen=True) class ParseErrorCorosyncConf(ReportItemMessage): """ Corosync config cannot be parsed, the cause is not specified. It is better to use more specific error if possible. """ _code = codes.PARSE_ERROR_COROSYNC_CONF @property def message(self) -> str: return "Unable to parse corosync config" @dataclass(frozen=True) class CorosyncConfigCannotSaveInvalidNamesValues(ReportItemMessage): """ cannot save corosync.conf - it contains forbidden characters which break it section_name_list -- bad names of sections attribute_name_list -- bad names of attributes attribute_value_pairs -- tuples (attribute_name, its_bad_value) """ section_name_list: List[str] attribute_name_list: List[str] attribute_value_pairs: List[Tuple[str, str]] _code = codes.COROSYNC_CONFIG_CANNOT_SAVE_INVALID_NAMES_VALUES @property def message(self) -> str: prefix = "Cannot save corosync.conf containing " if ( not self.section_name_list and not self.attribute_name_list and not self.attribute_value_pairs ): return ( f"{prefix}invalid section names, option names or option values" ) parts = [] if self.section_name_list: parts.append( "invalid section name(s): {}".format( format_list(self.section_name_list) ) ) if self.attribute_name_list: parts.append( "invalid option name(s): {}".format( format_list(self.attribute_name_list) ) ) if self.attribute_value_pairs: pairs = ", ".join( [ f"'{value}' (option '{name}')" for name, value in self.attribute_value_pairs ] ) parts.append(f"invalid option value(s): {pairs}") return "{}{}".format(prefix, "; ".join(parts)) @dataclass(frozen=True) class CorosyncConfigMissingNamesOfNodes(ReportItemMessage): """ Some nodes in corosync.conf do not have their name set, they will be omitted fatal -- if True, pcs cannot continue """ fatal: bool = False _code = codes.COROSYNC_CONFIG_MISSING_NAMES_OF_NODES @property def message(self) -> str: note = ( "unable to continue" if self.fatal else "those nodes were omitted" ) return ( f"Some nodes are missing names in corosync.conf, {note}. " "Edit corosync.conf and make sure all nodes have their name set." ) @dataclass(frozen=True) class CorosyncConfigMissingIdsOfNodes(ReportItemMessage): """ Some nodes in corosync.conf do not have their id set """ _code = codes.COROSYNC_CONFIG_MISSING_IDS_OF_NODES @property def message(self) -> str: return ( "Some nodes are missing IDs in corosync.conf. " "Edit corosync.conf and make sure all nodes have their nodeid set." ) @dataclass(frozen=True) class CorosyncConfigNoNodesDefined(ReportItemMessage): """ No nodes found in corosync.conf """ _code = codes.COROSYNC_CONFIG_NO_NODES_DEFINED @property def message(self) -> str: return "No nodes found in corosync.conf" @dataclass(frozen=True) class CorosyncOptionsIncompatibleWithQdevice(ReportItemMessage): """ Cannot set specified corosync options when qdevice is in use options -- incompatible options names """ options: List[str] _code = codes.COROSYNC_OPTIONS_INCOMPATIBLE_WITH_QDEVICE @property def message(self) -> str: return ( "These options cannot be set when the cluster uses a quorum " "device: {}" ).format(format_list(self.options)) @dataclass(frozen=True) class CorosyncClusterNameInvalidForGfs2(ReportItemMessage): """ Chosen cluster name will prevent using GFS2 volumes in the cluster cluster_name -- the entered cluster name max_length -- maximal cluster name length supported by GFS2 allowed_characters -- allowed cluster name characters supported by GFS2 """ cluster_name: str max_length: int allowed_characters: str _code = codes.COROSYNC_CLUSTER_NAME_INVALID_FOR_GFS2 @property def message(self) -> str: return ( f"Chosen cluster name '{self.cluster_name}' will prevent mounting " f"GFS2 volumes in the cluster, use at most {self.max_length} " f"of {self.allowed_characters} characters; you may safely " f"override this if you do not intend to use GFS2" ) @dataclass(frozen=True) class CorosyncBadNodeAddressesCount(ReportItemMessage): """ Wrong number of addresses set for a corosync node. actual_count -- how many addresses set for a node min_count -- minimal allowed addresses count max_count -- maximal allowed addresses count node_name -- optionally specify node name node_index -- optionally specify node index (helps to identify a node if a name is missing) """ actual_count: int min_count: int max_count: int node_name: Optional[str] = None node_index: Optional[int] = None _code = codes.COROSYNC_BAD_NODE_ADDRESSES_COUNT @property def message(self) -> str: if self.min_count == self.max_count: template = ( "{max_count} {addr_allowed} must be specified for a node, " "{actual_count} {addr_specified} specified{node_desc}" ) else: template = ( "At least {min_count} and at most {max_count} {addr_allowed} " "must be specified for a node, {actual_count} " "{addr_specified} specified{node_desc}" ) node_template = " for node '{}'" return template.format( node_desc=( format_optional(self.node_name, node_template) or format_optional(self.node_index, node_template) ), min_count=self.min_count, max_count=self.max_count, actual_count=self.actual_count, addr_allowed=format_plural(self.max_count, "address"), addr_specified=format_plural(self.actual_count, "address"), ) @dataclass(frozen=True) class CorosyncIpVersionMismatchInLinks(ReportItemMessage): """ Mixing IPv4 and IPv6 in one or more links, which is not allowed link_numbers -- numbers of links with mismatched IP versions """ link_numbers: List[str] = field(default_factory=list) _code = codes.COROSYNC_IP_VERSION_MISMATCH_IN_LINKS @property def message(self) -> str: links = format_optional( (format_list(self.link_numbers) if self.link_numbers else ""), " on link(s): {}", ) return ( "Using both IPv4 and IPv6 on one link is not allowed; please, use " f"either IPv4 or IPv6{links}" ) @dataclass(frozen=True) class CorosyncAddressIpVersionWrongForLink(ReportItemMessage): """ Cannot use an address in a link as it does not match the link's IP version. address -- a provided address expected_address_type -- an address type used in a link link_number -- number of the link """ address: str expected_address_type: str # Using Union is a bad practice as it may make deserialization impossible. # It works for int and str, though, as they are distinguishable. Code was # historically putting either of int and str in here. We need the Union here # for backward compatibility reasons. link_number: Optional[Union[int, str]] = None _code = codes.COROSYNC_ADDRESS_IP_VERSION_WRONG_FOR_LINK @property def message(self) -> str: link = format_optional(self.link_number, "link '{}'", "the link") return ( f"Address '{self.address}' cannot be used in {link} " f"because the link uses {self.expected_address_type} addresses" ) @dataclass(frozen=True) class CorosyncLinkNumberDuplication(ReportItemMessage): """ Trying to set one link_number for more links, link numbers must be unique link_number_list -- list of nonunique link numbers """ link_number_list: List[str] _code = codes.COROSYNC_LINK_NUMBER_DUPLICATION @property def message(self) -> str: nums = format_list(sorted(self.link_number_list, key=_key_numeric)) return f"Link numbers must be unique, duplicate link numbers: {nums}" @dataclass(frozen=True) class CorosyncNodeAddressCountMismatch(ReportItemMessage): """ Nodes do not have the same number of addresses dict node_addr_count -- key: node name, value: number of addresses """ node_addr_count: Mapping[str, int] _code = codes.COROSYNC_NODE_ADDRESS_COUNT_MISMATCH @property def message(self) -> str: count_node: Dict[int, List[str]] = defaultdict(list) for node_name, count in self.node_addr_count.items(): count_node[count].append(node_name) parts = ["All nodes must have the same number of addresses"] # List most common number of addresses first. for count, nodes in sorted( count_node.items(), key=lambda pair: len(pair[1]), reverse=True ): parts.append( "{node} {nodes} {has} {count} {address}".format( node=format_plural(nodes, "node"), nodes=format_list(nodes), has=format_plural(nodes, "has"), count=count, address=format_plural(count, "address"), ) ) return "; ".join(parts) @dataclass(frozen=True) class NodeAddressesAlreadyExist(ReportItemMessage): """ Trying add node(s) with addresses already used by other nodes address_list -- list of specified already existing addresses """ address_list: List[str] _code = codes.NODE_ADDRESSES_ALREADY_EXIST @property def message(self) -> str: pluralize = partial(format_plural, self.address_list) return ( "Node {address} {addr_list} {_is} already used by existing nodes; " "please, use other {address}" ).format( address=pluralize("address"), addr_list=format_list(self.address_list), _is=pluralize("is"), ) @dataclass(frozen=True) class NodeAddressesCannotBeEmpty(ReportItemMessage): """ Trying to set an empty node address or remove a node address in an update node_name_list -- list of node names with empty addresses """ node_name_list: List[str] _code = codes.NODE_ADDRESSES_CANNOT_BE_EMPTY @property def message(self) -> str: return ( "Empty address set for {node} {node_list}, an address cannot be " "empty" ).format( node=format_plural(self.node_name_list, "node"), node_list=format_list(self.node_name_list), ) @dataclass(frozen=True) class NodeAddressesDuplication(ReportItemMessage): """ Trying to set one address for more nodes or links, addresses must be unique address_list -- list of nonunique addresses """ address_list: List[str] _code = codes.NODE_ADDRESSES_DUPLICATION @property def message(self) -> str: addrs = format_list(self.address_list) return f"Node addresses must be unique, duplicate addresses: {addrs}" @dataclass(frozen=True) class NodeNamesAlreadyExist(ReportItemMessage): """ Trying add node(s) with name(s) already used by other nodes name_list -- list of specified already used node names """ name_list: List[str] _code = codes.NODE_NAMES_ALREADY_EXIST @property def message(self) -> str: pluralize = partial(format_plural, self.name_list) return ( "Node {name} {name_list} {_is} already used by existing nodes; " "please, use other {name}" ).format( name=pluralize("name"), name_list=format_list(self.name_list), _is=pluralize("is"), ) @dataclass(frozen=True) class NodeNamesDuplication(ReportItemMessage): """ Trying to set one node name for more nodes, node names must be unique name_list -- list of nonunique node names """ name_list: List[str] _code = codes.NODE_NAMES_DUPLICATION @property def message(self) -> str: names = format_list(self.name_list) return f"Node names must be unique, duplicate names: {names}" @dataclass(frozen=True) class CorosyncNodesMissing(ReportItemMessage): """ No nodes have been specified """ _code = codes.COROSYNC_NODES_MISSING @property def message(self) -> str: return "No nodes have been specified" @dataclass(frozen=True) class CorosyncTooManyLinksOptions(ReportItemMessage): """ Options for more links than defined by nodes' addresses have been specified links_options_count -- options for how many links have been specified links_count -- for how many links is defined """ links_options_count: int links_count: int _code = codes.COROSYNC_TOO_MANY_LINKS_OPTIONS @property def message(self) -> str: return ( "Cannot specify options for more links " f"({self.links_options_count}) than how many is defined by " f"number of addresses per node ({self.links_count})" ) @dataclass(frozen=True) class CorosyncCannotAddRemoveLinksBadTransport(ReportItemMessage): """ Cannot add or remove corosync links, used transport does not allow that actual_transport -- transport used in the cluster required_transports -- transports allowing links to be added / removed add_or_not_remove -- True for add, False for remove """ actual_transport: str required_transports: List[str] add_or_not_remove: bool _code = codes.COROSYNC_CANNOT_ADD_REMOVE_LINKS_BAD_TRANSPORT @property def message(self) -> str: action = "adding" if self.add_or_not_remove else "removing" return ( f"Cluster is using {self.actual_transport} transport which does " f"not support {action} links" ) # TODO: add_or_note_move should be changed to an action @dataclass(frozen=True) class CorosyncCannotAddRemoveLinksNoLinksSpecified(ReportItemMessage): """ Cannot add or remove links, no links were specified add_or_not_remove -- True for add, False for remove """ add_or_not_remove: bool _code = codes.COROSYNC_CANNOT_ADD_REMOVE_LINKS_NO_LINKS_SPECIFIED @property def message(self) -> str: return "Cannot {action} links, no links to {action} specified".format( action=("add" if self.add_or_not_remove else "remove"), ) @dataclass(frozen=True) class CorosyncCannotAddRemoveLinksTooManyFewLinks(ReportItemMessage): """ Cannot add or remove links, link count would exceed allowed limits links_change_count -- how many links to add / remove links_new_count -- how many links would be defined after the action links_limit_count -- maximal / minimal number of links allowed add_or_not_remove -- True for add, False for remove """ links_change_count: int links_new_count: int links_limit_count: int add_or_not_remove: bool _code = codes.COROSYNC_CANNOT_ADD_REMOVE_LINKS_TOO_MANY_FEW_LINKS @property def message(self) -> str: return ( "Cannot {action} {links_change_count} {link_change}, there " "would be {links_new_count} {link_new} defined which is " "{more_less} than allowed number of {links_limit_count} " "{link_limit}" ).format( links_change_count=self.links_change_count, links_new_count=self.links_new_count, links_limit_count=self.links_limit_count, action=("add" if self.add_or_not_remove else "remove"), more_less=("more" if self.add_or_not_remove else "less"), link_change=format_plural(self.links_change_count, "link"), link_new=format_plural(self.links_new_count, "link"), link_limit=format_plural(self.links_limit_count, "link"), ) @dataclass(frozen=True) class CorosyncLinkAlreadyExistsCannotAdd(ReportItemMessage): """ Cannot add a link with specified linknumber as it already exists """ link_number: str _code = codes.COROSYNC_LINK_ALREADY_EXISTS_CANNOT_ADD @property def message(self) -> str: return f"Cannot add link '{self.link_number}', it already exists" @dataclass(frozen=True) class CorosyncLinkDoesNotExistCannotRemove(ReportItemMessage): """ Cannot remove links which don't exist link_list -- links to remove which don't exist existing_link_list -- linknumbers of existing links """ link_list: List[str] existing_link_list: List[str] _code = codes.COROSYNC_LINK_DOES_NOT_EXIST_CANNOT_REMOVE @property def message(self) -> str: return ( "Cannot remove non-existent {link} {to_remove}, existing links: " "{existing}" ).format( link=format_plural(self.link_list, "link"), to_remove=format_list(self.link_list), existing=format_list(self.existing_link_list), ) @dataclass(frozen=True) class CorosyncLinkDoesNotExistCannotUpdate(ReportItemMessage): """ Cannot set options for the defined link because the link does not exist link_number -- number of the link to be updated existing_link_list -- linknumbers of existing links """ # Using Union is a bad practice as it may make deserialization impossible. # It works for int and str, though, as they are distinguishable. Code was # historically putting either of int and str in here. We need the Union here # for backward compatibility reasons. link_number: Union[int, str] existing_link_list: List[str] _code = codes.COROSYNC_LINK_DOES_NOT_EXIST_CANNOT_UPDATE @property def message(self) -> str: link_list = format_list(self.existing_link_list) return ( f"Cannot set options for non-existent link '{self.link_number}', " f"existing links: {link_list}" ) @dataclass(frozen=True) class CorosyncTransportUnsupportedOptions(ReportItemMessage): """ A type of options is not supported with the given transport """ option_type: str actual_transport: str required_transports: List[str] _code = codes.COROSYNC_TRANSPORT_UNSUPPORTED_OPTIONS @property def message(self) -> str: required_transports = format_list(self.required_transports) return ( f"The {self.actual_transport} transport does not support " f"'{self.option_type}' options, use {required_transports} transport" ) @dataclass(frozen=True) class ClusterUuidAlreadySet(ReportItemMessage): """ Cluster UUID has already been set in corosync.conf """ _code = codes.CLUSTER_UUID_ALREADY_SET @property def message(self) -> str: return "Cluster UUID has already been set" @dataclass(frozen=True) class QdeviceAlreadyDefined(ReportItemMessage): """ Qdevice is already set up in a cluster, when it was expected not to be """ _code = codes.QDEVICE_ALREADY_DEFINED @property def message(self) -> str: return "quorum device is already defined" @dataclass(frozen=True) class QdeviceNotDefined(ReportItemMessage): """ Qdevice is not set up in a cluster, when it was expected to be """ _code = codes.QDEVICE_NOT_DEFINED @property def message(self) -> str: return "no quorum device is defined in this cluster" @dataclass(frozen=True) class QdeviceClientReloadStarted(ReportItemMessage): """ Qdevice client configuration is about to be reloaded on nodes """ _code = codes.QDEVICE_CLIENT_RELOAD_STARTED @property def message(self) -> str: return "Reloading qdevice configuration on nodes..." @dataclass(frozen=True) class QdeviceAlreadyInitialized(ReportItemMessage): """ Cannot create qdevice on local host, it has been already created model -- qdevice model """ model: str _code = codes.QDEVICE_ALREADY_INITIALIZED @property def message(self) -> str: return f"Quorum device '{self.model}' has been already initialized" @dataclass(frozen=True) class QdeviceNotInitialized(ReportItemMessage): """ Cannot work with qdevice on local host, it has not been created yet model -- qdevice model """ model: str _code = codes.QDEVICE_NOT_INITIALIZED @property def message(self) -> str: return f"Quorum device '{self.model}' has not been initialized yet" @dataclass(frozen=True) class QdeviceInitializationSuccess(ReportItemMessage): """ qdevice was successfully initialized on local host model -- qdevice model """ model: str _code = codes.QDEVICE_INITIALIZATION_SUCCESS @property def message(self) -> str: return f"Quorum device '{self.model}' initialized" @dataclass(frozen=True) class QdeviceInitializationError(ReportItemMessage): """ An error occurred when creating qdevice on local host model -- qdevice model reason -- an error message """ model: str reason: str _code = codes.QDEVICE_INITIALIZATION_ERROR @property def message(self) -> str: return ( f"Unable to initialize quorum device '{self.model}': {self.reason}" ) @dataclass(frozen=True) class QdeviceCertificateDistributionStarted(ReportItemMessage): """ Qdevice certificates are about to be set up on nodes """ _code = codes.QDEVICE_CERTIFICATE_DISTRIBUTION_STARTED @property def message(self) -> str: return "Setting up qdevice certificates on nodes..." @dataclass(frozen=True) class QdeviceCertificateAcceptedByNode(ReportItemMessage): """ Qdevice certificates have been saved to a node node -- node on which certificates have been saved """ node: str _code = codes.QDEVICE_CERTIFICATE_ACCEPTED_BY_NODE @property def message(self) -> str: return f"{self.node}: Succeeded" @dataclass(frozen=True) class QdeviceCertificateRemovalStarted(ReportItemMessage): """ Qdevice certificates are about to be removed from nodes """ _code = codes.QDEVICE_CERTIFICATE_REMOVAL_STARTED @property def message(self) -> str: return "Removing qdevice certificates from nodes..." @dataclass(frozen=True) class QdeviceCertificateRemovedFromNode(ReportItemMessage): """ Qdevice certificates have been removed from a node node -- node on which certificates have been deleted """ node: str _code = codes.QDEVICE_CERTIFICATE_REMOVED_FROM_NODE @property def message(self) -> str: return f"{self.node}: Succeeded" @dataclass(frozen=True) class QdeviceCertificateImportError(ReportItemMessage): """ An error occurred when importing qdevice certificate to a node reason -- an error message """ reason: str _code = codes.QDEVICE_CERTIFICATE_IMPORT_ERROR @property def message(self) -> str: return f"Unable to import quorum device certificate: {self.reason}" @dataclass(frozen=True) class QdeviceCertificateReadError(ReportItemMessage): """ An error occurred when reading qdevice / qnetd certificate database reason -- an error message """ reason: str _code = codes.QDEVICE_CERTIFICATE_READ_ERROR @property def message(self) -> str: return f"Unable to read quorum device certificate: {self.reason}" @dataclass(frozen=True) class QdeviceCertificateBadFormat(ReportItemMessage): """ Qdevice / qnetd certificate has an unexpected format """ _code = codes.QDEVICE_CERTIFICATE_BAD_FORMAT @property def message(self) -> str: return "Unable to parse quorum device certificate" @dataclass(frozen=True) class QdeviceCertificateSignError(ReportItemMessage): """ an error occurred when signing qdevice certificate reason -- an error message """ reason: str _code = codes.QDEVICE_CERTIFICATE_SIGN_ERROR @property def message(self) -> str: return f"Unable to sign quorum device certificate: {self.reason}" @dataclass(frozen=True) class QdeviceDestroySuccess(ReportItemMessage): """ Qdevice configuration successfully removed from local host model -- qdevice model """ model: str _code = codes.QDEVICE_DESTROY_SUCCESS @property def message(self) -> str: return f"Quorum device '{self.model}' configuration files removed" @dataclass(frozen=True) class QdeviceDestroyError(ReportItemMessage): """ An error occurred when removing qdevice configuration from local host model -- qdevice model reason -- an error message """ model: str reason: str _code = codes.QDEVICE_DESTROY_ERROR @property def message(self) -> str: return f"Unable to destroy quorum device '{self.model}': {self.reason}" @dataclass(frozen=True) class QdeviceNotRunning(ReportItemMessage): """ Qdevice is expected to be running but is not running model -- qdevice model """ model: str _code = codes.QDEVICE_NOT_RUNNING @property def message(self) -> str: return f"Quorum device '{self.model}' is not running" @dataclass(frozen=True) class QdeviceGetStatusError(ReportItemMessage): """ Unable to get runtime status of qdevice model -- qdevice model reason -- an error message """ model: str reason: str _code = codes.QDEVICE_GET_STATUS_ERROR @property def message(self) -> str: return ( f"Unable to get status of quorum device '{self.model}': " f"{self.reason}" ) @dataclass(frozen=True) class QdeviceUsedByClusters(ReportItemMessage): """ Qdevice is currently being used by clusters, cannot stop it unless forced """ clusters: List[str] _code = codes.QDEVICE_USED_BY_CLUSTERS @property def message(self) -> str: cluster_list = format_list(self.clusters) return ( "Quorum device is currently being used by cluster(s): " f"{cluster_list}" ) @dataclass(frozen=True) class IdAlreadyExists(ReportItemMessage): """ Specified id already exists in CIB and cannot be used for a new CIB object id -- existing id """ id: str # pylint: disable=invalid-name _code = codes.ID_ALREADY_EXISTS @property def message(self) -> str: return f"'{self.id}' already exists" @dataclass(frozen=True) class IdBelongsToUnexpectedType(ReportItemMessage): """ Specified id exists but for another element than expected. For example user wants to create resource in group that is specifies by id. But id does not belong to group. """ id: str # pylint: disable=invalid-name expected_types: List[str] current_type: str _code = codes.ID_BELONGS_TO_UNEXPECTED_TYPE @property def message(self) -> str: expected_type = _typelist_to_string(self.expected_types, article=True) return f"'{self.id}' is not {expected_type}" @dataclass(frozen=True) class ObjectWithIdInUnexpectedContext(ReportItemMessage): """ Object specified by object_type (tag) and object_id exists but not inside given context (expected_context_type, expected_context_id). """ object_type: str object_id: str expected_context_type: str expected_context_id: str _code = codes.OBJECT_WITH_ID_IN_UNEXPECTED_CONTEXT @property def message(self) -> str: context_type = _type_to_string(self.expected_context_type) if self.expected_context_id: context = f"{context_type} '{self.expected_context_id}'" else: context = f"'{context_type}'" object_type = _type_to_string(self.object_type) return ( f"{object_type} '{self.object_id}' exists but does not belong to " f"{context}" ) @dataclass(frozen=True) class IdNotFound(ReportItemMessage): """ Specified id does not exist in CIB, user referenced a nonexisting id id -- specified id expected_types -- list of id's roles - expected types with the id context_type -- context_id's role / type context_id -- specifies the search area """ id: str # pylint: disable=invalid-name expected_types: List[str] context_type: str = "" context_id: str = "" _code = codes.ID_NOT_FOUND @property def message(self) -> str: desc = format_optional(_typelist_to_string(self.expected_types)) if not self.context_type or not self.context_id: return f"{desc}'{self.id}' does not exist" return ( f"there is no {desc}'{self.id}' in the {self.context_type} " f"'{self.context_id}'" ) @dataclass(frozen=True) class ResourceBundleAlreadyContainsAResource(ReportItemMessage): """ The bundle already contains a resource, another one caanot be added bundle_id -- id of the bundle resource_id -- id of the resource already contained in the bundle """ bundle_id: str resource_id: str _code = codes.RESOURCE_BUNDLE_ALREADY_CONTAINS_A_RESOURCE @property def message(self) -> str: return ( f"bundle '{self.bundle_id}' already contains resource " f"'{self.resource_id}', a bundle may contain at most one resource" ) @dataclass(frozen=True) class CannotGroupResourceWrongType(ReportItemMessage): """ Cannot put a resource into a group as the resource or its parent are of an unsupported type resource_id -- id of the element which cannot be put into a group resource_type -- tag of the element which cannot be put into a group parent_id -- id of the parent element which cannot be put into a group parent_type -- tag of the parent element which cannot be put into a group """ resource_id: str resource_type: str parent_id: Optional[str] parent_type: Optional[str] _code = codes.CANNOT_GROUP_RESOURCE_WRONG_TYPE @property def message(self) -> str: if self.parent_id and self.parent_type: return ( "'{resource_id}' cannot be put into a group because its parent " "'{parent_id}' is {_type_article} resource" ).format( resource_id=self.resource_id, parent_id=self.parent_id, _type_article=_type_to_string(self.parent_type, article=True), ) return ( "'{resource_id}' is {_type_article} resource, {_type} " "resources cannot be put into a group" ).format( resource_id=self.resource_id, _type_article=_type_to_string(self.resource_type, article=True), _type=_type_to_string(self.resource_type, article=False), ) @dataclass(frozen=True) class UnableToGetResourceOperationDigests(ReportItemMessage): """ Unable to get resource digests from pacemaker crm_resource tool. output -- stdout and stderr from crm_resource """ output: str _code = codes.UNABLE_TO_GET_RESOURCE_OPERATION_DIGESTS @property def message(self) -> str: return f"unable to get resource operation digests:\n{self.output}" @dataclass(frozen=True) class CloningStonithResourcesHasNoEffect(ReportItemMessage): """ Reject cloning of stonith resources with an explanation. stonith_id_list -- ids of stonith resources group_id -- optional id of a group containing stonith resources """ stonith_id_list: List[str] group_id: Optional[str] = None _code = codes.CLONING_STONITH_RESOURCES_HAS_NO_EFFECT @property def message(self) -> str: resources = format_plural(self.stonith_id_list, "resource") group = ( f"Group '{self.group_id}' contains stonith {resources}. " if self.group_id else "" ) stonith_list = format_list(self.stonith_id_list) return ( f"{group}No need to clone stonith {resources} " f"{stonith_list}, any node can use a stonith resource " "(unless specifically banned) regardless of whether the stonith " "resource is running on that node or not" ) @dataclass(frozen=True) class StonithResourcesDoNotExist(ReportItemMessage): """ specified stonith resource doesn't exist (e.g. when creating in constraints) stoniths -- list of specified stonith id """ stonith_ids: List[str] _code = codes.STONITH_RESOURCES_DO_NOT_EXIST @property def message(self) -> str: stoniths = format_list(self.stonith_ids) return f"Stonith resource(s) {stoniths} do not exist" @dataclass(frozen=True) class StonithRestartlessUpdateOfScsiDevicesNotSupported(ReportItemMessage): """ Pacemaker does not support the digests option for calculation of digests needed for restartless update of scsi devices. """ _code = codes.STONITH_RESTARTLESS_UPDATE_OF_SCSI_DEVICES_NOT_SUPPORTED @property def message(self) -> str: return ( "Restartless update of scsi devices is not supported, please " "upgrade pacemaker" ) @dataclass(frozen=True) class StonithRestartlessUpdateUnsupportedAgent(ReportItemMessage): """ Specified resource is not supported for scsi devices update. resource_id -- resource id resource_type -- resource type supported_stonith_types -- list of supported stonith types """ resource_id: str resource_type: str supported_stonith_types: List[str] _code = codes.STONITH_RESTARTLESS_UPDATE_UNSUPPORTED_AGENT @property def message(self) -> str: return ( "Resource '{resource_id}' is not a stonith resource or its type " "'{resource_type}' is not supported for devices update. Supported " "{_type}: {supported_types}" ).format( resource_id=self.resource_id, resource_type=self.resource_type, _type=format_plural(self.supported_stonith_types, "type"), supported_types=format_list(self.supported_stonith_types), ) @dataclass(frozen=True) class StonithUnfencingFailed(ReportItemMessage): """ Unfencing failed on a cluster node. """ reason: str _code = codes.STONITH_UNFENCING_FAILED @property def message(self) -> str: return f"Unfencing failed:\n{self.reason}" @dataclass(frozen=True) class StonithUnfencingDeviceStatusFailed(ReportItemMessage): """ Unfencing failed on a cluster node. """ device: str reason: str _code = codes.STONITH_UNFENCING_DEVICE_STATUS_FAILED @property def message(self) -> str: return ( "Unfencing failed, unable to check status of device " f"'{self.device}': {self.reason}" ) @dataclass(frozen=True) class StonithUnfencingSkippedDevicesFenced(ReportItemMessage): """ Unfencing skipped on a cluster node, because fenced devices were found on the node. """ devices: List[str] _code = codes.STONITH_UNFENCING_SKIPPED_DEVICES_FENCED @property def message(self) -> str: return ( "Unfencing skipped, {device_pl} {devices} {is_pl} fenced" ).format( device_pl=format_plural(self.devices, "device"), devices=format_list(self.devices), is_pl=format_plural(self.devices, "is", "are"), ) @dataclass(frozen=True) class StonithRestartlessUpdateUnableToPerform(ReportItemMessage): """ Unable to update scsi devices without restart for various reason reason -- reason reason_type -- type for reason differentiation """ reason: str reason_type: types.StonithRestartlessUpdateUnableToPerformReason = ( const.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM_REASON_OTHER ) _code = codes.STONITH_RESTARTLESS_UPDATE_UNABLE_TO_PERFORM @property def message(self) -> str: return ( "Unable to perform restartless update of scsi devices: " f"{self.reason}" ) @dataclass(frozen=True) class StonithRestartlessUpdateMissingMpathKeys(ReportItemMessage): """ Unable to update mpath devices because reservation keys for some nodes are missing. pcmk_host_map_value -- a string which specifies nodes to keys map missing_nodes -- nodes which do not have keys """ pcmk_host_map_value: Optional[str] missing_nodes: List[str] _code = codes.STONITH_RESTARTLESS_UPDATE_MISSING_MPATH_KEYS @property def message(self) -> str: if not self.pcmk_host_map_value: return "Missing mpath reservation keys, 'pcmk_host_map' not set" keys = format_plural(self.missing_nodes, "key") nodes = format_plural(self.missing_nodes, "node") node_list = format_list(self.missing_nodes) node_names = f": {node_list}," if not self.missing_nodes: keys = "keys" nodes = "nodes" node_names = "" return ( f"Missing mpath reservation {keys} for {nodes}{node_names} in " f"'pcmk_host_map' value: '{self.pcmk_host_map_value}'" ) @dataclass(frozen=True) class ResourceRunningOnNodes(ReportItemMessage): """ Resource is running on some nodes. Taken from cluster state. resource_id -- represent the resource """ resource_id: str roles_with_nodes: Dict[str, List[str]] _code = codes.RESOURCE_RUNNING_ON_NODES @property def message(self) -> str: role_label_map = { "Started": "running", } state_info: Dict[str, List[str]] = {} for state, node_list in self.roles_with_nodes.items(): state_info.setdefault( role_label_map.get(state, state.lower()), [] ).extend(node_list) return "resource '{resource_id}' is {detail_list}".format( resource_id=self.resource_id, detail_list="; ".join( sorted( [ "{run_type} on {node} {node_list}".format( run_type=run_type, node=format_plural(node_list, "node"), node_list=format_list(node_list), ) for run_type, node_list in state_info.items() ] ) ), ) @dataclass(frozen=True) class ResourceDoesNotRun(ReportItemMessage): """ Resource is not running on any node. Taken from cluster state. resource_id -- represent the resource """ resource_id: str _code = codes.RESOURCE_DOES_NOT_RUN @property def message(self) -> str: return f"resource '{self.resource_id}' is not running on any node" @dataclass(frozen=True) class ResourceIsGuestNodeAlready(ReportItemMessage): """ The resource is already used as guest node (i.e. has meta attribute remote-node). resource_id -- id of the resource that is guest node """ resource_id: str _code = codes.RESOURCE_IS_GUEST_NODE_ALREADY @property def message(self) -> str: return f"the resource '{self.resource_id}' is already a guest node" @dataclass(frozen=True) class ResourceIsUnmanaged(ReportItemMessage): """ The resource the user works with is unmanaged (e.g. in enable/disable) resource_id -- id of the unmanaged resource """ resource_id: str _code = codes.RESOURCE_IS_UNMANAGED @property def message(self) -> str: return f"'{self.resource_id}' is unmanaged" @dataclass(frozen=True) class ResourceManagedNoMonitorEnabled(ReportItemMessage): """ The resource which was set to managed mode has no monitor operations enabled resource_id -- id of the resource """ resource_id: str _code = codes.RESOURCE_MANAGED_NO_MONITOR_ENABLED @property def message(self) -> str: return ( f"Resource '{self.resource_id}' has no enabled monitor operations" ) @dataclass(frozen=True) class CibLoadError(ReportItemMessage): """ Cannot load cib from cibadmin, cibadmin exited with non-zero code reason -- error description """ reason: str _code = codes.CIB_LOAD_ERROR @property def message(self) -> str: return "unable to get cib" @dataclass(frozen=True) class CibLoadErrorGetNodesForValidation(ReportItemMessage): """ Unable to load CIB, unable to get remote and guest nodes for validation """ _code = codes.CIB_LOAD_ERROR_GET_NODES_FOR_VALIDATION @property def message(self) -> str: return ( "Unable to load CIB to get guest and remote nodes from it, " "those nodes cannot be considered in configuration validation" ) @dataclass(frozen=True) class CibLoadErrorScopeMissing(ReportItemMessage): """ Cannot load cib from cibadmin, specified scope is missing in the cib scope -- requested cib scope reason -- error description """ scope: str reason: str _code = codes.CIB_LOAD_ERROR_SCOPE_MISSING @property def message(self) -> str: return f"unable to get cib, scope '{self.scope}' not present in cib" @dataclass(frozen=True) class CibLoadErrorBadFormat(ReportItemMessage): """ Cib does not conform to the schema """ reason: str _code = codes.CIB_LOAD_ERROR_BAD_FORMAT @property def message(self) -> str: return f"unable to get cib, {self.reason}" @dataclass(frozen=True) class CibCannotFindMandatorySection(ReportItemMessage): """ CIB is missing a section which is required to be present section -- name of the missing section (element name or path) """ section: str _code = codes.CIB_CANNOT_FIND_MANDATORY_SECTION @property def message(self) -> str: return f"Unable to get '{self.section}' section of cib" @dataclass(frozen=True) class CibPushError(ReportItemMessage): """ Cannot push cib to cibadmin, cibadmin exited with non-zero code reason -- error description pushed_cib -- cib which failed to be pushed """ reason: str pushed_cib: str _code = codes.CIB_PUSH_ERROR @property def message(self) -> str: return f"Unable to update cib\n{self.reason}\n{self.pushed_cib}" @dataclass(frozen=True) class CibSaveTmpError(ReportItemMessage): """ Cannot save CIB into a temporary file reason -- error description """ reason: str _code = codes.CIB_SAVE_TMP_ERROR @property def message(self) -> str: return f"Unable to save CIB to a temporary file: {self.reason}" @dataclass(frozen=True) class CibDiffError(ReportItemMessage): """ Cannot obtain a diff of CIBs reason -- error description cib_old -- the CIB to be diffed against cib_new -- the CIB diffed against the old cib """ reason: str cib_old: str cib_new: str _code = codes.CIB_DIFF_ERROR @property def message(self) -> str: return f"Unable to diff CIB: {self.reason}\n{self.cib_new}" @dataclass(frozen=True) class CibSimulateError(ReportItemMessage): """ Cannot simulate effects a CIB would have on a live cluster reason -- error description """ reason: str _code = codes.CIB_SIMULATE_ERROR @property def message(self) -> str: return "Unable to simulate changes in CIB{_reason}".format( _reason=format_optional(self.reason, ": {0}"), ) @dataclass(frozen=True) class CrmMonError(ReportItemMessage): """ Cannot load cluster status from crm_mon, crm_mon exited with non-zero code reason -- description of the error """ reason: str _code = codes.CRM_MON_ERROR @property def message(self) -> str: return "error running crm_mon, is pacemaker running?{reason}".format( reason=( ("\n" + "\n".join(indent(self.reason.strip().splitlines()))) if self.reason.strip() else "" ), ) @dataclass(frozen=True) class BadClusterStateFormat(ReportItemMessage): """ crm_mon xml output does not conform to the schema """ _code = codes.BAD_CLUSTER_STATE_FORMAT @property def message(self) -> str: return "cannot load cluster status, xml does not conform to the schema" @dataclass(frozen=True) class BadClusterStateData(ReportItemMessage): """ crm_mon xml output is invalid despite conforming to the schema reason -- error description """ reason: Optional[str] = None _code = codes.BAD_CLUSTER_STATE_DATA @property def message(self) -> str: return ( "Cannot load cluster status, xml does not describe valid cluster " f"status{format_optional(self.reason, template=': {}')}" ) @dataclass(frozen=True) class ClusterStatusBundleMemberIdAsImplicit(ReportItemMessage): """ Member of bundle in cluster status xml has the same id as one of the implicit resources bundle_id -- id of the bundle bad_ids -- ids of the bad members """ bundle_id: str bad_ids: list[str] _code = codes.CLUSTER_STATUS_BUNDLE_MEMBER_ID_AS_IMPLICIT @property def message(self) -> str: return ( "Skipping bundle '{bundle_id}': {resource_word} " "{bad_ids} {has} the same id as some of the " "implicit bundle resources" ).format( bundle_id=self.bundle_id, resource_word=format_plural(self.bad_ids, "resource"), bad_ids=format_list(self.bad_ids), has=format_plural(self.bad_ids, "has"), ) @dataclass(frozen=True) class WaitForIdleStarted(ReportItemMessage): """ Waiting for cluster to apply updated configuration and to settle down timeout -- wait timeout in seconds """ timeout: int _code = codes.WAIT_FOR_IDLE_STARTED @property def message(self) -> str: timeout_str = ( " (timeout: {timeout} {second_pl})".format( timeout=self.timeout, second_pl=format_plural(self.timeout, "second"), ) if self.timeout > 0 else "" ) return ( "Waiting for the cluster to apply configuration changes" f"{timeout_str}..." ) @dataclass(frozen=True) class WaitForIdleTimedOut(ReportItemMessage): """ Waiting for resources (crm_resource --wait) failed, timeout expired reason -- error description """ reason: str _code = codes.WAIT_FOR_IDLE_TIMED_OUT @property def message(self) -> str: return f"waiting timeout\n\n{self.reason}" @dataclass(frozen=True) class WaitForIdleError(ReportItemMessage): """ Waiting for resources (crm_resource --wait) failed reason -- error description """ reason: str _code = codes.WAIT_FOR_IDLE_ERROR @property def message(self) -> str: return self.reason @dataclass(frozen=True) class WaitForIdleNotLiveCluster(ReportItemMessage): """ Cannot wait for the cluster if not running with a live cluster """ _code = codes.WAIT_FOR_IDLE_NOT_LIVE_CLUSTER @property def message(self) -> str: return "Cannot use 'mocked CIB' together with 'wait'" @dataclass(frozen=True) class ResourceRestartError(ReportItemMessage): """ An error occurred when restarting a resource in pacemaker reason -- error description resource -- resource which has been restarted node -- node where the resource has been restarted """ reason: str resource: str node: Optional[str] = None _code = codes.RESOURCE_RESTART_ERROR @property def message(self) -> str: return f"Unable to restart resource '{self.resource}':\n{self.reason}" @dataclass(frozen=True) class ResourceRestartNodeIsForMultiinstanceOnly(ReportItemMessage): """ Restart can be limited to a specified node only for multiinstance resources resource -- resource to be restarted resource_type -- actual type of the resource node -- node where the resource was to be restarted """ resource: str resource_type: str node: str _code = codes.RESOURCE_RESTART_NODE_IS_FOR_MULTIINSTANCE_ONLY @property def message(self) -> str: resource_type = _type_to_string(self.resource_type, article=True) return ( "Can only restart on a specific node for a clone or bundle, " f"'{self.resource}' is {resource_type}" ) @dataclass(frozen=True) class ResourceRestartUsingParentRersource(ReportItemMessage): """ Multiinstance parent is restarted instead of a specified primitive resource -- resource which has been asked to be restarted parent -- parent resource to be restarted instead """ resource: str parent: str _code = codes.RESOURCE_RESTART_USING_PARENT_RESOURCE @property def message(self) -> str: return ( f"Restarting '{self.parent}' instead...\n" "(If a resource is a clone or bundle, you must use the clone or " "bundle instead)" ) @dataclass(frozen=True) class ResourceCleanupError(ReportItemMessage): """ An error occurred when deleting resource failed operations in pacemaker reason -- error description resource -- resource which has been cleaned up node -- node which has been cleaned up """ reason: str resource: Optional[str] = None node: Optional[str] = None _code = codes.RESOURCE_CLEANUP_ERROR @property def message(self) -> str: if self.resource: return ( "Unable to forget failed operations of resource: " f"{self.resource}\n{self.reason}" ) return f"Unable to forget failed operations of resources\n{self.reason}" @dataclass(frozen=True) class ResourceRefreshError(ReportItemMessage): """ An error occurred when deleting resource history in pacemaker reason -- error description resource -- resource which has been cleaned up node -- node which has been cleaned up """ reason: str resource: Optional[str] = None node: Optional[str] = None _code = codes.RESOURCE_REFRESH_ERROR @property def message(self) -> str: if self.resource: return ( "Unable to delete history of resource: " f"{self.resource}\n{self.reason}" ) return f"Unable to delete history of resources\n{self.reason}" @dataclass(frozen=True) class ResourceRefreshTooTimeConsuming(ReportItemMessage): """ Resource refresh would execute more than threshold operations in a cluster threshold -- current threshold for triggering this error """ threshold: int _code = codes.RESOURCE_REFRESH_TOO_TIME_CONSUMING @property def message(self) -> str: return ( "Deleting history of all resources on all nodes will execute more " f"than {self.threshold} operations in the cluster, which may " "negatively impact the responsiveness of the cluster. " "Consider specifying resource and/or node" ) @dataclass(frozen=True) class ResourceOperationIntervalDuplication(ReportItemMessage): """ More operations with the same name and the same interval appeared. Each operation with the same name (e.g. monitoring) needs to have a unique interval. dict duplications see resource operation interval duplication in pcs/lib/exchange_formats.md """ duplications: Mapping[str, List[List[str]]] _code = codes.RESOURCE_OPERATION_INTERVAL_DUPLICATION @property def message(self) -> str: return ( "multiple specification of the same operation with the same " "interval:\n" + "\n".join( [ "{0} with intervals {1}".format(name, ", ".join(intervals)) for name, intervals_list in self.duplications.items() for intervals in intervals_list ] ) ) @dataclass(frozen=True) class ResourceOperationIntervalAdapted(ReportItemMessage): """ Interval of resource operation was adopted to operation (with the same name) intervals were unique. Each operation with the same name (e.g. monitoring) need to have unique interval. """ operation_name: str original_interval: str adapted_interval: str _code = codes.RESOURCE_OPERATION_INTERVAL_ADAPTED @property def message(self) -> str: return ( f"changing a {self.operation_name} operation interval from " f"{self.original_interval} to {self.adapted_interval} to make the " "operation unique" ) @dataclass(frozen=True) class NodeNotFound(ReportItemMessage): """ Specified node does not exist node -- specified node searched_types """ node: str searched_types: List[str] = field(default_factory=list) _code = codes.NODE_NOT_FOUND @property def message(self) -> str: desc = _build_node_description(self.searched_types) return f"{desc} '{self.node}' does not appear to exist in configuration" @dataclass(frozen=True) class NodeToClearIsStillInCluster(ReportItemMessage): """ specified node is still in cluster and `crm_node --remove` should be not used node -- specified node """ node: str _code = codes.NODE_TO_CLEAR_IS_STILL_IN_CLUSTER @property def message(self) -> str: return ( f"node '{self.node}' seems to be still in the cluster; this " "command should be used only with nodes that have been removed " "from the cluster" ) @dataclass(frozen=True) class NodeRemoveInPacemakerFailed(ReportItemMessage): """ Removing nodes from pacemaker failed. node_list_to_remove -- nodes which should be removed node -- node on which operation was performed reason -- reason of failure """ node_list_to_remove: List[str] node: str = "" reason: str = "" _code = codes.NODE_REMOVE_IN_PACEMAKER_FAILED @property def message(self) -> str: return ( "{node}Unable to remove node(s) {node_list} from pacemaker{reason}" ).format( node=format_optional(self.node, "{}: "), reason=format_optional(self.reason, ": {}"), node_list=format_list(self.node_list_to_remove), ) @dataclass(frozen=True) class MultipleResultsFound(ReportItemMessage): """ Multiple result was found when something was looked for. E.g. resource for remote node. result_type -- specifies what was looked for, e.g. "resource" result_identifier_list -- contains identifiers of results e.g. resource ids search_description -- e.g. name of remote_node """ result_type: str result_identifier_list: List[str] search_description: str = "" _code = codes.MULTIPLE_RESULTS_FOUND @property def message(self) -> str: return "more than one {result_type}{desc} found: {what_found}".format( what_found=format_list(self.result_identifier_list), desc=format_optional(self.search_description, " for '{}'"), result_type=self.result_type, ) @dataclass(frozen=True) class PacemakerSimulationResult(ReportItemMessage): """ This report contains crm_simulate output. str plaintext_output -- plaintext output from crm_simulate """ plaintext_output: str _code = codes.PACEMAKER_SIMULATION_RESULT @property def message(self) -> str: return f"\nSimulation result:\n{self.plaintext_output}" @dataclass(frozen=True) class PacemakerLocalNodeNameNotFound(ReportItemMessage): """ We are unable to figure out pacemaker's local node's name reason -- error message """ reason: str _code = codes.PACEMAKER_LOCAL_NODE_NAME_NOT_FOUND @property def message(self) -> str: return f"unable to get local node name from pacemaker: {self.reason}" @dataclass(frozen=True) class ServiceActionStarted(ReportItemMessage): """ System service action started action -- started service action service -- service name or description instance -- instance of service """ action: types.ServiceAction service: str instance: str = "" _code = codes.SERVICE_ACTION_STARTED @property def message(self) -> str: action_str = _service_action_str(self.action, "ing").capitalize() instance_suffix = format_optional(self.instance, INSTANCE_SUFFIX) return f"{action_str} {self.service}{instance_suffix}..." @dataclass(frozen=True) class ServiceActionFailed(ReportItemMessage): """ System service action failed action -- failed service action service -- service name or description reason -- error message node -- node on which service has been requested to start instance -- instance of service """ action: types.ServiceAction service: str reason: str node: str = "" instance: str = "" _code = codes.SERVICE_ACTION_FAILED @property def message(self) -> str: return ( "{node_prefix}Unable to {action} {service}{instance_suffix}:" " {reason}" ).format( action=_service_action_str(self.action), service=self.service, reason=self.reason, instance_suffix=format_optional(self.instance, INSTANCE_SUFFIX), node_prefix=format_optional(self.node, NODE_PREFIX), ) @dataclass(frozen=True) class ServiceActionSucceeded(ReportItemMessage): """ System service action was successful action -- successful service action service -- service name or description node -- node on which service has been requested to start instance -- instance of service """ action: types.ServiceAction service: str node: str = "" instance: str = "" _code = codes.SERVICE_ACTION_SUCCEEDED @property def message(self) -> str: return "{node_prefix}{service}{instance_suffix} {action}".format( action=_service_action_str(self.action, "ed"), service=self.service, instance_suffix=format_optional(self.instance, INSTANCE_SUFFIX), node_prefix=format_optional(self.node, NODE_PREFIX), ) @dataclass(frozen=True) class ServiceActionSkipped(ReportItemMessage): """ System service action was skipped, no error occurred action -- skipped service action service -- service name or description reason why the start has been skipped node node on which service has been requested to start instance instance of service """ action: types.ServiceAction service: str reason: str node: str = "" instance: str = "" _code = codes.SERVICE_ACTION_SKIPPED @property def message(self) -> str: return ( "{node_prefix}not {action} {service}{instance_suffix}: {reason}" ).format( action=_service_action_str(self.action, "ing"), service=self.service, reason=self.reason, instance_suffix=format_optional(self.instance, INSTANCE_SUFFIX), node_prefix=format_optional(self.node, NODE_PREFIX), ) @dataclass(frozen=True) class ServiceUnableToDetectInitSystem(ReportItemMessage): """ Autodetection of currently used init system was not successful, therefore system service management is not be available. """ _code = codes.SERVICE_UNABLE_TO_DETECT_INIT_SYSTEM @property def message(self) -> str: return ( "Unable to detect init system. All actions related to system " "services will be skipped." ) @dataclass(frozen=True) class UnableToGetAgentMetadata(ReportItemMessage): """ There were some issues trying to get metadata of agent agent -- agent which metadata were unable to obtain reason -- reason of failure """ agent: str reason: str _code = codes.UNABLE_TO_GET_AGENT_METADATA @property def message(self) -> str: return ( f"Agent '{self.agent}' is not installed or does not provide valid" f" metadata: {self.reason}" ) @dataclass(frozen=True) class InvalidResourceAgentName(ReportItemMessage): """ The entered resource agent name is not valid. This name has the internal structure. The code needs to work with parts of this structure and fails if parts can not be obtained. name -- entered name """ name: str _code = codes.INVALID_RESOURCE_AGENT_NAME @property def message(self) -> str: return ( f"Invalid resource agent name '{self.name}'." " Use standard:provider:type when standard is 'ocf' or" " standard:type otherwise." ) @dataclass(frozen=True) class InvalidStonithAgentName(ReportItemMessage): """ The entered stonith agent name is not valid. name -- entered stonith agent name """ name: str _code = codes.INVALID_STONITH_AGENT_NAME @property def message(self) -> str: return ( f"Invalid stonith agent name '{self.name}'. Agent name cannot " "contain the ':' character, do not use the 'stonith:' prefix." ) @dataclass(frozen=True) class AgentNameGuessed(ReportItemMessage): """ Resource agent name was deduced from the entered name. Pcs supports the using of abbreviated resource agent name (e.g. ocf:heartbeat:Delay => Delay) when it can be clearly deduced. entered_name -- entered name guessed_name -- deduced name """ entered_name: str guessed_name: str _code = codes.AGENT_NAME_GUESSED @property def message(self) -> str: return ( f"Assumed agent name '{self.guessed_name}' (deduced from " f"'{self.entered_name}')" ) @dataclass(frozen=True) class AgentNameGuessFoundMoreThanOne(ReportItemMessage): """ More than one agents found based on the search string, specify one of them agent -- searched name of an agent possible_agents -- full names of agents matching the search """ agent: str possible_agents: List[str] _code = codes.AGENT_NAME_GUESS_FOUND_MORE_THAN_ONE @property def message(self) -> str: possible = format_list_custom_last_separator( self.possible_agents, " or " ) return ( f"Multiple agents match '{self.agent}', please specify full name: " f"{possible}" ) @dataclass(frozen=True) class AgentNameGuessFoundNone(ReportItemMessage): """ Specified agent doesn't exist agent -- name of the agent which doesn't exist """ agent: str _code = codes.AGENT_NAME_GUESS_FOUND_NONE @property def message(self) -> str: return ( f"Unable to find agent '{self.agent}', try specifying its full name" ) @dataclass(frozen=True) class AgentImplementsUnsupportedOcfVersion(ReportItemMessage): """ Specified agent implements OCF version not supported by pcs agent -- name of the agent ocf_version -- OCF version implemented by the agent supported_versions -- OCF versions supported by pcs """ agent: str ocf_version: str supported_versions: List[str] _code = codes.AGENT_IMPLEMENTS_UNSUPPORTED_OCF_VERSION @property def message(self) -> str: _version = format_plural(self.supported_versions, "version") _is = format_plural(self.supported_versions, "is") _version_list = format_list(self.supported_versions) return ( f"Unable to process agent '{self.agent}' as it implements " f"unsupported OCF version '{self.ocf_version}', supported " f"{_version} {_is}: {_version_list}" ) @dataclass(frozen=True) class AgentGenericError(ReportItemMessage): """ Unspecifed error related to resource / fence agent agent -- name of the agent """ agent: str @property def message(self) -> str: return f"Unable to load agent '{self.agent}'" @dataclass(frozen=True) class OmittingNode(ReportItemMessage): """ Warning that specified node will be omitted in following actions node -- node name """ node: str _code = codes.OMITTING_NODE @property def message(self) -> str: return f"Omitting node '{self.node}'" @dataclass(frozen=True) class SbdCheckStarted(ReportItemMessage): """ Info that SBD pre-enabling checks started """ _code = codes.SBD_CHECK_STARTED @property def message(self) -> str: return "Running SBD pre-enabling checks..." @dataclass(frozen=True) class SbdCheckSuccess(ReportItemMessage): """ info that SBD pre-enabling check finished without issues on specified node node -- node name """ node: str _code = codes.SBD_CHECK_SUCCESS @property def message(self) -> str: return f"{self.node}: SBD pre-enabling checks done" @dataclass(frozen=True) class SbdConfigDistributionStarted(ReportItemMessage): """ Distribution of SBD configuration started """ _code = codes.SBD_CONFIG_DISTRIBUTION_STARTED @property def message(self) -> str: return "Distributing SBD config..." @dataclass(frozen=True) class SbdConfigAcceptedByNode(ReportItemMessage): """ info that SBD configuration has been saved successfully on specified node node -- node name """ node: str _code = codes.SBD_CONFIG_ACCEPTED_BY_NODE @property def message(self) -> str: return f"{self.node}: SBD config saved" @dataclass(frozen=True) class UnableToGetSbdConfig(ReportItemMessage): """ Unable to get SBD config from specified node (communication or parsing error) node -- node name reason -- reason of failure """ node: str reason: str _code = codes.UNABLE_TO_GET_SBD_CONFIG @property def message(self) -> str: return ( "Unable to get SBD configuration from node '{node}'{reason}" ).format( node=self.node, reason=format_optional(self.reason, ": {}"), ) @dataclass(frozen=True) class SbdDeviceInitializationStarted(ReportItemMessage): """ Initialization of SBD device(s) started """ device_list: List[str] _code = codes.SBD_DEVICE_INITIALIZATION_STARTED @property def message(self) -> str: return "Initializing {device} {device_list}...".format( device=format_plural(self.device_list, "device"), device_list=format_list(self.device_list), ) @dataclass(frozen=True) class SbdDeviceInitializationSuccess(ReportItemMessage): """ Initialization of SBD device(s) succeeded """ device_list: List[str] _code = codes.SBD_DEVICE_INITIALIZATION_SUCCESS @property def message(self) -> str: device = format_plural(self.device_list, "Device") return f"{device} initialized successfully" @dataclass(frozen=True) class SbdDeviceInitializationError(ReportItemMessage): """ Initialization of SBD device failed """ device_list: List[str] reason: str _code = codes.SBD_DEVICE_INITIALIZATION_ERROR @property def message(self) -> str: return ( "Initialization of {device} {device_list} failed: {reason}" ).format( device=format_plural(self.device_list, "device"), device_list=format_list(self.device_list), reason=self.reason, ) @dataclass(frozen=True) class SbdDeviceListError(ReportItemMessage): """ Command 'sbd list' failed """ device: str reason: str _code = codes.SBD_DEVICE_LIST_ERROR @property def message(self) -> str: return ( f"Unable to get list of messages from device '{self.device}': " f"{self.reason}" ) @dataclass(frozen=True) class SbdDeviceMessageError(ReportItemMessage): """ Unable to set message 'message' on shared block device 'device' for node 'node'. """ device: str node: str sbd_message: str reason: str _code = codes.SBD_DEVICE_MESSAGE_ERROR @property def message(self) -> str: return ( f"Unable to set message '{self.sbd_message}' for node " f"'{self.node}' on device '{self.device}': {self.reason}" ) @dataclass(frozen=True) class SbdDeviceDumpError(ReportItemMessage): """ Command 'sbd dump' failed """ device: str reason: str _code = codes.SBD_DEVICE_DUMP_ERROR @property def message(self) -> str: return ( f"Unable to get SBD headers from device '{self.device}': " f"{self.reason}" ) @dataclass(frozen=True) class FilesDistributionStarted(ReportItemMessage): """ files are about to be sent to nodes file_list -- files to be sent node_list -- node names where the files are being sent """ file_list: List[str] = field(default_factory=list) node_list: List[str] = field(default_factory=list) _code = codes.FILES_DISTRIBUTION_STARTED @property def message(self) -> str: return "Sending {description}{where}".format( where=format_optional(format_list(self.node_list), " to {}"), description=format_list(self.file_list), ) @dataclass(frozen=True) class FilesDistributionSkipped(ReportItemMessage): """ Files distribution skipped due to unreachable nodes or not live cluster reason_type -- why was the action skipped (unreachable, not_live_cib) file_list -- contains description of files node_list -- where the files should have been sent to """ reason_type: types.ReasonType file_list: List[str] node_list: List[str] _code = codes.FILES_DISTRIBUTION_SKIPPED @property def message(self) -> str: return ( "Distribution of {files} to {nodes} was skipped because " "{reason}. Please, distribute the file(s) manually." ).format( files=format_list(self.file_list), nodes=format_list(self.node_list), reason=_skip_reason_to_string(self.reason_type), ) @dataclass(frozen=True) class FileDistributionSuccess(ReportItemMessage): """ A file has been successfully distributed to a node node -- name of a destination node file_description -- name (code) of a successfully put file """ node: str file_description: str _code = codes.FILE_DISTRIBUTION_SUCCESS @property def message(self) -> str: return ( f"{self.node}: successful distribution of the file " f"'{self.file_description}'" ) @dataclass(frozen=True) class FileDistributionError(ReportItemMessage): """ Cannot put a file to a specific node node -- name of a destination node file_description -- code of a file reason -- an error message """ node: str file_description: str reason: str _code = codes.FILE_DISTRIBUTION_ERROR @property def message(self) -> str: return ( f"{self.node}: unable to distribute file " f"'{self.file_description}': {self.reason}" ) @dataclass(frozen=True) class FilesRemoveFromNodesStarted(ReportItemMessage): """ files are about to be removed from nodes file_list -- files to be sent node_list -- node names the files are being removed from """ file_list: List[str] = field(default_factory=list) node_list: List[str] = field(default_factory=list) _code = codes.FILES_REMOVE_FROM_NODES_STARTED @property def message(self) -> str: return "Requesting remove {description}{where}".format( where=format_optional(format_list(self.node_list), " from {}"), description=format_list(self.file_list), ) @dataclass(frozen=True) class FilesRemoveFromNodesSkipped(ReportItemMessage): """ Files removal skipped due to unreachable nodes or not live cluster reason_type -- why was the action skipped (unreachable, not_live_cib) file_list -- contains description of files node_list -- node names the files are being removed from """ reason_type: types.ReasonType file_list: List[str] node_list: List[str] _code = codes.FILES_REMOVE_FROM_NODES_SKIPPED @property def message(self) -> str: return ( "Removing {files} from {nodes} was skipped because {reason}. " "Please, remove the file(s) manually." ).format( files=format_list(self.file_list), nodes=format_list(self.node_list), reason=_skip_reason_to_string(self.reason_type), ) @dataclass(frozen=True) class FileRemoveFromNodeSuccess(ReportItemMessage): """ files was successfully removed nodes node -- name of destination node file_description -- name (code) of successfully put files """ node: str file_description: str _code = codes.FILE_REMOVE_FROM_NODE_SUCCESS @property def message(self) -> str: return ( f"{self.node}: successful removal of the file " f"'{self.file_description}'" ) @dataclass(frozen=True) class FileRemoveFromNodeError(ReportItemMessage): """ cannot remove files from specific nodes node -- name of destination node file_description -- is file code reason -- is error message """ node: str file_description: str reason: str _code = codes.FILE_REMOVE_FROM_NODE_ERROR @property def message(self) -> str: return ( f"{self.node}: unable to remove file '{self.file_description}': " f"{self.reason}" ) @dataclass(frozen=True) class ServiceCommandsOnNodesStarted(ReportItemMessage): """ Node was requested for actions """ action_list: List[str] = field(default_factory=list) node_list: List[str] = field(default_factory=list) _code = codes.SERVICE_COMMANDS_ON_NODES_STARTED @property def message(self) -> str: return "Requesting {description}{where}".format( where=format_optional(format_list(self.node_list), " on {}"), description=format_list(self.action_list), ) @dataclass(frozen=True) class ServiceCommandsOnNodesSkipped(ReportItemMessage): """ Service actions skipped due to unreachable nodes or not live cluster reason_type -- why was the action skipped (unreachable, not_live_cib) action_list -- contains description of service actions node_list -- destinations where the action should have been executed """ reason_type: types.ReasonType action_list: List[str] node_list: List[str] _code = codes.SERVICE_COMMANDS_ON_NODES_SKIPPED @property def message(self) -> str: return ( "Running action(s) {actions} on {nodes} was skipped because " "{reason}. Please, run the action(s) manually." ).format( actions=format_list(self.action_list), nodes=format_list(self.node_list), reason=_skip_reason_to_string(self.reason_type), ) @dataclass(frozen=True) class ServiceCommandOnNodeSuccess(ReportItemMessage): """ Files was successfully distributed on nodes service_command_description -- name (code) of successfully service command """ node: str service_command_description: str _code = codes.SERVICE_COMMAND_ON_NODE_SUCCESS @property def message(self) -> str: return ( f"{self.node}: successful run of " f"'{self.service_command_description}'" ) @dataclass(frozen=True) class ServiceCommandOnNodeError(ReportItemMessage): """ Action on nodes failed service_command_description -- name (code) of successfully service command reason -- is error message """ node: str service_command_description: str reason: str _code = codes.SERVICE_COMMAND_ON_NODE_ERROR @property def message(self) -> str: return ( f"{self.node}: service command failed: " f"{self.service_command_description}: {self.reason}" ) @dataclass(frozen=True) class InvalidResponseFormat(ReportItemMessage): """ Error message that response in invalid format has been received from specified node node -- node name """ node: str _code = codes.INVALID_RESPONSE_FORMAT @property def message(self) -> str: return f"{self.node}: Invalid format of response" @dataclass(frozen=True) class SbdNotUsedCannotSetSbdOptions(ReportItemMessage): """ The cluster is not using SBD, cannot specify SBD options options -- list of specified not allowed SBD options node -- node name """ options: List[str] node: str _code = codes.SBD_NOT_USED_CANNOT_SET_SBD_OPTIONS @property def message(self) -> str: return ( "Cluster is not configured to use SBD, cannot specify SBD " "option(s) {options} for node '{node}'" ).format( options=format_list(self.options), node=self.node, ) @dataclass(frozen=True) class SbdWithDevicesNotUsedCannotSetDevice(ReportItemMessage): """ The cluster is not using SBD with devices, cannot specify a device. node -- node name """ node: str _code = codes.SBD_WITH_DEVICES_NOT_USED_CANNOT_SET_DEVICE @property def message(self) -> str: return ( "Cluster is not configured to use SBD with shared storage, cannot " f"specify SBD devices for node '{self.node}'" ) @dataclass(frozen=True) class SbdNoDeviceForNode(ReportItemMessage): """ No SBD device defined for a node when it should be node -- node name sbd_enabled_in_cluster -- additional context for displaying the error """ node: str sbd_enabled_in_cluster: bool = False _code = codes.SBD_NO_DEVICE_FOR_NODE @property def message(self) -> str: if self.sbd_enabled_in_cluster: return ( "Cluster uses SBD with shared storage so SBD devices must be " "specified for all nodes, no device specified for node " f"'{self.node}'" ) return f"No SBD device specified for node '{self.node}'" @dataclass(frozen=True) class SbdTooManyDevicesForNode(ReportItemMessage): """ More than allowed number of SBD devices specified for a node node -- node name device_list -- list of SND devices specified for the node max_devices -- maximum number of SBD devices """ node: str device_list: List[str] max_devices: int _code = codes.SBD_TOO_MANY_DEVICES_FOR_NODE @property def message(self) -> str: devices = format_list(self.device_list) return ( f"At most {self.max_devices} SBD devices can be specified for a " f"node, {devices} specified for node '{self.node}'" ) @dataclass(frozen=True) class SbdDevicePathNotAbsolute(ReportItemMessage): """ Path of SBD device is not absolute """ device: str node: str _code = codes.SBD_DEVICE_PATH_NOT_ABSOLUTE @property def message(self) -> str: return ( f"Device path '{self.device}' on node '{self.node}' is not absolute" ) @dataclass(frozen=True) class SbdDeviceDoesNotExist(ReportItemMessage): """ Specified device on node doesn't exist """ device: str node: str _code = codes.SBD_DEVICE_DOES_NOT_EXIST @property def message(self) -> str: return f"{self.node}: device '{self.device}' not found" @dataclass(frozen=True) class SbdDeviceIsNotBlockDevice(ReportItemMessage): """ Specified device on node is not block device """ device: str node: str _code = codes.SBD_DEVICE_IS_NOT_BLOCK_DEVICE @property def message(self) -> str: return f"{self.node}: device '{self.device}' is not a block device" @dataclass(frozen=True) class StonithWatchdogTimeoutCannotBeSet(ReportItemMessage): """ Can't set stonith-watchdog-timeout """ reason: types.StonithWatchdogTimeoutCannotBeSetReason _code = codes.STONITH_WATCHDOG_TIMEOUT_CANNOT_BE_SET @property def message(self) -> str: return ( "stonith-watchdog-timeout can only be unset or set to 0 while " + _stonith_watchdog_timeout_reason_to_str(self.reason) ) @dataclass(frozen=True) class StonithWatchdogTimeoutCannotBeUnset(ReportItemMessage): """ Can't unset stonith-watchdog-timeout """ reason: types.StonithWatchdogTimeoutCannotBeSetReason _code = codes.STONITH_WATCHDOG_TIMEOUT_CANNOT_BE_UNSET @property def message(self) -> str: return ( "stonith-watchdog-timeout cannot be unset or set to 0 while " + _stonith_watchdog_timeout_reason_to_str(self.reason) ) @dataclass(frozen=True) class StonithWatchdogTimeoutTooSmall(ReportItemMessage): """ The value of stonith-watchdog-timeout is too small cluster_sbd_watchdog_timeout -- sbd watchdog timeout set in sbd config entered_watchdog_timeout -- entered stonith-watchdog-timeout property """ cluster_sbd_watchdog_timeout: int entered_watchdog_timeout: str _code = codes.STONITH_WATCHDOG_TIMEOUT_TOO_SMALL @property def message(self) -> str: return ( "The stonith-watchdog-timeout must be greater than SBD watchdog " f"timeout '{self.cluster_sbd_watchdog_timeout}', entered " f"'{self.entered_watchdog_timeout}'" ) @dataclass(frozen=True) class WatchdogNotFound(ReportItemMessage): """ Watchdog doesn't exist on specified node node -- node name watchdog -- watchdog device path """ node: str watchdog: str _code = codes.WATCHDOG_NOT_FOUND @property def message(self) -> str: return ( f"Watchdog '{self.watchdog}' does not exist on node '{self.node}'" ) @dataclass(frozen=True) class WatchdogInvalid(ReportItemMessage): """ Watchdog path is not an absolute path watchdog -- watchdog device path """ watchdog: str _code = codes.WATCHDOG_INVALID @property def message(self) -> str: return f"Watchdog path '{self.watchdog}' is invalid." @dataclass(frozen=True) class UnableToGetSbdStatus(ReportItemMessage): """ There was (communication or parsing) failure during obtaining status of SBD from specified node node -- node name reason -- reason of failure """ node: str reason: str _code = codes.UNABLE_TO_GET_SBD_STATUS @property def message(self) -> str: return "Unable to get status of SBD from node '{node}'{reason}".format( node=self.node, reason=format_optional(self.reason, ": {}"), ) @dataclass(frozen=True) class ClusterRestartRequiredToApplyChanges(ReportItemMessage): """ Warn user a cluster needs to be manually restarted to use new configuration """ _code = codes.CLUSTER_RESTART_REQUIRED_TO_APPLY_CHANGES @property def message(self) -> str: return "Cluster restart is required in order to apply these changes." @dataclass(frozen=True) class CibAlertRecipientAlreadyExists(ReportItemMessage): """ Recipient with specified value already exists in alert with id 'alert_id' alert_id -- id of alert to which recipient belongs recipient_value -- value of recipient """ alert: str recipient: str _code = codes.CIB_ALERT_RECIPIENT_ALREADY_EXISTS @property def message(self) -> str: return ( f"Recipient '{self.recipient}' in alert '{self.alert}' " "already exists" ) @dataclass(frozen=True) class CibAlertRecipientValueInvalid(ReportItemMessage): """ Invalid recipient value. recipient -- recipient value """ recipient: str _code = codes.CIB_ALERT_RECIPIENT_VALUE_INVALID @property def message(self) -> str: return f"Recipient value '{self.recipient}' is not valid." @dataclass(frozen=True) class CibUpgradeSuccessful(ReportItemMessage): """ Upgrade of CIB schema was successful. """ _code = codes.CIB_UPGRADE_SUCCESSFUL @property def message(self) -> str: return "CIB has been upgraded to the latest schema version." @dataclass(frozen=True) class CibUpgradeFailed(ReportItemMessage): """ Upgrade of CIB schema failed. reason -- reason of failure """ reason: str _code = codes.CIB_UPGRADE_FAILED @property def message(self) -> str: return f"Upgrading of CIB to the latest schema failed: {self.reason}" @dataclass(frozen=True) class CibUpgradeFailedToMinimalRequiredVersion(ReportItemMessage): """ Unable to upgrade CIB to minimal required schema version. current_version -- current version of CIB schema required_version -- required version of CIB schema """ current_version: str required_version: str _code = codes.CIB_UPGRADE_FAILED_TO_MINIMAL_REQUIRED_VERSION @property def message(self) -> str: return ( "Unable to upgrade CIB to required schema version" f" {self.required_version} or higher. Current version is" f" {self.current_version}. Newer version of pacemaker is needed." ) @dataclass(frozen=True) class FileAlreadyExists(ReportItemMessage): file_type_code: file_type_codes.FileTypeCode file_path: str node: str = "" _code = codes.FILE_ALREADY_EXISTS @property def message(self) -> str: return "{node}{file_role} file '{file_path}' already exists".format( file_path=self.file_path, node=format_optional(self.node, NODE_PREFIX), file_role=_format_file_role(self.file_type_code), ) @dataclass(frozen=True) class FileIoError(ReportItemMessage): """ Unable to work with a file file_type_code -- file type, item of pcs.common.file_type_codes operation -- failed action, item of pcs.common.file.RawFileError reason -- an error message file_path -- file path, optional for cases when unknown (GhostFiles) """ file_type_code: file_type_codes.FileTypeCode operation: FileAction reason: str file_path: str = "" _code = codes.FILE_IO_ERROR @property def message(self) -> str: return "Unable to {action} {file_role}{file_path}: {reason}".format( reason=self.reason, action=_format_file_action(self.operation), file_path=format_optional(self.file_path, " '{0}'"), file_role=_format_file_role(self.file_type_code), ) @dataclass(frozen=True) class UnsupportedOperationOnNonSystemdSystems(ReportItemMessage): _code = codes.UNSUPPORTED_OPERATION_ON_NON_SYSTEMD_SYSTEMS @property def message(self) -> str: return "unsupported operation on non systemd systems" @dataclass(frozen=True) class LiveEnvironmentRequired(ReportItemMessage): """ The command cannot operate in a non-live cluster (mocked / ghost files) forbidden_options -- list of items forbidden in the command """ forbidden_options: List[file_type_codes.FileTypeCode] _code = codes.LIVE_ENVIRONMENT_REQUIRED @property def message(self) -> str: return "This command does not support {forbidden_options}".format( forbidden_options=format_list( [str(item) for item in self.forbidden_options] ), ) @dataclass(frozen=True) class LiveEnvironmentRequiredForLocalNode(ReportItemMessage): """ The operation cannot be performed on CIB in file (not live cluster) if no node name is specified i.e. working with the local node """ _code = codes.LIVE_ENVIRONMENT_REQUIRED_FOR_LOCAL_NODE @property def message(self) -> str: return "Node(s) must be specified if mocked CIB is used" @dataclass(frozen=True) class LiveEnvironmentNotConsistent(ReportItemMessage): """ The command cannot operate with mixed live / non-live cluster configs mocked_files -- given mocked files (pcs.common.file_type_codes) required_files -- files that must be mocked as well """ mocked_files: List[file_type_codes.FileTypeCode] required_files: List[file_type_codes.FileTypeCode] _code = codes.LIVE_ENVIRONMENT_NOT_CONSISTENT @property def message(self) -> str: return ( "When {given} {_is} specified, {missing} must be specified as well" ).format( given=format_list([str(item) for item in self.mocked_files]), _is=format_plural(self.mocked_files, "is"), missing=format_list([str(item) for item in self.required_files]), ) @dataclass(frozen=True) class CorosyncNodeConflictCheckSkipped(ReportItemMessage): """ A command has been run with -f, can't check corosync.conf for node conflicts reason_type -- why was the action skipped (unreachable, not_live_cib) """ reason_type: types.ReasonType _code = codes.COROSYNC_NODE_CONFLICT_CHECK_SKIPPED @property def message(self) -> str: return ( "Unable to check if there is a conflict with nodes set in corosync " "because {reason}" ).format(reason=_skip_reason_to_string(self.reason_type)) @dataclass(frozen=True) class CorosyncQuorumAtbCannotBeDisabledDueToSbd(ReportItemMessage): """ Quorum option auto_tie_breaker cannot be disabled due to SBD. """ _code = codes.COROSYNC_QUORUM_ATB_CANNOT_BE_DISABLED_DUE_TO_SBD @property def message(self) -> str: return ( "Unable to disable auto_tie_breaker, SBD fencing would have no " "effect" ) @dataclass(frozen=True) class CorosyncQuorumAtbWillBeEnabledDueToSbd(ReportItemMessage): """ Quorum option auto_tie_breaker will be enabled due to a user action in order to keep SBD fencing effective. The cluster has to be stopped to make this change. """ _code = codes.COROSYNC_QUORUM_ATB_WILL_BE_ENABLED_DUE_TO_SBD @property def message(self) -> str: return ( "SBD fencing is enabled in the cluster. To keep it effective, " "auto_tie_breaker quorum option will be enabled." ) @dataclass(frozen=True) class CorosyncQuorumAtbWillBeEnabledDueToSbdClusterIsRunning(ReportItemMessage): """ Pcs needs to enable quorum option auto_tie_breaker due to a user action in order to keep SBD fencing effective. The cluster has to be stopped to make this change, but it is currently running. """ _code = ( codes.COROSYNC_QUORUM_ATB_WILL_BE_ENABLED_DUE_TO_SBD_CLUSTER_IS_RUNNING ) @property def message(self) -> str: return ( "SBD fencing is enabled in the cluster. To keep it effective, " "auto_tie_breaker quorum option needs to be enabled. This can only " "be done when the cluster is stopped. To proceed, stop the cluster, " "enable auto_tie_breaker, and start the cluster. Then, repeat the " "requested action." ) @dataclass(frozen=True) class CibAclRoleIsAlreadyAssignedToTarget(ReportItemMessage): """ Error that ACL target or group has already assigned role. """ role_id: str target_id: str _code = codes.CIB_ACL_ROLE_IS_ALREADY_ASSIGNED_TO_TARGET @property def message(self) -> str: return ( f"Role '{self.role_id}' is already assigned to '{self.target_id}'" ) @dataclass(frozen=True) class CibAclRoleIsNotAssignedToTarget(ReportItemMessage): """ Error that acl role is not assigned to target or group """ role_id: str target_id: str _code = codes.CIB_ACL_ROLE_IS_NOT_ASSIGNED_TO_TARGET @property def message(self) -> str: return f"Role '{self.role_id}' is not assigned to '{self.target_id}'" @dataclass(frozen=True) class CibAclTargetAlreadyExists(ReportItemMessage): """ Error that target with specified id already exists in configuration. """ target_id: str _code = codes.CIB_ACL_TARGET_ALREADY_EXISTS @property def message(self) -> str: return f"'{self.target_id}' already exists" @dataclass(frozen=True) class CibFencingLevelAlreadyExists(ReportItemMessage): """ Fencing level already exists, it cannot be created """ level: str target_type: FencingTargetType target_value: FencingTargetValue devices: list[str] _code = codes.CIB_FENCING_LEVEL_ALREADY_EXISTS @property def message(self) -> str: return ( "Fencing level for '{target}' at level '{level}' " "with device(s) {device_list} already exists" ).format( level=self.level, device_list=format_list(self.devices), target=_format_fencing_level_target( self.target_type, self.target_value ), ) @dataclass(frozen=True) class CibFencingLevelDoesNotExist(ReportItemMessage): """ Fencing level does not exist, it cannot be updated or deleted """ level: str = "" target_type: Optional[FencingTargetType] = None target_value: Optional[FencingTargetValue] = None devices: list[str] = field(default_factory=list) _code = codes.CIB_FENCING_LEVEL_DOES_NOT_EXIST @property def message(self) -> str: return ( "Fencing level {part_target}{part_level}{part_devices}does not " "exist" ).format( part_target=( "for '{0}' ".format( _format_fencing_level_target( self.target_type, self.target_value ) ) if self.target_type and self.target_value else "" ), part_level=format_optional(self.level, "at level '{}' "), part_devices=format_optional( format_list(self.devices), "with device(s) {0} " ), ) @dataclass(frozen=True) class CibRemoveResources(ReportItemMessage): """ Information about removal of resources from cib. """ id_list: list[str] _code = codes.CIB_REMOVE_RESOURCES @property def message(self) -> str: return "Removing {resource_pl}: {resource_list}".format( resource_pl=format_plural(self.id_list, "resource"), resource_list=format_list(self.id_list), ) @dataclass(frozen=True) class CibRemoveDependantElements(ReportItemMessage): """ Information about removal of additional cib elements due to dependencies. """ id_tag_map: Mapping[str, str] _code = codes.CIB_REMOVE_DEPENDANT_ELEMENTS @property def message(self) -> str: def _format_line(tag: str, ids: list[str]) -> str: tag_desc = format_plural(ids, _type_to_string(tag)).capitalize() id_list = format_list(ids) return f" {tag_desc}: {id_list}" element_pl = format_plural(self.id_tag_map, "element") tag_ids_map: Mapping[str, list[str]] = defaultdict(list) for _id, tag in self.id_tag_map.items(): tag_ids_map[tag].append(_id) info_lines = "\n".join( sorted([_format_line(tag, ids) for tag, ids in tag_ids_map.items()]) ) return f"Removing dependant {element_pl}:\n{info_lines}" @dataclass(frozen=True) class CibRemoveReferences(ReportItemMessage): """ Information about removal of references from cib elements due to dependencies. """ id_tag_map: Mapping[str, str] removing_references_from: Mapping[str, StringIterable] _code = codes.CIB_REMOVE_REFERENCES @property def message(self) -> str: id_tag_map = defaultdict(lambda: "element", self.id_tag_map) def _format_line(tag: str, ids: list[str]) -> str: tag_desc = format_plural(ids, _type_to_string(tag)).capitalize() id_list = format_list(ids) return f" {tag_desc}: {id_list}" def _format_one_element(element_id: str, ids: StringIterable) -> str: tag_ids_map = defaultdict(list) for _id in ids: tag_ids_map[id_tag_map[_id]].append(_id) info_lines = "\n".join( sorted( [_format_line(tag, ids) for tag, ids in tag_ids_map.items()] ) ) tag_desc = _type_to_string(id_tag_map[element_id]).capitalize() return f" {tag_desc} '{element_id}' from:\n{info_lines}" lines = "\n".join( _format_one_element(key, self.removing_references_from[key]) for key in sorted(self.removing_references_from) ) return f"Removing references:\n{lines}" @dataclass(frozen=True) class UseCommandNodeAddRemote(ReportItemMessage): """ Advise the user for more appropriate command. """ _code = codes.USE_COMMAND_NODE_ADD_REMOTE @property def message(self) -> str: return "this command is not sufficient for creating a remote connection" @dataclass(frozen=True) class UseCommandNodeAddGuest(ReportItemMessage): """ Advise the user for more appropriate command. """ _code = codes.USE_COMMAND_NODE_ADD_GUEST @property def message(self) -> str: return "this command is not sufficient for creating a guest node" @dataclass(frozen=True) class UseCommandNodeRemoveRemote(ReportItemMessage): """ Advise the user for more appropriate command. """ resource_id: Optional[str] = None _code = codes.USE_COMMAND_NODE_REMOVE_REMOTE @property def message(self) -> str: return "this command is not sufficient for removing a remote node{id}".format( id=format_optional(self.resource_id, template=": '{}'") ) @dataclass(frozen=True) class UseCommandNodeRemoveGuest(ReportItemMessage): """ Advise the user for more appropriate command. """ resource_id: Optional[str] = None _code = codes.USE_COMMAND_NODE_REMOVE_GUEST @property def message(self) -> str: return "this command is not sufficient for removing a guest node{id}".format( id=format_optional(self.resource_id, template=": '{}") ) @dataclass(frozen=True) class TmpFileWrite(ReportItemMessage): """ It has been written into a temporary file file_path -- the file path content -- content which has been written """ file_path: str content: str _code = codes.TMP_FILE_WRITE @property def message(self) -> str: return ( f"Writing to a temporary file {self.file_path}:\n" f"--Debug Content Start--\n{self.content}\n--Debug Content End--\n" ) @dataclass(frozen=True) class NodeAddressesUnresolvable(ReportItemMessage): """ Unable to resolve addresses of cluster nodes to be added address_list -- a list of unresolvable addresses """ address_list: List[str] _code = codes.NODE_ADDRESSES_UNRESOLVABLE @property def message(self) -> str: addrs = format_list(self.address_list) return f"Unable to resolve addresses: {addrs}" @dataclass(frozen=True) class UnableToPerformOperationOnAnyNode(ReportItemMessage): """ This report is raised whenever pcs.lib.communication.tools.OneByOneStrategyMixin strategy mixin is used for network communication and operation failed on all available hosts and because of this it is not possible to continue. """ _code = codes.UNABLE_TO_PERFORM_OPERATION_ON_ANY_NODE @property def message(self) -> str: return ( "Unable to perform operation on any available node/host, therefore " "it is not possible to continue" ) @dataclass(frozen=True) class HostNotFound(ReportItemMessage): """ Hosts with names in host_list are not included in pcs known hosts, therefore it is not possible to communicate with them. """ host_list: List[str] _code = codes.HOST_NOT_FOUND @property def message(self) -> str: pluralize = partial(format_plural, self.host_list) return "{host} {hosts_comma} {_is} not known to pcs".format( host=pluralize("host"), hosts_comma=format_list(self.host_list), _is=pluralize("is"), ).capitalize() @dataclass(frozen=True) class NoneHostFound(ReportItemMessage): _code = codes.NONE_HOST_FOUND @property def message(self) -> str: return "None of hosts is known to pcs." @dataclass(frozen=True) class HostAlreadyAuthorized(ReportItemMessage): host_name: str _code = codes.HOST_ALREADY_AUTHORIZED @property def message(self) -> str: return f"{self.host_name}: Already authorized" @dataclass(frozen=True) class ClusterDestroyStarted(ReportItemMessage): host_name_list: List[str] _code = codes.CLUSTER_DESTROY_STARTED @property def message(self) -> str: hosts = format_list(self.host_name_list) return f"Destroying cluster on hosts: {hosts}..." @dataclass(frozen=True) class ClusterDestroySuccess(ReportItemMessage): node: str _code = codes.CLUSTER_DESTROY_SUCCESS @property def message(self) -> str: return f"{self.node}: Successfully destroyed cluster" @dataclass(frozen=True) class ClusterEnableStarted(ReportItemMessage): host_name_list: List[str] _code = codes.CLUSTER_ENABLE_STARTED @property def message(self) -> str: hosts = format_list(self.host_name_list) return f"Enabling cluster on hosts: {hosts}..." @dataclass(frozen=True) class ClusterEnableSuccess(ReportItemMessage): node: str _code = codes.CLUSTER_ENABLE_SUCCESS @property def message(self) -> str: return f"{self.node}: Cluster enabled" @dataclass(frozen=True) class ClusterStartStarted(ReportItemMessage): host_name_list: List[str] _code = codes.CLUSTER_START_STARTED @property def message(self) -> str: hosts = format_list(self.host_name_list) return f"Starting cluster on hosts: {hosts}..." @dataclass(frozen=True) class ClusterStartSuccess(ReportItemMessage): node: str _code = codes.CLUSTER_START_SUCCESS @property def message(self) -> str: return f"{self.node}: Cluster started" @dataclass(frozen=True) class ServiceNotInstalled(ReportItemMessage): node: str service_list: List[str] _code = codes.SERVICE_NOT_INSTALLED @property def message(self) -> str: services = format_list(self.service_list) return ( f"{self.node}: Required cluster services not installed: {services}" ) @dataclass(frozen=True) class HostAlreadyInClusterConfig(ReportItemMessage): """ A host, which is being added to a cluster, already has cluster configs host_name -- a name of the host which is in a cluster already """ host_name: str _code = codes.HOST_ALREADY_IN_CLUSTER_CONFIG @property def message(self) -> str: return ( f"{self.host_name}: The host seems to be in a cluster already as " "cluster configuration files have been found on the host" ) @dataclass(frozen=True) class HostAlreadyInClusterServices(ReportItemMessage): """ A host, which is being added to a cluster, already runs cluster daemons host_name -- a name of the host which is in a cluster already service_list -- list of cluster daemons running on the host """ host_name: str service_list: List[str] _code = codes.HOST_ALREADY_IN_CLUSTER_SERVICES @property def message(self) -> str: services = format_list(self.service_list) services_plural = format_plural(self.service_list, "service") are_plural = format_plural(self.service_list, "is") return ( f"{self.host_name}: The host seems to be in a cluster already as " f"the following {services_plural} {are_plural} found to be " f"running: {services}. If the host is not part of a cluster, stop " f"the {services_plural} and retry" ) @dataclass(frozen=True) class ServiceVersionMismatch(ReportItemMessage): service: str hosts_version: Mapping[str, str] _code = codes.SERVICE_VERSION_MISMATCH @property def message(self) -> str: version_host: Dict[str, List[str]] = defaultdict(list) for host_name, version in self.hosts_version.items(): version_host[version].append(host_name) parts = [f"Hosts do not have the same version of '{self.service}'"] # List most common versions first. for version, hosts in sorted( version_host.items(), key=lambda pair: len(pair[1]), reverse=True ): pluralize = partial(format_plural, hosts) parts.append( "{host} {hosts} {has} version {version}".format( host=pluralize("host"), hosts=format_list(hosts), has=pluralize("has"), version=version, ) ) return "; ".join(parts) @dataclass(frozen=True) class WaitForNodeStartupStarted(ReportItemMessage): node_name_list: List[str] _code = codes.WAIT_FOR_NODE_STARTUP_STARTED @property def message(self) -> str: nodes = format_list(self.node_name_list) return f"Waiting for node(s) to start: {nodes}..." @dataclass(frozen=True) class WaitForNodeStartupTimedOut(ReportItemMessage): _code = codes.WAIT_FOR_NODE_STARTUP_TIMED_OUT @property def message(self) -> str: return "Node(s) startup timed out" @dataclass(frozen=True) class WaitForNodeStartupError(ReportItemMessage): _code = codes.WAIT_FOR_NODE_STARTUP_ERROR @property def message(self) -> str: return "Unable to verify all nodes have started" @dataclass(frozen=True) class WaitForNodeStartupWithoutStart(ReportItemMessage): """ User requested waiting for nodes to start without instructing pcs to start the nodes """ _code = codes.WAIT_FOR_NODE_STARTUP_WITHOUT_START @property def message(self) -> str: return "Cannot specify 'wait' without specifying 'start'" @dataclass(frozen=True) class PcsdVersionTooOld(ReportItemMessage): node: str _code = codes.PCSD_VERSION_TOO_OLD @property def message(self) -> str: return ( f"{self.node}: Old version of pcsd is running on the node, " "therefore it is unable to perform the action" ) @dataclass(frozen=True) class PcsdSslCertAndKeyDistributionStarted(ReportItemMessage): """ We are about to distribute pcsd SSL certificate and key to nodes node_name_list -- node names to distribute to """ node_name_list: List[str] _code = codes.PCSD_SSL_CERT_AND_KEY_DISTRIBUTION_STARTED @property def message(self) -> str: nodes = format_list(self.node_name_list) return f"Synchronizing pcsd SSL certificates on node(s) {nodes}..." @dataclass(frozen=True) class PcsdSslCertAndKeySetSuccess(ReportItemMessage): """ Pcsd SSL certificate and key have been successfully saved on a node node -- node name """ node: str _code = codes.PCSD_SSL_CERT_AND_KEY_SET_SUCCESS @property def message(self) -> str: return f"{self.node}: Success" @dataclass(frozen=True) class ClusterWillBeDestroyed(ReportItemMessage): """ If the user continues with force, cluster will be destroyed on some hosts """ _code = codes.CLUSTER_WILL_BE_DESTROYED @property def message(self) -> str: return ( "Some nodes are already in a cluster. Enforcing this will destroy " "existing cluster on those nodes. You should remove the nodes from " "their clusters instead to keep the clusters working properly" ) @dataclass(frozen=True) class ClusterSetupSuccess(ReportItemMessage): _code = codes.CLUSTER_SETUP_SUCCESS @property def message(self) -> str: return "Cluster has been successfully set up." @dataclass(frozen=True) class UsingDefaultAddressForHost(ReportItemMessage): """ When no address was specified for a host, a default address was used for it """ host_name: str address: str address_source: types.DefaultAddressSource _code = codes.USING_DEFAULT_ADDRESS_FOR_HOST @property def message(self) -> str: return ( f"No addresses specified for host '{self.host_name}', using " f"'{self.address}'" ) @dataclass(frozen=True) class ResourceInBundleNotAccessible(ReportItemMessage): bundle_id: str inner_resource_id: str _code = codes.RESOURCE_IN_BUNDLE_NOT_ACCESSIBLE @property def message(self) -> str: return ( f"Resource '{self.inner_resource_id}' will not be accessible by " f"the cluster inside bundle '{self.bundle_id}', at least one of " "bundle options 'control-port' or 'ip-range-start' has to be " "specified" ) @dataclass(frozen=True) class UsingDefaultWatchdog(ReportItemMessage): """ No watchdog has been specified for the node, therefore pcs will use a default watchdog. """ watchdog: str node: str _code = codes.USING_DEFAULT_WATCHDOG @property def message(self) -> str: return ( f"No watchdog has been specified for node '{self.node}'. Using " f"default watchdog '{self.watchdog}'" ) @dataclass(frozen=True) class CannotRemoveAllClusterNodes(ReportItemMessage): """ It is not possible to remove all cluster nodes using 'pcs cluster node remove' command. 'pcs cluster destroy --all' should be used in such case. """ _code = codes.CANNOT_REMOVE_ALL_CLUSTER_NODES @property def message(self) -> str: return "No nodes would be left in the cluster" @dataclass(frozen=True) class UnableToConnectToAnyRemainingNode(ReportItemMessage): _code = codes.UNABLE_TO_CONNECT_TO_ANY_REMAINING_NODE @property def message(self) -> str: return "Unable to connect to any remaining cluster node" @dataclass(frozen=True) class UnableToConnectToAllRemainingNodes(ReportItemMessage): """ Some of remaining cluster nodes are unreachable. 'pcs cluster sync' should be executed on now online nodes when the offline nodes come back online. node_list -- names of nodes which are staying in the cluster and are currently unreachable """ node_list: List[str] _code = codes.UNABLE_TO_CONNECT_TO_ALL_REMAINING_NODE @property def message(self) -> str: return ("Remaining cluster {node} {nodes} could not be reached").format( node=format_plural(self.node_list, "node"), nodes=format_list(self.node_list), ) @dataclass(frozen=True) class NodesToRemoveUnreachable(ReportItemMessage): """ Nodes which should be removed are currently unreachable. 'pcs cluster destroy' should be executed on these nodes when they come back online. node_list -- names of nodes which are being removed from the cluster but they are currently unreachable """ node_list: List[str] _code = codes.NODES_TO_REMOVE_UNREACHABLE @property def message(self) -> str: return ( "Removed {node} {nodes} could not be reached and subsequently " "deconfigured" ).format( node=format_plural(self.node_list, "node"), nodes=format_list(self.node_list), ) @dataclass(frozen=True) class NodeUsedAsTieBreaker(ReportItemMessage): """ Node which should be removed is currently used as a tie breaker for a qdevice, therefore it is not possible to remove it from the cluster. node -- node name node_id -- node id """ node: Optional[str] node_id: Optional[str] _code = codes.NODE_USED_AS_TIE_BREAKER @property def message(self) -> str: return ( f"Node '{self.node}' with id '{self.node_id}' is used as a tie " "breaker for a qdevice and therefore cannot be removed" ) @dataclass(frozen=True) class CorosyncQuorumWillBeLost(ReportItemMessage): """ Ongoing action will cause loss of the quorum in the cluster. """ _code = codes.COROSYNC_QUORUM_WILL_BE_LOST @property def message(self) -> str: return "This action will cause a loss of the quorum" @dataclass(frozen=True) class CorosyncQuorumLossUnableToCheck(ReportItemMessage): """ It is not possible to check if ongoing action will cause loss of the quorum """ _code = codes.COROSYNC_QUORUM_LOSS_UNABLE_TO_CHECK @property def message(self) -> str: return ( "Unable to determine whether this action will cause a loss of the " "quorum" ) @dataclass(frozen=True) class SbdListWatchdogError(ReportItemMessage): """ Unable to get list of available watchdogs from sbd. Sbd cmd reutrned non 0. reason -- stderr of command """ reason: str _code = codes.SBD_LIST_WATCHDOG_ERROR @property def message(self) -> str: return f"Unable to query available watchdogs from sbd: {self.reason}" @dataclass(frozen=True) class SbdWatchdogNotSupported(ReportItemMessage): """ Specified watchdog is not supported in sbd (softdog?). node -- node name watchdog -- watchdog path """ node: str watchdog: str _code = codes.SBD_WATCHDOG_NOT_SUPPORTED @property def message(self) -> str: return ( f"{self.node}: Watchdog '{self.watchdog}' is not supported (it " "may be a software watchdog)" ) @dataclass(frozen=True) class SbdWatchdogValidationInactive(ReportItemMessage): """ Warning message about not validating watchdog. """ _code = codes.SBD_WATCHDOG_VALIDATION_INACTIVE @property def message(self) -> str: return "Not validating the watchdog" @dataclass(frozen=True) class SbdWatchdogTestError(ReportItemMessage): """ Sbd test watchdog exited with an error. """ reason: str _code = codes.SBD_WATCHDOG_TEST_ERROR @property def message(self) -> str: return f"Unable to initialize test of the watchdog: {self.reason}" @dataclass(frozen=True) class SbdWatchdogTestMultipleDevices(ReportItemMessage): """ No watchdog device has been specified for test. Because of multiple available watchdogs, watchdog device to test has to be specified. """ _code = codes.SBD_WATCHDOG_TEST_MULTIPLE_DEVICES @property def message(self) -> str: return ( "Multiple watchdog devices available, therefore, watchdog which " "should be tested has to be specified." ) @dataclass(frozen=True) class SbdWatchdogTestFailed(ReportItemMessage): """ System has not been reset. """ _code = codes.SBD_WATCHDOG_TEST_FAILED @property def message(self) -> str: return "System should have been reset already" @dataclass(frozen=True) class SystemWillReset(ReportItemMessage): _code = codes.SYSTEM_WILL_RESET @property def message(self) -> str: return "System will reset shortly" @dataclass(frozen=True) class ResourceBundleUnsupportedContainerType(ReportItemMessage): bundle_id: str supported_container_types: List[str] updating_options: bool = True _code = codes.RESOURCE_BUNDLE_UNSUPPORTED_CONTAINER_TYPE @property def message(self) -> str: container_types = format_list(self.supported_container_types) inner_text = format_optional( self.updating_options, ", therefore it is not possible to set its container options", ) return ( f"Bundle '{self.bundle_id}' uses unsupported container type" f"{inner_text}. Supported container types are: {container_types}" ) @dataclass(frozen=True) class FenceHistoryCommandError(ReportItemMessage): """ Pacemaker command for working with fence history returned an error reason -- output of the pacemaker command command -- the action of the command - what it should have achieved """ reason: str command: types.FenceHistoryCommandType _code = codes.FENCE_HISTORY_COMMAND_ERROR @property def message(self) -> str: command_label = { const.FENCE_HISTORY_COMMAND_CLEANUP: "cleanup", const.FENCE_HISTORY_COMMAND_SHOW: "show", const.FENCE_HISTORY_COMMAND_UPDATE: "update", }.get(self.command, self.command) return f"Unable to {command_label} fence history: {self.reason}" @dataclass(frozen=True) class FenceHistoryNotSupported(ReportItemMessage): """ Pacemaker does not support the fence history feature """ _code = codes.FENCE_HISTORY_NOT_SUPPORTED @property def message(self) -> str: return "Fence history is not supported, please upgrade pacemaker" @dataclass(frozen=True) class ResourceInstanceAttrValueNotUnique(ReportItemMessage): """ Value of a resource instance attribute is not unique in the configuration when creating/updating a resource instance_attr_name -- name of attr which should be unique instance_attr_value -- value which is already used by some resources agent_name -- resource agent name of resource resource_id_list -- resource ids which already have the instance_attr_name set to instance_attr_value """ instance_attr_name: str instance_attr_value: str agent_name: str resource_id_list: List[str] _code = codes.RESOURCE_INSTANCE_ATTR_VALUE_NOT_UNIQUE @property def message(self) -> str: return ( "Value '{val}' of option '{attr}' is not unique across " "'{agent}' resources. Following resources are configured " "with the same value of the instance attribute: {res_id_list}" ).format( val=self.instance_attr_value, attr=self.instance_attr_name, agent=self.agent_name, res_id_list=format_list(self.resource_id_list), ) @dataclass(frozen=True) class ResourceInstanceAttrGroupValueNotUnique(ReportItemMessage): """ Value of a group of resource instance attributes is not unique in the configuration when creating/updating a resource group_name -- name of a group of attributes instance_attrs_map -- attributes which should be unique and their values agent_name -- resource agent name of the resources resource_id_list -- resources which already have the same instance_attr_values """ group_name: str instance_attrs_map: Dict[str, str] agent_name: str resource_id_list: List[str] _code = codes.RESOURCE_INSTANCE_ATTR_GROUP_VALUE_NOT_UNIQUE @property def message(self) -> str: attr_names, attr_values = zip(*sorted(self.instance_attrs_map.items())) attr_names_str = format_list_dont_sort(list(attr_names)) attr_values_str = format_list_dont_sort(list(attr_values)) options = format_plural(self.instance_attrs_map, "option") res_id_list = format_list(self.resource_id_list) return ( f"Value {attr_values_str} of {options} {attr_names_str} (group " f"'{self.group_name}') is not unique across '{self.agent_name}' " f"resources. Following resources are configured with the same " f"values of the instance attributes: {res_id_list}" ) @dataclass(frozen=True) class CannotLeaveGroupEmptyAfterMove(ReportItemMessage): """ User is trying to add resources to another group and their old group would be left empty and need to be deleted. Deletion is not yet migrated to lib. str group_id -- ID of original group that would be deleted list inner_resource_ids -- List of group members """ group_id: str inner_resource_ids: List[str] _code = codes.CANNOT_LEAVE_GROUP_EMPTY_AFTER_MOVE @property def message(self) -> str: return ( "Unable to move {resource_pl} {resource_list} as it would leave " "group '{group_id}' empty." ).format( resource_pl=format_plural(self.inner_resource_ids, "resource"), resource_list=format_list(self.inner_resource_ids), group_id=self.group_id, ) @dataclass(frozen=True) class CannotMoveResourceBundleInner(ReportItemMessage): """ User is trying to move a bundle's inner resource resource_id -- id of the resource to be moved bundle_id -- id of the relevant parent bundle resource """ resource_id: str bundle_id: str _code = codes.CANNOT_MOVE_RESOURCE_BUNDLE_INNER @property def message(self) -> str: return ( "Resources cannot be moved out of their bundles. If you want to " f"move a bundle, use the bundle id ({self.bundle_id})" ) @dataclass(frozen=True) class CannotMoveResourceMultipleInstances(ReportItemMessage): """ User is trying to move a resource of which more than one instance is running resource_id -- id of the resource to be moved """ resource_id: str _code = codes.CANNOT_MOVE_RESOURCE_MULTIPLE_INSTANCES @property def message(self) -> str: return ( f"more than one instance of resource '{self.resource_id}' is " "running, thus the resource cannot be moved" ) @dataclass(frozen=True) class CannotMoveResourceMultipleInstancesNoNodeSpecified(ReportItemMessage): """ User is trying to move a resource of which more than one instance is running without specifying a destination node resource_id -- id of the resource to be moved """ resource_id: str _code = codes.CANNOT_MOVE_RESOURCE_MULTIPLE_INSTANCES_NO_NODE_SPECIFIED @property def message(self) -> str: return ( f"more than one instance of resource '{self.resource_id}' is " "running, thus the resource cannot be moved, " "unless a destination node is specified" ) @dataclass(frozen=True) class CannotMoveResourceCloneInner(ReportItemMessage): """ User is trying to move a clone's inner resource which is not possible resource_id -- id of the resource to be moved clone_id -- id of relevant parent clone resource """ resource_id: str clone_id: str _code = codes.CANNOT_MOVE_RESOURCE_CLONE_INNER @property def message(self) -> str: return ( "to move clone resources you must use the clone id " f"({self.clone_id})" ) @dataclass(frozen=True) class CannotMoveResourcePromotableInner(ReportItemMessage): """ User is trying to move a promotable clone's inner resource resource_id -- id of the resource to be moved promotable_id -- id of relevant parent promotable resource """ resource_id: str promotable_id: str _code = codes.CANNOT_MOVE_RESOURCE_PROMOTABLE_INNER @property def message(self) -> str: return ( "to move promotable clone resources you must use the " f"promotable clone id ({self.promotable_id})" ) @dataclass(frozen=True) class CannotMoveResourceMasterResourceNotPromotable(ReportItemMessage): """ User is trying to move a non-promotable resource and limit it to master role resource_id -- id of the resource to be moved promotable_id -- id of relevant parent promotable resource """ resource_id: str promotable_id: str = "" _code = codes.CANNOT_MOVE_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE @property def message(self) -> str: return _resource_move_ban_clear_master_resource_not_promotable( self.promotable_id ) @dataclass(frozen=True) class CannotMoveResourceNotRunning(ReportItemMessage): """ It is not possible to move a stopped resource and remove constraint used for moving it resource_id -- id of the resource to be moved """ resource_id: str _code = codes.CANNOT_MOVE_RESOURCE_NOT_RUNNING @property def message(self) -> str: return ( f"It is not possible to move resource '{self.resource_id}' as it " "is not running at the moment" ) @dataclass(frozen=True) class CannotMoveResourceStoppedNoNodeSpecified(ReportItemMessage): """ When moving a stopped resource, a node to move it to must be specified resource_id -- id of the resource to be moved """ resource_id: str _code = codes.CANNOT_MOVE_RESOURCE_STOPPED_NO_NODE_SPECIFIED @property def message(self) -> str: # Use both "moving" and "banning" to let user know using "ban" instead # of "move" will not help return "You must specify a node when moving/banning a stopped resource" @dataclass(frozen=True) class ResourceMovePcmkError(ReportItemMessage): """ crm_resource exited with an error when moving a resource resource_id -- id of the resource to be moved stdout -- stdout of crm_resource stderr -- stderr of crm_resource """ resource_id: str stdout: str stderr: str _code = codes.RESOURCE_MOVE_PCMK_ERROR @property def message(self) -> str: return _stdout_stderr_to_string( self.stdout, self.stderr, prefix=f"cannot move resource '{self.resource_id}'", ) @dataclass(frozen=True) class ResourceMovePcmkSuccess(ReportItemMessage): """ crm_resource exited successfully when moving a resource resource_id -- id of the resource to be moved stdout -- stdout of crm_resource stderr -- stderr of crm_resource """ resource_id: str stdout: str stderr: str _code = codes.RESOURCE_MOVE_PCMK_SUCCESS @property def message(self) -> str: return _resource_move_ban_pcmk_success(self.stdout, self.stderr) @dataclass(frozen=True) class CannotBanResourceBundleInner(ReportItemMessage): """ User is trying to ban a bundle's inner resource resource_id -- id of the resource to be banned bundle_id -- id of the relevant parent bundle resource """ resource_id: str bundle_id: str _code = codes.CANNOT_BAN_RESOURCE_BUNDLE_INNER @property def message(self) -> str: return ( f"Resource '{self.resource_id}' is in a bundle and cannot be banned. " f"If you want to ban the bundle, use the bundle id ({self.bundle_id})" ) @dataclass(frozen=True) class CannotBanResourceMasterResourceNotPromotable(ReportItemMessage): """ User is trying to ban a non-promotable resource and limit it to master role resource_id -- id of the resource to be banned promotable_id -- id of relevant parent promotable resource """ resource_id: str promotable_id: str = "" _code = codes.CANNOT_BAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE @property def message(self) -> str: return _resource_move_ban_clear_master_resource_not_promotable( self.promotable_id ) @dataclass(frozen=True) class CannotBanResourceMultipleInstancesNoNodeSpecified(ReportItemMessage): """ User is trying to ban a resource of which more than one instance is running without specifying a destination node resource_id -- id of the resource to be banned """ resource_id: str _code = codes.CANNOT_BAN_RESOURCE_MULTIPLE_INSTANCES_NO_NODE_SPECIFIED @property def message(self) -> str: return ( f"more than one instance of resource '{self.resource_id}' is " "running, thus the resource cannot be banned, " "unless a destination node is specified" ) @dataclass(frozen=True) class CannotBanResourceStoppedNoNodeSpecified(ReportItemMessage): """ When banning a stopped resource, a node to ban it on must be specified resource_id -- id of the resource to be banned """ resource_id: str _code = codes.CANNOT_BAN_RESOURCE_STOPPED_NO_NODE_SPECIFIED @property def message(self) -> str: # Use both "moving" and "banning" to let user know using "move" instead # of "ban" will not help return "You must specify a node when moving/banning a stopped resource" @dataclass(frozen=True) class StoppingResourcesBeforeDeleting(ReportItemMessage): """ Resources are going to be stopped before deletion resource_id_list -- ids of resources that are going to be stopped """ resource_id_list: list[str] _code = codes.STOPPING_RESOURCES_BEFORE_DELETING @property def message(self) -> str: return "Stopping {resource} {resource_list} before deleting".format( resource=format_plural(self.resource_id_list, "resource"), resource_list=format_list(self.resource_id_list), ) @dataclass(frozen=True) class StoppingResourcesBeforeDeletingSkipped(ReportItemMessage): """ Resources are not going to be stopped before deletion. """ _code = codes.STOPPING_RESOURCES_BEFORE_DELETING_SKIPPED @property def message(self) -> str: return ( "Resources are not going to be stopped before deletion, this may " "result in orphaned resources being present in the cluster" ) @dataclass(frozen=True) class CannotStopResourcesBeforeDeleting(ReportItemMessage): """ Cannot stop resources that are being removed resource_id_list -- ids of resources that cannot be stopped """ resource_id_list: list[str] _code = codes.CANNOT_STOP_RESOURCES_BEFORE_DELETING @property def message(self) -> str: return "Cannot stop {resource} {resource_list} before deleting".format( resource=format_plural(self.resource_id_list, "resource"), resource_list=format_list(self.resource_id_list), ) @dataclass(frozen=True) class ConfiguredResourceMissingInStatus(ReportItemMessage): """ Cannot check status of resource, because the resource is missing in cluster status despite being configured in CIB. This happens for misconfigured resources, e.g. bundle with primitive resource inside and no IP address for the bundle specified. resource_id -- id of the resource checked_state -- expected state of the resource """ resource_id: str checked_state: Optional[ResourceState] = None _code = codes.CONFIGURED_RESOURCE_MISSING_IN_STATUS @property def message(self) -> str: return ( "Cannot check if the resource '{resource_id}' is in expected " "state{state}, since the resource is missing in cluster status" ).format( resource_id=self.resource_id, state=format_optional( self.checked_state and self.checked_state.name.lower(), " ({})" ), ) @dataclass(frozen=True) class ResourceBanPcmkError(ReportItemMessage): """ crm_resource exited with an error when banning a resource resource_id -- id of the resource to be banned stdout -- stdout of crm_resource stderr -- stderr of crm_resource """ resource_id: str stdout: str stderr: str _code = codes.RESOURCE_BAN_PCMK_ERROR @property def message(self) -> str: # Pacemaker no longer prints crm_resource specific options since commit # 8008a5f0c0aa728fbce25f60069d622d0bcbbc9f. There is no need to # translate them or anything else anymore. return _stdout_stderr_to_string( self.stdout, self.stderr, prefix=f"cannot ban resource '{self.resource_id}'", ) @dataclass(frozen=True) class ResourceBanPcmkSuccess(ReportItemMessage): """ crm_resource exited successfully when banning a resource resource_id -- id of the resource to be banned stdout -- stdout of crm_resource stderr -- stderr of crm_resource """ resource_id: str stdout: str stderr: str _code = codes.RESOURCE_BAN_PCMK_SUCCESS @property def message(self) -> str: return _resource_move_ban_pcmk_success(self.stdout, self.stderr) @dataclass(frozen=True) class CannotUnmoveUnbanResourceMasterResourceNotPromotable(ReportItemMessage): """ User is trying to unmove/unban master of a non-promotable resource resource_id -- id of the resource to be unmoved/unbanned promotable_id -- id of relevant parent promotable resource """ resource_id: str promotable_id: str = "" _code = codes.CANNOT_UNMOVE_UNBAN_RESOURCE_MASTER_RESOURCE_NOT_PROMOTABLE @property def message(self) -> str: return _resource_move_ban_clear_master_resource_not_promotable( self.promotable_id ) @dataclass(frozen=True) class ResourceUnmoveUnbanPcmkExpiredNotSupported(ReportItemMessage): """ crm_resource does not support --expired when unmoving/unbanning a resource """ _code = codes.RESOURCE_UNMOVE_UNBAN_PCMK_EXPIRED_NOT_SUPPORTED @property def message(self) -> str: return "expired is not supported, please upgrade pacemaker" @dataclass(frozen=True) class ResourceUnmoveUnbanPcmkError(ReportItemMessage): """ crm_resource exited with an error when unmoving/unbanning a resource resource_id -- id of the resource to be unmoved/unbanned stdout -- stdout of crm_resource stderr -- stderr of crm_resource """ resource_id: str stdout: str stderr: str _code = codes.RESOURCE_UNMOVE_UNBAN_PCMK_ERROR @property def message(self) -> str: return _stdout_stderr_to_string( self.stdout, self.stderr, prefix=f"cannot clear resource '{self.resource_id}'", ) @dataclass(frozen=True) class ResourceUnmoveUnbanPcmkSuccess(ReportItemMessage): """ crm_resource exited successfully when clearing unmoving/unbanning a resource resource_id -- id of the resource to be unmoved/unbanned stdout -- stdout of crm_resource stderr -- stderr of crm_resource """ resource_id: str stdout: str stderr: str _code = codes.RESOURCE_UNMOVE_UNBAN_PCMK_SUCCESS @property def message(self) -> str: return _stdout_stderr_to_string(self.stdout, self.stderr) @dataclass(frozen=True) class ResourceMayOrMayNotMove(ReportItemMessage): """ A move constraint has been created and the resource may or may not move depending on other configuration. resource_id -- id of the resource to be moved """ resource_id: str _code = codes.RESOURCE_MAY_OR_MAY_NOT_MOVE @property def message(self) -> str: return ( "A move constraint has been created and the resource " f"'{self.resource_id}' may or may not move depending on other " "configuration" ) @dataclass(frozen=True) class ResourceMoveConstraintCreated(ReportItemMessage): """ A constraint to move resource has been created. resource_id -- id of the resource to be moved """ resource_id: str _code = codes.RESOURCE_MOVE_CONSTRAINT_CREATED @property def message(self) -> str: return ( f"Location constraint to move resource '{self.resource_id}' has " "been created" ) @dataclass(frozen=True) class ResourceMoveConstraintRemoved(ReportItemMessage): """ A constraint to move resource has been removed. resource_id -- id of the resource to be moved """ resource_id: str _code = codes.RESOURCE_MOVE_CONSTRAINT_REMOVED @property def message(self) -> str: return ( f"Location constraint created to move resource " f"'{self.resource_id}' has been removed" ) @dataclass(frozen=True) class ResourceMoveNotAffectingResource(ReportItemMessage): """ Creating a location constraint to move a resource has no effect on the resource. resource_id -- id of the resource to be moved """ resource_id: str _code = codes.RESOURCE_MOVE_NOT_AFFECTING_RESOURCE @property def message(self) -> str: return ( f"Unable to move resource '{self.resource_id}' using a location " "constraint. Current location of the resource may be affected by " "some other constraint." ) @dataclass(frozen=True) class ResourceMoveAffectsOtherResources(ReportItemMessage): """ Moving a resource will also affect other resources. resource_id -- id of the resource to be moved affected_resources -- resources affected by the move operation """ resource_id: str affected_resources: List[str] _code = codes.RESOURCE_MOVE_AFFECTS_OTRHER_RESOURCES @property def message(self) -> str: return ( "Moving resource '{resource_id}' affects {resource_pl}: " "{affected_resources}" ).format( resource_id=self.resource_id, resource_pl=format_plural(self.affected_resources, "resource"), affected_resources=format_list(self.affected_resources), ) @dataclass(frozen=True) class ResourceMoveAutocleanSimulationFailure(ReportItemMessage): """ Autocleaning a constraint used for moving the resource would cause moving the resource itself or other resources. resource_id -- id of the resource to be moved others_affected -- True if also other resource would be affected, False otherwise node -- target node the resource should be moved to move_constraint_left_in_cib -- move has happened and the failure occurred when trying to remove the move constraint from the live cib """ resource_id: str others_affected: bool node: Optional[str] = None move_constraint_left_in_cib: bool = False _code = codes.RESOURCE_MOVE_AUTOCLEAN_SIMULATION_FAILURE @property def message(self) -> str: template = ( "Unable to ensure that moved resource '{resource_id}'{others} will " "stay on the same node after a constraint used for moving it is " "removed." ) if self.move_constraint_left_in_cib: template += ( " The constraint to move the resource has not been removed " "from configuration. Consider removing it manually. Be aware " "that removing the constraint may cause resources to move " "to other nodes." ) return template.format( resource_id=self.resource_id, others=" or other resources" if self.others_affected else "", ) @dataclass(frozen=True) class ParseErrorJsonFile(ReportItemMessage): """ Unable to parse a file with JSON data file_type_code -- item from pcs.common.file_type_codes line_number -- the line where parsing failed column_number -- the column where parsing failed position -- the start index of the file where parsing failed reason -- the unformatted error message full_msg -- full error message including above int attributes file_path -- path to the parsed file if available """ file_type_code: file_type_codes.FileTypeCode line_number: int column_number: int position: int reason: str full_msg: str file_path: Optional[str] _code = codes.PARSE_ERROR_JSON_FILE @property def message(self) -> str: return ( "Unable to parse {_file_type} file{_file_path}: {full_msg}" ).format( _file_path=format_optional(self.file_path, " '{}'"), _file_type=_format_file_role(self.file_type_code), full_msg=self.full_msg, ) @dataclass(frozen=True) class ResourceDisableAffectsOtherResources(ReportItemMessage): """ User requested disabling resources without affecting other resources but some resources would be affected disabled_resource_list -- list of resources to disable affected_resource_list -- other affected resources """ disabled_resource_list: List[str] affected_resource_list: List[str] _code = codes.RESOURCE_DISABLE_AFFECTS_OTHER_RESOURCES @property def message(self) -> str: return ( "Disabling specified {disabled_resource_pl} would have an effect " "on {this_pl} {affected_resource_pl}: " "{affected_resource_list}".format( disabled_resource_pl=format_plural( self.disabled_resource_list, "resource" ), this_pl=format_plural( self.affected_resource_list, "this", "these" ), affected_resource_pl=format_plural( self.affected_resource_list, "resource" ), affected_resource_list=format_list(self.affected_resource_list), ) ) @dataclass(frozen=True) class DrConfigAlreadyExist(ReportItemMessage): """ Disaster recovery config exists when the opposite was expected """ _code = codes.DR_CONFIG_ALREADY_EXIST @property def message(self) -> str: return "Disaster-recovery already configured" @dataclass(frozen=True) class DrConfigDoesNotExist(ReportItemMessage): """ Disaster recovery config does not exist when the opposite was expected """ _code = codes.DR_CONFIG_DOES_NOT_EXIST @property def message(self) -> str: return "Disaster-recovery is not configured" @dataclass(frozen=True) class NodeInLocalCluster(ReportItemMessage): """ Node is part of local cluster and it cannot be used for example to set up disaster-recovery site node -- node which is part of local cluster """ node: str _code = codes.NODE_IN_LOCAL_CLUSTER @property def message(self) -> str: return f"Node '{self.node}' is part of local cluster" @dataclass(frozen=True) class BoothPathNotExists(ReportItemMessage): """ Path '/etc/booth' is generated when Booth is installed, so it can be used to check whether Booth is installed path -- The path generated by booth installation """ path: str _code = codes.BOOTH_PATH_NOT_EXISTS @property def message(self) -> str: return ( f"Configuration directory for booth '{self.path}' is missing. " "Is booth installed?" ) @dataclass(frozen=True) class BoothLackOfSites(ReportItemMessage): """ Less than 2 booth sites entered. But it does not make sense. sites -- contains currently entered sites """ sites: List[str] _code = codes.BOOTH_LACK_OF_SITES @property def message(self) -> str: sites = format_list(self.sites) if self.sites else "missing" return ( "lack of sites for booth configuration (need 2 at least): sites " f"{sites}" ) @dataclass(frozen=True) class BoothEvenPeersNumber(ReportItemMessage): """ Booth requires odd number of peers. But even number of peers was entered. number -- determines how many peers was entered """ number: int _code = codes.BOOTH_EVEN_PEERS_NUM @property def message(self) -> str: return f"odd number of peers is required (entered {self.number} peers)" @dataclass(frozen=True) class BoothAddressDuplication(ReportItemMessage): """ Address of each peer must be unique. But address duplication appeared. duplicate_addresses -- contains addresses entered multiple times """ duplicate_addresses: List[str] _code = codes.BOOTH_ADDRESS_DUPLICATION @property def message(self) -> str: addresses = format_list(self.duplicate_addresses) return f"duplicate address for booth configuration: {addresses}" @dataclass(frozen=True) class BoothConfigUnexpectedLines(ReportItemMessage): """ Lines not conforming to expected config structure found in a booth config line_list -- not valid lines file_path -- path to the conf file if available """ line_list: List[str] file_path: str = "" _code = codes.BOOTH_CONFIG_UNEXPECTED_LINES @property def message(self) -> str: return "unexpected {line_pl} in booth config{path}:\n{lines}".format( line_pl=format_plural(self.line_list, "line"), path=format_optional(self.file_path, " '{}'"), lines="\n".join(self.line_list), ) @dataclass(frozen=True) class BoothInvalidName(ReportItemMessage): """ Booth instance name is not valid name -- entered booth instance name forbidden_characters -- characters the name cannot contain """ name: str forbidden_characters: str _code = codes.BOOTH_INVALID_NAME @property def message(self) -> str: return ( f"booth name '{self.name}' is not valid, it cannot contain " f"{self.forbidden_characters} characters" ) @dataclass(frozen=True) class BoothTicketNameInvalid(ReportItemMessage): """ Name of booth ticket may consists of alphanumeric characters or dash. Entered ticket name is violating this rule. ticket_name -- entered booth ticket name """ ticket_name: str _code = codes.BOOTH_TICKET_NAME_INVALID @property def message(self) -> str: return ( f"booth ticket name '{self.ticket_name}' is not valid, use " "up to 63 alphanumeric characters or dash" ) @dataclass(frozen=True) class BoothTicketDuplicate(ReportItemMessage): """ Each booth ticket name must be unique. But duplicate booth ticket name was entered. ticket_name -- entered booth ticket name """ ticket_name: str _code = codes.BOOTH_TICKET_DUPLICATE @property def message(self) -> str: return ( f"booth ticket name '{self.ticket_name}' already exists in " "configuration" ) @dataclass(frozen=True) class BoothTicketDoesNotExist(ReportItemMessage): """ Some operations (like ticket remove) expect the ticket name in booth configuration. But the ticket name was not found in booth configuration. ticket_name -- entered booth ticket name """ ticket_name: str _code = codes.BOOTH_TICKET_DOES_NOT_EXIST @property def message(self) -> str: return f"booth ticket name '{self.ticket_name}' does not exist" @dataclass(frozen=True) class BoothTicketNotInCib(ReportItemMessage): """ Expected ticket is not in CIB ticket_name -- name of the ticket """ ticket_name: str _code = codes.BOOTH_TICKET_NOT_IN_CIB @property def message(self) -> str: return f"Unable to find ticket '{self.ticket_name}' in CIB" @dataclass(frozen=True) class BoothAlreadyInCib(ReportItemMessage): """ Each booth instance should be in a cib once maximally. Existence of booth instance in cib detected during creating new one. name -- booth instance name """ name: str _code = codes.BOOTH_ALREADY_IN_CIB @property def message(self) -> str: return ( f"booth instance '{self.name}' is already created as cluster " "resource" ) @dataclass(frozen=True) class BoothNotExistsInCib(ReportItemMessage): """ Remove booth instance from cib required. But no such instance found in cib. name -- booth instance name """ name: str _code = codes.BOOTH_NOT_EXISTS_IN_CIB @property def message(self) -> str: return f"booth instance '{self.name}' not found in cib" @dataclass(frozen=True) class BoothConfigIsUsed(ReportItemMessage): """ Booth config use detected during destroy request. name -- booth instance name detail -- provides more details (for example booth instance is used as cluster resource or is started/enabled under systemd) resource_name -- which resource uses the booth instance, only valid if detail == BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE """ name: str detail: types.BoothConfigUsedWhere resource_name: Optional[str] = None _code = codes.BOOTH_CONFIG_IS_USED @property def message(self) -> str: detail_map = { const.BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE: "in a cluster resource", const.BOOTH_CONFIG_USED_ENABLED_IN_SYSTEMD: "- it is enabled in systemd", const.BOOTH_CONFIG_USED_RUNNING_IN_SYSTEMD: "- it is running by systemd", } detail = detail_map.get(self.detail, str(self.detail)) if ( self.detail == const.BOOTH_CONFIG_USED_IN_CLUSTER_RESOURCE and self.resource_name ): detail = f"in cluster resource '{self.resource_name}'" return f"booth instance '{self.name}' is used {detail}" @dataclass(frozen=True) class BoothMultipleTimesInCib(ReportItemMessage): """ Each booth instance should be in a cib once maximally. But multiple occurrences detected. For example during remove booth instance from cib. Notify user about this fact is required. When operation is forced user should be notified about multiple occurrences. name -- booth instance name """ name: str _code = codes.BOOTH_MULTIPLE_TIMES_IN_CIB @property def message(self) -> str: return f"found more than one booth instance '{self.name}' in cib" @dataclass(frozen=True) class BoothConfigDistributionStarted(ReportItemMessage): """ Booth configuration is about to be sent to nodes """ _code = codes.BOOTH_CONFIG_DISTRIBUTION_STARTED @property def message(self) -> str: return "Sending booth configuration to cluster nodes..." @dataclass(frozen=True) class BoothConfigAcceptedByNode(ReportItemMessage): """ Booth config has been saved on specified node. node -- name of node name_list -- list of names of booth instance """ node: str = "" name_list: List[str] = field(default_factory=list) _code = codes.BOOTH_CONFIG_ACCEPTED_BY_NODE @property def message(self) -> str: desc = "" if self.name_list and self.name_list not in [["booth"]]: desc = "{_s} {_list}".format( _s="s" if len(self.name_list) > 1 else "", _list=format_list(self.name_list), ) return "{node}Booth config{desc} saved".format( node=format_optional(self.node, "{}: "), desc=desc, ) @dataclass(frozen=True) class BoothConfigDistributionNodeError(ReportItemMessage): """ Saving booth config failed on specified node. node -- node name reason -- reason of failure name -- name of booth instance """ node: str reason: str name: str = "" _code = codes.BOOTH_CONFIG_DISTRIBUTION_NODE_ERROR @property def message(self) -> str: desc = _format_booth_default(self.name, " '{}'") return ( f"Unable to save booth config{desc} on node '{self.node}': " f"{self.reason}" ) @dataclass(frozen=True) class BoothFetchingConfigFromNode(ReportItemMessage): """ Fetching of booth config from specified node started. node -- node from which config is fetching config -- config name """ node: str config: str = "" _code = codes.BOOTH_FETCHING_CONFIG_FROM_NODE @property def message(self) -> str: desc = _format_booth_default(self.config, " '{}'") return f"Fetching booth config{desc} from node '{self.node}'..." @dataclass(frozen=True) class BoothUnsupportedFileLocation(ReportItemMessage): """ A booth file (config, authfile) is not in the expected dir, skipping it. file_path -- the actual path of the file expected_dir -- where the file is supposed to be file_type_code -- item from pcs.common.file_type_codes """ file_path: str expected_dir: str file_type_code: file_type_codes.FileTypeCode _code = codes.BOOTH_UNSUPPORTED_FILE_LOCATION @property def message(self) -> str: file_role = _format_file_role(self.file_type_code) return ( f"{file_role} '{self.file_path}' is outside of supported booth " f"config directory '{self.expected_dir}', ignoring the file" ) @dataclass(frozen=True) class BoothDaemonStatusError(ReportItemMessage): """ Unable to get status of booth daemon because of error. reason -- reason """ reason: str _code = codes.BOOTH_DAEMON_STATUS_ERROR @property def message(self) -> str: return f"unable to get status of booth daemon: {self.reason}" @dataclass(frozen=True) class BoothTicketStatusError(ReportItemMessage): """ Unable to get status of booth tickets because of error. reason -- reason """ reason: str = "" _code = codes.BOOTH_TICKET_STATUS_ERROR @property def message(self) -> str: reason = format_optional(self.reason, ": {}") return f"unable to get status of booth tickets{reason}" @dataclass(frozen=True) class BoothPeersStatusError(ReportItemMessage): """ Unable to get status of booth peers because of error. reason -- reason """ reason: str = "" _code = codes.BOOTH_PEERS_STATUS_ERROR @property def message(self) -> str: reason = format_optional(self.reason, ": {}") return f"unable to get status of booth peers{reason}" @dataclass(frozen=True) class BoothCannotDetermineLocalSiteIp(ReportItemMessage): """ Some booth operations are performed on specific site and requires to specify site ip. When site specification omitted pcs can try determine local ip. But determine local site ip failed. """ _code = codes.BOOTH_CANNOT_DETERMINE_LOCAL_SITE_IP @property def message(self) -> str: return "cannot determine local site ip, please specify site parameter" @dataclass(frozen=True) class BoothTicketOperationFailed(ReportItemMessage): """ Pcs uses external tools for some ticket_name operations. For example booth tools are used for grant and revoke, or pacemaker tools are used for standby and cleanup. But the external command failed. operation -- determine what was intended perform with ticket_name reason -- error description from external booth command site_ip -- specify what site had to run the command ticket_name -- specify with which ticket had to run the command """ operation: str reason: str site_ip: Optional[str] ticket_name: str _code = codes.BOOTH_TICKET_OPERATION_FAILED @property def message(self) -> str: return ( "unable to {operation} booth ticket '{ticket_name}'{site}, " "reason: {reason}" ).format( operation=self.operation, ticket_name=self.ticket_name, reason=self.reason, site=format_optional(self.site_ip, template=" for site '{}'"), ) @dataclass(frozen=True) class BoothTicketChangingState(ReportItemMessage): """ The state of the ticket is changing ticket_name -- name of the ticket state -- new state of the ticket """ ticket_name: str state: Literal["active", "standby"] _code = codes.BOOTH_TICKET_CHANGING_STATE @property def message(self) -> str: return f"Changing state of ticket '{self.ticket_name}' to {self.state}" @dataclass(frozen=True) class BoothTicketCleanup(ReportItemMessage): """ The booth ticket is going to be removed from CIB ticket_name -- name of the ticket """ ticket_name: str _code = codes.BOOTH_TICKET_CLEANUP @property def message(self) -> str: return f"Cleaning up ticket '{self.ticket_name}' from CIB" # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagAddRemoveIdsDuplication(ReportItemMessage): """ Duplicate reference ids were found in tag create or update add/remove specification. """ duplicate_ids_list: List[str] add_or_not_remove: bool = True _code = codes.TAG_ADD_REMOVE_IDS_DUPLICATION @property def message(self) -> str: action = "add" if self.add_or_not_remove else "remove" duplicate_ids = format_list(self.duplicate_ids_list) return f"Ids to {action} must be unique, duplicate ids: {duplicate_ids}" # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagAdjacentReferenceIdNotInTheTag(ReportItemMessage): """ Cannot put reference ids next to an adjacent reference id in a tag, because the adjacent reference id does not belong to the tag. adjacent_id -- adjacent reference id tag_id -- tag id """ adjacent_id: str tag_id: str _code = codes.TAG_ADJACENT_REFERENCE_ID_NOT_IN_THE_TAG @property def message(self) -> str: return ( f"There is no reference id '{self.adjacent_id}' in the tag " f"'{self.tag_id}', cannot put reference ids next to it in the tag" ) # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagCannotAddAndRemoveIdsAtTheSameTime(ReportItemMessage): """ Cannot add and remove ids at the same time. Avoid operation without an effect. idref_list -- common ids from add and remove lists """ idref_list: List[str] _code = codes.TAG_CANNOT_ADD_AND_REMOVE_IDS_AT_THE_SAME_TIME @property def message(self) -> str: idref_list = format_list(self.idref_list) return f"Ids cannot be added and removed at the same time: {idref_list}" # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagCannotAddReferenceIdsAlreadyInTheTag(ReportItemMessage): """ Cannot add reference ids already in the tag. tag_id -- tag id idref_list -- reference ids already in tag """ tag_id: str idref_list: List[str] _code = codes.TAG_CANNOT_ADD_REFERENCE_IDS_ALREADY_IN_THE_TAG @property def message(self) -> str: return ( "Cannot add reference {ids} already in the tag '{tag_id}': " "{idref_list}" ).format( ids=format_plural(self.idref_list, "id"), tag_id=self.tag_id, idref_list=format_list(self.idref_list), ) @dataclass(frozen=True) class TagCannotContainItself(ReportItemMessage): """ List of object reference ids contains the same id as specified tag_id. """ _code = codes.TAG_CANNOT_CONTAIN_ITSELF @property def message(self) -> str: return "Tag cannot contain itself" @dataclass(frozen=True) class TagCannotCreateEmptyTagNoIdsSpecified(ReportItemMessage): """ Cannot create empty tag, no reference ids were specified. """ _code = codes.TAG_CANNOT_CREATE_EMPTY_TAG_NO_IDS_SPECIFIED @property def message(self) -> str: return "Cannot create empty tag, no resource ids specified" # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagCannotPutIdNextToItself(ReportItemMessage): """ Cannot put id next to itself. Wrong adjacent id. adjacent_id -- adjacent reference id """ adjacent_id: str _code = codes.TAG_CANNOT_PUT_ID_NEXT_TO_ITSELF @property def message(self) -> str: return f"Cannot put id '{self.adjacent_id}' next to itself." # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagCannotRemoveAdjacentId(ReportItemMessage): """ Cannot remove adjacent id. adjacent_id -- adjacent reference id """ adjacent_id: str _code = codes.TAG_CANNOT_REMOVE_ADJACENT_ID @property def message(self) -> str: return ( f"Cannot remove id '{self.adjacent_id}' next to which ids are being" " added" ) # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagCannotRemoveReferencesWithoutRemovingTag(ReportItemMessage): """ Cannot remove references without removing a tag. """ tag_id: str _code = codes.TAG_CANNOT_REMOVE_REFERENCES_WITHOUT_REMOVING_TAG @property def message(self) -> str: return f"There would be no references left in the tag '{self.tag_id}'" @dataclass(frozen=True) class TagCannotRemoveTagReferencedInConstraints(ReportItemMessage): """ Cannot remove tag which is referenced in constraints. tag_id -- tag id constraint_id_list -- list of constraint ids which are referencing tag """ tag_id: str constraint_id_list: List[str] _code = codes.TAG_CANNOT_REMOVE_TAG_REFERENCED_IN_CONSTRAINTS @property def message(self) -> str: return ( "Tag '{tag_id}' cannot be removed because it is referenced in " "{constraints} {constraint_id_list}" ).format( tag_id=self.tag_id, constraints=format_plural(self.constraint_id_list, "constraint"), constraint_id_list=format_list(self.constraint_id_list), ) @dataclass(frozen=True) class TagCannotRemoveTagsNoTagsSpecified(ReportItemMessage): """ Cannot remove tags, no tags were specified. """ _code = codes.TAG_CANNOT_REMOVE_TAGS_NO_TAGS_SPECIFIED @property def message(self) -> str: return "Cannot remove tags, no tags to remove specified" # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagCannotSpecifyAdjacentIdWithoutIdsToAdd(ReportItemMessage): """ Cannot specify adjacent id without ids to add. adjacent_id -- adjacent reference id """ adjacent_id: str _code = codes.TAG_CANNOT_SPECIFY_ADJACENT_ID_WITHOUT_IDS_TO_ADD @property def message(self) -> str: return ( f"Cannot specify adjacent id '{self.adjacent_id}' without ids to " "add" ) # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagCannotUpdateTagNoIdsSpecified(ReportItemMessage): """ Cannot update tag, no ids specified. """ _code = codes.TAG_CANNOT_UPDATE_TAG_NO_IDS_SPECIFIED @property def message(self) -> str: return "Cannot update tag, no ids to be added or removed specified" # TODO: remove, use ADD_REMOVE reports @dataclass(frozen=True) class TagIdsNotInTheTag(ReportItemMessage): """ Specified ids are not present in the specified tag. """ tag_id: str id_list: List[str] _code = codes.TAG_IDS_NOT_IN_THE_TAG @property def message(self) -> str: return "Tag '{tag_id}' does not contain {ids}: {id_list}".format( tag_id=self.tag_id, ids=format_plural(self.id_list, "id"), id_list=format_list(self.id_list), ) @dataclass(frozen=True) class RuleInEffectStatusDetectionNotSupported(ReportItemMessage): """ Pacemaker tool for detecting if a rule is expired or not is not available """ _code = codes.RULE_IN_EFFECT_STATUS_DETECTION_NOT_SUPPORTED @property def message(self) -> str: return ( "crm_rule is not available, therefore expired parts of " "configuration may not be detected. Consider upgrading pacemaker." ) @dataclass(frozen=True) class RuleExpressionOptionsDuplication(ReportItemMessage): """ Keys are specified more than once in a single rule (sub)expression duplicate_option_list -- list of keys duplicated in a single (sub)expression """ duplicate_option_list: List[str] _code = codes.RULE_EXPRESSION_OPTIONS_DUPLICATION @property def message(self) -> str: options = format_list(self.duplicate_option_list) return f"Duplicate options in a single (sub)expression: {options}" @dataclass(frozen=True) class RuleExpressionParseError(ReportItemMessage): """ Unable to parse pacemaker cib rule expression string rule_string -- the whole rule expression string reason -- error message from rule parser rule_line -- part of rule_string - the line where the error occurred line_number -- the line where parsing failed column_number -- the column where parsing failed position -- the start index where parsing failed """ rule_string: str reason: str rule_line: str line_number: int column_number: int position: int _code = codes.RULE_EXPRESSION_PARSE_ERROR @property def message(self) -> str: # Messages coming from the parser are not very useful and readable, # they mostly contain one line grammar expression covering the whole # rule. No user would be able to parse that. Therefore we omit the # messages. return ( f"'{self.rule_string}' is not a valid rule expression, parse error " f"near or after line {self.line_number} column {self.column_number}" ) @dataclass(frozen=True) class RuleExpressionNotAllowed(ReportItemMessage): """ Used rule expression is not allowed in current context expression_type -- disallowed expression type """ expression_type: CibRuleExpressionType _code = codes.RULE_EXPRESSION_NOT_ALLOWED @property def message(self) -> str: type_map = { CibRuleExpressionType.EXPRESSION: ( "Keywords 'defined', 'not_defined', 'eq', 'ne', 'gte', 'gt', " "'lte' and 'lt'" ), CibRuleExpressionType.OP_EXPRESSION: "Keyword 'op'", CibRuleExpressionType.RSC_EXPRESSION: "Keyword 'resource'", } return ( f"{type_map[self.expression_type]} cannot be used " "in a rule in this command" ) @dataclass(frozen=True) class RuleExpressionSinceGreaterThanUntil(ReportItemMessage): """ In a date expression, 'until' predates 'since' """ since: str until: str _code = codes.RULE_EXPRESSION_SINCE_GREATER_THAN_UNTIL @property def message(self) -> str: return f"Since '{self.since}' is not sooner than until '{self.until}'" @dataclass(frozen=True) class RuleNoExpressionSpecified(ReportItemMessage): """ No rule was specified / empty rule was specified when a rule is required """ _code = codes.RULE_NO_EXPRESSION_SPECIFIED @property def message(self) -> str: return "No rule expression was specified" @dataclass(frozen=True) class CibNvsetAmbiguousProvideNvsetId(ReportItemMessage): """ An old command supporting only one nvset have been used when several nvsets exist. We require an nvset ID the command should work with to be specified. """ pcs_command: types.PcsCommand _code = codes.CIB_NVSET_AMBIGUOUS_PROVIDE_NVSET_ID @property def message(self) -> str: return "Several options sets exist, please specify an option set ID" @dataclass(frozen=True) class AddRemoveItemsNotSpecified(ReportItemMessage): """ Cannot modify container, no add or remove items specified. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str _code = codes.ADD_REMOVE_ITEMS_NOT_SPECIFIED @property def message(self) -> str: container = _add_remove_container_str(self.container_type) items = get_plural(_add_remove_item_str(self.item_type)) return ( f"Cannot modify {container} '{self.container_id}', no {items} to " "add or remove specified" ) @dataclass(frozen=True) class AddRemoveItemsDuplication(ReportItemMessage): """ Duplicate items were found in add/remove item lists. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container duplicate_items_list -- list of duplicate items """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str duplicate_items_list: List[str] _code = codes.ADD_REMOVE_ITEMS_DUPLICATION @property def message(self) -> str: items = get_plural(_add_remove_item_str(self.item_type)) duplicate_items = format_list(self.duplicate_items_list) return ( f"{items.capitalize()} to add or remove must be unique, duplicate " f"{items}: {duplicate_items}" ) @dataclass(frozen=True) class AddRemoveCannotAddItemsAlreadyInTheContainer(ReportItemMessage): """ Cannot add items already existing in the container. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container item_list -- list of items already in the container """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str item_list: List[str] _code = codes.ADD_REMOVE_CANNOT_ADD_ITEMS_ALREADY_IN_THE_CONTAINER @property def message(self) -> str: items = format_plural( self.item_list, _add_remove_item_str(self.item_type) ) item_list = format_list(self.item_list) they = format_plural(self.item_list, "it") are = format_plural(self.item_list, "is") container = _add_remove_container_str(self.container_type) return ( f"Cannot add {items} {item_list}, {they} {are} already present in " f"{container} '{self.container_id}'" ) @dataclass(frozen=True) class AddRemoveCannotRemoveItemsNotInTheContainer(ReportItemMessage): """ Cannot remove items not existing in the container. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container item_list -- list of items not in the container """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str item_list: List[str] _code = codes.ADD_REMOVE_CANNOT_REMOVE_ITEMS_NOT_IN_THE_CONTAINER @property def message(self) -> str: items = format_plural( self.item_list, _add_remove_item_str(self.item_type) ) item_list = format_list(self.item_list) they = format_plural(self.item_list, "it") are = format_plural(self.item_list, "is") container = _add_remove_container_str(self.container_type) items = format_plural( self.item_list, _add_remove_item_str(self.item_type) ) return ( f"Cannot remove {items} {item_list}, {they} {are} not present in " f"{container} '{self.container_id}'" ) @dataclass(frozen=True) class AddRemoveCannotAddAndRemoveItemsAtTheSameTime(ReportItemMessage): """ Cannot add and remove items at the same time. Avoid operation without an effect. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container item_list -- common items from add and remove item lists """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str item_list: List[str] _code = codes.ADD_REMOVE_CANNOT_ADD_AND_REMOVE_ITEMS_AT_THE_SAME_TIME @property def message(self) -> str: items = format_plural( self.item_list, _add_remove_item_str(self.item_type) ) item_list = format_list(self.item_list) return ( f"{items.capitalize()} cannot be added and removed at the same " f"time: {item_list}" ) @dataclass(frozen=True) class AddRemoveCannotRemoveAllItemsFromTheContainer(ReportItemMessage): """ Cannot remove all items from a container. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container item_list -- common items from add and remove item lists """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str item_list: List[str] _code = codes.ADD_REMOVE_CANNOT_REMOVE_ALL_ITEMS_FROM_THE_CONTAINER @property def message(self) -> str: container = _add_remove_container_str(self.container_type) items = get_plural(_add_remove_item_str(self.item_type)) return ( f"Cannot remove all {items} from {container} '{self.container_id}'" ) @dataclass(frozen=True) class AddRemoveAdjacentItemNotInTheContainer(ReportItemMessage): """ Cannot put items next to an adjacent item in the container, because the adjacent item does not exist in the container. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container adjacent_item_id -- id of an adjacent item """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str adjacent_item_id: str _code = codes.ADD_REMOVE_ADJACENT_ITEM_NOT_IN_THE_CONTAINER @property def message(self) -> str: container = _add_remove_container_str(self.container_type) item = _add_remove_item_str(self.item_type) items = get_plural(item) return ( f"There is no {item} '{self.adjacent_item_id}' in the " f"{container} '{self.container_id}', cannot add {items} next to it" ) @dataclass(frozen=True) class AddRemoveCannotPutItemNextToItself(ReportItemMessage): """ Cannot put an item into a container next to itself. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container adjacent_item_id -- id of an adjacent item """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str adjacent_item_id: str _code = codes.ADD_REMOVE_CANNOT_PUT_ITEM_NEXT_TO_ITSELF @property def message(self) -> str: item = _add_remove_item_str(self.item_type) return f"Cannot put {item} '{self.adjacent_item_id}' next to itself" @dataclass(frozen=True) class AddRemoveCannotSpecifyAdjacentItemWithoutItemsToAdd(ReportItemMessage): """ Cannot specify adjacent item without items to add. container_type -- type of item container item_type -- type of item in a container container_id -- id of a container adjacent_item_id -- id of an adjacent item """ container_type: types.AddRemoveContainerType item_type: types.AddRemoveItemType container_id: str adjacent_item_id: str _code = codes.ADD_REMOVE_CANNOT_SPECIFY_ADJACENT_ITEM_WITHOUT_ITEMS_TO_ADD @property def message(self) -> str: item = _add_remove_item_str(self.item_type) items = get_plural(item) return ( f"Cannot specify adjacent {item} '{self.adjacent_item_id}' without " f"{items} to add" ) @dataclass(frozen=True) class ResourceWaitDeprecated(ReportItemMessage): """ Deprecated wait parameter was used in command. """ _code = codes.RESOURCE_WAIT_DEPRECATED @property def message(self) -> str: return ( "Ability of this command to accept 'wait' argument is " "deprecated and will be removed in a future release." ) @dataclass(frozen=True) class CommandInvalidPayload(ReportItemMessage): reason: str _code = codes.COMMAND_INVALID_PAYLOAD @property def message(self) -> str: return f"Invalid command payload: {self.reason}" @dataclass(frozen=True) class CommandUnknown(ReportItemMessage): command: str _code = codes.COMMAND_UNKNOWN @property def message(self) -> str: return f"Unknown command '{self.command}'" @dataclass(frozen=True) class NotAuthorized(ReportItemMessage): _code = codes.NOT_AUTHORIZED @property def message(self) -> str: return "Current user is not authorized for this operation" @dataclass(frozen=True) class AgentSelfValidationResult(ReportItemMessage): """ Result of running of resource options by agent itself result -- output of agent """ result: str _code = codes.AGENT_SELF_VALIDATION_RESULT @property def message(self) -> str: return "Validation result from agent:\n{result}".format( result="\n".join(indent(self.result.splitlines())) ) @dataclass(frozen=True) class AgentSelfValidationInvalidData(ReportItemMessage): """ Agent self validation produced an invalid data reason -- text description of the issue """ reason: str _code = codes.AGENT_SELF_VALIDATION_INVALID_DATA @property def message(self) -> str: return f"Invalid validation data from agent: {self.reason}" @dataclass(frozen=True) class AgentSelfValidationSkippedUpdatedResourceMisconfigured(ReportItemMessage): """ Agent self validation is skipped when updating a resource as it is misconfigured in its current state. """ result: str _code = codes.AGENT_SELF_VALIDATION_SKIPPED_UPDATED_RESOURCE_MISCONFIGURED @property def message(self) -> str: return ( "The resource was misconfigured before the update, therefore agent " "self-validation will not be run for the updated configuration. " "Validation output of the original configuration:\n{result}" ).format(result="\n".join(indent(self.result.splitlines()))) class AgentSelfValidationAutoOnWithWarnings(ReportItemMessage): """ Agent self validation is enabled for all applicable commands and it produces warnings. In a future version, this may be switched to errors. """ _code = codes.AGENT_SELF_VALIDATION_AUTO_ON_WITH_WARNINGS @property def message(self) -> str: return ( "Validating resource options using the resource agent itself is " "enabled by default and produces warnings. In a future version, " "this might be changed to errors. Enable agent validation to " "switch to the future behavior." ) @dataclass(frozen=True) class ResourceCloneIncompatibleMetaAttributes(ReportItemMessage): """ Some clone specific meta attributes are not compatible with some resource agents attribute -- incompatible attribute name resource_agent -- agent which doesn't support specified attribute resource_id -- id of primitive resource to which this apply group_id -- id of resource group in which resource_id is placed """ attribute: str resource_agent: ResourceAgentNameDto resource_id: Optional[str] = None group_id: Optional[str] = None _code = codes.RESOURCE_CLONE_INCOMPATIBLE_META_ATTRIBUTES @property def message(self) -> str: resource_desc = "" if self.resource_id: resource_desc = f" of resource '{self.resource_id}'" if self.group_id: resource_desc += f" in group '{self.group_id}'" return ( f"Clone option '{self.attribute}' is not compatible with " f"'{get_resource_agent_full_name(self.resource_agent)}' resource " f"agent{resource_desc}" ) @dataclass(frozen=True) class BoothAuthfileNotUsed(ReportItemMessage): """ Booth autfile configured but has no effect, another option should be enabled as well. """ instance: Optional[str] _code = codes.BOOTH_AUTHFILE_NOT_USED @property def message(self) -> str: return "Booth authfile is not enabled" @dataclass(frozen=True) class BoothUnsupportedOptionEnableAuthfile(ReportItemMessage): """ Booth enable-autfile option is present in the booth configuration but is not accepted by booth, which will cause booth to fail at startup. """ instance: Optional[str] _code = codes.BOOTH_UNSUPPORTED_OPTION_ENABLE_AUTHFILE @property def message(self) -> str: return ( "Unsupported option 'enable-authfile' is set in booth configuration" ) @dataclass(frozen=True) class CannotCreateDefaultClusterPropertySet(ReportItemMessage): """ Cannot create default cluster properties nvset nvset_id -- id of the nvset """ nvset_id: str _code = codes.CANNOT_CREATE_DEFAULT_CLUSTER_PROPERTY_SET @property def message(self) -> str: return ( "Cannot create default cluster_property_set element, ID " f"'{self.nvset_id}' already exists. Find elements with the ID and " "remove them from cluster configuration." ) @dataclass(frozen=True) class CommandArgumentTypeMismatch(ReportItemMessage): """ Command does not accept specific type of an argument. not_accepted_type -- description of an entity not being accepted command_to_use_instead -- identifier of a command to use instead """ not_accepted_type: str command_to_use_instead: Optional[types.PcsCommand] = None _code = codes.COMMAND_ARGUMENT_TYPE_MISMATCH @property def message(self) -> str: return f"This command does not accept {self.not_accepted_type}." @dataclass(frozen=True) class ClusterOptionsMetadataNotSupported(ReportItemMessage): """ Pacemaker crm_attribute does not support new cluster options metadata. """ _code = codes.CLUSTER_OPTIONS_METADATA_NOT_SUPPORTED @property def message(self) -> str: return ( "Cluster options metadata are not supported, please upgrade " "pacemaker" ) pcs-0.12.0.2/pcs/common/reports/processor.py000066400000000000000000000020311500417470700206640ustar00rootroot00000000000000import abc from .item import ( ReportItem, ReportItemList, ReportItemSeverity, ) class ReportProcessor(abc.ABC): def __init__(self) -> None: self._has_errors = False @property def has_errors(self) -> bool: return self._has_errors def report(self, report_item: ReportItem) -> "ReportProcessor": if _is_error(report_item): self._has_errors = True self._do_report(report_item) return self def report_list(self, report_list: ReportItemList) -> "ReportProcessor": for report_item in report_list: self.report(report_item) return self @abc.abstractmethod def _do_report(self, report_item: ReportItem) -> None: raise NotImplementedError() def has_errors(report_list: ReportItemList) -> bool: for report_item in report_list: if _is_error(report_item): return True return False def _is_error(report_item: ReportItem) -> bool: return report_item.severity.level == ReportItemSeverity.ERROR pcs-0.12.0.2/pcs/common/reports/types.py000066400000000000000000000015421500417470700200170ustar00rootroot00000000000000from typing import NewType AddRemoveContainerType = NewType("AddRemoveContainerType", str) AddRemoveItemType = NewType("AddRemoveItemType", str) BoothConfigUsedWhere = NewType("BoothConfigUsedWhere", str) DefaultAddressSource = NewType("DefaultAddressSource", str) FenceHistoryCommandType = NewType("FenceHistoryCommandType", str) ForceCode = NewType("ForceCode", str) MessageCode = NewType("MessageCode", str) DeprecatedMessageCode = NewType("DeprecatedMessageCode", str) PcsCommand = NewType("PcsCommand", str) ReasonType = NewType("ReasonType", str) ServiceAction = NewType("ServiceAction", str) SeverityLevel = NewType("SeverityLevel", str) StonithRestartlessUpdateUnableToPerformReason = NewType( "StonithRestartlessUpdateUnableToPerformReason", str ) StonithWatchdogTimeoutCannotBeSetReason = NewType( "StonithWatchdogTimeoutCannotBeSetReason", str ) pcs-0.12.0.2/pcs/common/resource_agent/000077500000000000000000000000001500417470700176065ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/resource_agent/__init__.py000066400000000000000000000000461500417470700217170ustar00rootroot00000000000000from . import ( const, dto, ) pcs-0.12.0.2/pcs/common/resource_agent/const.py000066400000000000000000000004351500417470700213100ustar00rootroot00000000000000# OCF 1.0 doesn't define unique groups, they are defined since OCF 1.1. Pcs # transforms OCF 1.0 agents to OCF 1.1 structure and therefore needs to create # a group name for OCF 1.0 unique attrs. The name is: {this_prefix}{attr_name} DEFAULT_UNIQUE_GROUP_PREFIX = "_pcs_unique_group_" pcs-0.12.0.2/pcs/common/resource_agent/dto.py000066400000000000000000000054111500417470700207470ustar00rootroot00000000000000from dataclasses import ( dataclass, field, ) from typing import ( List, Optional, ) from pcs.common.interface.dto import ( DataTransferObject, meta, ) @dataclass(frozen=True) class ResourceAgentNameDto(DataTransferObject): standard: str provider: Optional[str] type: str def get_resource_agent_full_name(agent_name: ResourceAgentNameDto) -> str: return ":".join( filter( None, [agent_name.standard, agent_name.provider, agent_name.type] ) ) @dataclass(frozen=True) class ListResourceAgentNameDto(DataTransferObject): names: List[ResourceAgentNameDto] @dataclass(frozen=True) class ResourceAgentActionDto(DataTransferObject): # pylint: disable=too-many-instance-attributes # (start, stop, promote...), mandatory by both OCF 1.0 and 1.1 name: str # mandatory by both OCF 1.0 and 1.1, sometimes not defined by agents timeout: Optional[str] # optional by both OCF 1.0 and 1.1 interval: Optional[str] # optional by OCF 1.1 # not allowed by OCF 1.0, defined in OCF 1.0 agents anyway role: Optional[str] # OCF name: 'start-delay', optional by both OCF 1.0 and 1.1 start_delay: Optional[str] = field(metadata=meta(name="start-delay")) # optional by both OCF 1.0 and 1.1 depth: Optional[str] # not allowed by any OCF, defined in OCF 1.0 agents anyway automatic: bool # not allowed by any OCF, defined in OCF 1.0 agents anyway on_target: bool @dataclass(frozen=True) class ResourceAgentParameterDto(DataTransferObject): # pylint: disable=too-many-instance-attributes # name of the parameter name: str # short description shortdesc: Optional[str] # long description longdesc: Optional[str] # data type of the parameter type: str # default value of the parameter default: Optional[str] # allowed values, only defined if type == 'select' enum_values: Optional[List[str]] # True if it is a required parameter, False otherwise required: bool # True if the parameter is meant for advanced users advanced: bool # True if the parameter is deprecated, False otherwise deprecated: bool # list of parameters deprecating this one deprecated_by: List[str] # text describing / explaining the deprecation deprecated_desc: Optional[str] # should the parameter's value be unique across same agent resources? unique_group: Optional[str] # changing this parameter's value triggers a reload instead of a restart reloadable: bool @dataclass(frozen=True) class ResourceAgentMetadataDto(DataTransferObject): name: ResourceAgentNameDto shortdesc: Optional[str] longdesc: Optional[str] parameters: List[ResourceAgentParameterDto] actions: List[ResourceAgentActionDto] pcs-0.12.0.2/pcs/common/resource_status.py000066400000000000000000001134411500417470700204110ustar00rootroot00000000000000from collections import defaultdict from dataclasses import dataclass from enum import ( Enum, auto, ) from typing import ( Final, Iterable, Literal, Optional, Sequence, Union, cast, ) from pcs.common.const import ( PCMK_ROLE_STOPPED, PCMK_STATUS_ROLE_DEMOTING, PCMK_STATUS_ROLE_MIGRATING, PCMK_STATUS_ROLE_PROMOTED, PCMK_STATUS_ROLE_PROMOTING, PCMK_STATUS_ROLE_STARTED, PCMK_STATUS_ROLE_STARTING, PCMK_STATUS_ROLE_STOPPED, PCMK_STATUS_ROLE_STOPPING, PCMK_STATUS_ROLE_UNPROMOTED, PCMK_STATUS_ROLES_PENDING, ) from pcs.common.status_dto import ( AnyResourceStatusDto, BundleReplicaStatusDto, BundleStatusDto, CloneStatusDto, GroupStatusDto, PrimitiveStatusDto, ResourcesStatusDto, ) class ResourceType(Enum): PRIMITIVE = "primitive" GROUP = "group" CLONE = "clone" BUNDLE = "bundle" # used to check if any value other than None is present # in optional attributes of status dto class NotNoneValue: pass NOT_NONE: Final = NotNoneValue() StateValueType = Union[str, bool, NotNoneValue, set[str]] AttributeTuple = tuple[str, StateValueType] class ResourceState(Enum): """ possible values for checking the state of the resource """ STARTED: AttributeTuple = ("role", PCMK_STATUS_ROLE_STARTED) STOPPED: AttributeTuple = ("role", PCMK_STATUS_ROLE_STOPPED) PROMOTED: AttributeTuple = ("role", PCMK_STATUS_ROLE_PROMOTED) UNPROMOTED: AttributeTuple = ("role", PCMK_STATUS_ROLE_UNPROMOTED) STARTING: AttributeTuple = ("role", PCMK_STATUS_ROLE_STARTING) STOPPING: AttributeTuple = ("role", PCMK_STATUS_ROLE_STOPPING) MIGRATING: AttributeTuple = ("role", PCMK_STATUS_ROLE_MIGRATING) PROMOTING: AttributeTuple = ("role", PCMK_STATUS_ROLE_PROMOTING) DEMOTING: AttributeTuple = ("role", PCMK_STATUS_ROLE_DEMOTING) MONITORING: AttributeTuple = ("pending", "Monitoring") DISABLED: AttributeTuple = ("disabled", True) ENABLED: AttributeTuple = ("disabled", False) MANAGED: AttributeTuple = ("managed", True) UNMANAGED: AttributeTuple = ("managed", False) MAINTENANCE: AttributeTuple = ("maintenance", True) FAILED: AttributeTuple = ("failed", True) ACTIVE: AttributeTuple = ("active", True) ORPHANED: AttributeTuple = ("orphaned", True) BLOCKED: AttributeTuple = ("blocked", True) FAILURE_IGNORED: AttributeTuple = ("failure_ignored", True) PENDING: list[AttributeTuple] = [ ("role", set(PCMK_STATUS_ROLES_PENDING)), ("pending", "Monitoring"), ] LOCKED_TO: AttributeTuple = ("locked_to", NOT_NONE) ResourceStateExactCheck = Literal[ResourceState.LOCKED_TO] EXACT_CHECK_STATES = (ResourceState.LOCKED_TO,) class MoreChildrenQuantifierType(Enum): ALL = auto() ANY = auto() NONE = auto() @dataclass(frozen=True) class PrimitiveInstances: instances: Sequence[PrimitiveStatusDto] @dataclass(frozen=True) class GroupInstances: instances: Sequence[GroupStatusDto] CheckedResourceType = Union[ PrimitiveInstances, GroupInstances, CloneStatusDto, BundleStatusDto, ] _TYPE_MAP = { PrimitiveStatusDto: ResourceType.PRIMITIVE, GroupStatusDto: ResourceType.GROUP, CloneStatusDto: ResourceType.CLONE, BundleStatusDto: ResourceType.BUNDLE, } _UNIQUE_RESOURCES_TYPES = (CloneStatusDto, BundleStatusDto) _UNIQUE_RESOURCES_CONST = tuple( _TYPE_MAP[res_type] for res_type in _UNIQUE_RESOURCES_TYPES ) _PROMOTABLE_RESOURCES_TYPES = (CloneStatusDto,) _PROMOTABLE_RESOURCES_CONST = tuple( _TYPE_MAP[res_type] for res_type in _PROMOTABLE_RESOURCES_TYPES ) def none(iterable: Iterable[object]) -> bool: return not any(iterable) _MORE_CHILDREN_QUANTIFIER_MAP = { MoreChildrenQuantifierType.ALL: all, MoreChildrenQuantifierType.ANY: any, MoreChildrenQuantifierType.NONE: none, } class QueryException(Exception): pass class MembersQuantifierUnsupportedException(QueryException): pass class InstancesQuantifierUnsupportedException(QueryException): pass class ResourceException(Exception): def __init__(self, resource_id: str, instance_id: Optional[str]): self.resource_id = resource_id self.instance_id = instance_id class ResourceNonExistentException(ResourceException): pass class ResourceNotInGroupException(ResourceException): pass class ResourceUnexpectedTypeException(ResourceException): def __init__( self, resource_id: str, instance_id: Optional[str], resource_type: ResourceType, expected_types: list[ResourceType], ): super().__init__(resource_id, instance_id) self.resource_type = resource_type self.expected_types = expected_types def can_be_unique(resource_type: ResourceType) -> bool: return resource_type in _UNIQUE_RESOURCES_CONST def can_be_promotable(resource_type: ResourceType) -> bool: return resource_type in _PROMOTABLE_RESOURCES_CONST class ResourcesStatusFacade: def __init__(self, resources: Sequence[AnyResourceStatusDto]): self._resources = resources self._resource_map: dict[str, list[AnyResourceStatusDto]] = defaultdict( list ) self._child_parent_map: dict[str, str] = {} self._bundle_member_node_map: dict[int, list[str]] = {} self._bundle_defined = False for resource in resources: self._resource_map[resource.resource_id].append(resource) if isinstance(resource, GroupStatusDto): self.__add_group_children_to_maps(resource) elif isinstance(resource, CloneStatusDto): for instance in resource.instances: self._child_parent_map[instance.resource_id] = ( resource.resource_id ) self._resource_map[instance.resource_id].append(instance) if isinstance(instance, GroupStatusDto): self.__add_group_children_to_maps(instance) elif isinstance(resource, BundleStatusDto): self._bundle_defined = True for replica in resource.replicas: member = replica.member if member is not None: self._child_parent_map[member.resource_id] = ( resource.resource_id ) self._resource_map[member.resource_id].append(member) self._bundle_member_node_map[id(member)] = ( replica.container.node_names ) @classmethod def from_resources_status_dto( cls, resources_status_dto: ResourcesStatusDto ) -> "ResourcesStatusFacade": """ Create ResourcesStatusFacade from ResourcesStatusDto resources_status_dto -- dto with status of the resources in cluster """ return cls(resources_status_dto.resources) def get_resource_one_instance( self, resource_id: str, instance_id: Optional[str] ) -> Optional[AnyResourceStatusDto]: """ Get one instance of resource with given id. Get instance that appears first in the status xml if instance_id is not specified. Return None if resource with given id does not exist. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource_list = self._resource_map.get(resource_id) if resource_list is None: return None if instance_id is None: return resource_list[0] for resource in resource_list: if ( hasattr(resource, "instance_id") and resource.instance_id == instance_id ): return resource return None def get_resource_all_instances( self, resource_id: str ) -> Optional[list[AnyResourceStatusDto]]: """ Get a list of all the instances of resource with the given id. Return None if resource with the given id does not exist. resource_id -- id of the resource """ return self._resource_map.get(resource_id) def exists(self, resource_id: str, instance_id: Optional[str]) -> bool: """ Check if resource with the given id exists in the cluster resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ return ( self.get_resource_one_instance(resource_id, instance_id) is not None ) def get_type( self, resource_id: str, instance_id: Optional[str] ) -> ResourceType: """ Return the type of the resource resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource = self.get_resource_one_instance(resource_id, instance_id) if resource is None: raise ResourceNonExistentException(resource_id, instance_id) return _TYPE_MAP[type(resource)] def is_stonith(self, resource_id: str, instance_id: Optional[str]) -> bool: """ Check if the resource with the given id is a stonith resource resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource = self.get_resource_one_instance(resource_id, instance_id) if resource is None: raise ResourceNonExistentException(resource_id, instance_id) return isinstance( resource, PrimitiveStatusDto ) and resource.resource_agent.startswith("stonith:") def is_promotable( self, resource_id: str, instance_id: Optional[str] ) -> bool: """ Check if the resource with the given id is promotable. Usable only of clone resources. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource = self.get_resource_one_instance(resource_id, instance_id) if resource is None: raise ResourceNonExistentException(resource_id, instance_id) if not isinstance(resource, _PROMOTABLE_RESOURCES_TYPES): raise ResourceUnexpectedTypeException( resource_id, instance_id, _TYPE_MAP[type(resource)], list(_PROMOTABLE_RESOURCES_CONST), ) return resource.multi_state def is_unique(self, resource_id: str, instance_id: Optional[str]) -> bool: """ Check if the resource with the given id is globally unique. Usable only on clone and bundle resources. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource = self.get_resource_one_instance(resource_id, instance_id) if resource is None: raise ResourceNonExistentException(resource_id, instance_id) if not isinstance(resource, _UNIQUE_RESOURCES_TYPES): raise ResourceUnexpectedTypeException( resource_id, instance_id, _TYPE_MAP[type(resource)], list(_UNIQUE_RESOURCES_CONST), ) return resource.unique def _get_instances_for_state_check( self, resource_id: str, instance_id: Optional[str] ) -> CheckedResourceType: if not self.exists(resource_id, instance_id): raise ResourceNonExistentException(resource_id, instance_id) is_in_clone = ( self.get_type(resource_id, instance_id) in (ResourceType.PRIMITIVE, ResourceType.GROUP) and self.get_parent_clone_id(resource_id, instance_id) is not None ) is_clone = self.get_type(resource_id, instance_id) == ResourceType.CLONE if self._bundle_defined and (is_in_clone or is_clone): # This is due to pacemaker providing broken data in this case. # See issue: https://projects.clusterlabs.org/T722 raise NotImplementedError( "Queries on clone state when bundle resources are present " "in the cluster are unsupported" ) resource = self.get_resource_one_instance(resource_id, instance_id) if isinstance(resource, (CloneStatusDto, BundleStatusDto)): return resource if instance_id is None: instance_list = cast( Optional[list[Union[PrimitiveStatusDto, GroupStatusDto]]], self.get_resource_all_instances(resource_id), ) else: instance_list = [resource] if resource is not None else None if instance_list is None: raise ResourceNonExistentException(resource_id, instance_id) if is_in_clone: instance_list = _filter_clone_orphans(instance_list) if not instance_list: raise ResourceNonExistentException(resource_id, instance_id) if self.get_type(resource_id, instance_id) == ResourceType.PRIMITIVE: return PrimitiveInstances( cast(list[PrimitiveStatusDto], instance_list) ) return GroupInstances(cast(list[GroupStatusDto], instance_list)) def can_have_multiple_members( self, resource_id: str, instance_id: Optional[str] = None ) -> bool: """ Check if the resource with the given id can have multiple inner members. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource_type = self.get_type(resource_id, instance_id) return resource_type == ResourceType.GROUP or ( resource_type == ResourceType.CLONE and any( self.get_type(member_id, None) == ResourceType.GROUP for member_id in self.get_members(resource_id, instance_id) ) ) def can_have_multiple_instances( self, resource_id: str, instance_id: Optional[str] = None ) -> bool: """ Check if the resource with the given id can have multiple instances. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource_type = self.get_type(resource_id, instance_id) return instance_id is None and ( resource_type in (ResourceType.CLONE, ResourceType.BUNDLE) or self.get_parent_clone_id(resource_id, None) is not None or ( resource_type == ResourceType.PRIMITIVE and self.get_parent_bundle_id(resource_id, None) is not None ) ) def _validate_quantifiers( self, resource_id: str, instance_id: Optional[str], members_quantifier: Optional[MoreChildrenQuantifierType], instances_quantifier: Optional[MoreChildrenQuantifierType], ) -> None: if ( members_quantifier is not None and not self.can_have_multiple_members(resource_id, instance_id) ): raise MembersQuantifierUnsupportedException() if ( instances_quantifier is not None and not self.can_have_multiple_instances(resource_id, instance_id) ): raise InstancesQuantifierUnsupportedException() def is_state( self, resource_id: str, instance_id: Optional[str], state: ResourceState, expected_node_name: Optional[str] = None, members_quantifier: Optional[MoreChildrenQuantifierType] = None, instances_quantifier: Optional[MoreChildrenQuantifierType] = None, ) -> bool: """ Check if the resource with the given id is in expected state. With groups, the state is read and evaluated on the group first. If the state cannot be determined from only the group, evaluate the state of the member resources and return true if the query is true for ALL of the members. With clones and bundles, the state is read and evaluated on the clone or bundle first. If the state cannot be determined from only the clone or bundle, evaluate the state on the instances and return true if the query is true for ANY of the instances. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource state -- expected state of the resource expected_node_name -- Check if the resource is in given state on a given node. If specified, always check only group members, clone instances and bundle replicas even when the attribute is present in the group, clone or bundle status dto respectively. members_quantifier -- Specify how to treat group members. If specified, check only member resources even when the attribute is present in the group status dto. instances_quantifier -- Specify how to treat instances of clones and bundles. If specified, check only instances even when the attribute is present in the clone/bundle status dto. """ resource = self._get_instances_for_state_check(resource_id, instance_id) self._validate_quantifiers( resource_id, instance_id, members_quantifier, instances_quantifier ) if not isinstance(state.value, list): checked_state = [state.value] else: checked_state = state.value return any( self._check_resources_state_attributes( resource, checked_attribute, real_expected_value, expected_node_name, members_quantifier, instances_quantifier, ) for checked_attribute, real_expected_value in checked_state ) def is_state_exact_value( self, resource_id: str, instance_id: Optional[str], state: ResourceStateExactCheck, expected_state_value: str, expected_node_name: Optional[str] = None, members_quantifier: Optional[MoreChildrenQuantifierType] = None, instances_quantifier: Optional[MoreChildrenQuantifierType] = None, ) -> bool: """ Check if the state attribute of the resource contains the expected value. With groups, the state is read and evaluated on the group first. If the state cannot be determined from only the group, evaluate the state of the member resources and return true if the query is true for ALL of the members. With clones and bundles, the state is read and evaluated on the clone or bundle first. If the state cannot be determined from only the clone or bundle, evaluate the state on the instances and return true if the query is true for ANY of the instances. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource state -- state describing which state attribute will be checked expected_state_value -- expected value of the attribute expected_node_name -- Check if the resource is in given state on a given node. If specified, always check only group members, clone instances and bundle replicas even when the attribute is present in the group, clone or bundle status dto respectively. members_quantifier -- Specify how to treat group members. If specified, check only member resources even when the attribute is present in the group status dto. instances_quantifier -- Specify how to treat instances of clones and bundles. If specified, check only instances even when the attribute is present in the clone/bundle status dto. """ resource = self._get_instances_for_state_check(resource_id, instance_id) self._validate_quantifiers( resource_id, instance_id, members_quantifier, instances_quantifier ) if not isinstance(state.value, list): checked_state = [state.value] else: checked_state = state.value return any( self._check_resources_state_attributes( resource, checked_attribute, expected_state_value, expected_node_name, members_quantifier, instances_quantifier, ) for checked_attribute, _ in checked_state ) def get_parent_group_id( self, resource_id: str, instance_id: Optional[str], ) -> Optional[str]: """ Check if the resource is in any group and return group id if the resource is in group. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource = self.get_resource_one_instance(resource_id, instance_id) if resource is None: raise ResourceNonExistentException(resource_id, instance_id) if not isinstance(resource, PrimitiveStatusDto): raise ResourceUnexpectedTypeException( resource_id, instance_id, _TYPE_MAP[type(resource)], [ResourceType.PRIMITIVE], ) if not self._check_parent_type(resource_id, ResourceType.GROUP): return None return self._child_parent_map[resource_id] def get_parent_clone_id( self, resource_id: str, instance_id: Optional[str], ) -> Optional[str]: """ Check if the resource is inside any clone and return clone id if the resource is in clone. Member of a cloned group is in clone as well. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource = self.get_resource_one_instance(resource_id, instance_id) if resource is None: raise ResourceNonExistentException(resource_id, instance_id) if not isinstance(resource, (PrimitiveStatusDto, GroupStatusDto)): raise ResourceUnexpectedTypeException( resource_id, instance_id, _TYPE_MAP[type(resource)], [ResourceType.PRIMITIVE, ResourceType.GROUP], ) checked_id = resource_id if self._check_parent_type(checked_id, ResourceType.GROUP): checked_id = self._child_parent_map[resource_id] if not self._check_parent_type(checked_id, ResourceType.CLONE): return None return self._child_parent_map[checked_id] def get_parent_bundle_id( self, resource_id: str, instance_id: Optional[str], ) -> Optional[str]: """ Check if the resource is inside any bundle and return bundle id if the resource is in bundle. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource = self.get_resource_one_instance(resource_id, instance_id) if resource is None: raise ResourceNonExistentException(resource_id, instance_id) if not isinstance(resource, PrimitiveStatusDto): raise ResourceUnexpectedTypeException( resource_id, instance_id, _TYPE_MAP[type(resource)], [ResourceType.PRIMITIVE], ) if not self._check_parent_type(resource_id, ResourceType.BUNDLE): return None return self._child_parent_map[resource_id] def get_index_in_group( self, resource_id: str, instance_id: Optional[str] ) -> int: """ Return the index of the resource in a group. Usable only for primitive resources. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ if not self.exists(resource_id, instance_id): raise ResourceNonExistentException(resource_id, instance_id) resource_type = self.get_type(resource_id, instance_id) if resource_type != ResourceType.PRIMITIVE: raise ResourceUnexpectedTypeException( resource_id, instance_id, resource_type, [ResourceType.PRIMITIVE], ) parent_id = self._child_parent_map.get(resource_id, None) if parent_id is None: raise ResourceNotInGroupException(resource_id, instance_id) parent_resource = self.get_resource_one_instance(parent_id, None) if not isinstance(parent_resource, GroupStatusDto): raise ResourceNotInGroupException(resource_id, instance_id) return [res.resource_id for res in parent_resource.members].index( resource_id ) def get_members( self, resource_id: str, instance_id: Optional[str] ) -> list[str]: """ Return resource ids of members of a group, clone or bundle resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ resource = self.get_resource_one_instance(resource_id, instance_id) if resource is None: raise ResourceNonExistentException(resource_id, instance_id) if isinstance(resource, GroupStatusDto): return [member.resource_id for member in resource.members] if isinstance(resource, CloneStatusDto): return list( set( instance.resource_id for instance in resource.instances if not _is_orphaned(instance) ) ) if isinstance(resource, BundleStatusDto): return list( set( replica.member.resource_id for replica in resource.replicas if replica.member is not None ) ) raise ResourceUnexpectedTypeException( resource_id, instance_id, ResourceType.PRIMITIVE, [ResourceType.GROUP, ResourceType.CLONE, ResourceType.BUNDLE], ) def get_nodes( self, resource_id: str, instance_id: Optional[str] ) -> list[str]: """ Return nodes on which resource is running. For groups, return nodes on which any of the members is running. For clones or bundles return nodes on which any of the instances or replicas are running. resource_id -- id of the resource instance_id -- id describing unique instance of cloned or bundled resource """ if instance_id is not None: resource = self.get_resource_one_instance(resource_id, instance_id) instance_list = [resource] if resource is not None else None else: instance_list = self.get_resource_all_instances(resource_id) if instance_list is None: raise ResourceNonExistentException(resource_id, instance_id) nodes = [] for instance in instance_list: if isinstance(instance, PrimitiveStatusDto): nodes.extend(self._get_primitive_nodes(instance)) elif isinstance(instance, GroupStatusDto): nodes.extend(self._get_group_nodes(instance)) elif isinstance(instance, CloneStatusDto): nodes.extend(self._get_clone_nodes(instance)) else: nodes.extend(self._get_bundle_nodes(instance)) return sorted(nodes) def _get_primitive_nodes(self, primitive: PrimitiveStatusDto) -> list[str]: if id(primitive) in self._bundle_member_node_map: return self._bundle_member_node_map[id(primitive)] return primitive.node_names def _get_group_nodes(self, group: GroupStatusDto) -> list[str]: node_set = set() for member in group.members: node_set.update(self._get_primitive_nodes(member)) return list(node_set) def _get_clone_nodes(self, clone: CloneStatusDto) -> list[str]: node_list = [] instance_list = _filter_clone_orphans(clone.instances) for instance in instance_list: if isinstance(instance, PrimitiveStatusDto): node_list.extend(self._get_primitive_nodes(instance)) elif isinstance(instance, GroupStatusDto): node_list.extend(self._get_group_nodes(instance)) return node_list def _get_bundle_nodes(self, bundle: BundleStatusDto) -> list[str]: node_list = [] for replica in bundle.replicas: if replica.member is not None: node_list.extend(self._get_primitive_nodes(replica.member)) else: node_list.extend(self._get_primitive_nodes(replica.container)) return node_list def _check_parent_type( self, resource_id: str, expected_parent_type: ResourceType, ) -> bool: parent_id = self._child_parent_map.get(resource_id) return ( parent_id is not None and self.get_type(parent_id, None) == expected_parent_type ) def __add_group_children_to_maps(self, group: GroupStatusDto) -> None: for child in group.members: self._resource_map[child.resource_id].append(child) self._child_parent_map[child.resource_id] = group.resource_id def _check_resources_state_attributes( self, checked_resource: CheckedResourceType, checked_attribute: str, expected_attribute_value: StateValueType, expected_node_name: Optional[str] = None, members_quantifier: Optional[MoreChildrenQuantifierType] = None, instances_quantifier: Optional[MoreChildrenQuantifierType] = None, ) -> bool: if isinstance(checked_resource, CloneStatusDto): return self._clone_state( checked_resource, checked_attribute, expected_attribute_value, members_quantifier, instances_quantifier, expected_node_name, ) if isinstance(checked_resource, BundleStatusDto): return self._bundle_state( checked_resource, checked_attribute, expected_attribute_value, instances_quantifier, expected_node_name, ) more_children_check = ( instances_quantifier or MoreChildrenQuantifierType.ANY ) if isinstance(checked_resource, PrimitiveInstances): return _MORE_CHILDREN_QUANTIFIER_MAP[more_children_check]( self._primitive_state( resource, checked_attribute, expected_attribute_value, expected_node_name, ) for resource in checked_resource.instances ) if isinstance(checked_resource, GroupInstances): return _MORE_CHILDREN_QUANTIFIER_MAP[more_children_check]( self._group_state( resource, checked_attribute, expected_attribute_value, members_quantifier, expected_node_name, ) for resource in checked_resource.instances ) return False def _primitive_state( self, primitive: PrimitiveStatusDto, checked_attribute: str, expected_attribute_value: StateValueType, expected_node_name: Optional[str], ) -> bool: if checked_attribute == ResourceState.DISABLED.value[0]: result = ( primitive.target_role == PCMK_ROLE_STOPPED ) == expected_attribute_value else: result = _check_attribute_value( primitive, checked_attribute, expected_attribute_value ) if expected_node_name is None: return result node_names = primitive.node_names if id(primitive) in self._bundle_member_node_map: node_names = self._bundle_member_node_map[id(primitive)] return result and expected_node_name in node_names def _group_state( self, group: GroupStatusDto, checked_attribute: str, expected_attribute_value: StateValueType, members_quantifier: Optional[MoreChildrenQuantifierType], expected_node_name: Optional[str], ) -> bool: if _can_check_non_primitive( group, checked_attribute, members_quantifier, expected_node_name is not None, ): return _check_attribute_value( group, checked_attribute, expected_attribute_value ) if not group.members: return False if members_quantifier is None: members_quantifier = MoreChildrenQuantifierType.ALL return _MORE_CHILDREN_QUANTIFIER_MAP[members_quantifier]( self._primitive_state( primitive, checked_attribute, expected_attribute_value, expected_node_name, ) for primitive in group.members ) def _clone_state( self, clone: CloneStatusDto, checked_attribute: str, expected_attribute_value: StateValueType, members_quantifier: Optional[MoreChildrenQuantifierType], instances_quantifier: Optional[MoreChildrenQuantifierType], expected_node_name: Optional[str], ) -> bool: if _can_check_non_primitive( clone, checked_attribute, instances_quantifier, expected_node_name is not None, ): return _check_attribute_value( clone, checked_attribute, expected_attribute_value ) instance_list = _filter_clone_orphans(clone.instances) if not instance_list: return False if instances_quantifier is None: instances_quantifier = MoreChildrenQuantifierType.ANY return _MORE_CHILDREN_QUANTIFIER_MAP[instances_quantifier]( ( self._primitive_state( instance, checked_attribute, expected_attribute_value, expected_node_name, ) if isinstance(instance, PrimitiveStatusDto) else self._group_state( instance, checked_attribute, expected_attribute_value, members_quantifier, expected_node_name, ) ) for instance in instance_list ) def _bundle_state( self, bundle: BundleStatusDto, checked_attribute: str, expected_attribute_value: StateValueType, instances_quantifier: Optional[MoreChildrenQuantifierType], expected_node_name: Optional[str], ) -> bool: if _can_check_non_primitive( bundle, checked_attribute, instances_quantifier, expected_node_name is not None, ): return _check_attribute_value( bundle, checked_attribute, expected_attribute_value ) if not bundle.replicas: return False if instances_quantifier is None: instances_quantifier = MoreChildrenQuantifierType.ANY return _MORE_CHILDREN_QUANTIFIER_MAP[instances_quantifier]( self._bundle_replica_state( replica, checked_attribute, expected_attribute_value, expected_node_name, ) for replica in bundle.replicas ) def _bundle_replica_state( self, replica: BundleReplicaStatusDto, checked_attribute: str, expected_attribute_value: StateValueType, expected_node_name: Optional[str], ) -> bool: if replica.member is None: return self._primitive_state( replica.container, checked_attribute, expected_attribute_value, expected_node_name, ) return self._primitive_state( replica.member, checked_attribute, expected_attribute_value, expected_node_name, ) def _can_check_non_primitive( resource: AnyResourceStatusDto, attribute_name: str, more_children_check: Optional[MoreChildrenQuantifierType], check_nodes: bool, ) -> bool: return ( hasattr(resource, attribute_name) and more_children_check is None # we have to look at primitives if we want to see on which nodes # the resource is in given state and not check_nodes ) def _is_orphaned(resource: Union[PrimitiveStatusDto, GroupStatusDto]) -> bool: if isinstance(resource, PrimitiveStatusDto): return resource.orphaned return all(child.orphaned for child in resource.members) def _filter_clone_orphans( instance_list: Sequence[Union[PrimitiveStatusDto, GroupStatusDto]] ) -> list[Union[PrimitiveStatusDto, GroupStatusDto]]: return [ instance for instance in instance_list if not _is_orphaned(instance) ] def _check_attribute_value( status_dto: AnyResourceStatusDto, attribute_name: str, expected_value: StateValueType, ) -> bool: real_value = getattr(status_dto, attribute_name) if isinstance(expected_value, set): return real_value in expected_value if expected_value == NOT_NONE: return real_value is not None return real_value == expected_value pcs-0.12.0.2/pcs/common/services/000077500000000000000000000000001500417470700164245ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/services/__init__.py000066400000000000000000000001061500417470700205320ustar00rootroot00000000000000from . import ( drivers, errors, interfaces, types, ) pcs-0.12.0.2/pcs/common/services/common.py000066400000000000000000000001211500417470700202600ustar00rootroot00000000000000# pylint: disable=unused-import from pcs.common.str_tools import join_multilines pcs-0.12.0.2/pcs/common/services/drivers/000077500000000000000000000000001500417470700201025ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/services/drivers/__init__.py000066400000000000000000000001211500417470700222050ustar00rootroot00000000000000from .systemd import SystemdDriver from .sysvinit_rhel import SysVInitRhelDriver pcs-0.12.0.2/pcs/common/services/drivers/systemd.py000066400000000000000000000106571500417470700221550ustar00rootroot00000000000000import os.path import re from typing import ( List, Optional, ) from pcs.common.types import StringIterable from .. import errors from ..interfaces import ( ExecutorInterface, ServiceManagerInterface, ) class SystemdDriver(ServiceManagerInterface): def __init__( self, executor: ExecutorInterface, systemctl_bin: str, systemd_unit_paths: StringIterable, ) -> None: """ executor -- external commands used by this class are executed using this object systemctl_bin -- path to systemctl executable, it is used for managing services systemd_unit_paths -- paths to directories where unit files should be located. If at least one location is present on the system, systemd is considered as a current init system. """ self._executor = executor self._systemctl_bin = systemctl_bin self._systemd_unit_paths = systemd_unit_paths self._available_services: List[str] = [] def start(self, service: str, instance: Optional[str] = None) -> None: result = self._executor.run( [ self._systemctl_bin, "start", _format_service_name(service, instance), ] ) if result.retval != 0: raise errors.StartServiceError( service, result.joined_output, instance ) def stop(self, service: str, instance: Optional[str] = None) -> None: result = self._executor.run( [ self._systemctl_bin, "stop", _format_service_name(service, instance), ] ) if result.retval != 0: raise errors.StopServiceError( service, result.joined_output, instance ) def enable(self, service: str, instance: Optional[str] = None) -> None: result = self._executor.run( [ self._systemctl_bin, "enable", _format_service_name(service, instance), ] ) if result.retval != 0: raise errors.EnableServiceError( service, result.joined_output, instance ) def disable(self, service: str, instance: Optional[str] = None) -> None: if not self.is_installed(service): return result = self._executor.run( [ self._systemctl_bin, "disable", _format_service_name(service, instance), ] ) if result.retval != 0: raise errors.DisableServiceError( service, result.joined_output, instance ) def is_enabled(self, service: str, instance: Optional[str] = None) -> bool: result = self._executor.run( [ self._systemctl_bin, "is-enabled", _format_service_name(service, instance), ] ) return result.retval == 0 def is_running(self, service: str, instance: Optional[str] = None) -> bool: result = self._executor.run( [ self._systemctl_bin, "is-active", _format_service_name(service, instance), ] ) return result.retval == 0 def is_installed(self, service: str) -> bool: return service in self.get_available_services() def get_available_services(self) -> List[str]: if not self._available_services: self._available_services = self._get_available_services() return self._available_services def _get_available_services(self) -> List[str]: result = self._executor.run( [self._systemctl_bin, "list-unit-files", "--full"] ) if result.retval != 0: return [] service_list = [] for service in result.stdout.splitlines(): match = re.search(r"^([\S]*)\.service", service) if match: service_list.append(match.group(1)) return service_list def is_current_system_supported(self) -> bool: return any( os.path.isdir(path) for path in self._systemd_unit_paths ) and os.path.isfile(self._systemctl_bin) def _format_service_name(service: str, instance: Optional[str]) -> str: instance_str = f"@{instance}" if instance else "" return f"{service}{instance_str}.service" pcs-0.12.0.2/pcs/common/services/drivers/sysvinit_rhel.py000066400000000000000000000062411500417470700233610ustar00rootroot00000000000000import os.path from typing import ( List, Optional, ) from .. import errors from ..interfaces import ( ExecutorInterface, ServiceManagerInterface, ) class SysVInitRhelDriver(ServiceManagerInterface): def __init__( self, executor: ExecutorInterface, service_bin: str, chkconfig_bin: str ): """ executor -- external commands used by this class are executed using this object service_bin -- path to an executable used for starting and stopping services and to check if a service is running chkconfig_bin -- path to an executable used for enabling, disabling and listing available service and to check if service is enabled """ self._executor = executor self._service_bin = service_bin self._chkconfig_bin = chkconfig_bin self._available_services: List[str] = [] def start(self, service: str, instance: Optional[str] = None) -> None: result = self._executor.run([self._service_bin, service, "start"]) if result.retval != 0: raise errors.StartServiceError(service, result.joined_output) def stop(self, service: str, instance: Optional[str] = None) -> None: result = self._executor.run([self._service_bin, service, "stop"]) if result.retval != 0: raise errors.StopServiceError(service, result.joined_output) def enable(self, service: str, instance: Optional[str] = None) -> None: result = self._executor.run([self._chkconfig_bin, service, "on"]) if result.retval != 0: raise errors.EnableServiceError(service, result.joined_output) def disable(self, service: str, instance: Optional[str] = None) -> None: if not self.is_installed(service): return result = self._executor.run([self._chkconfig_bin, service, "off"]) if result.retval != 0: raise errors.DisableServiceError(service, result.joined_output) def is_enabled(self, service: str, instance: Optional[str] = None) -> bool: return self._executor.run([self._chkconfig_bin, service]).retval == 0 def is_running(self, service: str, instance: Optional[str] = None) -> bool: return ( self._executor.run([self._service_bin, service, "status"]).retval == 0 ) def is_installed(self, service: str) -> bool: return service in self.get_available_services() def get_available_services(self) -> List[str]: if not self._available_services: self._available_services = self._get_available_services() return self._available_services def _get_available_services(self) -> List[str]: result = self._executor.run([self._chkconfig_bin]) if result.retval != 0: return [] service_list = [] for service in result.stdout.splitlines(): service = service.split(" ", 1)[0] if service: service_list.append(service) return service_list def is_current_system_supported(self) -> bool: return all( os.path.isfile(binary) for binary in (self._service_bin, self._chkconfig_bin) ) pcs-0.12.0.2/pcs/common/services/errors.py000066400000000000000000000007771500417470700203250ustar00rootroot00000000000000from typing import Optional class ManageServiceError(Exception): def __init__( self, service: str, message: str, instance: Optional[str] = None, ): self.service = service self.message = message self.instance = instance class DisableServiceError(ManageServiceError): pass class EnableServiceError(ManageServiceError): pass class StartServiceError(ManageServiceError): pass class StopServiceError(ManageServiceError): pass pcs-0.12.0.2/pcs/common/services/interfaces/000077500000000000000000000000001500417470700205475ustar00rootroot00000000000000pcs-0.12.0.2/pcs/common/services/interfaces/__init__.py000066400000000000000000000001251500417470700226560ustar00rootroot00000000000000from .executor import ExecutorInterface from .manager import ServiceManagerInterface pcs-0.12.0.2/pcs/common/services/interfaces/executor.py000066400000000000000000000011161500417470700227560ustar00rootroot00000000000000from pcs.common.types import StringSequence from ..types import ExecutorResult class ExecutorInterface: """ Simple interface for executing external programs. """ def run(self, args: StringSequence) -> ExecutorResult: """ args -- Program and its arguments to execute. First item is path to a executable and rest of the items are arguments which will be provided to the executable. Execute a specified program synchronously and return its result after it's finished. """ raise NotImplementedError() pcs-0.12.0.2/pcs/common/services/interfaces/manager.py000066400000000000000000000061241500417470700225360ustar00rootroot00000000000000from typing import ( List, Optional, ) class ServiceManagerInterface: def start(self, service: str, instance: Optional[str] = None) -> None: """ service -- name of service to be started instance -- service instance identifier. Available only for system which supports multiple service instances (e.g. systemd) Start defined service. Raises StartServiceError on failure. """ raise NotImplementedError() def stop(self, service: str, instance: Optional[str] = None) -> None: """ service -- name of service to be stopped instance -- service instance identifier. Available only for system which supports multiple service instances (e.g. systemd) Stop defined service. Raises StopServiceError on failure. """ raise NotImplementedError() def enable(self, service: str, instance: Optional[str] = None) -> None: """ service -- name of service to be enabled instance -- service instance identifier. Available only for system which supports multiple service instances (e.g. systemd) Enable defined service. Raises EnableServiceError on failure. """ raise NotImplementedError() def disable(self, service: str, instance: Optional[str] = None) -> None: """ service -- name of service to be disabled instance -- service instance identifier. Available only for system which supports multiple service instances (e.g. systemd) Disable defined service. Raises DisableServiceError on failure. """ raise NotImplementedError() def is_enabled(self, service: str, instance: Optional[str] = None) -> bool: """ service -- name of service to be checked instance -- service instance identifier. Available only for system which supports multiple service instances (e.g. systemd) Returns True if specified service is enabled, False otherwise. """ raise NotImplementedError() def is_running(self, service: str, instance: Optional[str] = None) -> bool: """ service -- name of service to be checked instance -- service instance identifier. Available only for system which supports multiple service instances (e.g. systemd) Returns True if specified service is running (active), False otherwise. """ raise NotImplementedError() def is_installed(self, service: str) -> bool: """ service -- name of service to be checked Returns True if specified service is installed (manageable by init system), False otherwise. """ raise NotImplementedError() def get_available_services(self) -> List[str]: """ Returns list of service names recognized by init system. """ raise NotImplementedError() def is_current_system_supported(self) -> bool: """ Returns True if the instance of this class is able to manage current init system. """ raise NotImplementedError() pcs-0.12.0.2/pcs/common/services/types.py000066400000000000000000000004251500417470700201430ustar00rootroot00000000000000from dataclasses import dataclass from .common import join_multilines @dataclass(frozen=True) class ExecutorResult: retval: int stdout: str stderr: str @property def joined_output(self) -> str: return join_multilines([self.stderr, self.stdout]) pcs-0.12.0.2/pcs/common/services_dto.py000066400000000000000000000006241500417470700176460ustar00rootroot00000000000000from dataclasses import dataclass from typing import Optional from pcs.common.interface.dto import DataTransferObject @dataclass(frozen=True) class ServiceStatusDto(DataTransferObject): service: str installed: Optional[bool] enabled: Optional[bool] running: Optional[bool] @dataclass(frozen=True) class ServicesInfoResultDto(DataTransferObject): services: list[ServiceStatusDto] pcs-0.12.0.2/pcs/common/ssl.py000066400000000000000000000042661500417470700157640ustar00rootroot00000000000000import datetime import ssl from typing import List from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import ( hashes, serialization, ) from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID def check_cert_key(cert_path: str, key_path: str) -> List[str]: errors = [] try: ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ssl_context.load_cert_chain(cert_path, key_path) except ssl.SSLError as e: errors.append(f"SSL certificate does not match the key: {e}") except EnvironmentError as e: errors.append(f"Unable to load SSL certificate and/or key: {e}") return errors def generate_key(length: int = 3072) -> rsa.RSAPrivateKeyWithSerialization: return rsa.generate_private_key( public_exponent=65537, key_size=length, backend=default_backend() ) def generate_cert(key: rsa.RSAPrivateKey, server_name: str) -> x509.Certificate: now = datetime.datetime.utcnow() subject = x509.Name( [ x509.NameAttribute(NameOID.COUNTRY_NAME, "US"), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "MN"), x509.NameAttribute(NameOID.LOCALITY_NAME, "Minneapolis"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, "pcsd"), x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "pcsd"), x509.NameAttribute(NameOID.COMMON_NAME, server_name), ] ) return ( x509.CertificateBuilder() .subject_name(subject) .issuer_name(subject) .public_key(key.public_key()) .serial_number(int(now.timestamp() * 1000)) .not_valid_before(now) .not_valid_after(now + datetime.timedelta(days=3650)) .sign(key, hashes.SHA256(), default_backend()) ) def dump_cert(certificate: x509.Certificate) -> bytes: return certificate.public_bytes(serialization.Encoding.PEM) def dump_key(key: rsa.RSAPrivateKeyWithSerialization) -> bytes: return key.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption(), ) pcs-0.12.0.2/pcs/common/status_dto.py000066400000000000000000000042661500417470700173540ustar00rootroot00000000000000from dataclasses import dataclass from typing import ( Optional, Sequence, Union, ) from pcs.common.const import ( PcmkRoleType, PcmkStatusRoleType, ) from pcs.common.interface.dto import DataTransferObject @dataclass(frozen=True) class PrimitiveStatusDto(DataTransferObject): # pylint: disable=too-many-instance-attributes resource_id: str instance_id: Optional[str] resource_agent: str role: PcmkStatusRoleType target_role: Optional[PcmkRoleType] active: bool orphaned: bool blocked: bool maintenance: bool description: Optional[str] failed: bool managed: bool failure_ignored: bool node_names: list[str] pending: Optional[str] locked_to: Optional[str] @dataclass(frozen=True) class GroupStatusDto(DataTransferObject): resource_id: str instance_id: Optional[str] maintenance: bool description: Optional[str] managed: bool disabled: bool members: Sequence[PrimitiveStatusDto] @dataclass(frozen=True) class CloneStatusDto(DataTransferObject): # pylint: disable=too-many-instance-attributes resource_id: str multi_state: bool unique: bool maintenance: bool description: Optional[str] managed: bool disabled: bool failed: bool failure_ignored: bool target_role: Optional[PcmkRoleType] instances: Union[Sequence[PrimitiveStatusDto], Sequence[GroupStatusDto]] @dataclass(frozen=True) class BundleReplicaStatusDto(DataTransferObject): replica_id: str member: Optional[PrimitiveStatusDto] remote: Optional[PrimitiveStatusDto] container: PrimitiveStatusDto ip_address: Optional[PrimitiveStatusDto] @dataclass(frozen=True) class BundleStatusDto(DataTransferObject): # pylint: disable=too-many-instance-attributes resource_id: str type: str image: str unique: bool maintenance: bool description: Optional[str] managed: bool failed: bool replicas: Sequence[BundleReplicaStatusDto] AnyResourceStatusDto = Union[ PrimitiveStatusDto, GroupStatusDto, CloneStatusDto, BundleStatusDto ] @dataclass(frozen=True) class ResourcesStatusDto(DataTransferObject): resources: Sequence[AnyResourceStatusDto] pcs-0.12.0.2/pcs/common/str_tools.py000066400000000000000000000152301500417470700172040ustar00rootroot00000000000000from collections.abc import Iterable as IterableAbc from collections.abc import Sized from typing import ( Any, Mapping, Optional, Sequence, TypeVar, Union, ) from pcs.common.types import ( StringIterable, StringSequence, ) def indent(line_list: StringIterable, indent_step: int = 2) -> list[str]: """ return line list where each line of input is prefixed by N spaces line_list -- original lines indent_step -- count of spaces for line prefix """ return [ "{0}{1}".format(" " * indent_step, line) if line else line for line in line_list ] def outdent(line_list: StringSequence) -> list[str]: if not line_list: return [] smallest_indentation = min( len(line) - len(line.lstrip(" ")) for line in line_list if line ) return [line[smallest_indentation:] for line in line_list] def format_list_base(item_list: StringIterable, separator: str = ", ") -> str: return separator.join(item_list) def format_list_dont_sort( item_list: StringIterable, separator: str = ", ", ) -> str: return format_list_base(quote_items(item_list), separator) def format_list(item_list: StringIterable, separator: str = ", ") -> str: return format_list_dont_sort(sorted(item_list), separator) def format_list_custom_last_separator( item_list: StringIterable, last_separator: str, separator: str = ", ", ) -> str: return format_list_custom_last_separator_dont_sort( quote_items(sorted(item_list)), last_separator, separator ) def format_list_custom_last_separator_dont_sort( item_list: StringSequence, last_separator: str, separator: str = ", ", ) -> str: if len(item_list) < 2: return format_list_base(item_list) return format_list_base( [ format_list_base(item_list[:-1], separator=separator), format_list_base(item_list[-1:]), ], separator=last_separator, ) def quote_items(item_list: StringIterable) -> list[str]: return [f"'{item}'" for item in item_list] # For now, tuple[str, str] is sufficient. Feel free to change it if needed, # e.g. when values can be integers. def format_name_value_list(item_list: Sequence[tuple[str, str]]) -> list[str]: """ Turn 2-tuples to 'name=value' strings with standard quoting """ output = [] for name, value in item_list: name = quote(name, "= ") value = quote(value, "= ") output.append(f"{name}={value}") return output # For now, tuple[str, str, str] is sufficient. Feel free to change it if # needed, e.g. when values can be integers. def format_name_value_id_list( item_list: Sequence[tuple[str, str, str]] ) -> list[str]: """ Turn 3-tuples to 'name=value (id: id))' strings with standard quoting """ output = [] for name, value, an_id in item_list: name = quote(name, "= ") value = quote(value, "= ") output.append(f"{name}={value} (id: {an_id})") return output def pairs_to_text(pairs: Sequence[tuple[str, str]]) -> list[str]: if pairs: return [" ".join(format_name_value_list(pairs))] return [] def format_name_value_default_list( item_list: Sequence[tuple[str, str, bool]] ) -> list[str]: """ Turn 3-tuples to 'name=value' or 'name=value (default)' strings with standard quoting """ output = [] for name, value, is_default in item_list: name = quote(name, "= ") value = quote(value, "= ") default = " (default)" if is_default else "" output.append(f"{name}={value}{default}") return output def quote(string: str, chars_to_quote: str) -> str: """ Quote a string if it contains specified characters string -- the string to be processed chars_to_quote -- the characters causing quoting """ if not frozenset(chars_to_quote) & frozenset(string): return string if '"' not in string: return f'"{string}"' if "'" not in string: return f"'{string}'" return '"{string}"'.format(string=string.replace('"', '\\"')) def join_multilines(strings: StringSequence) -> str: return "\n".join([a.strip() for a in strings if a.strip()]) def split_multiline(string: str) -> list[str]: return [ line for line in [line.strip() for line in string.splitlines()] if line ] def format_optional( value: Any, template: str = "{} ", empty_case: str = "", ) -> str: # Number 0 is considered False which does not suit our needs so we check # for it explicitly. Beware that False == 0 is true, so we must have an # additional check for that (bool is a subclass of int). if value or ( isinstance(value, int) and not isinstance(value, bool) and value == 0 ): return template.format(value) return empty_case def _is_multiple(what: Union[int, Sized]) -> bool: """ Return True if 'what' does not mean one item, False otherwise what -- this will be counted """ retval = False if isinstance(what, int): retval = abs(what) != 1 elif not isinstance(what, str): try: retval = len(what) != 1 except TypeError: pass return retval def _add_s(word: str) -> str: """ add "s" or "es" to the word based on its ending word -- word where "s" or "es" should be added """ if word[-1:] in ("s", "x", "o") or word[-2:] in ("ss", "sh", "ch"): return word + "es" return word + "s" def get_plural(singular: str) -> str: """ Take singular word form and return plural. singular -- singular word (like: is, do, node) """ common_plurals = { "is": "are", "has": "have", "does": "do", "it": "they", "property": "properties", } if singular in common_plurals: return common_plurals[singular] return _add_s(singular) def format_plural( depends_on: Union[int, Sized], singular: str, plural: Optional[str] = None, ) -> str: """ Takes the singular word form and returns its plural form if depends_on is not equal to one/contains one item depends_on -- if number (of items) isn't equal to one, return plural singular -- singular word (like: is, do, node) plural -- optional irregular plural form """ if not _is_multiple(depends_on): return singular if plural: return plural return get_plural(singular) T = TypeVar("T") def transform(items: list[T], mapping: Mapping[T, str]) -> list[str]: return list(map(lambda item: mapping.get(item, str(item)), items)) def is_iterable_not_str(value: Union[IterableAbc, str]) -> bool: return isinstance(value, IterableAbc) and not isinstance(value, str) pcs-0.12.0.2/pcs/common/tools.py000066400000000000000000000077231500417470700163240ustar00rootroot00000000000000import uuid from dataclasses import ( astuple, dataclass, ) from typing import ( Generator, MutableSet, Optional, TypeVar, Union, ) from lxml import etree from lxml.etree import _Element from pcs.common.types import StringCollection T = TypeVar("T", bound=type) def bin_to_str(binary: bytes) -> str: return "".join(map(chr, binary)) def get_all_subclasses(cls: T) -> MutableSet[T]: subclasses = set(cls.__subclasses__()) return subclasses.union( {s for c in subclasses for s in get_all_subclasses(c)} ) def get_unique_uuid(already_used: StringCollection) -> str: is_duplicate = True while is_duplicate: candidate = str(uuid.uuid4()) is_duplicate = candidate in already_used return candidate def format_os_error(e: OSError) -> str: return f"{e.strerror}: '{e.filename}'" if e.filename else e.strerror def xml_fromstring(xml: str) -> _Element: # If the xml contains encoding declaration such as: # # we get an exception in python3: # ValueError: Unicode strings with encoding declaration are not supported. # Please use bytes input or XML fragments without declaration. # So we encode the string to bytes. return etree.fromstring( xml.encode("utf-8"), # it raises on a huge xml without the flag huge_tree=True # see https://bugzilla.redhat.com/show_bug.cgi?id=1506864 etree.XMLParser(huge_tree=True), ) def timeout_to_seconds(timeout: Union[int, str]) -> Optional[int]: """ Transform pacemaker style timeout to number of seconds. If `timeout` is not a valid timeout, `None` is returned. timeout -- timeout string """ try: candidate = int(timeout) if candidate >= 0: return candidate return None except ValueError: pass # Now we know the timeout is not an integer nor an integer string. # Let's make sure mypy knows the timeout is a string as well. timeout = str(timeout) suffix_multiplier = { "s": 1, "sec": 1, "m": 60, "min": 60, "h": 3600, "hr": 3600, } for suffix, multiplier in suffix_multiplier.items(): if timeout.endswith(suffix) and timeout[: -len(suffix)].isdigit(): return int(timeout[: -len(suffix)]) * multiplier return None @dataclass(frozen=True) class Version: major: int minor: Optional[int] = None revision: Optional[int] = None @property def as_full_tuple(self) -> tuple[int, int, int]: return ( self.major, self.minor if self.minor is not None else 0, self.revision if self.revision is not None else 0, ) def normalize(self) -> "Version": return self.__class__(*self.as_full_tuple) def __iter__(self) -> Generator[Optional[int], None, None]: yield from astuple(self) def __getitem__(self, index: int) -> Optional[int]: return astuple(self)[index] def __str__(self) -> str: return ".".join([str(x) for x in self if x is not None]) def __lt__(self, other: "Version") -> bool: return self.as_full_tuple < other.as_full_tuple def __le__(self, other: "Version") -> bool: return self.as_full_tuple <= other.as_full_tuple # See, https://stackoverflow.com/questions/37557411/why-does-defining-the-argument-types-for-eq-throw-a-mypy-type-error def __eq__(self, other: object) -> bool: if not isinstance(other, Version): return NotImplemented return self.as_full_tuple == other.as_full_tuple def __ne__(self, other: object) -> bool: if not isinstance(other, Version): return NotImplemented return self.as_full_tuple != other.as_full_tuple def __gt__(self, other: "Version") -> bool: return self.as_full_tuple > other.as_full_tuple def __ge__(self, other: "Version") -> bool: return self.as_full_tuple >= other.as_full_tuple pcs-0.12.0.2/pcs/common/types.py000066400000000000000000000044031500417470700163200ustar00rootroot00000000000000from collections.abc import Set from enum import ( Enum, auto, ) from typing import ( Generator, Literal, MutableSequence, Optional, Type, TypeVar, Union, ) StringSequence = Union[MutableSequence[str], tuple[str, ...]] StringCollection = Union[StringSequence, Set[str]] StringIterable = Union[StringCollection, Generator[str, None, None]] class AutoNameEnum(str, Enum): @staticmethod def _generate_next_value_( name: str, start: int, count: int, last_values: list[int], ) -> str: del start, count, last_values return name T = TypeVar("T", bound=AutoNameEnum) def str_to_enum(enum_type: Type[T], value: Optional[str]) -> Optional[T]: if value: value = value.upper() if value in {item.value for item in enum_type}: return enum_type(value) return None PcmkScore = Union[int, Literal["INFINITY", "+INFINITY", "-INFINITY"]] class CibRuleExpressionType(AutoNameEnum): RULE = auto() EXPRESSION = auto() # node attribute expression, named 'expression' in CIB DATE_EXPRESSION = auto() OP_EXPRESSION = auto() RSC_EXPRESSION = auto() class CibRuleInEffectStatus(AutoNameEnum): NOT_YET_IN_EFFECT = auto() IN_EFFECT = auto() EXPIRED = auto() UNKNOWN = auto() class ResourceRelationType(AutoNameEnum): ORDER = auto() ORDER_SET = auto() INNER_RESOURCES = auto() OUTER_RESOURCE = auto() RSC_PRIMITIVE = auto() RSC_CLONE = auto() RSC_GROUP = auto() RSC_BUNDLE = auto() RSC_UNKNOWN = auto() class DrRole(AutoNameEnum): PRIMARY = auto() RECOVERY = auto() class UnknownCorosyncTransportTypeException(Exception): def __init__(self, transport: str) -> None: super().__init__() self.transport = transport class CorosyncTransportType(AutoNameEnum): UDP = auto() UDPU = auto() KNET = auto() @classmethod def from_str(cls, transport: str) -> "CorosyncTransportType": try: return cls(transport.upper()) except ValueError: raise UnknownCorosyncTransportTypeException(transport) from None class CorosyncNodeAddressType(str, Enum): IPV4 = "IPv4" IPV6 = "IPv6" FQDN = "FQDN" UNRESOLVABLE = "unresolvable" pcs-0.12.0.2/pcs/common/validate.py000066400000000000000000000020411500417470700167410ustar00rootroot00000000000000import re from typing import ( Optional, Union, ) _INTEGER_RE = re.compile(r"^[+-]?[0-9]+$") def is_integer( value: Union[str, int, float], at_least: Optional[int] = None, at_most: Optional[int] = None, ) -> bool: """ Check if the specified value is an integer, optionally check a range value -- value to check at_least -- minimal allowed value at_most -- maximal allowed value """ try: if value is None or isinstance(value, float): return False if isinstance(value, str) and not _INTEGER_RE.fullmatch(value): return False value_int = int(value) if at_least is not None and value_int < at_least: return False if at_most is not None and value_int > at_most: return False except ValueError: return False return True def is_port_number(value: str) -> bool: """ Check if the specified value is a TCP or UDP port number value -- value to check """ return is_integer(value, 1, 65535) pcs-0.12.0.2/pcs/config.py000066400000000000000000000646021500417470700151400ustar00rootroot00000000000000import datetime import difflib import grp import json import os import os.path import pwd import re import shutil import sys import tarfile import tempfile import time from io import BytesIO from typing import cast from xml.dom.minidom import parse from pcs import ( alert, cluster, quorum, settings, status, usage, utils, ) from pcs.cli.cluster_property.output import ( PropertyConfigurationFacade, properties_to_text, ) from pcs.cli.common import middleware from pcs.cli.common.errors import CmdLineInputError from pcs.cli.common.output import ( INDENT_STEP, smart_wrap_text, ) from pcs.cli.constraint.output import constraints_to_text from pcs.cli.nvset import nvset_dto_list_to_lines from pcs.cli.reports import process_library_reports from pcs.cli.reports.output import ( print_to_stderr, warn, ) from pcs.cli.resource.output import ( ResourcesConfigurationFacade, resources_to_text, ) from pcs.cli.stonith.levels.output import stonith_level_config_to_text from pcs.cli.tag.output import tags_to_text from pcs.common.interface import dto from pcs.common.pacemaker.constraint import CibConstraintsDto from pcs.common.str_tools import indent from pcs.lib.errors import LibraryError from pcs.lib.node import get_existing_nodes_names def config_show(lib, argv, modifiers): """ Options: * -f - CIB file, when getting cluster name on remote node (corosync.conf doesn't exist) * --corosync_conf - corosync.conf file """ modifiers.ensure_only_supported("-f", "--corosync_conf") if argv: raise CmdLineInputError() corosync_conf_dto = None cluster_name = "" properties_facade = PropertyConfigurationFacade.from_properties_config( lib.cluster_property.get_properties(), ) try: corosync_conf_dto = lib.cluster.get_corosync_conf_struct() cluster_name = corosync_conf_dto.cluster_name except LibraryError: # there is no corosync.conf on remote nodes, we can try to # get cluster name from pacemaker pass if not cluster_name: cluster_name = properties_facade.get_property_value("cluster-name", "") print("Cluster Name: %s" % cluster_name) status.nodes_status(lib, ["config"], modifiers.get_subset("-f")) cib_lines = _config_show_cib_lines(lib, properties_facade=properties_facade) if cib_lines: print() print("\n".join(cib_lines)) if ( utils.hasCorosyncConf() and not modifiers.is_specified("-f") and not modifiers.is_specified("--corosync_conf") ): cluster.cluster_uidgid( lib, [], modifiers.get_subset(), silent_list=True ) if corosync_conf_dto: quorum_device_dict = {} if corosync_conf_dto.quorum_device: quorum_device_dict = dto.to_dict(corosync_conf_dto.quorum_device) config = dict( options=corosync_conf_dto.quorum_options, device=quorum_device_dict, ) quorum_lines = quorum.quorum_config_to_str(config) if quorum_lines: print() print("Quorum:") print("\n".join(indent(quorum_lines))) def _config_show_cib_lines(lib, properties_facade=None): """ Commandline options: * -f - CIB file """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements # update of pcs_options will change output of constraint show and # displaying resources and operations defaults utils.pcs_options["--full"] = 1 # get latest modifiers object after updating pcs_options modifiers = utils.get_input_modifiers() resources_facade = ResourcesConfigurationFacade.from_resources_dto( lib.resource.get_configured_resources() ) all_lines = [] resources_lines = smart_wrap_text( indent( resources_to_text(resources_facade.filter_stonith(False)), indent_step=INDENT_STEP, ) ) if resources_lines: all_lines.append("Resources:") all_lines.extend(resources_lines) stonith_lines = smart_wrap_text( indent( resources_to_text(resources_facade.filter_stonith(True)), indent_step=INDENT_STEP, ) ) if stonith_lines: if all_lines: all_lines.append("") all_lines.append("Stonith Devices:") all_lines.extend(stonith_lines) levels_lines = stonith_level_config_to_text( lib.fencing_topology.get_config_dto() ) if levels_lines: if all_lines: all_lines.append("") all_lines.append("Fencing Levels:") all_lines.extend(indent(levels_lines, indent_step=2)) constraints_lines = smart_wrap_text( constraints_to_text( cast( CibConstraintsDto, lib.constraint.get_config(evaluate_rules=False), ), modifiers.is_specified("--full"), ) ) if constraints_lines: if all_lines: all_lines.append("") all_lines.extend(constraints_lines) alert_lines = alert.alert_config_lines(lib) if alert_lines: if all_lines: all_lines.append("") all_lines.extend(alert_lines) resources_defaults_lines = indent( nvset_dto_list_to_lines( lib.cib_options.resource_defaults_config( evaluate_expired=False ).meta_attributes, nvset_label="Meta Attrs", with_ids=modifiers.get("--full"), ) ) if resources_defaults_lines: if all_lines: all_lines.append("") all_lines.append("Resources Defaults:") all_lines.extend(resources_defaults_lines) operations_defaults_lines = indent( nvset_dto_list_to_lines( lib.cib_options.operation_defaults_config( evaluate_expired=False ).meta_attributes, nvset_label="Meta Attrs", with_ids=modifiers.get("--full"), ) ) if operations_defaults_lines: if all_lines: all_lines.append("") all_lines.append("Operations Defaults:") all_lines.extend(operations_defaults_lines) if not properties_facade: properties_facade = PropertyConfigurationFacade.from_properties_config( lib.cluster_property.get_properties() ) properties_lines = properties_to_text(properties_facade) if properties_lines: if all_lines: all_lines.append("") all_lines.extend(properties_lines) tag_lines = smart_wrap_text(tags_to_text(lib.tag.get_config_dto([]))) if tag_lines: if all_lines: all_lines.append("") all_lines.append("Tags:") all_lines.extend(indent(tag_lines, indent_step=1)) return all_lines def config_backup(lib, argv, modifiers): """ Options: * --force - overwrite file if already exists """ del lib modifiers.ensure_only_supported("--force") if len(argv) > 1: raise CmdLineInputError() outfile_name = None if argv: outfile_name = argv[0] if not outfile_name.endswith(".tar.bz2"): outfile_name += ".tar.bz2" tar_data = config_backup_local() if outfile_name: ok, message = utils.write_file( outfile_name, tar_data, permissions=0o600, binary=True ) if not ok: utils.err(message) else: # in python3 stdout accepts str so we need to use buffer sys.stdout.buffer.write(tar_data) def config_backup_local(): """ Commandline options: no options """ file_list = config_backup_path_list() tar_data = BytesIO() try: with tarfile.open(fileobj=tar_data, mode="w|bz2") as tarball: config_backup_add_version_to_tarball(tarball) for tar_path, path_info in file_list.items(): if ( not os.path.exists(path_info["path"]) and not path_info["required"] ): continue tarball.add(path_info["path"], tar_path) except (tarfile.TarError, EnvironmentError) as e: utils.err("unable to create tarball: %s" % e) tar = tar_data.getvalue() tar_data.close() return tar def config_restore(lib, argv, modifiers): """ Options: * --local - restore config only on local node * --request-timeout - timeout for HTTP requests, used only if --local was not defined or user is not root """ del lib modifiers.ensure_only_supported("--local", "--request-timeout") if len(argv) > 1: raise CmdLineInputError() infile_name = infile_obj = None if argv: infile_name = argv[0] if not infile_name: # in python3 stdin returns str so we need to use buffer infile_obj = BytesIO(sys.stdin.buffer.read()) if os.getuid() == 0: if modifiers.get("--local"): config_restore_local(infile_name, infile_obj) else: config_restore_remote(infile_name, infile_obj) else: new_argv = ["config", "restore"] options = [] new_stdin = None if modifiers.get("--local"): options.append("--local") if infile_name: new_argv.append(os.path.abspath(infile_name)) else: new_stdin = infile_obj.read() err_msgs, exitcode, std_out, std_err = utils.call_local_pcsd( new_argv, options, new_stdin ) if err_msgs: for msg in err_msgs: utils.err(msg, False) sys.exit(1) print(std_out) sys.stderr.write(std_err) sys.exit(exitcode) def config_restore_remote(infile_name, infile_obj): """ Commandline options: * --request-timeout - timeout for HTTP requests """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals extracted = { "version.txt": "", "corosync.conf": "", } try: with tarfile.open(infile_name, "r|*", infile_obj) as tarball: while True: # next(tarball) does not work in python2.6 tar_member_info = tarball.next() if tar_member_info is None: break if tar_member_info.name in extracted: tar_member = tarball.extractfile(tar_member_info) extracted[tar_member_info.name] = tar_member.read() tar_member.close() except (tarfile.TarError, EnvironmentError) as e: utils.err("unable to read the tarball: %s" % e) config_backup_check_version(extracted["version.txt"]) node_list, report_list = get_existing_nodes_names( utils.get_corosync_conf_facade( conf_text=extracted["corosync.conf"].decode("utf-8") ) ) if report_list: process_library_reports(report_list) if not node_list: utils.err("no nodes found in the tarball") err_msgs = [] for node in node_list: try: retval, output = utils.checkStatus(node) if retval != 0: err_msgs.append(output) continue _status = json.loads(output) if any( _status["node"]["services"][service_name]["running"] for service_name in ( "corosync", "pacemaker", "pacemaker_remote", ) ): err_msgs.append( "Cluster is currently running on node %s. You need to stop " "the cluster in order to restore the configuration." % node ) continue except (ValueError, NameError, LookupError): err_msgs.append("unable to determine status of the node %s" % node) if err_msgs: for msg in err_msgs: utils.err(msg, False) sys.exit(1) # Temporarily disable config files syncing thread in pcsd so it will not # rewrite restored files. 10 minutes should be enough time to restore. # If node returns HTTP 404 it does not support config syncing at all. for node in node_list: retval, output = utils.pauseConfigSyncing(node, 10 * 60) if not (retval == 0 or "(HTTP error: 404)" in output): utils.err(output) if infile_obj: infile_obj.seek(0) tarball_data = infile_obj.read() else: with open(infile_name, "rb") as tarball: tarball_data = tarball.read() error_list = [] for node in node_list: retval, error = utils.restoreConfig(node, tarball_data) if retval != 0: error_list.append(error) if error_list: utils.err("unable to restore all nodes\n" + "\n".join(error_list)) def config_restore_local(infile_name, infile_obj): """ Commandline options: no options """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements service_manager = utils.get_service_manager() if ( service_manager.is_running("corosync") or service_manager.is_running("pacemaker") or service_manager.is_running("pacemaker_remote") ): utils.err( "Cluster is currently running on this node. You need to stop " "the cluster in order to restore the configuration." ) file_list = config_backup_path_list(with_uid_gid=True) tarball_file_list = [] version = None tmp_dir = None try: with tarfile.open(infile_name, "r|*", infile_obj) as tarball: while True: # next(tarball) does not work in python2.6 tar_member_info = tarball.next() if tar_member_info is None: break if tar_member_info.name == "version.txt": version_data = tarball.extractfile(tar_member_info) version = version_data.read() version_data.close() continue tarball_file_list.append(tar_member_info.name) required_file_list = [ tar_path for tar_path, path_info in file_list.items() if path_info["required"] ] missing = set(required_file_list) - set(tarball_file_list) if missing: utils.err( "unable to restore the cluster, missing files in backup: %s" % ", ".join(missing) ) config_backup_check_version(version) if infile_obj: infile_obj.seek(0) with tarfile.open(infile_name, "r|*", infile_obj) as tarball: while True: # next(tarball) does not work in python2.6 tar_member_info = tarball.next() if tar_member_info is None: break extract_info = None path = tar_member_info.name while path: if path in file_list: extract_info = file_list[path] break path = os.path.dirname(path) if not extract_info: continue path_full = None if hasattr(extract_info.get("pre_store_call"), "__call__"): extract_info["pre_store_call"]() if "rename" in extract_info and extract_info["rename"]: if tmp_dir is None: tmp_dir = tempfile.mkdtemp() if hasattr(tarfile, "data_filter"): # Safe way of extraction is available since Python 3.12, # hasattr above checks if it's available. # It's also backported to 3.11.4, 3.10.12, 3.9.17. # It may be backported to older versions in downstream. tarball.extractall( tmp_dir, [tar_member_info], filter="data" ) else: # Unsafe way of extraction # Remove once we don't support Python 3.8 and older tarball.extractall(tmp_dir, [tar_member_info]) path_full = extract_info["path"] shutil.move( os.path.join(tmp_dir, tar_member_info.name), path_full ) else: dir_path = os.path.dirname(extract_info["path"]) if hasattr(tarfile, "data_filter"): # Safe way of extraction is available since Python 3.12, # hasattr above checks if it's available. # It's also backported to 3.11.4, 3.10.12, 3.9.17. # It may be backported to older versions in downstream. tarball.extractall( dir_path, [tar_member_info], filter="data" ) else: # Unsafe way of extracting # Remove once we don't support Python 3.8 and older tarball.extractall(dir_path, [tar_member_info]) path_full = os.path.join(dir_path, tar_member_info.name) file_attrs = extract_info["attrs"] os.chmod(path_full, file_attrs["mode"]) os.chown(path_full, file_attrs["uid"], file_attrs["gid"]) except (tarfile.TarError, EnvironmentError, OSError) as e: utils.err("unable to restore the cluster: %s" % e) finally: if tmp_dir: shutil.rmtree(tmp_dir, ignore_errors=True) try: sig_path = os.path.join(settings.cib_dir, "cib.xml.sig") if os.path.exists(sig_path): os.remove(sig_path) except EnvironmentError as e: utils.err("unable to remove %s: %s" % (sig_path, e)) def config_backup_path_list(with_uid_gid=False): """ Commandline options: no option NOTE: corosync.conf path may be altered using --corosync_conf """ corosync_attrs = { "mtime": int(time.time()), "mode": 0o644, "uname": "root", "gname": "root", "uid": 0, "gid": 0, } corosync_authkey_attrs = dict(corosync_attrs) corosync_authkey_attrs["mode"] = 0o400 cib_attrs = { "mtime": int(time.time()), "mode": 0o600, "uname": settings.pacemaker_uname, "gname": settings.pacemaker_gname, } if with_uid_gid: cib_attrs["uid"] = _get_uid(cib_attrs["uname"]) cib_attrs["gid"] = _get_gid(cib_attrs["gname"]) pcmk_authkey_attrs = dict(cib_attrs) pcmk_authkey_attrs["mode"] = 0o440 file_list = { "cib.xml": { "path": os.path.join(settings.cib_dir, "cib.xml"), "required": True, "attrs": dict(cib_attrs), }, "corosync_authkey": { "path": settings.corosync_authkey_file, "required": False, "attrs": corosync_authkey_attrs, "restore_procedure": None, "rename": True, }, "pacemaker_authkey": { "path": settings.pacemaker_authkey_file, "required": False, "attrs": pcmk_authkey_attrs, "restore_procedure": None, "rename": True, "pre_store_call": _ensure_etc_pacemaker_exists, }, "corosync.conf": { "path": settings.corosync_conf_file, "required": True, "attrs": dict(corosync_attrs), }, "uidgid.d": { "path": settings.corosync_uidgid_dir, "required": False, "attrs": dict(corosync_attrs), }, "pcs_settings.conf": { "path": settings.pcsd_settings_conf_location, "required": False, "attrs": { "mtime": int(time.time()), "mode": 0o644, "uname": "root", "gname": "root", "uid": 0, "gid": 0, }, }, } return file_list def _get_uid(user_name): """ Commandline options: no options """ try: return pwd.getpwnam(user_name).pw_uid except KeyError: return utils.err( "Unable to determine uid of user '{0}'".format(user_name) ) def _get_gid(group_name): """ Commandline options: no options """ try: return grp.getgrnam(group_name).gr_gid except KeyError: return utils.err( "Unable to determine gid of group '{0}'".format(group_name) ) def _ensure_etc_pacemaker_exists(): """ Commandline options: no options """ dir_name = os.path.dirname(settings.pacemaker_authkey_file) if not os.path.exists(dir_name): os.mkdir(dir_name) os.chmod(dir_name, 0o750) os.chown( dir_name, _get_uid(settings.pacemaker_uname), _get_gid(settings.pacemaker_gname), ) def config_backup_check_version(version): """ Commandline options: no options """ try: version_number = int(version) supported_version = config_backup_version() if version_number > supported_version: utils.err( f"Unsupported version of the backup, supported version is " f"{supported_version}, backup version is {version_number}" ) if version_number < supported_version: warn( f"Restoring from the backup version {version_number}, current " f"supported version is {supported_version}" ) except TypeError: utils.err("Cannot determine version of the backup") def config_backup_add_version_to_tarball(tarball, version=None): """ Commandline options: no options """ ver = version if version is not None else str(config_backup_version()) return utils.tar_add_file_data(tarball, ver.encode("utf-8"), "version.txt") def config_backup_version(): """ Commandline options: no options """ return 1 def config_checkpoint_list(lib, argv, modifiers): """ Options: no options """ del lib modifiers.ensure_only_supported() if argv: raise CmdLineInputError() try: file_list = os.listdir(settings.cib_dir) except OSError as e: utils.err("unable to list checkpoints: %s" % e) cib_list = [] cib_name_re = re.compile(r"^cib-(\d+)\.raw$") for filename in file_list: match = cib_name_re.match(filename) if not match: continue file_path = os.path.join(settings.cib_dir, filename) try: if os.path.isfile(file_path): cib_list.append( (float(os.path.getmtime(file_path)), match.group(1)) ) except OSError: pass cib_list.sort() if not cib_list: print_to_stderr("No checkpoints available") return for cib_info in cib_list: print( "checkpoint %s: date %s" % (cib_info[1], datetime.datetime.fromtimestamp(round(cib_info[0]))) ) def _checkpoint_to_lines(lib, checkpoint_number): # backup current settings orig_usefile = utils.usefile orig_filename = utils.filename orig_middleware = lib.middleware_factory orig_env = lib.env # configure old code to read the CIB from a file utils.usefile = True utils.filename = os.path.join( settings.cib_dir, "cib-%s.raw" % checkpoint_number ) # configure new code to read the CIB from a file lib.middleware_factory = orig_middleware._replace( cib=middleware.cib(utils.filename, utils.touch_cib_file) ) lib.env = utils.get_cli_env() # export the CIB to text result = False, [] if os.path.isfile(utils.filename): result = True, _config_show_cib_lines(lib) # restore original settings utils.usefile = orig_usefile utils.filename = orig_filename lib.middleware_factory = orig_middleware lib.env = orig_env return result def config_checkpoint_view(lib, argv, modifiers): """ Options: no options """ modifiers.ensure_only_supported() if len(argv) != 1: print_to_stderr(usage.config(["checkpoint view"])) sys.exit(1) loaded, lines = _checkpoint_to_lines(lib, argv[0]) if not loaded: utils.err("unable to read the checkpoint") print("\n".join(lines)) def config_checkpoint_diff(lib, argv, modifiers): """ Commandline options: * -f - CIB file """ modifiers.ensure_only_supported("-f") if len(argv) != 2: print_to_stderr(usage.config(["checkpoint diff"])) sys.exit(1) if argv[0] == argv[1]: utils.err("cannot diff a checkpoint against itself") errors = [] checkpoints_lines = [] for checkpoint in argv: if checkpoint == "live": lines = _config_show_cib_lines(lib) if not lines: errors.append("unable to read live configuration") else: checkpoints_lines.append(lines) else: loaded, lines = _checkpoint_to_lines(lib, checkpoint) if not loaded: errors.append( "unable to read checkpoint '{0}'".format(checkpoint) ) else: checkpoints_lines.append(lines) if errors: utils.err("\n".join(errors)) print( "Differences between {0} (-) and {1} (+):".format( *[ ( "live configuration" if label == "live" else f"checkpoint {label}" ) for label in argv ] ) ) print( "\n".join( [ line.rstrip() for line in difflib.Differ().compare( checkpoints_lines[0], checkpoints_lines[1] ) ] ) ) def config_checkpoint_restore(lib, argv, modifiers): """ Options: * -f - CIB file, a checkpoint will be restored into a specified file """ del lib modifiers.ensure_only_supported("-f") if len(argv) != 1: print_to_stderr(usage.config(["checkpoint restore"])) sys.exit(1) cib_path = os.path.join(settings.cib_dir, "cib-%s.raw" % argv[0]) try: snapshot_dom = parse(cib_path) # pylint: disable=broad-except except Exception as e: utils.err("unable to read the checkpoint: %s" % e) utils.replace_cib_configuration(snapshot_dom) pcs-0.12.0.2/pcs/constraint.py000066400000000000000000001236441500417470700160610ustar00rootroot00000000000000import sys import xml.dom.minidom from enum import Enum from typing import ( Any, Iterable, Optional, Set, TypeVar, cast, ) from xml.dom.minidom import parseString import pcs.cli.constraint_order.command as order_command from pcs import utils from pcs.cli.common import parse_args from pcs.cli.common.errors import ( CmdLineInputError, raise_command_replaced, ) from pcs.cli.common.output import ( INDENT_STEP, lines_to_str, ) from pcs.cli.constraint.location.command import ( RESOURCE_TYPE_REGEXP, RESOURCE_TYPE_RESOURCE, ) from pcs.cli.constraint.output import ( CibConstraintLocationAnyDto, filter_constraints_by_rule_expired_status, location, print_config, ) from pcs.cli.reports import process_library_reports from pcs.cli.reports.output import ( deprecation_warning, print_to_stderr, warn, ) from pcs.common import ( const, pacemaker, reports, ) from pcs.common.pacemaker.constraint import ( CibConstraintColocationSetDto, CibConstraintLocationSetDto, CibConstraintOrderSetDto, CibConstraintsDto, CibConstraintTicketSetDto, get_all_constraints_ids, ) from pcs.common.pacemaker.resource.list import CibResourcesDto from pcs.common.pacemaker.types import CibResourceDiscovery from pcs.common.reports import ReportItem from pcs.common.str_tools import ( format_list, indent, ) from pcs.common.types import ( StringCollection, StringIterable, StringSequence, ) from pcs.lib.cib.constraint.order import ATTRIB as order_attrib from pcs.lib.node import get_existing_nodes_names from pcs.lib.pacemaker.values import ( SCORE_INFINITY, is_true, sanitize_id, ) # pylint: disable=invalid-name # pylint: disable=too-many-branches # pylint: disable=too-many-lines # pylint: disable=too-many-locals # pylint: disable=too-many-statements DEFAULT_ACTION = const.PCMK_ACTION_START DEFAULT_ROLE = const.PCMK_ROLE_STARTED OPTIONS_SYMMETRICAL = order_attrib["symmetrical"] LOCATION_NODE_VALIDATION_SKIP_MSG = ( "Validation for node existence in the cluster will be skipped" ) STANDALONE_SCORE_MSG = ( "Specifying score as a standalone value is deprecated and " "might be removed in a future release, use score=value instead" ) class CrmRuleReturnCode(Enum): IN_EFFECT = 0 EXPIRED = 110 TO_BE_IN_EFFECT = 111 def constraint_order_cmd(lib, argv, modifiers): if not argv: sub_cmd = "config" else: sub_cmd = argv.pop(0) try: if sub_cmd == "set": order_command.create_with_set(lib, argv, modifiers) elif sub_cmd in ["remove", "delete"]: order_rm(lib, argv, modifiers) elif sub_cmd == "show": raise_command_replaced( ["pcs constraint order config"], pcs_version="0.12" ) elif sub_cmd == "config": order_command.config_cmd(lib, argv, modifiers) else: order_start(lib, [sub_cmd] + argv, modifiers) except CmdLineInputError as e: utils.exit_on_cmdline_input_error(e, "constraint", ["order", sub_cmd]) def config_cmd( lib: Any, argv: list[str], modifiers: parse_args.InputModifiers ) -> None: modifiers.ensure_only_supported("-f", "--output-format", "--full", "--all") if argv: raise CmdLineInputError() print_config( cast( CibConstraintsDto, lib.constraint.get_config(evaluate_rules=True), ), modifiers, ) def _validate_constraint_resource(cib_dom, resource_id): ( resource_valid, resource_error, dummy_correct_id, ) = utils.validate_constraint_resource(cib_dom, resource_id) if not resource_valid: utils.err(resource_error) def _validate_resources_not_in_same_group(cib_dom, resource1, resource2): if not utils.validate_resources_not_in_same_group( cib_dom, resource1, resource2 ): utils.err( "Cannot create an order constraint for resources in the same group" ) # Syntax: colocation add [role] with [role] [score] [options] # possible commands: # with [score] [options] # with [score] [options] # with [score] [options] # with [score] [options] # Specifying score as a single argument is deprecated, though. The correct way # is score=value in options. def colocation_add(lib, argv, modifiers): """ Options: * -f - CIB file * --force - allow constraint on any resource, allow duplicate constraints """ def _parse_score_options(argv): # When passed an array of arguments if the first argument doesn't have # an '=' then it's the score, otherwise they're all arguments. Return a # tuple with the score and array of name,value pairs """ Commandline options: no options """ if not argv: return None, [] score = None if "=" not in argv[0]: score = argv.pop(0) # TODO added to pcs in the first 0.12.x version deprecation_warning(STANDALONE_SCORE_MSG) # create a list of 2-tuples (name, value) arg_array = [ parse_args.split_option(arg, allow_empty_value=False) for arg in argv ] return score, arg_array del lib modifiers.ensure_only_supported("-f", "--force") if len(argv) < 3: raise CmdLineInputError() role1 = "" role2 = "" cib_dom = utils.get_cib_dom() new_roles_supported = utils.isCibVersionSatisfied( cib_dom, const.PCMK_NEW_ROLES_CIB_VERSION ) def _validate_and_prepare_role(role): role_cleaned = role.lower().capitalize() if role_cleaned not in const.PCMK_ROLES: utils.err( "invalid role value '{0}', allowed values are: {1}".format( role, format_list(const.PCMK_ROLES) ) ) return pacemaker.role.get_value_for_cib( role_cleaned, new_roles_supported ) if argv[2] == "with": role1 = _validate_and_prepare_role(argv.pop(0)) resource1 = argv.pop(0) elif argv[1] == "with": resource1 = argv.pop(0) else: raise CmdLineInputError() if argv.pop(0) != "with": raise CmdLineInputError() if "with" in argv: raise CmdLineInputError( message="Multiple 'with's cannot be specified.", hint=( "Use the 'pcs constraint colocation set' command if you want " "to create a constraint for more than two resources." ), show_both_usage_and_message=True, ) if not argv: raise CmdLineInputError() if len(argv) == 1: resource2 = argv.pop(0) else: if utils.is_score_or_opt(argv[1]): resource2 = argv.pop(0) else: role2 = _validate_and_prepare_role(argv.pop(0)) resource2 = argv.pop(0) score, nv_pairs = _parse_score_options(argv) _validate_constraint_resource(cib_dom, resource1) _validate_constraint_resource(cib_dom, resource2) id_in_nvpairs = None for name, value in nv_pairs: if name == "id": id_valid, id_error = utils.validate_xml_id(value, "constraint id") if not id_valid: utils.err(id_error) if utils.does_id_exist(cib_dom, value): utils.err( "id '%s' is already in use, please specify another one" % value ) id_in_nvpairs = True elif name == "score": score = value if score is None: score = SCORE_INFINITY if not id_in_nvpairs: nv_pairs.append( ( "id", utils.find_unique_id( cib_dom, "colocation-%s-%s-%s" % (resource1, resource2, score), ), ) ) (dom, constraintsElement) = getCurrentConstraints(cib_dom) # If one role is specified, the other should default to "started" if role1 != "" and role2 == "": role2 = DEFAULT_ROLE if role2 != "" and role1 == "": role1 = DEFAULT_ROLE element = dom.createElement("rsc_colocation") element.setAttribute("rsc", resource1) element.setAttribute("with-rsc", resource2) element.setAttribute("score", score) if role1 != "": element.setAttribute("rsc-role", role1) if role2 != "": element.setAttribute("with-rsc-role", role2) for nv_pair in nv_pairs: element.setAttribute(nv_pair[0], nv_pair[1]) if not modifiers.get("--force"): def _constraint_export(constraint_info): options_dict = constraint_info["options"] co_resource1 = options_dict.get("rsc", "") co_resource2 = options_dict.get("with-rsc", "") co_id = options_dict.get("id", "") co_score = options_dict.get("score", "") score_text = "(score:" + co_score + ")" console_option_list = [ f"({option[0]}:{option[1]})" for option in sorted(options_dict.items()) if option[0] not in ("rsc", "with-rsc", "id", "score") ] console_option_list.append(f"(id:{co_id})") return " ".join( [co_resource1, "with", co_resource2, score_text] + console_option_list ) duplicates = colocation_find_duplicates(constraintsElement, element) if duplicates: utils.err( "duplicate constraint already exists, use --force to override\n" + "\n".join( [ " " + _constraint_export( {"options": dict(dup.attributes.items())} ) for dup in duplicates ] ) ) constraintsElement.appendChild(element) utils.replace_cib_configuration(dom) def colocation_find_duplicates(dom, constraint_el): """ Commandline options: no options """ new_roles_supported = utils.isCibVersionSatisfied( dom, const.PCMK_NEW_ROLES_CIB_VERSION ) def normalize(const_el): return ( const_el.getAttribute("rsc"), const_el.getAttribute("with-rsc"), pacemaker.role.get_value_for_cib( const_el.getAttribute("rsc-role").capitalize() or DEFAULT_ROLE, new_roles_supported, ), pacemaker.role.get_value_for_cib( const_el.getAttribute("with-rsc-role").capitalize() or DEFAULT_ROLE, new_roles_supported, ), ) normalized_el = normalize(constraint_el) return [ other_el for other_el in dom.getElementsByTagName("rsc_colocation") if not other_el.getElementsByTagName("resource_set") and constraint_el is not other_el and normalized_el == normalize(other_el) ] def order_rm(lib, argv, modifiers): """ Options: * -f - CIB file """ del lib modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() elementFound = False (dom, constraintsElement) = getCurrentConstraints() for resource in argv: for ord_loc in constraintsElement.getElementsByTagName("rsc_order")[:]: if ( ord_loc.getAttribute("first") == resource or ord_loc.getAttribute("then") == resource ): constraintsElement.removeChild(ord_loc) elementFound = True resource_refs_to_remove = [] for ord_set in constraintsElement.getElementsByTagName("resource_ref"): if ord_set.getAttribute("id") == resource: resource_refs_to_remove.append(ord_set) elementFound = True for res_ref in resource_refs_to_remove: res_set = res_ref.parentNode res_order = res_set.parentNode res_ref.parentNode.removeChild(res_ref) if not res_set.getElementsByTagName("resource_ref"): res_set.parentNode.removeChild(res_set) if not res_order.getElementsByTagName("resource_set"): res_order.parentNode.removeChild(res_order) if elementFound: utils.replace_cib_configuration(dom) else: utils.err("No matching resources found in ordering list") def order_start(lib, argv, modifiers): """ Options: * -f - CIB file * --force - allow constraint for any resource, allow duplicate constraints """ del lib modifiers.ensure_only_supported("-f", "--force") if len(argv) < 3: raise CmdLineInputError() first_action = DEFAULT_ACTION then_action = DEFAULT_ACTION action = argv[0] if action in const.PCMK_ACTIONS: first_action = action argv.pop(0) resource1 = argv.pop(0) if argv.pop(0) != "then": raise CmdLineInputError() if not argv: raise CmdLineInputError() action = argv[0] if action in const.PCMK_ACTIONS: then_action = action argv.pop(0) if not argv: raise CmdLineInputError() resource2 = argv.pop(0) order_options = [] if argv: order_options = order_options + argv[:] if "then" in order_options: raise CmdLineInputError( message="Multiple 'then's cannot be specified.", hint=( "Use the 'pcs constraint order set' command if you want to " "create a constraint for more than two resources." ), show_both_usage_and_message=True, ) order_options.append("first-action=" + first_action) order_options.append("then-action=" + then_action) _order_add(resource1, resource2, order_options, modifiers) def _order_add(resource1, resource2, options_list, modifiers): """ Commandline options: * -f - CIB file * --force - allow constraint for any resource, allow duplicate constraints """ cib_dom = utils.get_cib_dom() _validate_constraint_resource(cib_dom, resource1) _validate_constraint_resource(cib_dom, resource2) _validate_resources_not_in_same_group(cib_dom, resource1, resource2) order_options = [] id_specified = False sym = None for arg in options_list: if arg == "symmetrical": sym = "true" elif arg == "nonsymmetrical": sym = "false" else: name, value = parse_args.split_option(arg, allow_empty_value=False) if name == "id": id_valid, id_error = utils.validate_xml_id( value, "constraint id" ) if not id_valid: utils.err(id_error) if utils.does_id_exist(cib_dom, value): utils.err( "id '%s' is already in use, please specify another one" % value ) id_specified = True order_options.append((name, value)) elif name == "symmetrical": if value.lower() in OPTIONS_SYMMETRICAL: sym = value.lower() else: utils.err( "invalid symmetrical value '%s', allowed values are: %s" % (value, ", ".join(OPTIONS_SYMMETRICAL)) ) else: order_options.append((name, value)) if sym: order_options.append(("symmetrical", sym)) options = "" if order_options: options = " (Options: %s)" % " ".join( [ "%s=%s" % (name, value) for name, value in order_options if name not in ("kind", "score") ] ) scorekind = "kind: Mandatory" id_suffix = "mandatory" for opt in order_options: if opt[0] == "score": scorekind = "score: " + opt[1] id_suffix = opt[1] # TODO deprecated in pacemaker 2, to be removed in pacemaker 3 # added to pcs after 0.11.7 deprecation_warning( reports.messages.DeprecatedOption(opt[0], []).message ) break if opt[0] == "kind": scorekind = "kind: " + opt[1] id_suffix = opt[1] break if not id_specified: order_id = "order-" + resource1 + "-" + resource2 + "-" + id_suffix order_id = utils.find_unique_id(cib_dom, order_id) order_options.append(("id", order_id)) (dom, constraintsElement) = getCurrentConstraints() element = dom.createElement("rsc_order") element.setAttribute("first", resource1) element.setAttribute("then", resource2) for order_opt in order_options: element.setAttribute(order_opt[0], order_opt[1]) constraintsElement.appendChild(element) if not modifiers.get("--force"): def _constraint_export(constraint_info): options = constraint_info["options"] oc_resource1 = options.get("first", "") oc_resource2 = options.get("then", "") first_action = options.get("first-action", "") then_action = options.get("then-action", "") oc_id = options.get("id", "") oc_score = options.get("score", "") oc_kind = options.get("kind", "") oc_sym = "" oc_id_out = "" oc_options = "" if "symmetrical" in options and not is_true( options.get("symmetrical", "false") ): oc_sym = "(non-symmetrical)" if oc_kind != "": score_text = "(kind:" + oc_kind + ")" elif oc_kind == "" and oc_score == "": score_text = "(kind:Mandatory)" else: score_text = "(score:" + oc_score + ")" oc_id_out = "(id:" + oc_id + ")" already_processed_options = ( "first", "then", "first-action", "then-action", "id", "score", "kind", "symmetrical", ) oc_options = " ".join( [ f"{name}={value}" for name, value in options.items() if name not in already_processed_options ] ) if oc_options: oc_options = "(Options: " + oc_options + ")" return " ".join( [ arg for arg in [ first_action, oc_resource1, "then", then_action, oc_resource2, score_text, oc_sym, oc_options, oc_id_out, ] if arg ] ) duplicates = order_find_duplicates(constraintsElement, element) if duplicates: utils.err( "duplicate constraint already exists, use --force to override\n" + "\n".join( [ " " + _constraint_export( {"options": dict(dup.attributes.items())} ) for dup in duplicates ] ) ) print_to_stderr(f"Adding {resource1} {resource2} ({scorekind}){options}") utils.replace_cib_configuration(dom) def order_find_duplicates(dom, constraint_el): """ Commandline options: no options """ def normalize(constraint_el): return ( constraint_el.getAttribute("first"), constraint_el.getAttribute("then"), constraint_el.getAttribute("first-action").lower() or DEFAULT_ACTION, constraint_el.getAttribute("then-action").lower() or DEFAULT_ACTION, ) normalized_el = normalize(constraint_el) return [ other_el for other_el in dom.getElementsByTagName("rsc_order") if not other_el.getElementsByTagName("resource_set") and constraint_el is not other_el and normalized_el == normalize(other_el) ] _SetConstraint = TypeVar( "_SetConstraint", CibConstraintLocationSetDto, CibConstraintColocationSetDto, CibConstraintOrderSetDto, CibConstraintTicketSetDto, ) def _filter_set_constraints_by_resources( constraints_dto: Iterable[_SetConstraint], resources: Set[str] ) -> list[_SetConstraint]: return [ constraint_set_dto for constraint_set_dto in constraints_dto if any( set(resource_set.resources_ids) & resources for resource_set in constraint_set_dto.resource_sets ) ] def _filter_constraints_by_resources( constraints_dto: CibConstraintsDto, resources: StringIterable, patterns: StringIterable, ) -> CibConstraintsDto: required_resources_set = set(resources) required_patterns_set = set(patterns) return CibConstraintsDto( location=[ constraint_dto for constraint_dto in constraints_dto.location if ( constraint_dto.resource_id is not None and constraint_dto.resource_id in required_resources_set ) or ( constraint_dto.resource_pattern is not None and constraint_dto.resource_pattern in required_patterns_set ) ], location_set=_filter_set_constraints_by_resources( constraints_dto.location_set, required_resources_set ), colocation=[ constraint_dto for constraint_dto in constraints_dto.colocation if {constraint_dto.resource_id, constraint_dto.with_resource_id} & required_resources_set ], colocation_set=_filter_set_constraints_by_resources( constraints_dto.colocation_set, required_resources_set ), order=[ constraint_dto for constraint_dto in constraints_dto.order if { constraint_dto.first_resource_id, constraint_dto.then_resource_id, } & required_resources_set ], order_set=_filter_set_constraints_by_resources( constraints_dto.order_set, required_resources_set ), ticket=[ constraint_dto for constraint_dto in constraints_dto.ticket if constraint_dto.resource_id in required_resources_set ], ticket_set=_filter_set_constraints_by_resources( constraints_dto.ticket_set, required_resources_set ), ) def _filter_location_by_node_base( constraint_dtos: Iterable[CibConstraintLocationAnyDto], nodes: StringCollection, ) -> list[CibConstraintLocationAnyDto]: return [ constraint_dto for constraint_dto in constraint_dtos if constraint_dto.attributes.node is not None and constraint_dto.attributes.node in nodes ] def location_config_cmd( lib: Any, argv: parse_args.Argv, modifiers: parse_args.InputModifiers ) -> None: """ Options: * --all - print expired constraints * --full - print all details * -f - CIB file """ modifiers.ensure_only_supported("-f", "--output-format", "--full", "--all") filter_type: Optional[str] = None filter_items: parse_args.Argv = [] if argv: filter_type, *filter_items = argv allowed_types = ("resources", "nodes") if filter_type not in allowed_types: raise CmdLineInputError( f"Unknown keyword '{filter_type}'. Allowed keywords: " f"{format_list(allowed_types)}" ) if modifiers.get_output_format() != parse_args.OUTPUT_FORMAT_VALUE_TEXT: raise CmdLineInputError( "Output formats other than 'text' are not supported together " "with grouping and filtering by nodes or resources" ) constraints_dto = filter_constraints_by_rule_expired_status( lib.constraint.get_config(evaluate_rules=True), modifiers.is_specified("--all"), ) constraints_dto = CibConstraintsDto( location=constraints_dto.location, location_set=constraints_dto.location_set, ) def _print_lines(lines: StringSequence) -> None: if lines: print("Location Constraints:") print(lines_to_str(indent(lines, indent_step=INDENT_STEP))) if filter_type == "resources": if filter_items: resources = [] patterns = [] for item in filter_items: item_type, item_value = parse_args.parse_typed_arg( item, [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP], RESOURCE_TYPE_RESOURCE, ) if item_type == RESOURCE_TYPE_RESOURCE: resources.append(item_value) elif item_type == RESOURCE_TYPE_REGEXP: patterns.append(item_value) constraints_dto = _filter_constraints_by_resources( constraints_dto, resources, patterns ) _print_lines( location.constraints_to_grouped_by_resource_text( constraints_dto.location, modifiers.is_specified("--full"), ) ) return if filter_type == "nodes": if filter_items: constraints_dto = CibConstraintsDto( location=_filter_location_by_node_base( constraints_dto.location, filter_items ), location_set=_filter_location_by_node_base( constraints_dto.location_set, filter_items ), ) _print_lines( location.constraints_to_grouped_by_node_text( constraints_dto.location, modifiers.is_specified("--full"), ) ) return print_config(constraints_dto, modifiers) def _verify_node_name(node, existing_nodes): report_list = [] if node not in existing_nodes: report_list.append( ReportItem.error( reports.messages.NodeNotFound(node), force_code=reports.codes.FORCE, ) ) return report_list def _verify_score(score): if not utils.is_score(score): utils.err( "invalid score '%s', use integer or INFINITY or -INFINITY" % score ) def location_prefer( lib: Any, argv: parse_args.Argv, modifiers: parse_args.InputModifiers ) -> None: """ Options: * --force - allow unknown options, allow constraint for any resource type * -f - CIB file """ modifiers.ensure_only_supported("--force", "-f") rsc = argv.pop(0) prefer_option = argv.pop(0) dummy_rsc_type, rsc_value = parse_args.parse_typed_arg( rsc, [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP], RESOURCE_TYPE_RESOURCE, ) if prefer_option == "prefers": prefer = True elif prefer_option == "avoids": prefer = False else: raise CmdLineInputError() skip_node_check = False existing_nodes: list[str] = [] if modifiers.is_specified("-f") or modifiers.get("--force"): skip_node_check = True warn(LOCATION_NODE_VALIDATION_SKIP_MSG) else: lib_env = utils.get_lib_env() existing_nodes, report_list = get_existing_nodes_names( corosync_conf=lib_env.get_corosync_conf(), cib=lib_env.get_cib(), ) if report_list: process_library_reports(report_list) report_list = [] parameters_list = [] for nodeconf in argv: nodeconf_a = nodeconf.split("=", 1) node = nodeconf_a[0] if not skip_node_check: report_list += _verify_node_name(node, existing_nodes) if len(nodeconf_a) == 1: if prefer: score = "INFINITY" else: score = "-INFINITY" else: score = nodeconf_a[1] _verify_score(score) if not prefer: if score[0] == "-": score = score[1:] else: score = "-" + score parameters_list.append( [ sanitize_id(f"location-{rsc_value}-{node}-{score}"), rsc, node, f"score={score}", ] ) if report_list: process_library_reports(report_list) modifiers = modifiers.get_subset("--force", "-f") for parameters in parameters_list: location_add(lib, parameters, modifiers, skip_score_and_node_check=True) def location_add( lib: Any, argv: parse_args.Argv, modifiers: parse_args.InputModifiers, skip_score_and_node_check: bool = False, ) -> None: """ Options: * --force - allow unknown options, allow constraint for any resource type * -f - CIB file """ del lib modifiers.ensure_only_supported("--force", "-f") if len(argv) < 4: raise CmdLineInputError() constraint_id = argv.pop(0) rsc_type, rsc_value = parse_args.parse_typed_arg( argv.pop(0), [RESOURCE_TYPE_RESOURCE, RESOURCE_TYPE_REGEXP], RESOURCE_TYPE_RESOURCE, ) node = argv.pop(0) score = None if "=" not in argv[0]: score = argv.pop(0) # TODO added to pcs in the first 0.12.x version deprecation_warning(STANDALONE_SCORE_MSG) options = [] # For now we only allow setting resource-discovery and score for arg in argv: name, value = parse_args.split_option(arg, allow_empty_value=False) if name == "score": score = value elif name == "resource-discovery": if not modifiers.get("--force"): allowed_discovery = list( map( str, [ CibResourceDiscovery.ALWAYS, CibResourceDiscovery.EXCLUSIVE, CibResourceDiscovery.NEVER, ], ) ) if value not in allowed_discovery: utils.err( ( "invalid {0} value '{1}', allowed values are: {2}" ", use --force to override" ).format(name, value, format_list(allowed_discovery)) ) options.append([name, value]) elif modifiers.get("--force"): options.append([name, value]) else: utils.err("bad option '%s', use --force to override" % name) if score is None: score = "INFINITY" # Verify that specified node exists in the cluster and score is valid if not skip_score_and_node_check: if modifiers.is_specified("-f") or modifiers.get("--force"): warn(LOCATION_NODE_VALIDATION_SKIP_MSG) else: lib_env = utils.get_lib_env() existing_nodes, report_list = get_existing_nodes_names( corosync_conf=lib_env.get_corosync_conf(), cib=lib_env.get_cib(), ) report_list += _verify_node_name(node, existing_nodes) if report_list: process_library_reports(report_list) _verify_score(score) id_valid, id_error = utils.validate_xml_id(constraint_id, "constraint id") if not id_valid: utils.err(id_error) dom = utils.get_cib_dom() if rsc_type == RESOURCE_TYPE_RESOURCE: ( rsc_valid, rsc_error, dummy_correct_id, ) = utils.validate_constraint_resource(dom, rsc_value) if not rsc_valid: utils.err(rsc_error) # Verify current constraint doesn't already exist # If it does we replace it with the new constraint dummy_dom, constraintsElement = getCurrentConstraints(dom) elementsToRemove = [] # If the id matches, or the rsc & node match, then we replace/remove for rsc_loc in constraintsElement.getElementsByTagName("rsc_location"): # pylint: disable=too-many-boolean-expressions if rsc_loc.getAttribute("id") == constraint_id or ( rsc_loc.getAttribute("node") == node and ( ( RESOURCE_TYPE_RESOURCE == rsc_type and rsc_loc.getAttribute("rsc") == rsc_value ) or ( RESOURCE_TYPE_REGEXP == rsc_type and rsc_loc.getAttribute("rsc-pattern") == rsc_value ) ) ): elementsToRemove.append(rsc_loc) for etr in elementsToRemove: constraintsElement.removeChild(etr) element = dom.createElement("rsc_location") element.setAttribute("id", constraint_id) if rsc_type == RESOURCE_TYPE_RESOURCE: element.setAttribute("rsc", rsc_value) elif rsc_type == RESOURCE_TYPE_REGEXP: element.setAttribute("rsc-pattern", rsc_value) element.setAttribute("node", node) element.setAttribute("score", score) for option in options: element.setAttribute(option[0], option[1]) constraintsElement.appendChild(element) utils.replace_cib_configuration(dom) # Grabs the current constraints and returns the dom and constraint element def getCurrentConstraints(passed_dom=None): """ Commandline options: * -f - CIB file, only if passed_dom is None """ if passed_dom: dom = passed_dom else: current_constraints_xml = utils.get_cib_xpath("//constraints") if current_constraints_xml == "": utils.err("unable to process cib") # Verify current constraint doesn't already exist # If it does we replace it with the new constraint dom = parseString(current_constraints_xml) constraintsElement = dom.getElementsByTagName("constraints")[0] return (dom, constraintsElement) # If returnStatus is set, then we don't error out, we just print the error # and return false def constraint_rm( lib, argv, modifiers, returnStatus=False, constraintsElement=None, passed_dom=None, ): """ Options: * -f - CIB file, effective only if passed_dom is None """ if passed_dom is None: modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() bad_constraint = False if len(argv) != 1: for arg in argv: if not constraint_rm( lib, [arg], modifiers, returnStatus=True, passed_dom=passed_dom ): bad_constraint = True if bad_constraint: sys.exit(1) return None c_id = argv.pop(0) elementFound = False dom = None use_cibadmin = False if not constraintsElement: (dom, constraintsElement) = getCurrentConstraints(passed_dom) use_cibadmin = True for co in constraintsElement.childNodes[:]: if co.nodeType != xml.dom.Node.ELEMENT_NODE: continue if co.getAttribute("id") == c_id: constraintsElement.removeChild(co) elementFound = True if not elementFound: for rule in constraintsElement.getElementsByTagName("rule")[:]: if rule.getAttribute("id") == c_id: elementFound = True parent = rule.parentNode parent.removeChild(rule) if not parent.getElementsByTagName("rule"): parent.parentNode.removeChild(parent) if elementFound: if passed_dom: return dom if use_cibadmin: utils.replace_cib_configuration(dom) if returnStatus: return True else: utils.err("Unable to find constraint - '%s'" % c_id, False) if returnStatus: return False sys.exit(1) return None def _split_set_constraints( constraints_dto: CibConstraintsDto, ) -> tuple[CibConstraintsDto, CibConstraintsDto]: return ( CibConstraintsDto( location=constraints_dto.location, colocation=constraints_dto.colocation, order=constraints_dto.order, ticket=constraints_dto.ticket, ), CibConstraintsDto( location_set=constraints_dto.location_set, colocation_set=constraints_dto.colocation_set, order_set=constraints_dto.order_set, ticket_set=constraints_dto.ticket_set, ), ) def _find_constraints_containing_resource( resources_dto: CibResourcesDto, constraints_dto: CibConstraintsDto, resource_id: str, ) -> CibConstraintsDto: resources_filter = [resource_id] # Original implementation only included parent resource only if resource_id # was referring to a primitive resource, ignoring groups. This may change in # the future if necessary. if any( primitive_dto.id == resource_id for primitive_dto in resources_dto.primitives ): for clone_dto in resources_dto.clones: if clone_dto.member_id == resource_id: resources_filter.append(clone_dto.id) break return _filter_constraints_by_resources( constraints_dto, resources_filter, [] ) def ref( lib: Any, argv: list[str], modifiers: parse_args.InputModifiers ) -> None: modifiers.ensure_only_supported("-f") if not argv: raise CmdLineInputError() resources_dto = cast( CibResourcesDto, lib.resource.get_configured_resources() ) constraints_dto = cast( CibConstraintsDto, lib.constraint.get_config(evaluate_rules=False), ) for resource_id in sorted(set(argv)): constraint_ids = get_all_constraints_ids( _find_constraints_containing_resource( resources_dto, constraints_dto, resource_id ) ) print(f"Resource: {resource_id}") if constraint_ids: print( "\n".join( indent( sorted(constraint_ids), indent_step=INDENT_STEP, ) ) ) else: print(" No Matches") def remove_constraints_containing( resource_id: str, output=False, constraints_element=None, passed_dom=None ): """ Commandline options: * -f - CIB file, effective only if passed_dom is None """ lib = utils.get_library_wrapper() modifiers = utils.get_input_modifiers() resources_dto = cast( CibResourcesDto, lib.resource.get_configured_resources() ) constraints_dto, set_constraints_dto = _split_set_constraints( cast( CibConstraintsDto, lib.constraint.get_config(evaluate_rules=False), ) ) constraints = sorted( get_all_constraints_ids( _find_constraints_containing_resource( resources_dto, constraints_dto, resource_id ) ) ) set_constraints = sorted( get_all_constraints_ids( _find_constraints_containing_resource( resources_dto, set_constraints_dto, resource_id ) ) ) for c in constraints: if output: print_to_stderr(f"Removing Constraint - {c}") if constraints_element is not None: constraint_rm( lib, [c], modifiers, True, constraints_element, passed_dom=passed_dom, ) else: constraint_rm(lib, [c], modifiers, passed_dom=passed_dom) if set_constraints: (dom, constraintsElement) = getCurrentConstraints(passed_dom) for set_c in constraintsElement.getElementsByTagName("resource_ref")[:]: # If resource id is in a set, remove it from the set, if the set # is empty, then we remove the set, if the parent of the set # is empty then we remove it if set_c.getAttribute("id") == resource_id: pn = set_c.parentNode pn.removeChild(set_c) if output: print_to_stderr( "Removing {} from set {}".format( resource_id, pn.getAttribute("id") ) ) if pn.getElementsByTagName("resource_ref").length == 0: print_to_stderr( "Removing set {}".format(pn.getAttribute("id")) ) pn2 = pn.parentNode pn2.removeChild(pn) if pn2.getElementsByTagName("resource_set").length == 0: pn2.parentNode.removeChild(pn2) print_to_stderr( "Removing constraint {}".format( pn2.getAttribute("id") ) ) if passed_dom: return dom utils.replace_cib_configuration(dom) return None # Re-assign any constraints referencing a resource to its parent (a clone # or master) def constraint_resource_update(old_id, dom): """ Commandline options: no options """ new_id = None clone_ms_parent = utils.dom_get_resource_clone_ms_parent(dom, old_id) if clone_ms_parent: new_id = clone_ms_parent.getAttribute("id") if new_id: constraints = dom.getElementsByTagName("rsc_location") constraints += dom.getElementsByTagName("rsc_order") constraints += dom.getElementsByTagName("rsc_colocation") attrs_to_update = ["rsc", "first", "then", "with-rsc"] for constraint in constraints: for attr in attrs_to_update: if constraint.getAttribute(attr) == old_id: constraint.setAttribute(attr, new_id) return dom pcs-0.12.0.2/pcs/daemon/000077500000000000000000000000001500417470700145545ustar00rootroot00000000000000pcs-0.12.0.2/pcs/daemon/__init__.py000066400000000000000000000000001500417470700166530ustar00rootroot00000000000000pcs-0.12.0.2/pcs/daemon/app/000077500000000000000000000000001500417470700153345ustar00rootroot00000000000000pcs-0.12.0.2/pcs/daemon/app/__init__.py000066400000000000000000000000001500417470700174330ustar00rootroot00000000000000pcs-0.12.0.2/pcs/daemon/app/api_v0.py000066400000000000000000000211731500417470700170700ustar00rootroot00000000000000import json from dataclasses import dataclass from typing import ( Any, Iterable, Mapping, ) from tornado.web import Finish from pcs.common import reports from pcs.common.async_tasks import types from pcs.common.async_tasks.dto import ( CommandDto, CommandOptionsDto, ) from pcs.common.reports.dto import ReportItemDto from pcs.common.str_tools import format_list from pcs.daemon.app.auth import LegacyTokenAuthenticationHandler from pcs.daemon.async_tasks.scheduler import ( Scheduler, TaskNotFoundError, ) from pcs.daemon.async_tasks.types import Command from pcs.lib.auth.provider import AuthProvider from .common import RoutesType @dataclass(frozen=True) class SimplifiedResult: success: bool result: Any reports: list[ReportItemDto] _SEVERITY_LABEL = { reports.ReportItemSeverity.DEBUG: "Debug: ", reports.ReportItemSeverity.DEPRECATION: "Deprecation warning: ", reports.ReportItemSeverity.ERROR: "Error: ", reports.ReportItemSeverity.INFO: "", reports.ReportItemSeverity.WARNING: "Warning: ", } def _report_to_str(report_item: ReportItemDto) -> str: return ( _SEVERITY_LABEL.get(report_item.severity.level, "") + (f"{report_item.context.node}: " if report_item.context else "") + report_item.message.message ) def _reports_to_str(report_items: Iterable[ReportItemDto]) -> str: return "\n".join(_report_to_str(item) for item in report_items) class _BaseApiV0Handler(LegacyTokenAuthenticationHandler): """ Base class of handlers for the original API implemented in remote.rb """ _scheduler: Scheduler def initialize( self, scheduler: Scheduler, auth_provider: AuthProvider ) -> None: # pylint: disable=arguments-differ super().initialize(auth_provider) self._scheduler = scheduler async def _handle_request(self) -> None: """ Main method for handling requests """ raise NotImplementedError() def _error(self, message: str, http_code: int = 400) -> Finish: """ Helper method for exit request processing with an error """ self.set_status(http_code) self.write(message) return Finish() def _check_required_params(self, required_params: set[str]) -> None: missing_params = required_params - set(self.request.arguments.keys()) if missing_params: raise self._error( f"Required parameters missing: {format_list(missing_params)}" ) async def _process_request( self, cmd_name: str, cmd_params: Mapping[str, Any] ) -> SimplifiedResult: """ Helper method for calling pcs library commands """ command_dto = CommandDto( command_name=cmd_name, params=cmd_params, options=CommandOptionsDto( effective_username=self.effective_user.username, effective_groups=list(self.effective_user.groups), ), ) task_ident = self._scheduler.new_task( Command(command_dto, is_legacy_command=True), self.real_user, ) try: task_result_dto = await self._scheduler.wait_for_task( task_ident, self.real_user ) except TaskNotFoundError as e: raise self._error("Internal server error", 500) from e if ( task_result_dto.task_finish_type == types.TaskFinishType.FAIL and task_result_dto.reports and task_result_dto.reports[0].message.code == reports.codes.NOT_AUTHORIZED and not task_result_dto.reports[0].context ): raise self._error("Permission denied", 403) if task_result_dto.task_finish_type == types.TaskFinishType.KILL: if ( task_result_dto.kill_reason == types.TaskKillReason.COMPLETION_TIMEOUT ): raise self._error("Task processing timed out", 500) raise self._error("Task killed") if ( task_result_dto.task_finish_type == types.TaskFinishType.UNHANDLED_EXCEPTION ): raise self._error("Unhandled exception", 500) return SimplifiedResult( task_result_dto.task_finish_type == types.TaskFinishType.SUCCESS, task_result_dto.result, task_result_dto.reports, ) class ResourceManageUnmanageHandler(_BaseApiV0Handler): async def _handle_request(self) -> None: self._check_required_params({"resource_list_json"}) try: resource_list = json.loads(self.get_argument("resource_list_json")) except json.JSONDecodeError as e: raise self._error("Invalid input data format") from e result = await self._process_request( self._get_cmd(), dict(resource_or_tag_ids=resource_list) ) if not result.success: raise self._error(_reports_to_str(result.reports)) @staticmethod def _get_cmd() -> str: raise NotImplementedError() class ResourceManageHandler(ResourceManageUnmanageHandler): @staticmethod def _get_cmd() -> str: return "resource.manage" class ResourceUnmanageHandler(ResourceManageUnmanageHandler): @staticmethod def _get_cmd() -> str: return "resource.unmanage" class QdeviceNetGetCaCertificateHandler(_BaseApiV0Handler): async def _handle_request(self) -> None: result = await self._process_request( "qdevice.qdevice_net_get_ca_certificate", {} ) if not result.success: raise self._error(_reports_to_str(result.reports)) self.write(result.result) class QdeviceNetSignNodeCertificateHandler(_BaseApiV0Handler): async def _handle_request(self) -> None: self._check_required_params({"certificate_request", "cluster_name"}) result = await self._process_request( "qdevice.qdevice_net_sign_certificate_request", dict( certificate_request=self.get_argument("certificate_request"), cluster_name=self.get_body_argument("cluster_name"), ), ) if not result.success: raise self._error(_reports_to_str(result.reports)) self.write(result.result) class QdeviceNetClientInitCertificateStorageHandler(_BaseApiV0Handler): async def _handle_request(self) -> None: self._check_required_params({"ca_certificate"}) result = await self._process_request( "qdevice.client_net_setup", dict(ca_certificate=self.get_argument("ca_certificate")), ) if not result.success: raise self._error(_reports_to_str(result.reports)) class QdeviceNetClientImportCertificateHandler(_BaseApiV0Handler): async def _handle_request(self) -> None: self._check_required_params({"certificate"}) result = await self._process_request( "qdevice.client_net_import_certificate", dict(certificate=self.get_argument("certificate")), ) if not result.success: raise self._error(_reports_to_str(result.reports)) class QdeviceNetClientDestroyHandler(_BaseApiV0Handler): async def _handle_request(self) -> None: result = await self._process_request("qdevice.client_net_destroy", {}) if not result.success: raise self._error(_reports_to_str(result.reports)) def get_routes(scheduler: Scheduler, auth_provider: AuthProvider) -> RoutesType: def r(url: str) -> str: # pylint: disable=invalid-name return f"/remote/{url}" params = dict(scheduler=scheduler, auth_provider=auth_provider) return [ # resources (r("manage_resource"), ResourceManageHandler, params), (r("unmanage_resource"), ResourceUnmanageHandler, params), # qdevice ( r("qdevice_net_client_destroy"), QdeviceNetClientDestroyHandler, params, ), ( # /api/v1/qdevice-client-net-import-certificate/v1 r("qdevice_net_client_import_certificate"), QdeviceNetClientImportCertificateHandler, params, ), ( r("qdevice_net_client_init_certificate_storage"), QdeviceNetClientInitCertificateStorageHandler, params, ), ( r("qdevice_net_get_ca_certificate"), QdeviceNetGetCaCertificateHandler, params, ), ( # /api/v1/qdevice-qdevice-net-sign-certificate-request/v1 r("qdevice_net_sign_node_certificate"), QdeviceNetSignNodeCertificateHandler, params, ), ] pcs-0.12.0.2/pcs/daemon/app/api_v1.py000066400000000000000000000301431500417470700170660ustar00rootroot00000000000000import json from typing import ( Any, Dict, Mapping, Optional, ) from tornado.web import HTTPError from pcs.common import ( communication, reports, ) from pcs.common.async_tasks import types from pcs.common.async_tasks.dto import ( CommandDto, CommandOptionsDto, ) from pcs.common.interface.dto import to_dict from pcs.daemon.app.auth import ( LegacyTokenAuthProvider, NotAuthorizedException, ) from pcs.daemon.async_tasks.scheduler import ( Scheduler, TaskNotFoundError, ) from pcs.daemon.async_tasks.types import Command from pcs.lib.auth.provider import AuthProvider from pcs.lib.auth.types import AuthUser from .common import ( BaseHandler, RoutesType, ) API_V1_MAP: Mapping[str, str] = { "acl-create-role/v1": "acl.create_role", "acl-remove-role/v1": "acl.remove_role", "acl-assign-role-to-target/v1": "acl.assign_role_to_target", "acl-assign-role-to-group/v1": "acl.assign_role_to_group", "acl-unassign-role-from-target/v1": "acl.unassign_role_from_target", "acl-unassign-role-from-group/v1": "acl.unassign_role_from_group", "acl-create-target/v1": "acl.create_target", "acl-create-group/v1": "acl.create_group", "acl-remove-target/v1": "acl.remove_target", "acl-remove-group/v1": "acl.remove_group", "acl-add-permission/v1": "acl.add_permission", "acl-remove-permission/v1": "acl.remove_permission", "alert-create-alert/v1": "alert.create_alert", "alert-update-alert/v1": "alert.update_alert", "alert-remove-alert/v1": "alert.remove_alert", "alert-add-recipient/v1": "alert.add_recipient", "alert-update-recipient/v1": "alert.update_recipient", "alert-remove-recipient/v1": "alert.remove_recipient", "cluster-add-nodes/v1": "cluster.add_nodes", "cluster-node-clear/v1": "cluster.node_clear", "cluster-remove-nodes/v1": "cluster.remove_nodes", "cluster-setup/v1": "cluster.setup", "cluster-generate-cluster-uuid/v1": "cluster.generate_cluster_uuid", "constraint-colocation-create-with-set/v1": "constraint.colocation.create_with_set", "constraint-order-create-with-set/v1": "constraint.order.create_with_set", "constraint-ticket-create-with-set/v1": "constraint.ticket.create_with_set", "constraint-ticket-create/v1": "constraint.ticket.create", "constraint-ticket-remove/v1": "constraint.ticket.remove", "fencing-topology-add-level/v1": "fencing_topology.add_level", "fencing-topology-remove-all-levels/v1": "fencing_topology.remove_all_levels", "fencing-topology-remove-levels-by-params/v1": "fencing_topology.remove_levels_by_params", "fencing-topology-verify/v1": "fencing_topology.verify", "node-maintenance-unmaintenance/v1": "node.maintenance_unmaintenance_list", "node-maintenance-unmaintenance-all/v1": "node.maintenance_unmaintenance_all", "node-standby-unstandby/v1": "node.standby_unstandby_list", "node-standby-unstandby-all/v1": "node.standby_unstandby_all", "qdevice-client-net-import-certificate/v1": "qdevice.client_net_import_certificate", "qdevice-qdevice-net-sign-certificate-request/v1": "qdevice.qdevice_net_sign_certificate_request", # deprecated, use resource-agent-get-agent-metadata/v1 instead "resource-agent-describe-agent/v1": "resource_agent.describe_agent", "resource-agent-get-agents-list/v1": "resource_agent.get_agents_list", "resource-agent-get-agent-metadata/v1": "resource_agent.get_agent_metadata", # deprecated, use resource-agent-get-agents-list/v1 instead "resource-agent-list-agents/v1": "resource_agent.list_agents", "resource-agent-list-agents-for-standard-and-provider/v1": "resource_agent.list_agents_for_standard_and_provider", "resource-agent-list-ocf-providers/v1": "resource_agent.list_ocf_providers", "resource-agent-list-standards/v1": "resource_agent.list_standards", "resource-ban/v1": "resource.ban", "resource-create/v1": "resource.create", "resource-create-as-clone/v1": "resource.create_as_clone", "resource-create-in-group/v1": "resource.create_in_group", "resource-disable/v1": "resource.disable", "resource-disable-safe/v1": "resource.disable_safe", "resource-disable-simulate/v1": "resource.disable_simulate", "resource-enable/v1": "resource.enable", "resource-group-add/v1": "resource.group_add", "resource-manage/v1": "resource.manage", "resource-move/v1": "resource.move", "resource-move-autoclean/v1": "resource.move_autoclean", "resource-unmanage/v1": "resource.unmanage", "resource-unmove-unban/v1": "resource.unmove_unban", "sbd-disable-sbd/v1": "sbd.disable_sbd", "sbd-enable-sbd/v1": "sbd.enable_sbd", "scsi-unfence-node/v2": "scsi.unfence_node", "scsi-unfence-node-mpath/v1": "scsi.unfence_node_mpath", "status-full-cluster-status-plaintext/v1": "status.full_cluster_status_plaintext", # deprecated, use resource-agent-get-agent-metadata/v1 instead "stonith-agent-describe-agent/v1": "stonith_agent.describe_agent", # deprecated, use resource-agent-get-agents-list/v1 instead "stonith-agent-list-agents/v1": "stonith_agent.list_agents", "stonith-create/v1": "stonith.create", } class ApiError(HTTPError): def __init__( self, response_code: communication.types.CommunicationResultStatus, response_msg: str, http_code: int = 200, ) -> None: super().__init__(http_code) self.response_code = response_code self.response_msg = response_msg class InvalidInputError(ApiError): def __init__(self, msg: str = "Input is not valid JSON object"): super().__init__(communication.const.COM_STATUS_INPUT_ERROR, msg) class _BaseApiV1Handler(BaseHandler): """ Base handler for the REST API Defines all common functions used by handlers, message body preprocessing, and HTTP(S) settings. """ scheduler: Scheduler json: Optional[Dict[str, Any]] = None _auth_provider: LegacyTokenAuthProvider def initialize( self, scheduler: Scheduler, auth_provider: AuthProvider ) -> None: super().initialize() self._auth_provider = LegacyTokenAuthProvider(self, auth_provider) self.scheduler = scheduler def prepare(self) -> None: """JSON preprocessing""" self.add_header("Content-Type", "application/json") try: self.json = json.loads(self.request.body) except json.JSONDecodeError as e: raise InvalidInputError() from e async def get_auth_user(self) -> tuple[AuthUser, AuthUser]: try: return await self._auth_provider.auth_by_token_effective_user() except NotAuthorizedException as e: raise ApiError( response_code=communication.const.COM_STATUS_NOT_AUTHORIZED, response_msg="", http_code=401, ) from e def send_response( self, response: communication.dto.InternalCommunicationResultDto ) -> None: self.finish(json.dumps(to_dict(response))) def write_error(self, status_code: int, **kwargs: Any) -> None: response = communication.dto.InternalCommunicationResultDto( status=communication.const.COM_STATUS_EXCEPTION, status_msg=None, report_list=[], data=None, ) if "exc_info" in kwargs: _, exc, _ = kwargs["exc_info"] if isinstance(exc, ApiError): if ( exc.response_code == communication.const.COM_STATUS_NOT_AUTHORIZED ): self.finish(json.dumps({"notauthorized": "true"})) return response = communication.dto.InternalCommunicationResultDto( status=exc.response_code, status_msg=exc.response_msg, report_list=[], data=None, ) self.send_response(response) async def process_request( self, cmd: str ) -> communication.dto.InternalCommunicationResultDto: real_user, effective_user = await self.get_auth_user() if cmd not in API_V1_MAP: raise ApiError( communication.const.COM_STATUS_UNKNOWN_CMD, f"Unknown command '{cmd}'", ) if self.json is None: raise InvalidInputError() command_dto = CommandDto( command_name=API_V1_MAP[cmd], params=self.json, options=CommandOptionsDto( effective_username=effective_user.username, effective_groups=list(effective_user.groups), ), ) task_ident = self.scheduler.new_task( Command(command_dto, is_legacy_command=True), real_user ) try: task_result_dto = await self.scheduler.wait_for_task( task_ident, real_user ) except TaskNotFoundError as e: raise ApiError( communication.const.COM_STATUS_EXCEPTION, "Internal server error", ) from e if ( task_result_dto.task_finish_type == types.TaskFinishType.FAIL and task_result_dto.reports and task_result_dto.reports[0].message.code == reports.codes.NOT_AUTHORIZED and not task_result_dto.reports[0].context ): raise ApiError( communication.const.COM_STATUS_NOT_AUTHORIZED, "Not authorized" ) status_map = { types.TaskFinishType.SUCCESS: communication.const.COM_STATUS_SUCCESS, types.TaskFinishType.FAIL: communication.const.COM_STATUS_ERROR, } return communication.dto.InternalCommunicationResultDto( status=status_map.get( task_result_dto.task_finish_type, communication.const.COM_STATUS_EXCEPTION, ), status_msg=None, report_list=task_result_dto.reports, data=task_result_dto.result, ) class ApiV1Handler(_BaseApiV1Handler): async def post(self, cmd: str) -> None: self.send_response(await self.process_request(cmd)) # TODO: test get method async def get(self, cmd: str) -> None: self.send_response(await self.process_request(cmd)) class LegacyApiV1Handler(_BaseApiV1Handler): @staticmethod def _get_cmd() -> str: raise NotImplementedError() def prepare(self) -> None: self.add_header("Content-Type", "application/json") try: self.json = json.loads(self.get_argument("data_json", default="")) except json.JSONDecodeError as e: raise InvalidInputError() from e def send_response( self, response: communication.dto.InternalCommunicationResultDto ) -> None: result = to_dict(response) result["report_list"] = [ dict( severity=report.severity.level, code=report.message.code, info=report.message.payload, forceable=report.severity.force_code, report_text=report.message.message, ) for report in response.report_list ] self.finish(json.dumps(result)) async def post(self) -> None: self.send_response(await self.process_request(self._get_cmd())) # TODO: test get method async def get(self) -> None: self.send_response(await self.process_request(self._get_cmd())) class ClusterStatusLegacyHandler(LegacyApiV1Handler): @staticmethod def _get_cmd() -> str: return "status-full-cluster-status-plaintext/v1" class ClusterAddNodesLegacyHandler(LegacyApiV1Handler): @staticmethod def _get_cmd() -> str: return "cluster-add-nodes/v1" def get_routes(scheduler: Scheduler, auth_provider: AuthProvider) -> RoutesType: params = dict(scheduler=scheduler, auth_provider=auth_provider) return [ ( "/remote/cluster_status_plaintext", ClusterStatusLegacyHandler, params, ), ( "/remote/cluster_add_nodes", ClusterAddNodesLegacyHandler, params, ), (r"/api/v1/(.*)", ApiV1Handler, params), ] pcs-0.12.0.2/pcs/daemon/app/api_v2.py000066400000000000000000000201111500417470700170610ustar00rootroot00000000000000import json import logging from http.client import responses from typing import ( Any, Dict, Optional, Type, cast, ) from dacite import ( DaciteError, MissingValueError, UnexpectedDataError, ) from tornado.web import ( HTTPError, MissingArgumentError, ) from pcs.common.async_tasks.dto import ( CommandDto, TaskIdentDto, ) from pcs.common.interface.dto import ( DTOTYPE, PayloadConversionError, from_dict, to_dict, ) from pcs.daemon.app.auth import ( NotAuthorizedException, TokenAuthProvider, ) from pcs.daemon.async_tasks.scheduler import ( Scheduler, TaskNotFoundError, ) from pcs.daemon.async_tasks.types import Command from pcs.lib.auth.provider import AuthProvider from pcs.lib.auth.types import AuthUser from .common import ( BaseHandler, RoutesType, ) class APIError(HTTPError): def __init__( self, http_code: int = 500, http_error: Optional[str] = None, error_msg: Optional[str] = None, ) -> None: super().__init__(http_code, reason=http_error) self.error_msg = error_msg class RequestBodyMissingError(APIError): def __init__(self) -> None: super().__init__( 400, error_msg="Request body is missing, has wrong format or " "wrong/missing headers.", ) class _BaseApiV2Handler(BaseHandler): """ Base handler for the REST API Defines all common functions used by handlers, message body preprocessing, and HTTP(S) settings. """ scheduler: Scheduler json: Optional[Dict[str, Any]] = None logger: logging.Logger _auth_provider: TokenAuthProvider def initialize( self, scheduler: Scheduler, auth_provider: AuthProvider ) -> None: super().initialize() self._auth_provider = TokenAuthProvider(self, auth_provider) self.scheduler = scheduler # TODO: Turn into a constant self.logger = logging.getLogger("pcs.daemon.scheduler") def prepare(self) -> None: """JSON preprocessing""" self.add_header("Content-Type", "application/json") if ( "Content-Type" in self.request.headers and self.request.headers["Content-Type"] == "application/json" ): try: self.json = json.loads(self.request.body) except json.JSONDecodeError as exc: raise APIError( http_code=400, error_msg="Malformed JSON data." ) from exc async def get_auth_user(self) -> AuthUser: try: return await self._auth_provider.auth_by_token() except NotAuthorizedException as e: raise APIError(http_code=401) from e @staticmethod def _from_dict_exc_handled( convert_to: Type[DTOTYPE], dictionary: Dict[str, Any] ) -> DTOTYPE: """ Dacite conversion to DTO from JSON with handled exceptions :param convert_to: DTO type to return and validate against :return: DTO if JSON follows its structure, sends error response and connection ends otherwise """ try: return from_dict(convert_to, dictionary, strict=True) except MissingValueError as exc: raise APIError( http_code=400, error_msg=f'Required key "{exc.field_path}" is missing in ' f"request body.", ) from exc except UnexpectedDataError as exc: raise APIError( http_code=400, error_msg=f"Request body contains unexpected keys: " f"{', '.join(exc.keys)}.", ) from exc except (DaciteError, PayloadConversionError) as exc: raise APIError( http_code=400, error_msg="Malformed request body." ) from exc def write_error(self, status_code: int, **kwargs: Any) -> None: """ JSON error responder for all API handlers This function provides unified error response for the whole API. This function is called when tornado encounters any Exception while this handler is being used. No need to call set_status in this method, it is already set by tornado. :param status_code: HTTP status code """ response = { "http_code": status_code, "http_error": responses.get(status_code, "Unknown"), "error_message": None, } if "exc_info" in kwargs: _, exc, _ = kwargs["exc_info"] if isinstance(exc, HTTPError) and exc.reason: # Rewrite http reason autoconverted from http status code response["http_error"] = exc.reason if isinstance(exc, APIError): response["error_message"] = exc.error_msg self.finish(json.dumps(response)) class NewTaskHandler(_BaseApiV2Handler): """Create a new task from command""" async def post(self) -> None: auth_user = await self.get_auth_user() if self.json is None: raise RequestBodyMissingError() command_dto = self._from_dict_exc_handled(CommandDto, self.json) task_ident = self.scheduler.new_task(Command(command_dto), auth_user) self.write(json.dumps(to_dict(TaskIdentDto(task_ident)))) class RunTaskHandler(_BaseApiV2Handler): """Run command synchronously""" async def post(self) -> None: auth_user = await self.get_auth_user() if self.json is None: raise RequestBodyMissingError() command_dto = self._from_dict_exc_handled(CommandDto, self.json) task_ident = self.scheduler.new_task(Command(command_dto), auth_user) try: self.write( json.dumps( to_dict( await self.scheduler.wait_for_task( task_ident, auth_user ) ) ) ) except TaskNotFoundError as exc: raise APIError(http_code=500) from exc class TaskInfoHandler(_BaseApiV2Handler): """Get task status""" async def get(self) -> None: auth_user = await self.get_auth_user() try: task_ident = self.get_query_argument("task_ident") self.write( json.dumps( to_dict( self.scheduler.get_task( cast(str, task_ident), auth_user ) ) ) ) except MissingArgumentError as exc: raise APIError( http_code=400, error_msg=f'URL argument "{exc.arg_name}" is missing.', ) from exc except TaskNotFoundError as exc: raise APIError( http_code=404, error_msg="Task with this identifier does not exist.", ) from exc class KillTaskHandler(_BaseApiV2Handler): """Stop execution of a task""" async def post(self) -> None: auth_user = await self.get_auth_user() if self.json is None: raise RequestBodyMissingError() task_ident_dto = self._from_dict_exc_handled(TaskIdentDto, self.json) try: self.scheduler.kill_task(task_ident_dto.task_ident, auth_user) except TaskNotFoundError as exc: raise APIError( http_code=404, error_msg="Task with this identifier does not exist.", ) from exc self.set_status(200) self.finish() def get_routes( scheduler: Scheduler, auth_provider: AuthProvider, ) -> RoutesType: """ Returns mapping of URL routes to functions and links API to the scheduler :param scheduler: Scheduler's instance :return: URL to handler mapping """ params = dict(scheduler=scheduler, auth_provider=auth_provider) return [ ("/api/v2/task/result", TaskInfoHandler, params), ("/api/v2/task/create", NewTaskHandler, params), ("/api/v2/task/kill", KillTaskHandler, params), ("/api/v2/task/run", RunTaskHandler, params), ] pcs-0.12.0.2/pcs/daemon/app/auth.py000066400000000000000000000205711500417470700166540ustar00rootroot00000000000000import base64 import binascii import logging import pwd import socket import struct from typing import ( Optional, cast, ) from tornado.http1connection import HTTP1Connection from tornado.ioloop import IOLoop from tornado.web import ( HTTPError, RequestHandler, ) from pcs.lib.auth.const import SUPERUSER from pcs.lib.auth.provider import AuthProvider from pcs.lib.auth.tools import ( DesiredUser, get_effective_user, ) from pcs.lib.auth.types import AuthUser from .common import ( LegacyApiBaseHandler, LegacyApiHandler, RoutesType, ) class NotAuthorizedException(Exception): pass class _BaseLibAuthProvider: def __init__( self, handler: RequestHandler, auth_provider: AuthProvider ) -> None: self._auth_provider = auth_provider self._handler = handler self._auth_logger = logging.getLogger("pcs.daemon.auth") async def login_user(self, username: str) -> AuthUser: auth_user = await IOLoop.current().run_in_executor( executor=None, func=lambda: self._auth_provider.login_user(username), ) if auth_user is None: raise NotAuthorizedException() return auth_user def is_unix_socket_used(self) -> bool: # For whatever reason, handler.request.connection is typed as # HTTPConnection in tornado. That class, however, doesn't have stream # attribute. In reality, HTTP1Connection is probably used. return ( cast( HTTP1Connection, self._handler.request.connection ).stream.socket.family == socket.AF_UNIX ) def get_unix_socket_user(self) -> Optional[str]: if not self.is_unix_socket_used(): return None # It is not cached to prevent inapropriate cache when handler is (in # hypotetical future) somehow reused. The responsibility for cache is # left to the place, that uses it. # For whatever reason, handler.request.connection is typed as # HTTPConnection in tornado. That class, however, doesn't have stream # attribute. In reality, HTTP1Connection is probably used. credentials = cast( HTTP1Connection, self._handler.request.connection ).stream.socket.getsockopt( socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i"), ) dummy_pid, uid, dummy_gid = struct.unpack("3i", credentials) if uid == 0: # treat root as cluster superuser return SUPERUSER return pwd.getpwuid(uid).pw_name async def auth_by_socket_user(self) -> AuthUser: username = self.get_unix_socket_user() if username: auth_user = await self.login_user(username) if auth_user: return auth_user raise NotAuthorizedException() class PasswordAuthProvider(_BaseLibAuthProvider): async def auth_by_username_password( self, username: str, password: str ) -> AuthUser: auth_user = await IOLoop.current().run_in_executor( executor=None, func=lambda: self._auth_provider.auth_by_username_password( username, password ), ) if auth_user is None: raise NotAuthorizedException() return auth_user class TokenAuthProvider(_BaseLibAuthProvider): async def auth_by_token(self) -> AuthUser: token = self.auth_token if token: return await self._auth_by_token(token) return await self.auth_by_socket_user() async def _auth_by_token(self, token: str) -> AuthUser: auth_user = await IOLoop.current().run_in_executor( executor=None, func=lambda: self._auth_provider.auth_by_token(token), ) if auth_user is None: raise NotAuthorizedException() return auth_user async def create_token(self, user: AuthUser) -> Optional[str]: return await IOLoop.current().run_in_executor( executor=None, func=lambda: self._auth_provider.create_token(user.username), ) @property def auth_token(self) -> Optional[str]: return self._handler.get_cookie("token", default=None) class LegacyTokenAuthProvider(TokenAuthProvider): async def auth_by_token_effective_user(self) -> tuple[AuthUser, AuthUser]: real_user = await self.auth_by_token() self._auth_logger.debug( "Real user=%s groups=%s", real_user.username, ",".join(real_user.groups), ) if not real_user.is_superuser: return real_user, real_user effective_user = get_effective_user( real_user, self._get_effective_user() ) self._auth_logger.debug( "Effective user=%s groups=%s", effective_user.username, ",".join(effective_user.groups), ) return real_user, effective_user def _get_effective_user(self) -> DesiredUser: username = self._handler.get_cookie("CIB_user") groups = [] if username: # use groups only if user is specified as well groups_raw = self._handler.get_cookie("CIB_user_groups") if groups_raw: try: groups = ( base64.b64decode(groups_raw).decode("utf-8").split(" ") ) except (UnicodeError, binascii.Error): self._auth_logger.warning("Unable to decode users groups") return DesiredUser(username, groups) class UnixSocketAuthProvider(_BaseLibAuthProvider): # Every required functionality is already in _BaseLibAuthProvider pass class LegacyAuth(LegacyApiBaseHandler): _password_auth_provider: PasswordAuthProvider _token_auth_provider: TokenAuthProvider def initialize(self, auth_provider: AuthProvider) -> None: super().initialize() self._password_auth_provider = PasswordAuthProvider(self, auth_provider) self._token_auth_provider = TokenAuthProvider(self, auth_provider) async def auth(self) -> None: try: auth_user = ( await self._password_auth_provider.auth_by_username_password( self.get_body_argument("username") or "", self.get_body_argument("password") or "", ) ) token = await self._token_auth_provider.create_token(auth_user) if token: self.write(token) else: raise HTTPError(400, reason="Unable to store token") except NotAuthorizedException: # To stay backward compatible with original ruby implementation, # an empty response needs to be returned if authentication fails pass async def post(self) -> None: await self.auth() async def get(self) -> None: await self.auth() class LegacyTokenAuthenticationHandler(LegacyApiHandler): _token_auth_provider: LegacyTokenAuthProvider _real_user: Optional[AuthUser] _effective_user: Optional[AuthUser] def initialize(self, auth_provider: AuthProvider) -> None: super().initialize() self._token_auth_provider = LegacyTokenAuthProvider(self, auth_provider) async def prepare(self) -> None: # pylint: disable=invalid-overridden-method super().prepare() try: ( self._real_user, self._effective_user, ) = await self._token_auth_provider.auth_by_token_effective_user() except NotAuthorizedException as e: raise self.unauthorized() from e @property def real_user(self) -> AuthUser: if not self._real_user: raise self.unauthorized() return self._real_user @property def effective_user(self) -> AuthUser: if not self._effective_user: raise self.unauthorized() return self._effective_user async def _handle_request(self) -> None: raise NotImplementedError() class CheckAuth(LegacyTokenAuthenticationHandler): async def _handle_request(self) -> None: self.write('{"success":true}') def get_routes( auth_provider: AuthProvider, ) -> RoutesType: auth_payload = dict(auth_provider=auth_provider) return [ ("/remote/auth", LegacyAuth, auth_payload), ("/remote/check_auth", CheckAuth, auth_payload), ] pcs-0.12.0.2/pcs/daemon/app/capabilities.py000066400000000000000000000042231500417470700203400ustar00rootroot00000000000000import json from typing import Iterable from tornado.web import HTTPError from pcs.common.capabilities import Capability from pcs.daemon.app.auth import ( LegacyTokenAuthenticationHandler, NotAuthorizedException, TokenAuthProvider, ) from pcs.lib.auth.provider import AuthProvider from pcs.lib.auth.types import AuthUser from .common import ( BaseHandler, RoutesType, ) def _capabilities_to_str(capabilities: Iterable[Capability]) -> str: return json.dumps( { "pcsd_capabilities": [feat.code for feat in capabilities], } ) class LegacyCapabilitiesHandler(LegacyTokenAuthenticationHandler): _capabilities: Iterable[Capability] def initialize( self, auth_provider: AuthProvider, capabilities: Iterable[Capability] ) -> None: # pylint: disable=arguments-differ super().initialize(auth_provider) self._capabilities = capabilities async def _handle_request(self) -> None: self.write(_capabilities_to_str(self._capabilities)) class CapabilitiesHandler(BaseHandler): _auth_provider: TokenAuthProvider _capabilities: Iterable[Capability] def initialize( self, auth_provider: AuthProvider, capabilities: Iterable[Capability] ) -> None: super().initialize() self._auth_provider = TokenAuthProvider(self, auth_provider) self._capabilities = capabilities async def get_auth_user(self) -> AuthUser: try: return await self._auth_provider.auth_by_token() except NotAuthorizedException as e: raise HTTPError(401) from e async def get(self) -> None: await self.get_auth_user() self.add_header("Content-Type", "application/json") self.write(_capabilities_to_str(self._capabilities)) def get_routes( auth_provider: AuthProvider, capabilities: Iterable[Capability] ) -> RoutesType: """ Returns mapping of URL routes to functions """ params = dict(auth_provider=auth_provider, capabilities=capabilities) return [ ("/remote/capabilities", LegacyCapabilitiesHandler, params), ("/capabilities", CapabilitiesHandler, params), ] pcs-0.12.0.2/pcs/daemon/app/common.py000066400000000000000000000162221500417470700172010ustar00rootroot00000000000000from typing import ( Any, Iterable, Optional, Type, ) from tornado.web import ( Finish, HTTPError, ) from tornado.web import RedirectHandler as TornadoRedirectHandler from tornado.web import RequestHandler RoutesType = Iterable[ tuple[str, Type[RequestHandler], Optional[dict[str, Any]]] ] class EnhanceHeadersMixin: """ EnhanceHeadersMixin allows to add security headers to GUI urls. """ def set_header_strict_transport_security(self) -> None: # rhbz#1558063 rhbz#2097392 # The HTTP Strict-Transport-Security response header (often abbreviated # as HSTS) lets a web site tell browsers that it should only be # accessed using HTTPS, instead of using HTTP. # Do not set "includeSubDomains" as that would affect all web sites and # applications running on any subdomain of the domain where pcs web UI # is running. The fact that pcs web UI runs on a specific port doesn't # matter, subdomains would still be affected. self.set_header("Strict-Transport-Security", "max-age=63072000") def set_header_nosniff_content_type(self) -> None: # The X-Content-Type-Options response HTTP header is a marker used by # the server to indicate that the MIME types advertised in the # Content-Type headers should not be changed and be followed. This # allows to opt-out of MIME type sniffing, or, in other words, it is a # way to say that the webmasters knew what they were doing. self.set_header("X-Content-Type-Options", "nosniff") def clear_header_server(self) -> None: # The Server header describes the software used by the origin server # that handled the request — that is, the server that generated the # response. # # rhbz 2058278 # When a HTTP request is made against a cluster node running pcsd, the # HTTP response contains HTTP Server name in its headers. # This is perceived as a security threat. self.clear_header("Server") def set_header_frame_options(self) -> None: # The X-Frame-Options HTTP response header can be used to indicate # whether or not a browser should be allowed to render a page in a # ,