From e5a812082ae033afb1eed82c0f2df3d0f6bdc93f Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 17 Apr 2024 08:53:20 +0200 Subject: Adding upstream version 2.1.6. Signed-off-by: Daniel Baumann --- lib/Makefile.am | 21 + lib/cib/Makefile.am | 28 + lib/cib/cib_attrs.c | 732 +++ lib/cib/cib_client.c | 750 +++ lib/cib/cib_file.c | 919 ++++ lib/cib/cib_native.c | 502 ++ lib/cib/cib_ops.c | 869 ++++ lib/cib/cib_remote.c | 638 +++ lib/cib/cib_utils.c | 837 ++++ lib/cluster/Makefile.am | 29 + lib/cluster/cluster.c | 405 ++ lib/cluster/corosync.c | 814 ++++ lib/cluster/cpg.c | 1092 +++++ lib/cluster/crmcluster_private.h | 47 + lib/cluster/election.c | 727 +++ lib/cluster/membership.c | 1301 ++++++ lib/common/Makefile.am | 124 + lib/common/acl.c | 860 ++++ lib/common/agents.c | 213 + lib/common/alerts.c | 253 + lib/common/attrs.c | 89 + lib/common/cib.c | 156 + lib/common/cib_secrets.c | 192 + lib/common/cmdline.c | 379 ++ lib/common/crmcommon_private.h | 325 ++ lib/common/digest.c | 278 ++ lib/common/health.c | 70 + lib/common/io.c | 663 +++ lib/common/ipc_attrd.c | 590 +++ lib/common/ipc_client.c | 1576 +++++++ lib/common/ipc_common.c | 110 + lib/common/ipc_controld.c | 671 +++ lib/common/ipc_pacemakerd.c | 316 ++ lib/common/ipc_schedulerd.c | 180 + lib/common/ipc_server.c | 1008 ++++ lib/common/iso8601.c | 1970 ++++++++ lib/common/lists.c | 27 + lib/common/logging.c | 1192 +++++ lib/common/mainloop.c | 1480 ++++++ lib/common/messages.c | 291 ++ lib/common/mock.c | 427 ++ lib/common/mock_private.h | 77 + lib/common/nodes.c | 24 + lib/common/nvpair.c | 992 ++++ lib/common/operations.c | 530 +++ lib/common/options.c | 497 ++ lib/common/output.c | 318 ++ lib/common/output_html.c | 477 ++ lib/common/output_log.c | 353 ++ lib/common/output_none.c | 152 + lib/common/output_text.c | 446 ++ lib/common/output_xml.c | 541 +++ lib/common/patchset.c | 1516 ++++++ lib/common/patchset_display.c | 519 +++ lib/common/pid.c | 247 + lib/common/procfs.c | 227 + lib/common/remote.c | 1270 +++++ lib/common/results.c | 1049 +++++ lib/common/schemas.c | 1303 ++++++ lib/common/scores.c | 166 + lib/common/strings.c | 1363 ++++++ lib/common/tests/Makefile.am | 32 + lib/common/tests/acl/Makefile.am | 21 + lib/common/tests/acl/pcmk__is_user_in_group_test.c | 38 + lib/common/tests/acl/pcmk_acl_required_test.c | 26 + lib/common/tests/acl/xml_acl_denied_test.c | 61 + lib/common/tests/acl/xml_acl_enabled_test.c | 61 + lib/common/tests/agents/Makefile.am | 20 + lib/common/tests/agents/crm_generate_ra_key_test.c | 48 + .../tests/agents/crm_parse_agent_spec_test.c | 87 + lib/common/tests/agents/pcmk__effective_rc_test.c | 36 + lib/common/tests/agents/pcmk_get_ra_caps_test.c | 63 + lib/common/tests/agents/pcmk_stonith_param_test.c | 50 + lib/common/tests/cmdline/Makefile.am | 17 + .../tests/cmdline/pcmk__cmdline_preproc_test.c | 156 + .../tests/cmdline/pcmk__quote_cmdline_test.c | 56 + lib/common/tests/flags/Makefile.am | 20 + lib/common/tests/flags/pcmk__clear_flags_as_test.c | 41 + lib/common/tests/flags/pcmk__set_flags_as_test.c | 25 + lib/common/tests/flags/pcmk_all_flags_set_test.c | 33 + lib/common/tests/flags/pcmk_any_flags_set_test.c | 26 + lib/common/tests/health/Makefile.am | 17 + .../health/pcmk__parse_health_strategy_test.c | 56 + .../health/pcmk__validate_health_strategy_test.c | 38 + lib/common/tests/io/Makefile.am | 18 + lib/common/tests/io/pcmk__full_path_test.c | 52 + lib/common/tests/io/pcmk__get_tmpdir_test.c | 68 + lib/common/tests/iso8601/Makefile.am | 16 + .../tests/iso8601/pcmk__readable_interval_test.c | 27 + lib/common/tests/lists/Makefile.am | 20 + lib/common/tests/lists/pcmk__list_of_1_test.c | 45 + .../tests/lists/pcmk__list_of_multiple_test.c | 45 + lib/common/tests/lists/pcmk__subtract_lists_test.c | 144 + lib/common/tests/nvpair/Makefile.am | 18 + .../tests/nvpair/pcmk__xe_attr_is_true_test.c | 50 + .../tests/nvpair/pcmk__xe_get_bool_attr_test.c | 59 + .../tests/nvpair/pcmk__xe_set_bool_attr_test.c | 31 + lib/common/tests/operations/Makefile.am | 22 + .../tests/operations/copy_in_properties_test.c | 62 + .../tests/operations/expand_plus_plus_test.c | 256 ++ .../operations/fix_plus_plus_recursive_test.c | 47 + lib/common/tests/operations/parse_op_key_test.c | 275 ++ lib/common/tests/operations/pcmk_is_probe_test.c | 25 + .../tests/operations/pcmk_xe_is_probe_test.c | 43 + .../operations/pcmk_xe_mask_probe_failure_test.c | 150 + lib/common/tests/options/Makefile.am | 19 + .../tests/options/pcmk__env_option_enabled_test.c | 101 + lib/common/tests/options/pcmk__env_option_test.c | 134 + .../tests/options/pcmk__set_env_option_test.c | 154 + lib/common/tests/output/Makefile.am | 24 + lib/common/tests/output/pcmk__call_message_test.c | 156 + .../output/pcmk__output_and_clear_error_test.c | 82 + lib/common/tests/output/pcmk__output_free_test.c | 84 + lib/common/tests/output/pcmk__output_new_test.c | 148 + .../tests/output/pcmk__register_format_test.c | 63 + .../tests/output/pcmk__register_formats_test.c | 108 + .../tests/output/pcmk__register_message_test.c | 107 + .../tests/output/pcmk__register_messages_test.c | 191 + .../tests/output/pcmk__unregister_formats_test.c | 39 + lib/common/tests/procfs/Makefile.am | 18 + .../procfs/pcmk__procfs_has_pids_false_test.c | 42 + .../tests/procfs/pcmk__procfs_has_pids_true_test.c | 41 + .../tests/procfs/pcmk__procfs_pid2path_test.c | 92 + lib/common/tests/results/Makefile.am | 16 + lib/common/tests/results/pcmk__results_test.c | 61 + lib/common/tests/scores/Makefile.am | 19 + lib/common/tests/scores/char2score_test.c | 75 + lib/common/tests/scores/pcmk__add_scores_test.c | 74 + lib/common/tests/scores/pcmk_readable_score_test.c | 33 + lib/common/tests/strings/Makefile.am | 41 + lib/common/tests/strings/crm_get_msec_test.c | 50 + lib/common/tests/strings/crm_is_true_test.c | 57 + lib/common/tests/strings/crm_str_to_boolean_test.c | 92 + lib/common/tests/strings/pcmk__add_word_test.c | 93 + lib/common/tests/strings/pcmk__btoa_test.c | 22 + .../tests/strings/pcmk__char_in_any_str_test.c | 46 + lib/common/tests/strings/pcmk__compress_test.c | 58 + lib/common/tests/strings/pcmk__ends_with_test.c | 57 + lib/common/tests/strings/pcmk__g_strcat_test.c | 73 + .../tests/strings/pcmk__guint_from_hash_test.c | 76 + .../tests/strings/pcmk__numeric_strcasecmp_test.c | 79 + .../tests/strings/pcmk__parse_ll_range_test.c | 117 + lib/common/tests/strings/pcmk__s_test.c | 29 + lib/common/tests/strings/pcmk__scan_double_test.c | 158 + lib/common/tests/strings/pcmk__scan_min_int_test.c | 60 + lib/common/tests/strings/pcmk__scan_port_test.c | 59 + lib/common/tests/strings/pcmk__starts_with_test.c | 35 + lib/common/tests/strings/pcmk__str_any_of_test.c | 48 + lib/common/tests/strings/pcmk__str_in_list_test.c | 107 + .../tests/strings/pcmk__str_table_dup_test.c | 59 + lib/common/tests/strings/pcmk__str_update_test.c | 78 + lib/common/tests/strings/pcmk__strcmp_test.c | 80 + .../tests/strings/pcmk__strikey_table_test.c | 40 + lib/common/tests/strings/pcmk__strkey_table_test.c | 40 + lib/common/tests/strings/pcmk__trim_test.c | 72 + lib/common/tests/utils/Makefile.am | 28 + lib/common/tests/utils/compare_version_test.c | 55 + lib/common/tests/utils/crm_meta_name_test.c | 41 + lib/common/tests/utils/crm_meta_value_test.c | 56 + lib/common/tests/utils/crm_user_lookup_test.c | 127 + lib/common/tests/utils/pcmk__getpid_s_test.c | 38 + lib/common/tests/utils/pcmk_daemon_user_test.c | 83 + lib/common/tests/utils/pcmk_hostname_test.c | 56 + lib/common/tests/utils/pcmk_str_is_infinity_test.c | 57 + .../tests/utils/pcmk_str_is_minus_infinity_test.c | 54 + lib/common/tests/xml/Makefile.am | 17 + lib/common/tests/xml/pcmk__xe_foreach_child_test.c | 215 + lib/common/tests/xml/pcmk__xe_match_test.c | 106 + lib/common/tests/xpath/Makefile.am | 16 + lib/common/tests/xpath/pcmk__xpath_node_id_test.c | 59 + lib/common/utils.c | 594 +++ lib/common/watchdog.c | 311 ++ lib/common/xml.c | 2753 +++++++++++ lib/common/xml_display.c | 549 +++ lib/common/xpath.c | 378 ++ lib/fencing/Makefile.am | 28 + lib/fencing/fencing_private.h | 51 + lib/fencing/st_actions.c | 710 +++ lib/fencing/st_client.c | 2702 +++++++++++ lib/fencing/st_lha.c | 279 ++ lib/fencing/st_output.c | 600 +++ lib/fencing/st_rhcs.c | 316 ++ lib/gnu/Makefile.am | 130 + lib/gnu/byteswap.in.h | 44 + lib/gnu/gl_openssl.h | 116 + lib/gnu/md5.c | 492 ++ lib/gnu/md5.h | 147 + lib/gnu/stdalign.in.h | 126 + lib/libpacemaker.pc.in | 14 + lib/lrmd/Makefile.am | 21 + lib/lrmd/lrmd_alerts.c | 399 ++ lib/lrmd/lrmd_client.c | 2533 ++++++++++ lib/lrmd/lrmd_output.c | 146 + lib/lrmd/proxy_common.c | 315 ++ lib/pacemaker-cib.pc.in | 18 + lib/pacemaker-cluster.pc.in | 14 + lib/pacemaker-fencing.pc.in | 14 + lib/pacemaker-lrmd.pc.in | 14 + lib/pacemaker-pe_rules.pc.in | 15 + lib/pacemaker-pe_status.pc.in | 16 + lib/pacemaker-service.pc.in | 17 + lib/pacemaker.pc.in | 32 + lib/pacemaker/Makefile.am | 69 + lib/pacemaker/libpacemaker_private.h | 986 ++++ lib/pacemaker/pcmk_acl.c | 379 ++ lib/pacemaker/pcmk_cluster_queries.c | 900 ++++ lib/pacemaker/pcmk_fence.c | 626 +++ lib/pacemaker/pcmk_graph_consumer.c | 874 ++++ lib/pacemaker/pcmk_graph_logging.c | 234 + lib/pacemaker/pcmk_graph_producer.c | 1078 +++++ lib/pacemaker/pcmk_injections.c | 784 ++++ lib/pacemaker/pcmk_output.c | 2331 ++++++++++ lib/pacemaker/pcmk_resource.c | 173 + lib/pacemaker/pcmk_result_code.c | 167 + lib/pacemaker/pcmk_rule.c | 295 ++ lib/pacemaker/pcmk_sched_actions.c | 1919 ++++++++ lib/pacemaker/pcmk_sched_bundle.c | 876 ++++ lib/pacemaker/pcmk_sched_clone.c | 643 +++ lib/pacemaker/pcmk_sched_colocation.c | 1595 +++++++ lib/pacemaker/pcmk_sched_constraints.c | 421 ++ lib/pacemaker/pcmk_sched_fencing.c | 493 ++ lib/pacemaker/pcmk_sched_group.c | 865 ++++ lib/pacemaker/pcmk_sched_instances.c | 1659 +++++++ lib/pacemaker/pcmk_sched_location.c | 678 +++ lib/pacemaker/pcmk_sched_migration.c | 386 ++ lib/pacemaker/pcmk_sched_nodes.c | 351 ++ lib/pacemaker/pcmk_sched_ordering.c | 1463 ++++++ lib/pacemaker/pcmk_sched_primitive.c | 1573 +++++++ lib/pacemaker/pcmk_sched_probes.c | 896 ++++ lib/pacemaker/pcmk_sched_promotable.c | 1286 ++++++ lib/pacemaker/pcmk_sched_recurring.c | 716 +++ lib/pacemaker/pcmk_sched_remote.c | 729 +++ lib/pacemaker/pcmk_sched_resource.c | 722 +++ lib/pacemaker/pcmk_sched_tickets.c | 531 +++ lib/pacemaker/pcmk_sched_utilization.c | 469 ++ lib/pacemaker/pcmk_scheduler.c | 811 ++++ lib/pacemaker/pcmk_simulate.c | 999 ++++ lib/pacemaker/pcmk_status.c | 368 ++ lib/pengine/Makefile.am | 81 + lib/pengine/bundle.c | 2004 ++++++++ lib/pengine/clone.c | 1470 ++++++ lib/pengine/common.c | 564 +++ lib/pengine/complex.c | 1174 +++++ lib/pengine/failcounts.c | 403 ++ lib/pengine/group.c | 521 +++ lib/pengine/native.c | 1414 ++++++ lib/pengine/pe_actions.c | 1686 +++++++ lib/pengine/pe_digest.c | 592 +++ lib/pengine/pe_health.c | 157 + lib/pengine/pe_notif.c | 996 ++++ lib/pengine/pe_output.c | 3108 +++++++++++++ lib/pengine/pe_status_private.h | 121 + lib/pengine/remote.c | 270 ++ lib/pengine/rules.c | 1316 ++++++ lib/pengine/rules_alerts.c | 299 ++ lib/pengine/status.c | 483 ++ lib/pengine/tags.c | 111 + lib/pengine/tests/Makefile.am | 1 + lib/pengine/tests/native/Makefile.am | 22 + lib/pengine/tests/native/native_find_rsc_test.c | 677 +++ lib/pengine/tests/native/pe_base_name_eq_test.c | 149 + lib/pengine/tests/rules/Makefile.am | 18 + .../tests/rules/pe_cron_range_satisfied_test.c | 165 + lib/pengine/tests/status/Makefile.am | 22 + lib/pengine/tests/status/pe_find_node_any_test.c | 62 + lib/pengine/tests/status/pe_find_node_id_test.c | 51 + lib/pengine/tests/status/pe_find_node_test.c | 51 + lib/pengine/tests/status/pe_new_working_set_test.c | 46 + .../tests/status/set_working_set_defaults_test.c | 46 + lib/pengine/tests/unpack/Makefile.am | 18 + lib/pengine/tests/unpack/pe_base_name_end_test.c | 36 + lib/pengine/tests/utils/Makefile.am | 21 + lib/pengine/tests/utils/pe__cmp_node_name_test.c | 55 + .../tests/utils/pe__cmp_rsc_priority_test.c | 50 + lib/pengine/unpack.c | 4829 ++++++++++++++++++++ lib/pengine/utils.c | 938 ++++ lib/pengine/variant.h | 91 + lib/services/Makefile.am | 43 + lib/services/dbus.c | 776 ++++ lib/services/pcmk-dbus.h | 45 + lib/services/services.c | 1417 ++++++ lib/services/services_linux.c | 1438 ++++++ lib/services/services_lsb.c | 341 ++ lib/services/services_lsb.h | 21 + lib/services/services_nagios.c | 220 + lib/services/services_nagios.h | 28 + lib/services/services_ocf.c | 179 + lib/services/services_ocf.h | 31 + lib/services/services_private.h | 101 + lib/services/systemd.c | 1100 +++++ lib/services/systemd.h | 30 + lib/services/upstart.c | 701 +++ lib/services/upstart.h | 31 + 293 files changed, 116193 insertions(+) create mode 100644 lib/Makefile.am create mode 100644 lib/cib/Makefile.am create mode 100644 lib/cib/cib_attrs.c create mode 100644 lib/cib/cib_client.c create mode 100644 lib/cib/cib_file.c create mode 100644 lib/cib/cib_native.c create mode 100644 lib/cib/cib_ops.c create mode 100644 lib/cib/cib_remote.c create mode 100644 lib/cib/cib_utils.c create mode 100644 lib/cluster/Makefile.am create mode 100644 lib/cluster/cluster.c create mode 100644 lib/cluster/corosync.c create mode 100644 lib/cluster/cpg.c create mode 100644 lib/cluster/crmcluster_private.h create mode 100644 lib/cluster/election.c create mode 100644 lib/cluster/membership.c create mode 100644 lib/common/Makefile.am create mode 100644 lib/common/acl.c create mode 100644 lib/common/agents.c create mode 100644 lib/common/alerts.c create mode 100644 lib/common/attrs.c create mode 100644 lib/common/cib.c create mode 100644 lib/common/cib_secrets.c create mode 100644 lib/common/cmdline.c create mode 100644 lib/common/crmcommon_private.h create mode 100644 lib/common/digest.c create mode 100644 lib/common/health.c create mode 100644 lib/common/io.c create mode 100644 lib/common/ipc_attrd.c create mode 100644 lib/common/ipc_client.c create mode 100644 lib/common/ipc_common.c create mode 100644 lib/common/ipc_controld.c create mode 100644 lib/common/ipc_pacemakerd.c create mode 100644 lib/common/ipc_schedulerd.c create mode 100644 lib/common/ipc_server.c create mode 100644 lib/common/iso8601.c create mode 100644 lib/common/lists.c create mode 100644 lib/common/logging.c create mode 100644 lib/common/mainloop.c create mode 100644 lib/common/messages.c create mode 100644 lib/common/mock.c create mode 100644 lib/common/mock_private.h create mode 100644 lib/common/nodes.c create mode 100644 lib/common/nvpair.c create mode 100644 lib/common/operations.c create mode 100644 lib/common/options.c create mode 100644 lib/common/output.c create mode 100644 lib/common/output_html.c create mode 100644 lib/common/output_log.c create mode 100644 lib/common/output_none.c create mode 100644 lib/common/output_text.c create mode 100644 lib/common/output_xml.c create mode 100644 lib/common/patchset.c create mode 100644 lib/common/patchset_display.c create mode 100644 lib/common/pid.c create mode 100644 lib/common/procfs.c create mode 100644 lib/common/remote.c create mode 100644 lib/common/results.c create mode 100644 lib/common/schemas.c create mode 100644 lib/common/scores.c create mode 100644 lib/common/strings.c create mode 100644 lib/common/tests/Makefile.am create mode 100644 lib/common/tests/acl/Makefile.am create mode 100644 lib/common/tests/acl/pcmk__is_user_in_group_test.c create mode 100644 lib/common/tests/acl/pcmk_acl_required_test.c create mode 100644 lib/common/tests/acl/xml_acl_denied_test.c create mode 100644 lib/common/tests/acl/xml_acl_enabled_test.c create mode 100644 lib/common/tests/agents/Makefile.am create mode 100644 lib/common/tests/agents/crm_generate_ra_key_test.c create mode 100644 lib/common/tests/agents/crm_parse_agent_spec_test.c create mode 100644 lib/common/tests/agents/pcmk__effective_rc_test.c create mode 100644 lib/common/tests/agents/pcmk_get_ra_caps_test.c create mode 100644 lib/common/tests/agents/pcmk_stonith_param_test.c create mode 100644 lib/common/tests/cmdline/Makefile.am create mode 100644 lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c create mode 100644 lib/common/tests/cmdline/pcmk__quote_cmdline_test.c create mode 100644 lib/common/tests/flags/Makefile.am create mode 100644 lib/common/tests/flags/pcmk__clear_flags_as_test.c create mode 100644 lib/common/tests/flags/pcmk__set_flags_as_test.c create mode 100644 lib/common/tests/flags/pcmk_all_flags_set_test.c create mode 100644 lib/common/tests/flags/pcmk_any_flags_set_test.c create mode 100644 lib/common/tests/health/Makefile.am create mode 100644 lib/common/tests/health/pcmk__parse_health_strategy_test.c create mode 100644 lib/common/tests/health/pcmk__validate_health_strategy_test.c create mode 100644 lib/common/tests/io/Makefile.am create mode 100644 lib/common/tests/io/pcmk__full_path_test.c create mode 100644 lib/common/tests/io/pcmk__get_tmpdir_test.c create mode 100644 lib/common/tests/iso8601/Makefile.am create mode 100644 lib/common/tests/iso8601/pcmk__readable_interval_test.c create mode 100644 lib/common/tests/lists/Makefile.am create mode 100644 lib/common/tests/lists/pcmk__list_of_1_test.c create mode 100644 lib/common/tests/lists/pcmk__list_of_multiple_test.c create mode 100644 lib/common/tests/lists/pcmk__subtract_lists_test.c create mode 100644 lib/common/tests/nvpair/Makefile.am create mode 100644 lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c create mode 100644 lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c create mode 100644 lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c create mode 100644 lib/common/tests/operations/Makefile.am create mode 100644 lib/common/tests/operations/copy_in_properties_test.c create mode 100644 lib/common/tests/operations/expand_plus_plus_test.c create mode 100644 lib/common/tests/operations/fix_plus_plus_recursive_test.c create mode 100644 lib/common/tests/operations/parse_op_key_test.c create mode 100644 lib/common/tests/operations/pcmk_is_probe_test.c create mode 100644 lib/common/tests/operations/pcmk_xe_is_probe_test.c create mode 100644 lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c create mode 100644 lib/common/tests/options/Makefile.am create mode 100644 lib/common/tests/options/pcmk__env_option_enabled_test.c create mode 100644 lib/common/tests/options/pcmk__env_option_test.c create mode 100644 lib/common/tests/options/pcmk__set_env_option_test.c create mode 100644 lib/common/tests/output/Makefile.am create mode 100644 lib/common/tests/output/pcmk__call_message_test.c create mode 100644 lib/common/tests/output/pcmk__output_and_clear_error_test.c create mode 100644 lib/common/tests/output/pcmk__output_free_test.c create mode 100644 lib/common/tests/output/pcmk__output_new_test.c create mode 100644 lib/common/tests/output/pcmk__register_format_test.c create mode 100644 lib/common/tests/output/pcmk__register_formats_test.c create mode 100644 lib/common/tests/output/pcmk__register_message_test.c create mode 100644 lib/common/tests/output/pcmk__register_messages_test.c create mode 100644 lib/common/tests/output/pcmk__unregister_formats_test.c create mode 100644 lib/common/tests/procfs/Makefile.am create mode 100644 lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c create mode 100644 lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c create mode 100644 lib/common/tests/procfs/pcmk__procfs_pid2path_test.c create mode 100644 lib/common/tests/results/Makefile.am create mode 100644 lib/common/tests/results/pcmk__results_test.c create mode 100644 lib/common/tests/scores/Makefile.am create mode 100644 lib/common/tests/scores/char2score_test.c create mode 100644 lib/common/tests/scores/pcmk__add_scores_test.c create mode 100644 lib/common/tests/scores/pcmk_readable_score_test.c create mode 100644 lib/common/tests/strings/Makefile.am create mode 100644 lib/common/tests/strings/crm_get_msec_test.c create mode 100644 lib/common/tests/strings/crm_is_true_test.c create mode 100644 lib/common/tests/strings/crm_str_to_boolean_test.c create mode 100644 lib/common/tests/strings/pcmk__add_word_test.c create mode 100644 lib/common/tests/strings/pcmk__btoa_test.c create mode 100644 lib/common/tests/strings/pcmk__char_in_any_str_test.c create mode 100644 lib/common/tests/strings/pcmk__compress_test.c create mode 100644 lib/common/tests/strings/pcmk__ends_with_test.c create mode 100644 lib/common/tests/strings/pcmk__g_strcat_test.c create mode 100644 lib/common/tests/strings/pcmk__guint_from_hash_test.c create mode 100644 lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c create mode 100644 lib/common/tests/strings/pcmk__parse_ll_range_test.c create mode 100644 lib/common/tests/strings/pcmk__s_test.c create mode 100644 lib/common/tests/strings/pcmk__scan_double_test.c create mode 100644 lib/common/tests/strings/pcmk__scan_min_int_test.c create mode 100644 lib/common/tests/strings/pcmk__scan_port_test.c create mode 100644 lib/common/tests/strings/pcmk__starts_with_test.c create mode 100644 lib/common/tests/strings/pcmk__str_any_of_test.c create mode 100644 lib/common/tests/strings/pcmk__str_in_list_test.c create mode 100644 lib/common/tests/strings/pcmk__str_table_dup_test.c create mode 100644 lib/common/tests/strings/pcmk__str_update_test.c create mode 100644 lib/common/tests/strings/pcmk__strcmp_test.c create mode 100644 lib/common/tests/strings/pcmk__strikey_table_test.c create mode 100644 lib/common/tests/strings/pcmk__strkey_table_test.c create mode 100644 lib/common/tests/strings/pcmk__trim_test.c create mode 100644 lib/common/tests/utils/Makefile.am create mode 100644 lib/common/tests/utils/compare_version_test.c create mode 100644 lib/common/tests/utils/crm_meta_name_test.c create mode 100644 lib/common/tests/utils/crm_meta_value_test.c create mode 100644 lib/common/tests/utils/crm_user_lookup_test.c create mode 100644 lib/common/tests/utils/pcmk__getpid_s_test.c create mode 100644 lib/common/tests/utils/pcmk_daemon_user_test.c create mode 100644 lib/common/tests/utils/pcmk_hostname_test.c create mode 100644 lib/common/tests/utils/pcmk_str_is_infinity_test.c create mode 100644 lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c create mode 100644 lib/common/tests/xml/Makefile.am create mode 100644 lib/common/tests/xml/pcmk__xe_foreach_child_test.c create mode 100644 lib/common/tests/xml/pcmk__xe_match_test.c create mode 100644 lib/common/tests/xpath/Makefile.am create mode 100644 lib/common/tests/xpath/pcmk__xpath_node_id_test.c create mode 100644 lib/common/utils.c create mode 100644 lib/common/watchdog.c create mode 100644 lib/common/xml.c create mode 100644 lib/common/xml_display.c create mode 100644 lib/common/xpath.c create mode 100644 lib/fencing/Makefile.am create mode 100644 lib/fencing/fencing_private.h create mode 100644 lib/fencing/st_actions.c create mode 100644 lib/fencing/st_client.c create mode 100644 lib/fencing/st_lha.c create mode 100644 lib/fencing/st_output.c create mode 100644 lib/fencing/st_rhcs.c create mode 100644 lib/gnu/Makefile.am create mode 100644 lib/gnu/byteswap.in.h create mode 100644 lib/gnu/gl_openssl.h create mode 100644 lib/gnu/md5.c create mode 100644 lib/gnu/md5.h create mode 100644 lib/gnu/stdalign.in.h create mode 100644 lib/libpacemaker.pc.in create mode 100644 lib/lrmd/Makefile.am create mode 100644 lib/lrmd/lrmd_alerts.c create mode 100644 lib/lrmd/lrmd_client.c create mode 100644 lib/lrmd/lrmd_output.c create mode 100644 lib/lrmd/proxy_common.c create mode 100644 lib/pacemaker-cib.pc.in create mode 100644 lib/pacemaker-cluster.pc.in create mode 100644 lib/pacemaker-fencing.pc.in create mode 100644 lib/pacemaker-lrmd.pc.in create mode 100644 lib/pacemaker-pe_rules.pc.in create mode 100644 lib/pacemaker-pe_status.pc.in create mode 100644 lib/pacemaker-service.pc.in create mode 100644 lib/pacemaker.pc.in create mode 100644 lib/pacemaker/Makefile.am create mode 100644 lib/pacemaker/libpacemaker_private.h create mode 100644 lib/pacemaker/pcmk_acl.c create mode 100644 lib/pacemaker/pcmk_cluster_queries.c create mode 100644 lib/pacemaker/pcmk_fence.c create mode 100644 lib/pacemaker/pcmk_graph_consumer.c create mode 100644 lib/pacemaker/pcmk_graph_logging.c create mode 100644 lib/pacemaker/pcmk_graph_producer.c create mode 100644 lib/pacemaker/pcmk_injections.c create mode 100644 lib/pacemaker/pcmk_output.c create mode 100644 lib/pacemaker/pcmk_resource.c create mode 100644 lib/pacemaker/pcmk_result_code.c create mode 100644 lib/pacemaker/pcmk_rule.c create mode 100644 lib/pacemaker/pcmk_sched_actions.c create mode 100644 lib/pacemaker/pcmk_sched_bundle.c create mode 100644 lib/pacemaker/pcmk_sched_clone.c create mode 100644 lib/pacemaker/pcmk_sched_colocation.c create mode 100644 lib/pacemaker/pcmk_sched_constraints.c create mode 100644 lib/pacemaker/pcmk_sched_fencing.c create mode 100644 lib/pacemaker/pcmk_sched_group.c create mode 100644 lib/pacemaker/pcmk_sched_instances.c create mode 100644 lib/pacemaker/pcmk_sched_location.c create mode 100644 lib/pacemaker/pcmk_sched_migration.c create mode 100644 lib/pacemaker/pcmk_sched_nodes.c create mode 100644 lib/pacemaker/pcmk_sched_ordering.c create mode 100644 lib/pacemaker/pcmk_sched_primitive.c create mode 100644 lib/pacemaker/pcmk_sched_probes.c create mode 100644 lib/pacemaker/pcmk_sched_promotable.c create mode 100644 lib/pacemaker/pcmk_sched_recurring.c create mode 100644 lib/pacemaker/pcmk_sched_remote.c create mode 100644 lib/pacemaker/pcmk_sched_resource.c create mode 100644 lib/pacemaker/pcmk_sched_tickets.c create mode 100644 lib/pacemaker/pcmk_sched_utilization.c create mode 100644 lib/pacemaker/pcmk_scheduler.c create mode 100644 lib/pacemaker/pcmk_simulate.c create mode 100644 lib/pacemaker/pcmk_status.c create mode 100644 lib/pengine/Makefile.am create mode 100644 lib/pengine/bundle.c create mode 100644 lib/pengine/clone.c create mode 100644 lib/pengine/common.c create mode 100644 lib/pengine/complex.c create mode 100644 lib/pengine/failcounts.c create mode 100644 lib/pengine/group.c create mode 100644 lib/pengine/native.c create mode 100644 lib/pengine/pe_actions.c create mode 100644 lib/pengine/pe_digest.c create mode 100644 lib/pengine/pe_health.c create mode 100644 lib/pengine/pe_notif.c create mode 100644 lib/pengine/pe_output.c create mode 100644 lib/pengine/pe_status_private.h create mode 100644 lib/pengine/remote.c create mode 100644 lib/pengine/rules.c create mode 100644 lib/pengine/rules_alerts.c create mode 100644 lib/pengine/status.c create mode 100644 lib/pengine/tags.c create mode 100644 lib/pengine/tests/Makefile.am create mode 100644 lib/pengine/tests/native/Makefile.am create mode 100644 lib/pengine/tests/native/native_find_rsc_test.c create mode 100644 lib/pengine/tests/native/pe_base_name_eq_test.c create mode 100644 lib/pengine/tests/rules/Makefile.am create mode 100644 lib/pengine/tests/rules/pe_cron_range_satisfied_test.c create mode 100644 lib/pengine/tests/status/Makefile.am create mode 100644 lib/pengine/tests/status/pe_find_node_any_test.c create mode 100644 lib/pengine/tests/status/pe_find_node_id_test.c create mode 100644 lib/pengine/tests/status/pe_find_node_test.c create mode 100644 lib/pengine/tests/status/pe_new_working_set_test.c create mode 100644 lib/pengine/tests/status/set_working_set_defaults_test.c create mode 100644 lib/pengine/tests/unpack/Makefile.am create mode 100644 lib/pengine/tests/unpack/pe_base_name_end_test.c create mode 100644 lib/pengine/tests/utils/Makefile.am create mode 100644 lib/pengine/tests/utils/pe__cmp_node_name_test.c create mode 100644 lib/pengine/tests/utils/pe__cmp_rsc_priority_test.c create mode 100644 lib/pengine/unpack.c create mode 100644 lib/pengine/utils.c create mode 100644 lib/pengine/variant.h create mode 100644 lib/services/Makefile.am create mode 100644 lib/services/dbus.c create mode 100644 lib/services/pcmk-dbus.h create mode 100644 lib/services/services.c create mode 100644 lib/services/services_linux.c create mode 100644 lib/services/services_lsb.c create mode 100644 lib/services/services_lsb.h create mode 100644 lib/services/services_nagios.c create mode 100644 lib/services/services_nagios.h create mode 100644 lib/services/services_ocf.c create mode 100644 lib/services/services_ocf.h create mode 100644 lib/services/services_private.h create mode 100644 lib/services/systemd.c create mode 100644 lib/services/systemd.h create mode 100644 lib/services/upstart.c create mode 100644 lib/services/upstart.h (limited to 'lib') diff --git a/lib/Makefile.am b/lib/Makefile.am new file mode 100644 index 0000000..ed5bfa3 --- /dev/null +++ b/lib/Makefile.am @@ -0,0 +1,21 @@ +# +# Copyright 2003-2021 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# +MAINTAINERCLEANFILES = Makefile.in + +LIBS = cib lrmd service fencing cluster + +pkgconfig_DATA = $(LIBS:%=pacemaker-%.pc) \ + libpacemaker.pc \ + pacemaker.pc \ + pacemaker-pe_rules.pc \ + pacemaker-pe_status.pc + +EXTRA_DIST = $(pkgconfig_DATA:%=%.in) + +SUBDIRS = gnu common pengine cib services fencing lrmd cluster pacemaker diff --git a/lib/cib/Makefile.am b/lib/cib/Makefile.am new file mode 100644 index 0000000..721fca1 --- /dev/null +++ b/lib/cib/Makefile.am @@ -0,0 +1,28 @@ +# +# Copyright 2004-2018 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# +include $(top_srcdir)/mk/common.mk + +## libraries +lib_LTLIBRARIES = libcib.la + +## SOURCES +libcib_la_SOURCES = cib_ops.c cib_utils.c cib_client.c cib_native.c cib_attrs.c +libcib_la_SOURCES += cib_file.c cib_remote.c + +libcib_la_LDFLAGS = -version-info 31:0:4 +libcib_la_CPPFLAGS = -I$(top_srcdir) $(AM_CPPFLAGS) + +libcib_la_CFLAGS = $(CFLAGS_HARDENED_LIB) +libcib_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) + +libcib_la_LIBADD = $(top_builddir)/lib/pengine/libpe_rules.la \ + $(top_builddir)/lib/common/libcrmcommon.la + +clean-generic: + rm -f *.log *.debug *.xml *~ diff --git a/lib/cib/cib_attrs.c b/lib/cib/cib_attrs.c new file mode 100644 index 0000000..5f3a722 --- /dev/null +++ b/lib/cib/cib_attrs.c @@ -0,0 +1,732 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +static pcmk__output_t * +new_output_object(const char *ty) +{ + int rc = pcmk_rc_ok; + pcmk__output_t *out = NULL; + const char* argv[] = { "", NULL }; + pcmk__supported_format_t formats[] = { + PCMK__SUPPORTED_FORMAT_LOG, + PCMK__SUPPORTED_FORMAT_TEXT, + { NULL, NULL, NULL } + }; + + pcmk__register_formats(NULL, formats); + rc = pcmk__output_new(&out, ty, NULL, (char**)argv); + if ((rc != pcmk_rc_ok) || (out == NULL)) { + crm_err("Can't out due to internal error: %s", pcmk_rc_str(rc)); + return NULL; + } + + return out; +} + +static int +find_attr(cib_t *cib, const char *section, const char *node_uuid, + const char *attr_set_type, const char *set_name, const char *attr_id, + const char *attr_name, const char *user_name, xmlNode **result) +{ + int rc = pcmk_rc_ok; + + const char *xpath_base = NULL; + GString *xpath = NULL; + xmlNode *xml_search = NULL; + const char *set_type = NULL; + const char *node_type = NULL; + + if (attr_set_type) { + set_type = attr_set_type; + } else { + set_type = XML_TAG_ATTR_SETS; + } + + if (pcmk__str_eq(section, XML_CIB_TAG_CRMCONFIG, pcmk__str_casei)) { + node_uuid = NULL; + set_type = XML_CIB_TAG_PROPSET; + + } else if (pcmk__strcase_any_of(section, XML_CIB_TAG_OPCONFIG, XML_CIB_TAG_RSCCONFIG, + NULL)) { + node_uuid = NULL; + set_type = XML_TAG_META_SETS; + + } else if (pcmk__str_eq(section, XML_CIB_TAG_TICKETS, pcmk__str_casei)) { + node_uuid = NULL; + section = XML_CIB_TAG_STATUS; + node_type = XML_CIB_TAG_TICKETS; + + } else if (node_uuid == NULL) { + return EINVAL; + } + + xpath_base = pcmk_cib_xpath_for(section); + if (xpath_base == NULL) { + crm_warn("%s CIB section not known", section); + return ENOMSG; + } + + xpath = g_string_sized_new(1024); + g_string_append(xpath, xpath_base); + + if (pcmk__str_eq(node_type, XML_CIB_TAG_TICKETS, pcmk__str_casei)) { + pcmk__g_strcat(xpath, "//", node_type, NULL); + + } else if (node_uuid) { + const char *node_type = XML_CIB_TAG_NODE; + + if (pcmk__str_eq(section, XML_CIB_TAG_STATUS, pcmk__str_casei)) { + node_type = XML_CIB_TAG_STATE; + set_type = XML_TAG_TRANSIENT_NODEATTRS; + } + pcmk__g_strcat(xpath, + "//", node_type, "[@" XML_ATTR_ID "='", node_uuid, "']", + NULL); + } + + pcmk__g_strcat(xpath, "//", set_type, NULL); + if (set_name) { + pcmk__g_strcat(xpath, "[@" XML_ATTR_ID "='", set_name, "']", NULL); + } + + g_string_append(xpath, "//nvpair"); + + if (attr_id && attr_name) { + pcmk__g_strcat(xpath, + "[@" XML_ATTR_ID "='", attr_id, "' " + "and @" XML_ATTR_NAME "='", attr_name, "']", NULL); + + } else if (attr_id) { + pcmk__g_strcat(xpath, "[@" XML_ATTR_ID "='", attr_id, "']", NULL); + + } else if (attr_name) { + pcmk__g_strcat(xpath, "[@" XML_ATTR_NAME "='", attr_name, "']", NULL); + } + + rc = cib_internal_op(cib, PCMK__CIB_REQUEST_QUERY, NULL, + (const char *) xpath->str, NULL, &xml_search, + cib_sync_call|cib_scope_local|cib_xpath, user_name); + if (rc < 0) { + rc = pcmk_legacy2rc(rc); + crm_trace("Query failed for attribute %s (section=%s, node=%s, set=%s, xpath=%s): %s", + attr_name, section, pcmk__s(node_uuid, ""), + pcmk__s(set_name, ""), (const char *) xpath->str, + pcmk_rc_str(rc)); + } else { + rc = pcmk_rc_ok; + crm_log_xml_debug(xml_search, "Match"); + } + + g_string_free(xpath, TRUE); + *result = xml_search; + return rc; +} + +static int +handle_multiples(pcmk__output_t *out, xmlNode *search, const char *attr_name) +{ + if (xml_has_children(search)) { + xmlNode *child = NULL; + out->info(out, "Multiple attributes match name=%s", attr_name); + + for (child = pcmk__xml_first_child(search); child != NULL; + child = pcmk__xml_next(child)) { + out->info(out, " Value: %s \t(id=%s)", + crm_element_value(child, XML_NVPAIR_ATTR_VALUE), ID(child)); + } + + return ENOTUNIQ; + + } else { + return pcmk_rc_ok; + } +} + +int +cib__update_node_attr(pcmk__output_t *out, cib_t *cib, int call_options, const char *section, + const char *node_uuid, const char *set_type, const char *set_name, + const char *attr_id, const char *attr_name, const char *attr_value, + const char *user_name, const char *node_type) +{ + const char *tag = NULL; + int rc = pcmk_rc_ok; + xmlNode *xml_top = NULL; + xmlNode *xml_obj = NULL; + xmlNode *xml_search = NULL; + + char *local_attr_id = NULL; + char *local_set_name = NULL; + + CRM_CHECK(section != NULL, return EINVAL); + CRM_CHECK(attr_value != NULL, return EINVAL); + CRM_CHECK(attr_name != NULL || attr_id != NULL, return EINVAL); + + rc = find_attr(cib, section, node_uuid, set_type, set_name, attr_id, + attr_name, user_name, &xml_search); + + if (rc == pcmk_rc_ok) { + if (handle_multiples(out, xml_search, attr_name) == ENOTUNIQ) { + free_xml(xml_search); + return ENOTUNIQ; + } else { + pcmk__str_update(&local_attr_id, crm_element_value(xml_search, XML_ATTR_ID)); + attr_id = local_attr_id; + free_xml(xml_search); + goto do_modify; + } + + } else if (rc != ENXIO) { + free_xml(xml_search); + return rc; + + /* } else if(attr_id == NULL) { */ + /* return EINVAL; */ + + } else { + free_xml(xml_search); + crm_trace("%s does not exist, create it", attr_name); + if (pcmk__str_eq(section, XML_CIB_TAG_TICKETS, pcmk__str_casei)) { + node_uuid = NULL; + section = XML_CIB_TAG_STATUS; + node_type = XML_CIB_TAG_TICKETS; + + xml_top = create_xml_node(xml_obj, XML_CIB_TAG_STATUS); + xml_obj = create_xml_node(xml_top, XML_CIB_TAG_TICKETS); + + } else if (pcmk__str_eq(section, XML_CIB_TAG_NODES, pcmk__str_casei)) { + + if (node_uuid == NULL) { + return EINVAL; + } + + if (pcmk__str_eq(node_type, "remote", pcmk__str_casei)) { + xml_top = create_xml_node(xml_obj, XML_CIB_TAG_NODES); + xml_obj = create_xml_node(xml_top, XML_CIB_TAG_NODE); + crm_xml_add(xml_obj, XML_ATTR_TYPE, "remote"); + crm_xml_add(xml_obj, XML_ATTR_ID, node_uuid); + crm_xml_add(xml_obj, XML_ATTR_UNAME, node_uuid); + } else { + tag = XML_CIB_TAG_NODE; + } + + } else if (pcmk__str_eq(section, XML_CIB_TAG_STATUS, pcmk__str_casei)) { + tag = XML_TAG_TRANSIENT_NODEATTRS; + if (node_uuid == NULL) { + return EINVAL; + } + + xml_top = create_xml_node(xml_obj, XML_CIB_TAG_STATE); + crm_xml_add(xml_top, XML_ATTR_ID, node_uuid); + xml_obj = xml_top; + + } else { + tag = section; + node_uuid = NULL; + } + + if (set_name == NULL) { + if (pcmk__str_eq(section, XML_CIB_TAG_CRMCONFIG, pcmk__str_casei)) { + local_set_name = strdup(CIB_OPTIONS_FIRST); + + } else if (pcmk__str_eq(node_type, XML_CIB_TAG_TICKETS, pcmk__str_casei)) { + local_set_name = crm_strdup_printf("%s-%s", section, + XML_CIB_TAG_TICKETS); + + } else if (node_uuid) { + local_set_name = crm_strdup_printf("%s-%s", section, node_uuid); + + if (set_type) { + char *tmp_set_name = local_set_name; + + local_set_name = crm_strdup_printf("%s-%s", tmp_set_name, + set_type); + free(tmp_set_name); + } + } else { + local_set_name = crm_strdup_printf("%s-options", section); + } + set_name = local_set_name; + } + + if (attr_id == NULL) { + local_attr_id = crm_strdup_printf("%s-%s", set_name, attr_name); + crm_xml_sanitize_id(local_attr_id); + attr_id = local_attr_id; + + } else if (attr_name == NULL) { + attr_name = attr_id; + } + + crm_trace("Creating %s/%s", section, tag); + if (tag != NULL) { + xml_obj = create_xml_node(xml_obj, tag); + crm_xml_add(xml_obj, XML_ATTR_ID, node_uuid); + if (xml_top == NULL) { + xml_top = xml_obj; + } + } + + if (node_uuid == NULL && !pcmk__str_eq(node_type, XML_CIB_TAG_TICKETS, pcmk__str_casei)) { + if (pcmk__str_eq(section, XML_CIB_TAG_CRMCONFIG, pcmk__str_casei)) { + xml_obj = create_xml_node(xml_obj, XML_CIB_TAG_PROPSET); + } else { + xml_obj = create_xml_node(xml_obj, XML_TAG_META_SETS); + } + + } else if (set_type) { + xml_obj = create_xml_node(xml_obj, set_type); + + } else { + xml_obj = create_xml_node(xml_obj, XML_TAG_ATTR_SETS); + } + crm_xml_add(xml_obj, XML_ATTR_ID, set_name); + + if (xml_top == NULL) { + xml_top = xml_obj; + } + } + + do_modify: + xml_obj = crm_create_nvpair_xml(xml_obj, attr_id, attr_name, attr_value); + if (xml_top == NULL) { + xml_top = xml_obj; + } + + crm_log_xml_trace(xml_top, "update_attr"); + rc = cib_internal_op(cib, PCMK__CIB_REQUEST_MODIFY, NULL, section, xml_top, + NULL, call_options, user_name); + if (rc < 0) { + rc = pcmk_legacy2rc(rc); + + out->err(out, "Error setting %s=%s (section=%s, set=%s): %s", + attr_name, attr_value, section, pcmk__s(set_name, ""), + pcmk_rc_str(rc)); + crm_log_xml_info(xml_top, "Update"); + } else { + rc = pcmk_rc_ok; + } + + free(local_set_name); + free(local_attr_id); + free_xml(xml_top); + + return rc; +} + +int +cib__get_node_attrs(pcmk__output_t *out, cib_t *cib, const char *section, + const char *node_uuid, const char *set_type, const char *set_name, + const char *attr_id, const char *attr_name, const char *user_name, + xmlNode **result) +{ + int rc = pcmk_rc_ok; + + CRM_ASSERT(result != NULL); + CRM_CHECK(section != NULL, return EINVAL); + + *result = NULL; + + rc = find_attr(cib, section, node_uuid, set_type, set_name, attr_id, attr_name, + user_name, result); + + if (rc != pcmk_rc_ok) { + crm_trace("Query failed for attribute %s (section=%s node=%s set=%s): %s", + pcmk__s(attr_name, "with unspecified name"), + section, pcmk__s(set_name, ""), + pcmk__s(node_uuid, ""), pcmk_strerror(rc)); + } + + return rc; +} + +int +cib__delete_node_attr(pcmk__output_t *out, cib_t *cib, int options, const char *section, + const char *node_uuid, const char *set_type, const char *set_name, + const char *attr_id, const char *attr_name, const char *attr_value, + const char *user_name) +{ + int rc = pcmk_rc_ok; + xmlNode *xml_obj = NULL; + xmlNode *xml_search = NULL; + char *local_attr_id = NULL; + + CRM_CHECK(section != NULL, return EINVAL); + CRM_CHECK(attr_name != NULL || attr_id != NULL, return EINVAL); + + if (attr_id == NULL) { + rc = find_attr(cib, section, node_uuid, set_type, set_name, attr_id, + attr_name, user_name, &xml_search); + + if (rc != pcmk_rc_ok || handle_multiples(out, xml_search, attr_name) == ENOTUNIQ) { + free_xml(xml_search); + return rc; + } else { + pcmk__str_update(&local_attr_id, crm_element_value(xml_search, XML_ATTR_ID)); + attr_id = local_attr_id; + free_xml(xml_search); + } + } + + xml_obj = crm_create_nvpair_xml(NULL, attr_id, attr_name, attr_value); + + rc = cib_internal_op(cib, PCMK__CIB_REQUEST_DELETE, NULL, section, xml_obj, + NULL, options, user_name); + if (rc < 0) { + rc = pcmk_legacy2rc(rc); + } else { + rc = pcmk_rc_ok; + out->info(out, "Deleted %s %s: id=%s%s%s%s%s", + section, node_uuid ? "attribute" : "option", local_attr_id, + set_name ? " set=" : "", set_name ? set_name : "", + attr_name ? " name=" : "", attr_name ? attr_name : ""); + } + + free(local_attr_id); + free_xml(xml_obj); + return rc; +} + +int +find_nvpair_attr_delegate(cib_t *cib, const char *attr, const char *section, + const char *node_uuid, const char *attr_set_type, const char *set_name, + const char *attr_id, const char *attr_name, gboolean to_console, + char **value, const char *user_name) +{ + pcmk__output_t *out = NULL; + xmlNode *xml_search = NULL; + int rc = pcmk_ok; + + out = new_output_object(to_console ? "text" : "log"); + if (out == NULL) { + return pcmk_err_generic; + } + + rc = find_attr(cib, section, node_uuid, attr_set_type, set_name, attr_id, + attr_name, user_name, &xml_search); + + if (rc == pcmk_rc_ok) { + rc = handle_multiples(out, xml_search, attr_name); + + if (rc == pcmk_rc_ok) { + pcmk__str_update(value, crm_element_value(xml_search, attr)); + } + } + + out->finish(out, CRM_EX_OK, true, NULL); + free_xml(xml_search); + pcmk__output_free(out); + return pcmk_rc2legacy(rc); +} + +int +update_attr_delegate(cib_t *cib, int call_options, const char *section, + const char *node_uuid, const char *set_type, const char *set_name, + const char *attr_id, const char *attr_name, const char *attr_value, + gboolean to_console, const char *user_name, const char *node_type) +{ + pcmk__output_t *out = NULL; + int rc = pcmk_ok; + + out = new_output_object(to_console ? "text" : "log"); + if (out == NULL) { + return pcmk_err_generic; + } + + rc = cib__update_node_attr(out, cib, call_options, section, node_uuid, set_type, + set_name, attr_id, attr_name, attr_value, user_name, + node_type); + + out->finish(out, CRM_EX_OK, true, NULL); + pcmk__output_free(out); + return pcmk_rc2legacy(rc); +} + +int +read_attr_delegate(cib_t *cib, const char *section, const char *node_uuid, + const char *set_type, const char *set_name, const char *attr_id, + const char *attr_name, char **attr_value, gboolean to_console, + const char *user_name) +{ + pcmk__output_t *out = NULL; + xmlNode *result = NULL; + int rc = pcmk_ok; + + out = new_output_object(to_console ? "text" : "log"); + if (out == NULL) { + return pcmk_err_generic; + } + + rc = cib__get_node_attrs(out, cib, section, node_uuid, set_type, set_name, + attr_id, attr_name, user_name, &result); + + if (rc == pcmk_rc_ok) { + if (!xml_has_children(result)) { + pcmk__str_update(attr_value, crm_element_value(result, XML_NVPAIR_ATTR_VALUE)); + } else { + rc = ENOTUNIQ; + } + } + + out->finish(out, CRM_EX_OK, true, NULL); + free_xml(result); + pcmk__output_free(out); + return pcmk_rc2legacy(rc); +} + +int +delete_attr_delegate(cib_t *cib, int options, const char *section, const char *node_uuid, + const char *set_type, const char *set_name, const char *attr_id, + const char *attr_name, const char *attr_value, gboolean to_console, + const char *user_name) +{ + pcmk__output_t *out = NULL; + int rc = pcmk_ok; + + out = new_output_object(to_console ? "text" : "log"); + if (out == NULL) { + return pcmk_err_generic; + } + + rc = cib__delete_node_attr(out, cib, options, section, node_uuid, set_type, + set_name, attr_id, attr_name, attr_value, user_name); + + out->finish(out, CRM_EX_OK, true, NULL); + pcmk__output_free(out); + return pcmk_rc2legacy(rc); +} + +/*! + * \internal + * \brief Parse node UUID from search result + * + * \param[in] result XML search result + * \param[out] uuid If non-NULL, where to store parsed UUID + * \param[out] is_remote If non-NULL, set TRUE if result is remote node + * + * \return pcmk_ok if UUID was successfully parsed, -ENXIO otherwise + */ +static int +get_uuid_from_result(const xmlNode *result, char **uuid, int *is_remote) +{ + int rc = -ENXIO; + const char *tag; + const char *parsed_uuid = NULL; + int parsed_is_remote = FALSE; + + if (result == NULL) { + return rc; + } + + /* If there are multiple results, the first is sufficient */ + tag = (const char *) (result->name); + if (pcmk__str_eq(tag, "xpath-query", pcmk__str_casei)) { + result = pcmk__xml_first_child(result); + CRM_CHECK(result != NULL, return rc); + tag = (const char *) (result->name); + } + + if (pcmk__str_eq(tag, XML_CIB_TAG_NODE, pcmk__str_casei)) { + /* Result is tag from section */ + + if (pcmk__str_eq(crm_element_value(result, XML_ATTR_TYPE), "remote", pcmk__str_casei)) { + parsed_uuid = crm_element_value(result, XML_ATTR_UNAME); + parsed_is_remote = TRUE; + } else { + parsed_uuid = ID(result); + parsed_is_remote = FALSE; + } + + } else if (pcmk__str_eq(tag, XML_CIB_TAG_RESOURCE, pcmk__str_casei)) { + /* Result is for ocf:pacemaker:remote resource */ + + parsed_uuid = ID(result); + parsed_is_remote = TRUE; + + } else if (pcmk__str_eq(tag, XML_CIB_TAG_NVPAIR, pcmk__str_casei)) { + /* Result is remote-node parameter of for guest node */ + + parsed_uuid = crm_element_value(result, XML_NVPAIR_ATTR_VALUE); + parsed_is_remote = TRUE; + + } else if (pcmk__str_eq(tag, XML_CIB_TAG_STATE, pcmk__str_casei)) { + /* Result is tag from section */ + + parsed_uuid = crm_element_value(result, XML_ATTR_UNAME); + if (pcmk__xe_attr_is_true(result, XML_NODE_IS_REMOTE)) { + parsed_is_remote = TRUE; + } + } + + if (parsed_uuid) { + if (uuid) { + *uuid = strdup(parsed_uuid); + } + if (is_remote) { + *is_remote = parsed_is_remote; + } + rc = pcmk_ok; + } + + return rc; +} + +/* Search string to find a node by name, as: + * - cluster or remote node in nodes section + * - remote node in resources section + * - guest node in resources section + * - orphaned remote node or bundle guest node in status section + */ +#define XPATH_UPPER_TRANS "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +#define XPATH_LOWER_TRANS "abcdefghijklmnopqrstuvwxyz" +#define XPATH_NODE \ + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_NODES \ + "/" XML_CIB_TAG_NODE "[translate(@" XML_ATTR_UNAME ",'" XPATH_UPPER_TRANS "','" XPATH_LOWER_TRANS "') ='%s']" \ + "|/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RESOURCES \ + "/" XML_CIB_TAG_RESOURCE \ + "[@class='ocf'][@provider='pacemaker'][@type='remote'][translate(@id,'" XPATH_UPPER_TRANS "','" XPATH_LOWER_TRANS "') ='%s']" \ + "|/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RESOURCES \ + "/" XML_CIB_TAG_RESOURCE "/" XML_TAG_META_SETS "/" XML_CIB_TAG_NVPAIR \ + "[@name='" XML_RSC_ATTR_REMOTE_NODE "'][translate(@value,'" XPATH_UPPER_TRANS "','" XPATH_LOWER_TRANS "') ='%s']" \ + "|/" XML_TAG_CIB "/" XML_CIB_TAG_STATUS "/" XML_CIB_TAG_STATE \ + "[@" XML_NODE_IS_REMOTE "='true'][translate(@" XML_ATTR_ID ",'" XPATH_UPPER_TRANS "','" XPATH_LOWER_TRANS "') ='%s']" + +int +query_node_uuid(cib_t * the_cib, const char *uname, char **uuid, int *is_remote_node) +{ + int rc = pcmk_ok; + char *xpath_string; + xmlNode *xml_search = NULL; + char *host_lowercase = NULL; + + CRM_ASSERT(uname != NULL); + + host_lowercase = g_ascii_strdown(uname, -1); + + if (uuid) { + *uuid = NULL; + } + if (is_remote_node) { + *is_remote_node = FALSE; + } + + xpath_string = crm_strdup_printf(XPATH_NODE, host_lowercase, host_lowercase, host_lowercase, host_lowercase); + if (cib_internal_op(the_cib, PCMK__CIB_REQUEST_QUERY, NULL, xpath_string, + NULL, &xml_search, + cib_sync_call|cib_scope_local|cib_xpath, + NULL) == pcmk_ok) { + rc = get_uuid_from_result(xml_search, uuid, is_remote_node); + } else { + rc = -ENXIO; + } + free(xpath_string); + free_xml(xml_search); + g_free(host_lowercase); + + if (rc != pcmk_ok) { + crm_debug("Could not map node name '%s' to a UUID: %s", + uname, pcmk_strerror(rc)); + } else { + crm_info("Mapped node name '%s' to UUID %s", uname, (uuid? *uuid : "")); + } + return rc; +} + +int +query_node_uname(cib_t * the_cib, const char *uuid, char **uname) +{ + int rc = pcmk_ok; + xmlNode *a_child = NULL; + xmlNode *xml_obj = NULL; + xmlNode *fragment = NULL; + const char *child_name = NULL; + + CRM_ASSERT(uname != NULL); + CRM_ASSERT(uuid != NULL); + + rc = the_cib->cmds->query(the_cib, XML_CIB_TAG_NODES, &fragment, + cib_sync_call | cib_scope_local); + if (rc != pcmk_ok) { + return rc; + } + + xml_obj = fragment; + CRM_CHECK(pcmk__str_eq(crm_element_name(xml_obj), XML_CIB_TAG_NODES, pcmk__str_casei), + return -ENOMSG); + CRM_ASSERT(xml_obj != NULL); + crm_log_xml_trace(xml_obj, "Result section"); + + rc = -ENXIO; + *uname = NULL; + + for (a_child = pcmk__xml_first_child(xml_obj); a_child != NULL; + a_child = pcmk__xml_next(a_child)) { + + if (pcmk__str_eq((const char *)a_child->name, XML_CIB_TAG_NODE, + pcmk__str_none)) { + child_name = ID(a_child); + if (pcmk__str_eq(uuid, child_name, pcmk__str_casei)) { + child_name = crm_element_value(a_child, XML_ATTR_UNAME); + if (child_name != NULL) { + *uname = strdup(child_name); + rc = pcmk_ok; + } + break; + } + } + } + + free_xml(fragment); + return rc; +} + +int +set_standby(cib_t * the_cib, const char *uuid, const char *scope, const char *standby_value) +{ + int rc = pcmk_ok; + char *attr_id = NULL; + + CRM_CHECK(uuid != NULL, return -EINVAL); + CRM_CHECK(standby_value != NULL, return -EINVAL); + + if (pcmk__strcase_any_of(scope, "reboot", XML_CIB_TAG_STATUS, NULL)) { + scope = XML_CIB_TAG_STATUS; + attr_id = crm_strdup_printf("transient-standby-%.256s", uuid); + + } else { + scope = XML_CIB_TAG_NODES; + attr_id = crm_strdup_printf("standby-%.256s", uuid); + } + + rc = update_attr_delegate(the_cib, cib_sync_call, scope, uuid, NULL, NULL, + attr_id, "standby", standby_value, TRUE, NULL, NULL); + + free(attr_id); + return rc; +} diff --git a/lib/cib/cib_client.c b/lib/cib/cib_client.c new file mode 100644 index 0000000..2d179e0 --- /dev/null +++ b/lib/cib/cib_client.c @@ -0,0 +1,750 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +static GHashTable *cib_op_callback_table = NULL; + +#define op_common(cib) do { \ + if(cib == NULL) { \ + return -EINVAL; \ + } else if(cib->delegate_fn == NULL) { \ + return -EPROTONOSUPPORT; \ + } \ + } while(0) + +static int +cib_client_set_op_callback(cib_t *cib, + void (*callback) (const xmlNode * msg, int call_id, + int rc, xmlNode * output)) +{ + if (callback == NULL) { + crm_info("Un-Setting operation callback"); + + } else { + crm_trace("Setting operation callback"); + } + cib->op_callback = callback; + return pcmk_ok; +} + +static gint +ciblib_GCompareFunc(gconstpointer a, gconstpointer b) +{ + int rc = 0; + const cib_notify_client_t *a_client = a; + const cib_notify_client_t *b_client = b; + + CRM_CHECK(a_client->event != NULL && b_client->event != NULL, return 0); + rc = strcmp(a_client->event, b_client->event); + if (rc == 0) { + if (a_client->callback == b_client->callback) { + return 0; + } else if (((long)a_client->callback) < ((long)b_client->callback)) { + crm_trace("callbacks for %s are not equal: %p < %p", + a_client->event, a_client->callback, b_client->callback); + return -1; + } + crm_trace("callbacks for %s are not equal: %p > %p", + a_client->event, a_client->callback, b_client->callback); + return 1; + } + return rc; +} + +static int +cib_client_add_notify_callback(cib_t * cib, const char *event, + void (*callback) (const char *event, + xmlNode * msg)) +{ + GList *list_item = NULL; + cib_notify_client_t *new_client = NULL; + + if ((cib->variant != cib_native) && (cib->variant != cib_remote)) { + return -EPROTONOSUPPORT; + } + + crm_trace("Adding callback for %s events (%d)", + event, g_list_length(cib->notify_list)); + + new_client = calloc(1, sizeof(cib_notify_client_t)); + new_client->event = event; + new_client->callback = callback; + + list_item = g_list_find_custom(cib->notify_list, new_client, + ciblib_GCompareFunc); + + if (list_item != NULL) { + crm_warn("Callback already present"); + free(new_client); + return -EINVAL; + + } else { + cib->notify_list = g_list_append(cib->notify_list, new_client); + + cib->cmds->register_notification(cib, event, 1); + + crm_trace("Callback added (%d)", g_list_length(cib->notify_list)); + } + return pcmk_ok; +} + +static int +get_notify_list_event_count(cib_t *cib, const char *event) +{ + int count = 0; + + for (GList *iter = g_list_first(cib->notify_list); iter != NULL; + iter = iter->next) { + cib_notify_client_t *client = (cib_notify_client_t *) iter->data; + + if (strcmp(client->event, event) == 0) { + count++; + } + } + crm_trace("event(%s) count : %d", event, count); + return count; +} + +static int +cib_client_del_notify_callback(cib_t *cib, const char *event, + void (*callback) (const char *event, + xmlNode *msg)) +{ + GList *list_item = NULL; + cib_notify_client_t *new_client = NULL; + + if (cib->variant != cib_native && cib->variant != cib_remote) { + return -EPROTONOSUPPORT; + } + + if (get_notify_list_event_count(cib, event) == 0) { + crm_debug("The callback of the event does not exist(%s)", event); + return pcmk_ok; + } + + crm_debug("Removing callback for %s events", event); + + new_client = calloc(1, sizeof(cib_notify_client_t)); + new_client->event = event; + new_client->callback = callback; + + list_item = g_list_find_custom(cib->notify_list, new_client, ciblib_GCompareFunc); + + if (list_item != NULL) { + cib_notify_client_t *list_client = list_item->data; + + cib->notify_list = g_list_remove(cib->notify_list, list_client); + free(list_client); + + crm_trace("Removed callback"); + + } else { + crm_trace("Callback not present"); + } + + if (get_notify_list_event_count(cib, event) == 0) { + /* When there is not the registration of the event, the processing turns off a notice. */ + cib->cmds->register_notification(cib, event, 0); + } + + free(new_client); + return pcmk_ok; +} + +static gboolean +cib_async_timeout_handler(gpointer data) +{ + struct timer_rec_s *timer = data; + + crm_debug("Async call %d timed out after %ds", + timer->call_id, timer->timeout); + cib_native_callback(timer->cib, NULL, timer->call_id, -ETIME); + + // We remove the handler in remove_cib_op_callback() + return G_SOURCE_CONTINUE; +} + +static gboolean +cib_client_register_callback_full(cib_t *cib, int call_id, int timeout, + gboolean only_success, void *user_data, + const char *callback_name, + void (*callback)(xmlNode *, int, int, + xmlNode *, void *), + void (*free_func)(void *)) +{ + cib_callback_client_t *blob = NULL; + + if (call_id < 0) { + if (only_success == FALSE) { + callback(NULL, call_id, call_id, NULL, user_data); + } else { + crm_warn("CIB call failed: %s", pcmk_strerror(call_id)); + } + if (user_data && free_func) { + free_func(user_data); + } + return FALSE; + } + + blob = calloc(1, sizeof(cib_callback_client_t)); + blob->id = callback_name; + blob->only_success = only_success; + blob->user_data = user_data; + blob->callback = callback; + blob->free_func = free_func; + + if (timeout > 0) { + struct timer_rec_s *async_timer = NULL; + + async_timer = calloc(1, sizeof(struct timer_rec_s)); + blob->timer = async_timer; + + async_timer->cib = cib; + async_timer->call_id = call_id; + async_timer->timeout = timeout * 1000; + async_timer->ref = g_timeout_add(async_timer->timeout, + cib_async_timeout_handler, + async_timer); + } + + crm_trace("Adding callback %s for call %d", callback_name, call_id); + pcmk__intkey_table_insert(cib_op_callback_table, call_id, blob); + + return TRUE; +} + +static gboolean +cib_client_register_callback(cib_t *cib, int call_id, int timeout, + gboolean only_success, void *user_data, + const char *callback_name, + void (*callback) (xmlNode *, int, int, xmlNode *, + void *)) +{ + return cib_client_register_callback_full(cib, call_id, timeout, + only_success, user_data, + callback_name, callback, NULL); +} + +static int +cib_client_noop(cib_t * cib, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_NOOP, NULL, NULL, NULL, NULL, + call_options, NULL); +} + +static int +cib_client_ping(cib_t * cib, xmlNode ** output_data, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, CRM_OP_PING, NULL, NULL, NULL, output_data, call_options, NULL); +} + +static int +cib_client_query(cib_t * cib, const char *section, xmlNode ** output_data, int call_options) +{ + return cib->cmds->query_from(cib, NULL, section, output_data, call_options); +} + +static int +cib_client_query_from(cib_t * cib, const char *host, const char *section, + xmlNode ** output_data, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_QUERY, host, section, NULL, + output_data, call_options, NULL); +} + +static int +is_primary(cib_t *cib) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_IS_PRIMARY, NULL, NULL, NULL, + NULL, cib_scope_local|cib_sync_call, NULL); +} + +static int +set_secondary(cib_t *cib, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_SECONDARY, NULL, NULL, NULL, + NULL, call_options, NULL); +} + +static int +set_all_secondary(cib_t * cib, int call_options) +{ + return -EPROTONOSUPPORT; +} + +static int +set_primary(cib_t *cib, int call_options) +{ + op_common(cib); + crm_trace("Adding cib_scope_local to options"); + return cib_internal_op(cib, PCMK__CIB_REQUEST_PRIMARY, NULL, NULL, NULL, + NULL, call_options|cib_scope_local, NULL); +} + +static int +cib_client_bump_epoch(cib_t * cib, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_BUMP, NULL, NULL, NULL, NULL, + call_options, NULL); +} + +static int +cib_client_upgrade(cib_t * cib, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_UPGRADE, NULL, NULL, NULL, + NULL, call_options, NULL); +} + +static int +cib_client_sync(cib_t * cib, const char *section, int call_options) +{ + return cib->cmds->sync_from(cib, NULL, section, call_options); +} + +static int +cib_client_sync_from(cib_t * cib, const char *host, const char *section, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_SYNC_TO_ALL, host, section, + NULL, NULL, call_options, NULL); +} + +static int +cib_client_create(cib_t * cib, const char *section, xmlNode * data, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_CREATE, NULL, section, data, + NULL, call_options, NULL); +} + +static int +cib_client_modify(cib_t * cib, const char *section, xmlNode * data, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_MODIFY, NULL, section, data, + NULL, call_options, NULL); +} + +static int +cib_client_replace(cib_t * cib, const char *section, xmlNode * data, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_REPLACE, NULL, section, data, + NULL, call_options, NULL); +} + +static int +cib_client_delete(cib_t * cib, const char *section, xmlNode * data, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_DELETE, NULL, section, data, + NULL, call_options, NULL); +} + +static int +cib_client_delete_absolute(cib_t * cib, const char *section, xmlNode * data, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_ABS_DELETE, NULL, section, + data, NULL, call_options, NULL); +} + +static int +cib_client_erase(cib_t * cib, xmlNode ** output_data, int call_options) +{ + op_common(cib); + return cib_internal_op(cib, PCMK__CIB_REQUEST_ERASE, NULL, NULL, NULL, + output_data, call_options, NULL); +} + +static void +cib_destroy_op_callback(gpointer data) +{ + cib_callback_client_t *blob = data; + + if (blob->timer && blob->timer->ref > 0) { + g_source_remove(blob->timer->ref); + } + free(blob->timer); + + if (blob->user_data && blob->free_func) { + blob->free_func(blob->user_data); + } + + free(blob); +} + +static void +destroy_op_callback_table(void) +{ + if (cib_op_callback_table != NULL) { + g_hash_table_destroy(cib_op_callback_table); + cib_op_callback_table = NULL; + } +} + +char * +get_shadow_file(const char *suffix) +{ + char *cib_home = NULL; + char *fullname = NULL; + char *name = crm_strdup_printf("shadow.%s", suffix); + const char *dir = getenv("CIB_shadow_dir"); + + if (dir == NULL) { + uid_t uid = geteuid(); + struct passwd *pwent = getpwuid(uid); + const char *user = NULL; + + if (pwent) { + user = pwent->pw_name; + } else { + user = getenv("USER"); + crm_perror(LOG_ERR, + "Assuming %s because cannot get user details for user ID %d", + (user? user : "unprivileged user"), uid); + } + + if (pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) { + dir = CRM_CONFIG_DIR; + + } else { + const char *home = NULL; + + if ((home = getenv("HOME")) == NULL) { + if (pwent) { + home = pwent->pw_dir; + } + } + + dir = pcmk__get_tmpdir(); + if (home && home[0] == '/') { + int rc = 0; + + cib_home = crm_strdup_printf("%s/.cib", home); + + rc = mkdir(cib_home, 0700); + if (rc < 0 && errno != EEXIST) { + crm_perror(LOG_ERR, "Couldn't create user-specific shadow directory: %s", + cib_home); + errno = 0; + + } else { + dir = cib_home; + } + } + } + } + + fullname = crm_strdup_printf("%s/%s", dir, name); + free(cib_home); + free(name); + + return fullname; +} + +cib_t * +cib_shadow_new(const char *shadow) +{ + cib_t *new_cib = NULL; + char *shadow_file = NULL; + + CRM_CHECK(shadow != NULL, return NULL); + + shadow_file = get_shadow_file(shadow); + new_cib = cib_file_new(shadow_file); + free(shadow_file); + + return new_cib; +} + +/*! + * \brief Create a new CIB connection object, ignoring any active shadow CIB + * + * Create a new live, file, or remote CIB connection object based on the values + * of CIB-related environment variables (CIB_file, CIB_port, CIB_server, + * CIB_user, and CIB_passwd). The object will not be connected. + * + * \return Newly allocated CIB connection object + * \note The CIB API does not fully support opening multiple CIB connection + * objects simultaneously, so the returned object should be treated as a + * singleton. + */ +cib_t * +cib_new_no_shadow(void) +{ + const char *shadow = getenv("CIB_shadow"); + cib_t *cib = NULL; + + unsetenv("CIB_shadow"); + cib = cib_new(); + + if (shadow != NULL) { + setenv("CIB_shadow", shadow, 1); + } + return cib; +} + +/*! + * \brief Create a new CIB connection object + * + * Create a new live, remote, file, or shadow file CIB connection object based + * on the values of CIB-related environment variables (CIB_shadow, CIB_file, + * CIB_port, CIB_server, CIB_user, and CIB_passwd). The object will not be + * connected. + * + * \return Newly allocated CIB connection object + * \note The CIB API does not fully support opening multiple CIB connection + * objects simultaneously, so the returned object should be treated as a + * singleton. + */ +/* @TODO Ensure all APIs support multiple simultaneous CIB connection objects + * (at least cib_free_callbacks() currently does not). + */ +cib_t * +cib_new(void) +{ + const char *value = getenv("CIB_shadow"); + int port; + + if (value && value[0] != 0) { + return cib_shadow_new(value); + } + + value = getenv("CIB_file"); + if (value) { + return cib_file_new(value); + } + + value = getenv("CIB_port"); + if (value) { + gboolean encrypted = TRUE; + const char *server = getenv("CIB_server"); + const char *user = getenv("CIB_user"); + const char *pass = getenv("CIB_passwd"); + + /* We don't ensure port is valid (>= 0) because cib_new() currently + * can't return NULL in practice, and introducing a NULL return here + * could cause core dumps that would previously just cause signon() + * failures. + */ + pcmk__scan_port(value, &port); + + value = getenv("CIB_encrypted"); + if (value && crm_is_true(value) == FALSE) { + crm_info("Disabling TLS"); + encrypted = FALSE; + } + + if (user == NULL) { + user = CRM_DAEMON_USER; + crm_info("Defaulting to user: %s", user); + } + + if (server == NULL) { + server = "localhost"; + crm_info("Defaulting to localhost"); + } + + return cib_remote_new(server, user, pass, port, encrypted); + } + + return cib_native_new(); +} + +/*! + * \internal + * \brief Create a generic CIB connection instance + * + * \return Newly allocated and initialized cib_t instance + * + * \note This is called by each variant's cib_*_new() function before setting + * variant-specific values. + */ +cib_t * +cib_new_variant(void) +{ + cib_t *new_cib = NULL; + + new_cib = calloc(1, sizeof(cib_t)); + + if (new_cib == NULL) { + return NULL; + } + + remove_cib_op_callback(0, TRUE); /* remove all */ + + new_cib->call_id = 1; + new_cib->variant = cib_undefined; + + new_cib->type = cib_no_connection; + new_cib->state = cib_disconnected; + + new_cib->op_callback = NULL; + new_cib->variant_opaque = NULL; + new_cib->notify_list = NULL; + + /* the rest will get filled in by the variant constructor */ + new_cib->cmds = calloc(1, sizeof(cib_api_operations_t)); + + if (new_cib->cmds == NULL) { + free(new_cib); + return NULL; + } + + new_cib->cmds->set_op_callback = cib_client_set_op_callback; + new_cib->cmds->add_notify_callback = cib_client_add_notify_callback; + new_cib->cmds->del_notify_callback = cib_client_del_notify_callback; + new_cib->cmds->register_callback = cib_client_register_callback; + new_cib->cmds->register_callback_full = cib_client_register_callback_full; + + new_cib->cmds->noop = cib_client_noop; + new_cib->cmds->ping = cib_client_ping; + new_cib->cmds->query = cib_client_query; + new_cib->cmds->sync = cib_client_sync; + + new_cib->cmds->query_from = cib_client_query_from; + new_cib->cmds->sync_from = cib_client_sync_from; + + new_cib->cmds->is_master = is_primary; // Deprecated method + + new_cib->cmds->set_primary = set_primary; + new_cib->cmds->set_master = set_primary; // Deprecated method + + new_cib->cmds->set_secondary = set_secondary; + new_cib->cmds->set_slave = set_secondary; // Deprecated method + + new_cib->cmds->set_slave_all = set_all_secondary; // Deprecated method + + new_cib->cmds->upgrade = cib_client_upgrade; + new_cib->cmds->bump_epoch = cib_client_bump_epoch; + + new_cib->cmds->create = cib_client_create; + new_cib->cmds->modify = cib_client_modify; + new_cib->cmds->update = cib_client_modify; // Deprecated method + new_cib->cmds->replace = cib_client_replace; + new_cib->cmds->remove = cib_client_delete; + new_cib->cmds->erase = cib_client_erase; + + new_cib->cmds->delete_absolute = cib_client_delete_absolute; + + return new_cib; +} + +void +cib_free_notify(cib_t *cib) +{ + + if (cib) { + GList *list = cib->notify_list; + + while (list != NULL) { + cib_notify_client_t *client = g_list_nth_data(list, 0); + + list = g_list_remove(list, client); + free(client); + } + cib->notify_list = NULL; + } +} + +/*! + * \brief Free all callbacks for a CIB connection + * + * \param[in,out] cib CIB connection to clean up + */ +void +cib_free_callbacks(cib_t *cib) +{ + cib_free_notify(cib); + + destroy_op_callback_table(); +} + +/*! + * \brief Free all memory used by CIB connection + * + * \param[in,out] cib CIB connection to delete + */ +void +cib_delete(cib_t *cib) +{ + cib_free_callbacks(cib); + if (cib) { + cib->cmds->free(cib); + } +} + +void +remove_cib_op_callback(int call_id, gboolean all_callbacks) +{ + if (all_callbacks) { + destroy_op_callback_table(); + cib_op_callback_table = pcmk__intkey_table(cib_destroy_op_callback); + } else { + pcmk__intkey_table_remove(cib_op_callback_table, call_id); + } +} + +int +num_cib_op_callbacks(void) +{ + if (cib_op_callback_table == NULL) { + return 0; + } + return g_hash_table_size(cib_op_callback_table); +} + +static void +cib_dump_pending_op(gpointer key, gpointer value, gpointer user_data) +{ + int call = GPOINTER_TO_INT(key); + cib_callback_client_t *blob = value; + + crm_debug("Call %d (%s): pending", call, pcmk__s(blob->id, "without ID")); +} + +void +cib_dump_pending_callbacks(void) +{ + if (cib_op_callback_table == NULL) { + return; + } + return g_hash_table_foreach(cib_op_callback_table, cib_dump_pending_op, NULL); +} + +cib_callback_client_t* +cib__lookup_id (int call_id) +{ + return pcmk__intkey_table_lookup(cib_op_callback_table, call_id); +} diff --git a/lib/cib/cib_file.c b/lib/cib/cib_file.c new file mode 100644 index 0000000..7d05965 --- /dev/null +++ b/lib/cib/cib_file.c @@ -0,0 +1,919 @@ +/* + * Original copyright 2004 International Business Machines + * Later changes copyright 2008-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define CIB_SERIES "cib" +#define CIB_SERIES_MAX 100 +#define CIB_SERIES_BZIP FALSE /* Must be false because archived copies are + created with hard links + */ + +#define CIB_LIVE_NAME CIB_SERIES ".xml" + +enum cib_file_flags { + cib_file_flag_dirty = (1 << 0), + cib_file_flag_live = (1 << 1), +}; + +typedef struct cib_file_opaque_s { + uint32_t flags; // Group of enum cib_file_flags + char *filename; +} cib_file_opaque_t; + +struct cib_func_entry { + const char *op; + gboolean read_only; + cib_op_t fn; +}; + +static struct cib_func_entry cib_file_ops[] = { + { PCMK__CIB_REQUEST_QUERY, TRUE, cib_process_query }, + { PCMK__CIB_REQUEST_MODIFY, FALSE, cib_process_modify }, + { PCMK__CIB_REQUEST_APPLY_PATCH, FALSE, cib_process_diff }, + { PCMK__CIB_REQUEST_BUMP, FALSE, cib_process_bump }, + { PCMK__CIB_REQUEST_REPLACE, FALSE, cib_process_replace }, + { PCMK__CIB_REQUEST_CREATE, FALSE, cib_process_create }, + { PCMK__CIB_REQUEST_DELETE, FALSE, cib_process_delete }, + { PCMK__CIB_REQUEST_ERASE, FALSE, cib_process_erase }, + { PCMK__CIB_REQUEST_UPGRADE, FALSE, cib_process_upgrade }, +}; + +static xmlNode *in_mem_cib = NULL; + +/* cib_file_backup() and cib_file_write_with_digest() need to chown the + * written files only in limited circumstances, so these variables allow + * that to be indicated without affecting external callers + */ +static uid_t cib_file_owner = 0; +static uid_t cib_file_group = 0; +static gboolean cib_do_chown = FALSE; + +#define cib_set_file_flags(cibfile, flags_to_set) do { \ + (cibfile)->flags = pcmk__set_flags_as(__func__, __LINE__, \ + LOG_TRACE, "CIB file", \ + cibfile->filename, \ + (cibfile)->flags, \ + (flags_to_set), \ + #flags_to_set); \ + } while (0) + +#define cib_clear_file_flags(cibfile, flags_to_clear) do { \ + (cibfile)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ + LOG_TRACE, "CIB file", \ + cibfile->filename, \ + (cibfile)->flags, \ + (flags_to_clear), \ + #flags_to_clear); \ + } while (0) + +/*! + * \internal + * \brief Check whether a file is the live CIB + * + * \param[in] filename Name of file to check + * + * \return TRUE if file exists and its real path is same as live CIB's + */ +static gboolean +cib_file_is_live(const char *filename) +{ + gboolean same = FALSE; + + if (filename != NULL) { + // Canonicalize file names for true comparison + char *real_filename = NULL; + + if (pcmk__real_path(filename, &real_filename) == pcmk_rc_ok) { + char *real_livename = NULL; + + if (pcmk__real_path(CRM_CONFIG_DIR "/" CIB_LIVE_NAME, + &real_livename) == pcmk_rc_ok) { + same = !strcmp(real_filename, real_livename); + free(real_livename); + } + free(real_filename); + } + } + return same; +} + +static int +cib_file_perform_op_delegate(cib_t *cib, const char *op, const char *host, + const char *section, xmlNode *data, + xmlNode **output_data, int call_options, + const char *user_name) +{ + int rc = pcmk_ok; + char *effective_user = NULL; + gboolean query = FALSE; + gboolean changed = FALSE; + xmlNode *request = NULL; + xmlNode *output = NULL; + xmlNode *cib_diff = NULL; + xmlNode *result_cib = NULL; + cib_op_t *fn = NULL; + int lpc = 0; + static int max_msg_types = PCMK__NELEM(cib_file_ops); + cib_file_opaque_t *private = cib->variant_opaque; + + crm_info("Handling %s operation for %s as %s", + (op? op : "invalid"), (section? section : "entire CIB"), + (user_name? user_name : "default user")); + + cib__set_call_options(call_options, "file operation", + cib_no_mtime|cib_inhibit_bcast|cib_scope_local); + + if (cib->state == cib_disconnected) { + return -ENOTCONN; + } + + if (output_data != NULL) { + *output_data = NULL; + } + + if (op == NULL) { + return -EINVAL; + } + + for (lpc = 0; lpc < max_msg_types; lpc++) { + if (pcmk__str_eq(op, cib_file_ops[lpc].op, pcmk__str_casei)) { + fn = &(cib_file_ops[lpc].fn); + query = cib_file_ops[lpc].read_only; + break; + } + } + + if (fn == NULL) { + return -EPROTONOSUPPORT; + } + + cib->call_id++; + request = cib_create_op(cib->call_id, op, host, section, data, call_options, + user_name); + if(user_name) { + crm_xml_add(request, XML_ACL_TAG_USER, user_name); + } + + /* Mirror the logic in cib_prepare_common() */ + if (section != NULL && data != NULL && pcmk__str_eq(crm_element_name(data), XML_TAG_CIB, pcmk__str_none)) { + data = pcmk_find_cib_element(data, section); + } + + rc = cib_perform_op(op, call_options, fn, query, + section, request, data, TRUE, &changed, in_mem_cib, &result_cib, &cib_diff, + &output); + + free_xml(request); + if (rc == -pcmk_err_schema_validation) { + validate_xml_verbose(result_cib); + } + + if (rc != pcmk_ok) { + free_xml(result_cib); + + } else if (query == FALSE) { + pcmk__output_t *out = NULL; + + rc = pcmk_rc2legacy(pcmk__log_output_new(&out)); + CRM_CHECK(rc == pcmk_ok, goto done); + + pcmk__output_set_log_level(out, LOG_DEBUG); + rc = out->message(out, "xml-patchset", cib_diff); + out->finish(out, pcmk_rc2exitc(rc), true, NULL); + pcmk__output_free(out); + rc = pcmk_ok; + + free_xml(in_mem_cib); + in_mem_cib = result_cib; + cib_set_file_flags(private, cib_file_flag_dirty); + } + + if (cib->op_callback != NULL) { + cib->op_callback(NULL, cib->call_id, rc, output); + } + + if ((output_data != NULL) && (output != NULL)) { + *output_data = (output == in_mem_cib)? copy_xml(output) : output; + } + +done: + free_xml(cib_diff); + + if ((output_data == NULL) && (output != in_mem_cib)) { + /* Don't free output if we're still using it. (output_data != NULL) + * means we may have assigned *output_data = output above. + */ + free_xml(output); + } + free(effective_user); + return rc; +} + +/*! + * \internal + * \brief Read CIB from disk and validate it against XML schema + * + * \param[in] filename Name of file to read CIB from + * + * \return pcmk_ok on success, + * -ENXIO if file does not exist (or stat() otherwise fails), or + * -pcmk_err_schema_validation if XML doesn't parse or validate + * \note If filename is the live CIB, this will *not* verify its digest, + * though that functionality would be trivial to add here. + * Also, this will *not* verify that the file is writable, + * because some callers might not need to write. + */ +static int +load_file_cib(const char *filename) +{ + struct stat buf; + xmlNode *root = NULL; + + /* Ensure file is readable */ + if (strcmp(filename, "-") && (stat(filename, &buf) < 0)) { + return -ENXIO; + } + + /* Parse XML from file */ + root = filename2xml(filename); + if (root == NULL) { + return -pcmk_err_schema_validation; + } + + /* Add a status section if not already present */ + if (find_xml_node(root, XML_CIB_TAG_STATUS, FALSE) == NULL) { + create_xml_node(root, XML_CIB_TAG_STATUS); + } + + /* Validate XML against its specified schema */ + if (validate_xml(root, NULL, TRUE) == FALSE) { + const char *schema = crm_element_value(root, XML_ATTR_VALIDATION); + + crm_err("CIB does not validate against %s", schema); + free_xml(root); + return -pcmk_err_schema_validation; + } + + /* Remember the parsed XML for later use */ + in_mem_cib = root; + return pcmk_ok; +} + +static int +cib_file_signon(cib_t *cib, const char *name, enum cib_conn_type type) +{ + int rc = pcmk_ok; + cib_file_opaque_t *private = cib->variant_opaque; + + if (private->filename == NULL) { + rc = -EINVAL; + } else { + rc = load_file_cib(private->filename); + } + + if (rc == pcmk_ok) { + crm_debug("Opened connection to local file '%s' for %s", + private->filename, name); + cib->state = cib_connected_command; + cib->type = cib_command; + + } else { + crm_info("Connection to local file '%s' for %s failed: %s\n", + private->filename, name, pcmk_strerror(rc)); + } + return rc; +} + +/*! + * \internal + * \brief Write out the in-memory CIB to a live CIB file + * + * param[in,out] path Full path to file to write + * + * \return 0 on success, -1 on failure + */ +static int +cib_file_write_live(char *path) +{ + uid_t uid = geteuid(); + struct passwd *daemon_pwent; + char *sep = strrchr(path, '/'); + const char *cib_dirname, *cib_filename; + int rc = 0; + + /* Get the desired uid/gid */ + errno = 0; + daemon_pwent = getpwnam(CRM_DAEMON_USER); + if (daemon_pwent == NULL) { + crm_perror(LOG_ERR, "Could not find %s user", CRM_DAEMON_USER); + return -1; + } + + /* If we're root, we can change the ownership; + * if we're daemon, anything we create will be OK; + * otherwise, block access so we don't create wrong owner + */ + if ((uid != 0) && (uid != daemon_pwent->pw_uid)) { + crm_perror(LOG_ERR, "Must be root or %s to modify live CIB", + CRM_DAEMON_USER); + return 0; + } + + /* fancy footwork to separate dirname from filename + * (we know the canonical name maps to the live CIB, + * but the given name might be relative, or symlinked) + */ + if (sep == NULL) { /* no directory component specified */ + cib_dirname = "./"; + cib_filename = path; + } else if (sep == path) { /* given name is in / */ + cib_dirname = "/"; + cib_filename = path + 1; + } else { /* typical case; split given name into parts */ + *sep = '\0'; + cib_dirname = path; + cib_filename = sep + 1; + } + + /* if we're root, we want to update the file ownership */ + if (uid == 0) { + cib_file_owner = daemon_pwent->pw_uid; + cib_file_group = daemon_pwent->pw_gid; + cib_do_chown = TRUE; + } + + /* write the file */ + if (cib_file_write_with_digest(in_mem_cib, cib_dirname, + cib_filename) != pcmk_ok) { + rc = -1; + } + + /* turn off file ownership changes, for other callers */ + if (uid == 0) { + cib_do_chown = FALSE; + } + + /* undo fancy stuff */ + if ((sep != NULL) && (*sep == '\0')) { + *sep = '/'; + } + + return rc; +} + +/*! + * \internal + * \brief Sign-off method for CIB file variants + * + * This will write the file to disk if needed, and free the in-memory CIB. If + * the file is the live CIB, it will compute and write a signature as well. + * + * \param[in,out] cib CIB object to sign off + * + * \return pcmk_ok on success, pcmk_err_generic on failure + * \todo This method should refuse to write the live CIB if the CIB manager is + * running. + */ +static int +cib_file_signoff(cib_t *cib) +{ + int rc = pcmk_ok; + cib_file_opaque_t *private = cib->variant_opaque; + + crm_debug("Disconnecting from the CIB manager"); + cib->state = cib_disconnected; + cib->type = cib_no_connection; + + /* If the in-memory CIB has been changed, write it to disk */ + if (pcmk_is_set(private->flags, cib_file_flag_dirty)) { + + /* If this is the live CIB, write it out with a digest */ + if (pcmk_is_set(private->flags, cib_file_flag_live)) { + if (cib_file_write_live(private->filename) < 0) { + rc = pcmk_err_generic; + } + + /* Otherwise, it's a simple write */ + } else { + gboolean do_bzip = pcmk__ends_with_ext(private->filename, ".bz2"); + + if (write_xml_file(in_mem_cib, private->filename, do_bzip) <= 0) { + rc = pcmk_err_generic; + } + } + + if (rc == pcmk_ok) { + crm_info("Wrote CIB to %s", private->filename); + cib_clear_file_flags(private, cib_file_flag_dirty); + } else { + crm_err("Could not write CIB to %s", private->filename); + } + } + + /* Free the in-memory CIB */ + free_xml(in_mem_cib); + in_mem_cib = NULL; + return rc; +} + +static int +cib_file_free(cib_t *cib) +{ + int rc = pcmk_ok; + + if (cib->state != cib_disconnected) { + rc = cib_file_signoff(cib); + } + + if (rc == pcmk_ok) { + cib_file_opaque_t *private = cib->variant_opaque; + + free(private->filename); + free(cib->cmds); + free(private); + free(cib); + + } else { + fprintf(stderr, "Couldn't sign off: %d\n", rc); + } + + return rc; +} + +static int +cib_file_inputfd(cib_t *cib) +{ + return -EPROTONOSUPPORT; +} + +static int +cib_file_register_notification(cib_t *cib, const char *callback, int enabled) +{ + return -EPROTONOSUPPORT; +} + +static int +cib_file_set_connection_dnotify(cib_t *cib, + void (*dnotify) (gpointer user_data)) +{ + return -EPROTONOSUPPORT; +} + +/*! + * \internal + * \brief Get the given CIB connection's unique client identifier + * + * \param[in] cib CIB connection + * \param[out] async_id If not \p NULL, where to store asynchronous client ID + * \param[out] sync_id If not \p NULL, where to store synchronous client ID + * + * \return Legacy Pacemaker return code (specifically, \p -EPROTONOSUPPORT) + * + * \note This is the \p cib_file variant implementation of + * \p cib_api_operations_t:client_id(). + * \note A \p cib_file object doesn't connect to the CIB and is never assigned a + * client ID. + */ +static int +cib_file_client_id(const cib_t *cib, const char **async_id, + const char **sync_id) +{ + if (async_id != NULL) { + *async_id = NULL; + } + if (sync_id != NULL) { + *sync_id = NULL; + } + return -EPROTONOSUPPORT; +} + +cib_t * +cib_file_new(const char *cib_location) +{ + cib_file_opaque_t *private = NULL; + cib_t *cib = cib_new_variant(); + + if (cib == NULL) { + return NULL; + } + + private = calloc(1, sizeof(cib_file_opaque_t)); + + if (private == NULL) { + free(cib); + return NULL; + } + + cib->variant = cib_file; + cib->variant_opaque = private; + + if (cib_location == NULL) { + cib_location = getenv("CIB_file"); + CRM_CHECK(cib_location != NULL, return NULL); // Shouldn't be possible + } + private->flags = 0; + if (cib_file_is_live(cib_location)) { + cib_set_file_flags(private, cib_file_flag_live); + crm_trace("File %s detected as live CIB", cib_location); + } + private->filename = strdup(cib_location); + + /* assign variant specific ops */ + cib->delegate_fn = cib_file_perform_op_delegate; + cib->cmds->signon = cib_file_signon; + cib->cmds->signoff = cib_file_signoff; + cib->cmds->free = cib_file_free; + cib->cmds->inputfd = cib_file_inputfd; + + cib->cmds->register_notification = cib_file_register_notification; + cib->cmds->set_connection_dnotify = cib_file_set_connection_dnotify; + + cib->cmds->client_id = cib_file_client_id; + + return cib; +} + +/*! + * \internal + * \brief Compare the calculated digest of an XML tree against a signature file + * + * \param[in] root Root of XML tree to compare + * \param[in] sigfile Name of signature file containing digest to compare + * + * \return TRUE if digests match or signature file does not exist, else FALSE + */ +static gboolean +cib_file_verify_digest(xmlNode *root, const char *sigfile) +{ + gboolean passed = FALSE; + char *expected; + int rc = pcmk__file_contents(sigfile, &expected); + + switch (rc) { + case pcmk_rc_ok: + if (expected == NULL) { + crm_err("On-disk digest at %s is empty", sigfile); + return FALSE; + } + break; + case ENOENT: + crm_warn("No on-disk digest present at %s", sigfile); + return TRUE; + default: + crm_err("Could not read on-disk digest from %s: %s", + sigfile, pcmk_rc_str(rc)); + return FALSE; + } + passed = pcmk__verify_digest(root, expected); + free(expected); + return passed; +} + +/*! + * \internal + * \brief Read an XML tree from a file and verify its digest + * + * \param[in] filename Name of XML file to read + * \param[in] sigfile Name of signature file containing digest to compare + * \param[out] root If non-NULL, will be set to pointer to parsed XML tree + * + * \return 0 if file was successfully read, parsed and verified, otherwise: + * -errno on stat() failure, + * -pcmk_err_cib_corrupt if file size is 0 or XML is not parseable, or + * -pcmk_err_cib_modified if digests do not match + * \note If root is non-NULL, it is the caller's responsibility to free *root on + * successful return. + */ +int +cib_file_read_and_verify(const char *filename, const char *sigfile, xmlNode **root) +{ + int s_res; + struct stat buf; + char *local_sigfile = NULL; + xmlNode *local_root = NULL; + + CRM_ASSERT(filename != NULL); + if (root) { + *root = NULL; + } + + /* Verify that file exists and its size is nonzero */ + s_res = stat(filename, &buf); + if (s_res < 0) { + crm_perror(LOG_WARNING, "Could not verify cluster configuration file %s", filename); + return -errno; + } else if (buf.st_size == 0) { + crm_warn("Cluster configuration file %s is corrupt (size is zero)", filename); + return -pcmk_err_cib_corrupt; + } + + /* Parse XML */ + local_root = filename2xml(filename); + if (local_root == NULL) { + crm_warn("Cluster configuration file %s is corrupt (unparseable as XML)", filename); + return -pcmk_err_cib_corrupt; + } + + /* If sigfile is not specified, use original file name plus .sig */ + if (sigfile == NULL) { + sigfile = local_sigfile = crm_strdup_printf("%s.sig", filename); + } + + /* Verify that digests match */ + if (cib_file_verify_digest(local_root, sigfile) == FALSE) { + free(local_sigfile); + free_xml(local_root); + return -pcmk_err_cib_modified; + } + + free(local_sigfile); + if (root) { + *root = local_root; + } else { + free_xml(local_root); + } + return pcmk_ok; +} + +/*! + * \internal + * \brief Back up a CIB + * + * \param[in] cib_dirname Directory containing CIB file and backups + * \param[in] cib_filename Name (relative to cib_dirname) of CIB file to back up + * + * \return 0 on success, -1 on error + */ +static int +cib_file_backup(const char *cib_dirname, const char *cib_filename) +{ + int rc = 0; + unsigned int seq; + char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename); + char *cib_digest = crm_strdup_printf("%s.sig", cib_path); + char *backup_path; + char *backup_digest; + + // Determine backup and digest file names + if (pcmk__read_series_sequence(cib_dirname, CIB_SERIES, + &seq) != pcmk_rc_ok) { + // @TODO maybe handle errors better ... + seq = 0; + } + backup_path = pcmk__series_filename(cib_dirname, CIB_SERIES, seq, + CIB_SERIES_BZIP); + backup_digest = crm_strdup_printf("%s.sig", backup_path); + + /* Remove the old backups if they exist */ + unlink(backup_path); + unlink(backup_digest); + + /* Back up the CIB, by hard-linking it to the backup name */ + if ((link(cib_path, backup_path) < 0) && (errno != ENOENT)) { + crm_perror(LOG_ERR, "Could not archive %s by linking to %s", + cib_path, backup_path); + rc = -1; + + /* Back up the CIB signature similarly */ + } else if ((link(cib_digest, backup_digest) < 0) && (errno != ENOENT)) { + crm_perror(LOG_ERR, "Could not archive %s by linking to %s", + cib_digest, backup_digest); + rc = -1; + + /* Update the last counter and ensure everything is sync'd to media */ + } else { + pcmk__write_series_sequence(cib_dirname, CIB_SERIES, ++seq, + CIB_SERIES_MAX); + if (cib_do_chown) { + int rc2; + + if ((chown(backup_path, cib_file_owner, cib_file_group) < 0) + && (errno != ENOENT)) { + crm_perror(LOG_ERR, "Could not set owner of %s", backup_path); + rc = -1; + } + if ((chown(backup_digest, cib_file_owner, cib_file_group) < 0) + && (errno != ENOENT)) { + crm_perror(LOG_ERR, "Could not set owner of %s", backup_digest); + rc = -1; + } + rc2 = pcmk__chown_series_sequence(cib_dirname, CIB_SERIES, + cib_file_owner, cib_file_group); + if (rc2 != pcmk_rc_ok) { + crm_err("Could not set owner of sequence file in %s: %s", + cib_dirname, pcmk_rc_str(rc2)); + rc = -1; + } + } + pcmk__sync_directory(cib_dirname); + crm_info("Archived previous version as %s", backup_path); + } + + free(cib_path); + free(cib_digest); + free(backup_path); + free(backup_digest); + return rc; +} + +/*! + * \internal + * \brief Prepare CIB XML to be written to disk + * + * Set num_updates to 0, set cib-last-written to the current timestamp, + * and strip out the status section. + * + * \param[in,out] root Root of CIB XML tree + * + * \return void + */ +static void +cib_file_prepare_xml(xmlNode *root) +{ + xmlNode *cib_status_root = NULL; + + /* Always write out with num_updates=0 and current last-written timestamp */ + crm_xml_add(root, XML_ATTR_NUMUPDATES, "0"); + pcmk__xe_add_last_written(root); + + /* Delete status section before writing to file, because + * we discard it on startup anyway, and users get confused by it */ + cib_status_root = find_xml_node(root, XML_CIB_TAG_STATUS, TRUE); + CRM_LOG_ASSERT(cib_status_root != NULL); + if (cib_status_root != NULL) { + free_xml(cib_status_root); + } +} + +/*! + * \internal + * \brief Write CIB to disk, along with a signature file containing its digest + * + * \param[in,out] cib_root Root of XML tree to write + * \param[in] cib_dirname Directory containing CIB and signature files + * \param[in] cib_filename Name (relative to cib_dirname) of file to write + * + * \return pcmk_ok on success, + * pcmk_err_cib_modified if existing cib_filename doesn't match digest, + * pcmk_err_cib_backup if existing cib_filename couldn't be backed up, + * or pcmk_err_cib_save if new cib_filename couldn't be saved + */ +int +cib_file_write_with_digest(xmlNode *cib_root, const char *cib_dirname, + const char *cib_filename) +{ + int exit_rc = pcmk_ok; + int rc, fd; + char *digest = NULL; + + /* Detect CIB version for diagnostic purposes */ + const char *epoch = crm_element_value(cib_root, XML_ATTR_GENERATION); + const char *admin_epoch = crm_element_value(cib_root, + XML_ATTR_GENERATION_ADMIN); + + /* Determine full CIB and signature pathnames */ + char *cib_path = crm_strdup_printf("%s/%s", cib_dirname, cib_filename); + char *digest_path = crm_strdup_printf("%s.sig", cib_path); + + /* Create temporary file name patterns for writing out CIB and signature */ + char *tmp_cib = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname); + char *tmp_digest = crm_strdup_printf("%s/cib.XXXXXX", cib_dirname); + + CRM_ASSERT((cib_path != NULL) && (digest_path != NULL) + && (tmp_cib != NULL) && (tmp_digest != NULL)); + + /* Ensure the admin didn't modify the existing CIB underneath us */ + crm_trace("Reading cluster configuration file %s", cib_path); + rc = cib_file_read_and_verify(cib_path, NULL, NULL); + if ((rc != pcmk_ok) && (rc != -ENOENT)) { + crm_err("%s was manually modified while the cluster was active!", + cib_path); + exit_rc = pcmk_err_cib_modified; + goto cleanup; + } + + /* Back up the existing CIB */ + if (cib_file_backup(cib_dirname, cib_filename) < 0) { + exit_rc = pcmk_err_cib_backup; + goto cleanup; + } + + crm_debug("Writing CIB to disk"); + umask(S_IWGRP | S_IWOTH | S_IROTH); + cib_file_prepare_xml(cib_root); + + /* Write the CIB to a temporary file, so we can deploy (near) atomically */ + fd = mkstemp(tmp_cib); + if (fd < 0) { + crm_perror(LOG_ERR, "Couldn't open temporary file %s for writing CIB", + tmp_cib); + exit_rc = pcmk_err_cib_save; + goto cleanup; + } + + /* Protect the temporary file */ + if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) { + crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", + tmp_cib); + exit_rc = pcmk_err_cib_save; + goto cleanup; + } + if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) { + crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", + tmp_cib); + exit_rc = pcmk_err_cib_save; + goto cleanup; + } + + /* Write out the CIB */ + if (write_xml_fd(cib_root, tmp_cib, fd, FALSE) <= 0) { + crm_err("Changes couldn't be written to %s", tmp_cib); + exit_rc = pcmk_err_cib_save; + goto cleanup; + } + + /* Calculate CIB digest */ + digest = calculate_on_disk_digest(cib_root); + CRM_ASSERT(digest != NULL); + crm_info("Wrote version %s.%s.0 of the CIB to disk (digest: %s)", + (admin_epoch ? admin_epoch : "0"), (epoch ? epoch : "0"), digest); + + /* Write the CIB digest to a temporary file */ + fd = mkstemp(tmp_digest); + if (fd < 0) { + crm_perror(LOG_ERR, "Could not create temporary file for CIB digest"); + exit_rc = pcmk_err_cib_save; + goto cleanup; + } + if (cib_do_chown && (fchown(fd, cib_file_owner, cib_file_group) < 0)) { + crm_perror(LOG_ERR, "Couldn't protect temporary file %s for writing CIB", + tmp_cib); + exit_rc = pcmk_err_cib_save; + close(fd); + goto cleanup; + } + rc = pcmk__write_sync(fd, digest); + if (rc != pcmk_rc_ok) { + crm_err("Could not write digest to %s: %s", + tmp_digest, pcmk_rc_str(rc)); + exit_rc = pcmk_err_cib_save; + close(fd); + goto cleanup; + } + close(fd); + crm_debug("Wrote digest %s to disk", digest); + + /* Verify that what we wrote is sane */ + crm_info("Reading cluster configuration file %s (digest: %s)", + tmp_cib, tmp_digest); + rc = cib_file_read_and_verify(tmp_cib, tmp_digest, NULL); + CRM_ASSERT(rc == 0); + + /* Rename temporary files to live, and sync directory changes to media */ + crm_debug("Activating %s", tmp_cib); + if (rename(tmp_cib, cib_path) < 0) { + crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_cib, cib_path); + exit_rc = pcmk_err_cib_save; + } + if (rename(tmp_digest, digest_path) < 0) { + crm_perror(LOG_ERR, "Couldn't rename %s as %s", tmp_digest, + digest_path); + exit_rc = pcmk_err_cib_save; + } + pcmk__sync_directory(cib_dirname); + + cleanup: + free(cib_path); + free(digest_path); + free(digest); + free(tmp_digest); + free(tmp_cib); + return exit_rc; +} diff --git a/lib/cib/cib_native.c b/lib/cib/cib_native.c new file mode 100644 index 0000000..4a87f56 --- /dev/null +++ b/lib/cib/cib_native.c @@ -0,0 +1,502 @@ +/* + * Copyright 2004 International Business Machines + * Later changes copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +typedef struct cib_native_opaque_s { + char *token; + crm_ipc_t *ipc; + void (*dnotify_fn) (gpointer user_data); + mainloop_io_t *source; +} cib_native_opaque_t; + +static int +cib_native_perform_op_delegate(cib_t *cib, const char *op, const char *host, + const char *section, xmlNode *data, + xmlNode **output_data, int call_options, + const char *user_name) +{ + int rc = pcmk_ok; + int reply_id = 0; + enum crm_ipc_flags ipc_flags = crm_ipc_flags_none; + + xmlNode *op_msg = NULL; + xmlNode *op_reply = NULL; + + cib_native_opaque_t *native = cib->variant_opaque; + + if (cib->state == cib_disconnected) { + return -ENOTCONN; + } + + if (output_data != NULL) { + *output_data = NULL; + } + + if (op == NULL) { + crm_err("No operation specified"); + return -EINVAL; + } + + if (call_options & cib_sync_call) { + pcmk__set_ipc_flags(ipc_flags, "client", crm_ipc_client_response); + } + + cib->call_id++; + if (cib->call_id < 1) { + cib->call_id = 1; + } + + op_msg = cib_create_op(cib->call_id, op, host, section, data, call_options, + user_name); + if (op_msg == NULL) { + return -EPROTO; + } + + crm_trace("Sending %s message to the CIB manager (timeout=%ds)", op, cib->call_timeout); + rc = crm_ipc_send(native->ipc, op_msg, ipc_flags, cib->call_timeout * 1000, &op_reply); + free_xml(op_msg); + + if (rc < 0) { + crm_err("Couldn't perform %s operation (timeout=%ds): %s (%d)", op, + cib->call_timeout, pcmk_strerror(rc), rc); + rc = -ECOMM; + goto done; + } + + crm_log_xml_trace(op_reply, "Reply"); + + if (!(call_options & cib_sync_call)) { + crm_trace("Async call, returning %d", cib->call_id); + CRM_CHECK(cib->call_id != 0, return -ENOMSG); + free_xml(op_reply); + return cib->call_id; + } + + rc = pcmk_ok; + crm_element_value_int(op_reply, F_CIB_CALLID, &reply_id); + if (reply_id == cib->call_id) { + xmlNode *tmp = get_message_xml(op_reply, F_CIB_CALLDATA); + + crm_trace("Synchronous reply %d received", reply_id); + if (crm_element_value_int(op_reply, F_CIB_RC, &rc) != 0) { + rc = -EPROTO; + } + + if (output_data == NULL || (call_options & cib_discard_reply)) { + crm_trace("Discarding reply"); + + } else if (tmp != NULL) { + *output_data = copy_xml(tmp); + } + + } else if (reply_id <= 0) { + crm_err("Received bad reply: No id set"); + crm_log_xml_err(op_reply, "Bad reply"); + rc = -ENOMSG; + goto done; + + } else { + crm_err("Received bad reply: %d (wanted %d)", reply_id, cib->call_id); + crm_log_xml_err(op_reply, "Old reply"); + rc = -ENOMSG; + goto done; + } + + if (op_reply == NULL && cib->state == cib_disconnected) { + rc = -ENOTCONN; + + } else if (rc == pcmk_ok && op_reply == NULL) { + rc = -ETIME; + } + + switch (rc) { + case pcmk_ok: + case -EPERM: + break; + + /* This is an internal value that clients do not and should not care about */ + case -pcmk_err_diff_resync: + rc = pcmk_ok; + break; + + /* These indicate internal problems */ + case -EPROTO: + case -ENOMSG: + crm_err("Call failed: %s", pcmk_strerror(rc)); + if (op_reply) { + crm_log_xml_err(op_reply, "Invalid reply"); + } + break; + + default: + if (!pcmk__str_eq(op, PCMK__CIB_REQUEST_QUERY, pcmk__str_none)) { + crm_warn("Call failed: %s", pcmk_strerror(rc)); + } + } + + done: + if (!crm_ipc_connected(native->ipc)) { + crm_err("The CIB manager disconnected"); + cib->state = cib_disconnected; + } + + free_xml(op_reply); + return rc; +} + +static int +cib_native_dispatch_internal(const char *buffer, ssize_t length, + gpointer userdata) +{ + const char *type = NULL; + xmlNode *msg = NULL; + + cib_t *cib = userdata; + + crm_trace("dispatching %p", userdata); + + if (cib == NULL) { + crm_err("No CIB!"); + return 0; + } + + msg = string2xml(buffer); + + if (msg == NULL) { + crm_warn("Received a NULL message from the CIB manager"); + return 0; + } + + /* do callbacks */ + type = crm_element_value(msg, F_TYPE); + crm_trace("Activating %s callbacks...", type); + crm_log_xml_explicit(msg, "cib-reply"); + + if (pcmk__str_eq(type, T_CIB, pcmk__str_casei)) { + cib_native_callback(cib, msg, 0, 0); + + } else if (pcmk__str_eq(type, T_CIB_NOTIFY, pcmk__str_casei)) { + g_list_foreach(cib->notify_list, cib_native_notify, msg); + + } else { + crm_err("Unknown message type: %s", type); + } + + free_xml(msg); + return 0; +} + +static void +cib_native_destroy(void *userdata) +{ + cib_t *cib = userdata; + cib_native_opaque_t *native = cib->variant_opaque; + + crm_trace("destroying %p", userdata); + cib->state = cib_disconnected; + native->source = NULL; + native->ipc = NULL; + + if (native->dnotify_fn) { + native->dnotify_fn(userdata); + } +} + +static int +cib_native_signoff(cib_t *cib) +{ + cib_native_opaque_t *native = cib->variant_opaque; + + crm_debug("Disconnecting from the CIB manager"); + + cib_free_notify(cib); + remove_cib_op_callback(0, TRUE); + + if (native->source != NULL) { + /* Attached to mainloop */ + mainloop_del_ipc_client(native->source); + native->source = NULL; + native->ipc = NULL; + + } else if (native->ipc) { + /* Not attached to mainloop */ + crm_ipc_t *ipc = native->ipc; + + native->ipc = NULL; + crm_ipc_close(ipc); + crm_ipc_destroy(ipc); + } + + cib->state = cib_disconnected; + cib->type = cib_no_connection; + + return pcmk_ok; +} + +static int +cib_native_signon_raw(cib_t *cib, const char *name, enum cib_conn_type type, + int *async_fd) +{ + int rc = pcmk_ok; + const char *channel = NULL; + cib_native_opaque_t *native = cib->variant_opaque; + + struct ipc_client_callbacks cib_callbacks = { + .dispatch = cib_native_dispatch_internal, + .destroy = cib_native_destroy + }; + + cib->call_timeout = PCMK__IPC_TIMEOUT; + + if (type == cib_command) { + cib->state = cib_connected_command; + channel = PCMK__SERVER_BASED_RW; + + } else if (type == cib_command_nonblocking) { + cib->state = cib_connected_command; + channel = PCMK__SERVER_BASED_SHM; + + } else if (type == cib_query) { + cib->state = cib_connected_query; + channel = PCMK__SERVER_BASED_RO; + + } else { + return -ENOTCONN; + } + + crm_trace("Connecting %s channel", channel); + + if (async_fd != NULL) { + native->ipc = crm_ipc_new(channel, 0); + + if (native->ipc && crm_ipc_connect(native->ipc)) { + *async_fd = crm_ipc_get_fd(native->ipc); + + } else if (native->ipc) { + rc = -ENOTCONN; + } + + } else { + native->source = + mainloop_add_ipc_client(channel, G_PRIORITY_HIGH, 512 * 1024 /* 512k */ , cib, + &cib_callbacks); + native->ipc = mainloop_get_ipc_client(native->source); + } + + if (rc != pcmk_ok || native->ipc == NULL || !crm_ipc_connected(native->ipc)) { + crm_info("Could not connect to CIB manager for %s", name); + rc = -ENOTCONN; + } + + if (rc == pcmk_ok) { + xmlNode *reply = NULL; + xmlNode *hello = create_xml_node(NULL, "cib_command"); + + crm_xml_add(hello, F_TYPE, T_CIB); + crm_xml_add(hello, F_CIB_OPERATION, CRM_OP_REGISTER); + crm_xml_add(hello, F_CIB_CLIENTNAME, name); + crm_xml_add_int(hello, F_CIB_CALLOPTS, cib_sync_call); + + if (crm_ipc_send(native->ipc, hello, crm_ipc_client_response, -1, &reply) > 0) { + const char *msg_type = crm_element_value(reply, F_CIB_OPERATION); + + rc = pcmk_ok; + crm_log_xml_trace(reply, "reg-reply"); + + if (!pcmk__str_eq(msg_type, CRM_OP_REGISTER, pcmk__str_casei)) { + crm_info("Reply to CIB registration message has " + "unknown type '%s'", msg_type); + rc = -EPROTO; + + } else { + native->token = crm_element_value_copy(reply, F_CIB_CLIENTID); + if (native->token == NULL) { + rc = -EPROTO; + } + } + free_xml(reply); + + } else { + rc = -ECOMM; + } + + free_xml(hello); + } + + if (rc == pcmk_ok) { + crm_info("Successfully connected to CIB manager for %s", name); + return pcmk_ok; + } + + crm_info("Connection to CIB manager for %s failed: %s", + name, pcmk_strerror(rc)); + cib_native_signoff(cib); + return rc; +} + +static int +cib_native_signon(cib_t *cib, const char *name, enum cib_conn_type type) +{ + return cib_native_signon_raw(cib, name, type, NULL); +} + +static int +cib_native_free(cib_t *cib) +{ + int rc = pcmk_ok; + + if (cib->state != cib_disconnected) { + rc = cib_native_signoff(cib); + } + + if (cib->state == cib_disconnected) { + cib_native_opaque_t *native = cib->variant_opaque; + + free(native->token); + free(cib->variant_opaque); + free(cib->cmds); + free(cib); + } + + return rc; +} + +static int +cib_native_register_notification(cib_t *cib, const char *callback, int enabled) +{ + int rc = pcmk_ok; + xmlNode *notify_msg = create_xml_node(NULL, "cib-callback"); + cib_native_opaque_t *native = cib->variant_opaque; + + if (cib->state != cib_disconnected) { + crm_xml_add(notify_msg, F_CIB_OPERATION, T_CIB_NOTIFY); + crm_xml_add(notify_msg, F_CIB_NOTIFY_TYPE, callback); + crm_xml_add_int(notify_msg, F_CIB_NOTIFY_ACTIVATE, enabled); + rc = crm_ipc_send(native->ipc, notify_msg, crm_ipc_client_response, + 1000 * cib->call_timeout, NULL); + if (rc <= 0) { + crm_trace("Notification not registered: %d", rc); + rc = -ECOMM; + } + } + + free_xml(notify_msg); + return rc; +} + +static int +cib_native_set_connection_dnotify(cib_t *cib, + void (*dnotify) (gpointer user_data)) +{ + cib_native_opaque_t *native = NULL; + + if (cib == NULL) { + crm_err("No CIB!"); + return FALSE; + } + + native = cib->variant_opaque; + native->dnotify_fn = dnotify; + + return pcmk_ok; +} + +/*! + * \internal + * \brief Get the given CIB connection's unique client identifier + * + * These can be used to check whether this client requested the action that + * triggered a CIB notification. + * + * \param[in] cib CIB connection + * \param[out] async_id If not \p NULL, where to store asynchronous client ID + * \param[out] sync_id If not \p NULL, where to store synchronous client ID + * + * \return Legacy Pacemaker return code (specifically, \p pcmk_ok) + * + * \note This is the \p cib_native variant implementation of + * \p cib_api_operations_t:client_id(). + * \note For \p cib_native objects, \p async_id and \p sync_id are the same. + * \note The client ID is assigned during CIB sign-on. + */ +static int +cib_native_client_id(const cib_t *cib, const char **async_id, + const char **sync_id) +{ + cib_native_opaque_t *native = cib->variant_opaque; + + if (async_id != NULL) { + *async_id = native->token; + } + if (sync_id != NULL) { + *sync_id = native->token; + } + return pcmk_ok; +} + +cib_t * +cib_native_new(void) +{ + cib_native_opaque_t *native = NULL; + cib_t *cib = cib_new_variant(); + + if (cib == NULL) { + return NULL; + } + + native = calloc(1, sizeof(cib_native_opaque_t)); + + if (native == NULL) { + free(cib); + return NULL; + } + + cib->variant = cib_native; + cib->variant_opaque = native; + + native->ipc = NULL; + native->source = NULL; + native->dnotify_fn = NULL; + + /* assign variant specific ops */ + cib->delegate_fn = cib_native_perform_op_delegate; + cib->cmds->signon = cib_native_signon; + cib->cmds->signon_raw = cib_native_signon_raw; + cib->cmds->signoff = cib_native_signoff; + cib->cmds->free = cib_native_free; + + cib->cmds->register_notification = cib_native_register_notification; + cib->cmds->set_connection_dnotify = cib_native_set_connection_dnotify; + + cib->cmds->client_id = cib_native_client_id; + + return cib; +} diff --git a/lib/cib/cib_ops.c b/lib/cib/cib_ops.c new file mode 100644 index 0000000..d3293c4 --- /dev/null +++ b/lib/cib/cib_ops.c @@ -0,0 +1,869 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include +#include + +int +cib_process_query(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, + xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) +{ + xmlNode *obj_root = NULL; + int result = pcmk_ok; + + crm_trace("Processing %s for %s section", + op, pcmk__s(section, "unspecified")); + + if (options & cib_xpath) { + return cib_process_xpath(op, options, section, req, input, + existing_cib, result_cib, answer); + } + + CRM_CHECK(*answer == NULL, free_xml(*answer)); + *answer = NULL; + + if (pcmk__str_eq(XML_CIB_TAG_SECTION_ALL, section, pcmk__str_casei)) { + section = NULL; + } + + obj_root = pcmk_find_cib_element(existing_cib, section); + + if (obj_root == NULL) { + result = -ENXIO; + + } else if (options & cib_no_children) { + const char *tag = TYPE(obj_root); + xmlNode *shallow = create_xml_node(*answer, tag); + + copy_in_properties(shallow, obj_root); + *answer = shallow; + + } else { + *answer = obj_root; + } + + if (result == pcmk_ok && *answer == NULL) { + crm_err("Error creating query response"); + result = -ENOMSG; + } + + return result; +} + +static int +update_counter(xmlNode *xml_obj, const char *field, bool reset) +{ + char *new_value = NULL; + char *old_value = NULL; + int int_value = -1; + + if (!reset && crm_element_value(xml_obj, field) != NULL) { + old_value = crm_element_value_copy(xml_obj, field); + } + if (old_value != NULL) { + int_value = atoi(old_value); + new_value = pcmk__itoa(++int_value); + } else { + new_value = strdup("1"); + CRM_ASSERT(new_value != NULL); + } + + crm_trace("Update %s from %s to %s", + field, pcmk__s(old_value, "unset"), new_value); + crm_xml_add(xml_obj, field, new_value); + + free(new_value); + free(old_value); + + return pcmk_ok; +} + +int +cib_process_erase(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, + xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) +{ + int result = pcmk_ok; + + crm_trace("Processing \"%s\" event", op); + *answer = NULL; + free_xml(*result_cib); + *result_cib = createEmptyCib(0); + + copy_in_properties(*result_cib, existing_cib); + update_counter(*result_cib, XML_ATTR_GENERATION_ADMIN, false); + + return result; +} + +int +cib_process_upgrade(const char *op, int options, const char *section, xmlNode * req, + xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, + xmlNode ** answer) +{ + int rc = 0; + int new_version = 0; + int current_version = 0; + int max_version = 0; + const char *max = crm_element_value(req, F_CIB_SCHEMA_MAX); + const char *value = crm_element_value(existing_cib, XML_ATTR_VALIDATION); + + *answer = NULL; + crm_trace("Processing \"%s\" event with max=%s", op, max); + + if (value != NULL) { + current_version = get_schema_version(value); + } + + if (max) { + max_version = get_schema_version(max); + } + + rc = update_validation(result_cib, &new_version, max_version, TRUE, + !(options & cib_verbose)); + if (new_version > current_version) { + update_counter(*result_cib, XML_ATTR_GENERATION_ADMIN, false); + update_counter(*result_cib, XML_ATTR_GENERATION, true); + update_counter(*result_cib, XML_ATTR_NUMUPDATES, true); + return pcmk_ok; + } + + return rc; +} + +int +cib_process_bump(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, + xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) +{ + int result = pcmk_ok; + + crm_trace("Processing %s for epoch='%s'", op, + pcmk__s(crm_element_value(existing_cib, XML_ATTR_GENERATION), "")); + + *answer = NULL; + update_counter(*result_cib, XML_ATTR_GENERATION, false); + + return result; +} + +int +cib_process_replace(const char *op, int options, const char *section, xmlNode * req, + xmlNode * input, xmlNode * existing_cib, xmlNode ** result_cib, + xmlNode ** answer) +{ + const char *tag = NULL; + int result = pcmk_ok; + + crm_trace("Processing %s for %s section", + op, pcmk__s(section, "unspecified")); + + if (options & cib_xpath) { + return cib_process_xpath(op, options, section, req, input, + existing_cib, result_cib, answer); + } + + *answer = NULL; + + if (input == NULL) { + return -EINVAL; + } + + tag = crm_element_name(input); + + if (pcmk__str_eq(XML_CIB_TAG_SECTION_ALL, section, pcmk__str_casei)) { + section = NULL; + + } else if (pcmk__str_eq(tag, section, pcmk__str_casei)) { + section = NULL; + } + + if (pcmk__str_eq(tag, XML_TAG_CIB, pcmk__str_casei)) { + int updates = 0; + int epoch = 0; + int admin_epoch = 0; + + int replace_updates = 0; + int replace_epoch = 0; + int replace_admin_epoch = 0; + + const char *reason = NULL; + const char *peer = crm_element_value(req, F_ORIG); + const char *digest = crm_element_value(req, XML_ATTR_DIGEST); + + if (digest) { + const char *version = crm_element_value(req, XML_ATTR_CRM_VERSION); + char *digest_verify = calculate_xml_versioned_digest(input, FALSE, TRUE, + version ? version : + CRM_FEATURE_SET); + + if (!pcmk__str_eq(digest_verify, digest, pcmk__str_casei)) { + crm_err("Digest mis-match on replace from %s: %s vs. %s (expected)", peer, + digest_verify, digest); + reason = "digest mismatch"; + + } else { + crm_info("Digest matched on replace from %s: %s", peer, digest); + } + free(digest_verify); + + } else { + crm_trace("No digest to verify"); + } + + cib_version_details(existing_cib, &admin_epoch, &epoch, &updates); + cib_version_details(input, &replace_admin_epoch, &replace_epoch, &replace_updates); + + if (replace_admin_epoch < admin_epoch) { + reason = XML_ATTR_GENERATION_ADMIN; + + } else if (replace_admin_epoch > admin_epoch) { + /* no more checks */ + + } else if (replace_epoch < epoch) { + reason = XML_ATTR_GENERATION; + + } else if (replace_epoch > epoch) { + /* no more checks */ + + } else if (replace_updates < updates) { + reason = XML_ATTR_NUMUPDATES; + } + + if (reason != NULL) { + crm_info("Replacement %d.%d.%d from %s not applied to %d.%d.%d:" + " current %s is greater than the replacement", + replace_admin_epoch, replace_epoch, + replace_updates, peer, admin_epoch, epoch, updates, reason); + result = -pcmk_err_old_data; + } else { + crm_info("Replaced %d.%d.%d with %d.%d.%d from %s", + admin_epoch, epoch, updates, + replace_admin_epoch, replace_epoch, replace_updates, peer); + } + + free_xml(*result_cib); + *result_cib = copy_xml(input); + + } else { + xmlNode *obj_root = NULL; + gboolean ok = TRUE; + + obj_root = pcmk_find_cib_element(*result_cib, section); + ok = replace_xml_child(NULL, obj_root, input, FALSE); + if (ok == FALSE) { + crm_trace("No matching object to replace"); + result = -ENXIO; + } + } + + return result; +} + +int +cib_process_delete(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, + xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) +{ + xmlNode *obj_root = NULL; + + crm_trace("Processing \"%s\" event", op); + + if (options & cib_xpath) { + return cib_process_xpath(op, options, section, req, input, + existing_cib, result_cib, answer); + } + + if (input == NULL) { + crm_err("Cannot perform modification with no data"); + return -EINVAL; + } + + obj_root = pcmk_find_cib_element(*result_cib, section); + if(pcmk__str_eq(crm_element_name(input), section, pcmk__str_casei)) { + xmlNode *child = NULL; + for (child = pcmk__xml_first_child(input); child; + child = pcmk__xml_next(child)) { + if (replace_xml_child(NULL, obj_root, child, TRUE) == FALSE) { + crm_trace("No matching object to delete: %s=%s", child->name, ID(child)); + } + } + + } else if (replace_xml_child(NULL, obj_root, input, TRUE) == FALSE) { + crm_trace("No matching object to delete: %s=%s", input->name, ID(input)); + } + + return pcmk_ok; +} + +int +cib_process_modify(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, + xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) +{ + xmlNode *obj_root = NULL; + + crm_trace("Processing \"%s\" event", op); + + if (options & cib_xpath) { + return cib_process_xpath(op, options, section, req, input, + existing_cib, result_cib, answer); + } + + if (input == NULL) { + crm_err("Cannot perform modification with no data"); + return -EINVAL; + } + + obj_root = pcmk_find_cib_element(*result_cib, section); + if (obj_root == NULL) { + xmlNode *tmp_section = NULL; + const char *path = pcmk_cib_parent_name_for(section); + + if (path == NULL) { + return -EINVAL; + } + + tmp_section = create_xml_node(NULL, section); + cib_process_xpath(PCMK__CIB_REQUEST_CREATE, 0, path, NULL, tmp_section, + NULL, result_cib, answer); + free_xml(tmp_section); + + obj_root = pcmk_find_cib_element(*result_cib, section); + } + + CRM_CHECK(obj_root != NULL, return -EINVAL); + + if (update_xml_child(obj_root, input) == FALSE) { + if (options & cib_can_create) { + add_node_copy(obj_root, input); + } else { + return -ENXIO; + } + } + + if(options & cib_mixed_update) { + int max = 0, lpc; + xmlXPathObjectPtr xpathObj = xpath_search(*result_cib, "//@__delete__"); + + if (xpathObj) { + max = numXpathResults(xpathObj); + crm_log_xml_trace(*result_cib, "Mixed result"); + } + + for (lpc = 0; lpc < max; lpc++) { + xmlNode *match = getXpathResult(xpathObj, lpc); + xmlChar *match_path = xmlGetNodePath(match); + + crm_debug("Destroying %s", match_path); + free(match_path); + free_xml(match); + } + + freeXpathObject(xpathObj); + } + return pcmk_ok; +} + +static int +update_cib_object(xmlNode * parent, xmlNode * update) +{ + int result = pcmk_ok; + xmlNode *target = NULL; + xmlNode *a_child = NULL; + const char *replace = NULL; + const char *object_id = NULL; + const char *object_name = NULL; + + CRM_CHECK(update != NULL, return -EINVAL); + CRM_CHECK(parent != NULL, return -EINVAL); + + object_name = crm_element_name(update); + CRM_CHECK(object_name != NULL, return -EINVAL); + + object_id = ID(update); + crm_trace("Processing update for <%s%s%s%s>", object_name, + ((object_id == NULL)? "" : " " XML_ATTR_ID "='"), + pcmk__s(object_id, ""), + ((object_id == NULL)? "" : "'")); + + if (object_id == NULL) { + /* placeholder object */ + target = find_xml_node(parent, object_name, FALSE); + + } else { + target = pcmk__xe_match(parent, object_name, XML_ATTR_ID, object_id); + } + + if (target == NULL) { + target = create_xml_node(parent, object_name); + } + + crm_trace("Found node <%s%s%s%s> to update", object_name, + ((object_id == NULL)? "" : " " XML_ATTR_ID "='"), + pcmk__s(object_id, ""), + ((object_id == NULL)? "" : "'")); + + // @COMPAT: XML_CIB_ATTR_REPLACE is unused internally. Remove at break. + replace = crm_element_value(update, XML_CIB_ATTR_REPLACE); + if (replace != NULL) { + xmlNode *remove = NULL; + int last = 0, lpc = 0, len = 0; + + len = strlen(replace); + while (lpc <= len) { + if (replace[lpc] == ',' || replace[lpc] == 0) { + char *replace_item = NULL; + + if (last == lpc) { + /* nothing to do */ + last = lpc + 1; + goto incr; + } + + replace_item = strndup(replace + last, lpc - last); + remove = find_xml_node(target, replace_item, FALSE); + if (remove != NULL) { + crm_trace("Replacing node <%s> in <%s>", + replace_item, crm_element_name(target)); + free_xml(remove); + remove = NULL; + } + free(replace_item); + last = lpc + 1; + } + incr: + lpc++; + } + xml_remove_prop(update, XML_CIB_ATTR_REPLACE); + xml_remove_prop(target, XML_CIB_ATTR_REPLACE); + } + + copy_in_properties(target, update); + + if (xml_acl_denied(target)) { + crm_notice("Cannot update <%s " XML_ATTR_ID "=%s>", + pcmk__s(object_name, ""), + pcmk__s(object_id, "")); + return -EACCES; + } + + crm_trace("Processing children of <%s%s%s%s>", object_name, + ((object_id == NULL)? "" : " " XML_ATTR_ID "='"), + pcmk__s(object_id, ""), + ((object_id == NULL)? "" : "'")); + + for (a_child = pcmk__xml_first_child(update); a_child != NULL; + a_child = pcmk__xml_next(a_child)) { + int tmp_result = 0; + + crm_trace("Updating child <%s%s%s%s>", crm_element_name(a_child), + ((ID(a_child) == NULL)? "" : " " XML_ATTR_ID "='"), + pcmk__s(ID(a_child), ""), ((ID(a_child) == NULL)? "" : "'")); + + tmp_result = update_cib_object(target, a_child); + + /* only the first error is likely to be interesting */ + if (tmp_result != pcmk_ok) { + crm_err("Error updating child <%s%s%s%s>", + crm_element_name(a_child), + ((ID(a_child) == NULL)? "" : " " XML_ATTR_ID "='"), + pcmk__s(ID(a_child), ""), + ((ID(a_child) == NULL)? "" : "'")); + + if (result == pcmk_ok) { + result = tmp_result; + } + } + } + + crm_trace("Finished handling update for <%s%s%s%s>", object_name, + ((object_id == NULL)? "" : " " XML_ATTR_ID "='"), + pcmk__s(object_id, ""), + ((object_id == NULL)? "" : "'")); + + return result; +} + +static int +add_cib_object(xmlNode * parent, xmlNode * new_obj) +{ + const char *object_name = NULL; + const char *object_id = NULL; + xmlNode *equiv_node = NULL; + + if ((parent == NULL) || (new_obj == NULL)) { + return -EINVAL; + } + + object_name = crm_element_name(new_obj); + if (object_name == NULL) { + return -EINVAL; + } + + object_id = ID(new_obj); + + crm_trace("Processing creation of <%s%s%s%s>", object_name, + ((object_id == NULL)? "" : " " XML_ATTR_ID "='"), + pcmk__s(object_id, ""), + ((object_id == NULL)? "" : "'")); + + if (object_id == NULL) { + equiv_node = find_xml_node(parent, object_name, FALSE); + } else { + equiv_node = pcmk__xe_match(parent, object_name, XML_ATTR_ID, + object_id); + } + if (equiv_node != NULL) { + return -EEXIST; + } + + return update_cib_object(parent, new_obj); +} + +static bool +update_results(xmlNode *failed, xmlNode *target, const char *operation, + int return_code) +{ + xmlNode *xml_node = NULL; + bool was_error = false; + const char *error_msg = NULL; + + if (return_code != pcmk_ok) { + error_msg = pcmk_strerror(return_code); + + was_error = true; + xml_node = create_xml_node(failed, XML_FAIL_TAG_CIB); + add_node_copy(xml_node, target); + + crm_xml_add(xml_node, XML_FAILCIB_ATTR_ID, ID(target)); + crm_xml_add(xml_node, XML_FAILCIB_ATTR_OBJTYPE, TYPE(target)); + crm_xml_add(xml_node, XML_FAILCIB_ATTR_OP, operation); + crm_xml_add(xml_node, XML_FAILCIB_ATTR_REASON, error_msg); + + crm_warn("Action %s failed: %s (cde=%d)", + operation, error_msg, return_code); + } + + return was_error; +} + +int +cib_process_create(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, + xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) +{ + xmlNode *failed = NULL; + int result = pcmk_ok; + xmlNode *update_section = NULL; + + crm_trace("Processing %s for %s section", + op, pcmk__s(section, "unspecified")); + if (pcmk__str_eq(XML_CIB_TAG_SECTION_ALL, section, pcmk__str_casei)) { + section = NULL; + + } else if (pcmk__str_eq(XML_TAG_CIB, section, pcmk__str_casei)) { + section = NULL; + + } else if (pcmk__str_eq(crm_element_name(input), XML_TAG_CIB, pcmk__str_casei)) { + section = NULL; + } + + CRM_CHECK(strcmp(op, PCMK__CIB_REQUEST_CREATE) == 0, return -EINVAL); + + if (input == NULL) { + crm_err("Cannot perform modification with no data"); + return -EINVAL; + } + + if (section == NULL) { + return cib_process_modify(op, options, section, req, input, existing_cib, result_cib, + answer); + } + + failed = create_xml_node(NULL, XML_TAG_FAILED); + + update_section = pcmk_find_cib_element(*result_cib, section); + if (pcmk__str_eq(crm_element_name(input), section, pcmk__str_casei)) { + xmlNode *a_child = NULL; + + for (a_child = pcmk__xml_first_child(input); a_child != NULL; + a_child = pcmk__xml_next(a_child)) { + result = add_cib_object(update_section, a_child); + if (update_results(failed, a_child, op, result)) { + break; + } + } + + } else { + result = add_cib_object(update_section, input); + update_results(failed, input, op, result); + } + + if ((result == pcmk_ok) && xml_has_children(failed)) { + result = -EINVAL; + } + + if (result != pcmk_ok) { + crm_log_xml_err(failed, "CIB Update failures"); + *answer = failed; + + } else { + free_xml(failed); + } + + return result; +} + +int +cib_process_diff(const char *op, int options, const char *section, xmlNode * req, xmlNode * input, + xmlNode * existing_cib, xmlNode ** result_cib, xmlNode ** answer) +{ + const char *originator = NULL; + + if (req != NULL) { + originator = crm_element_value(req, F_ORIG); + } + + crm_trace("Processing \"%s\" event from %s%s", + op, originator, + (pcmk_is_set(options, cib_force_diff)? " (global update)" : "")); + + free_xml(*result_cib); + *result_cib = copy_xml(existing_cib); + return xml_apply_patchset(*result_cib, input, TRUE); +} + +// @COMPAT: v1-only +bool +cib__config_changed_v1(xmlNode *last, xmlNode *next, xmlNode **diff) +{ + int lpc = 0, max = 0; + bool config_changes = false; + xmlXPathObject *xpathObj = NULL; + int format = 1; + + CRM_ASSERT(diff != NULL); + + if (*diff == NULL && last != NULL && next != NULL) { + *diff = diff_xml_object(last, next, FALSE); + } + + if (*diff == NULL) { + goto done; + } + + crm_element_value_int(*diff, "format", &format); + CRM_LOG_ASSERT(format == 1); + + xpathObj = xpath_search(*diff, "//" XML_CIB_TAG_CONFIGURATION); + if (numXpathResults(xpathObj) > 0) { + config_changes = true; + goto done; + } + freeXpathObject(xpathObj); + + /* + * Do not check XML_TAG_DIFF_ADDED "//" XML_TAG_CIB + * This always contains every field and would produce a false positive + * every time if the checked value existed + */ + xpathObj = xpath_search(*diff, "//" XML_TAG_DIFF_REMOVED "//" XML_TAG_CIB); + max = numXpathResults(xpathObj); + + for (lpc = 0; lpc < max; lpc++) { + xmlNode *top = getXpathResult(xpathObj, lpc); + + if (crm_element_value(top, XML_ATTR_GENERATION) != NULL) { + config_changes = true; + goto done; + } + if (crm_element_value(top, XML_ATTR_GENERATION_ADMIN) != NULL) { + config_changes = true; + goto done; + } + + if (crm_element_value(top, XML_ATTR_VALIDATION) != NULL) { + config_changes = true; + goto done; + } + if (crm_element_value(top, XML_ATTR_CRM_VERSION) != NULL) { + config_changes = true; + goto done; + } + if (crm_element_value(top, "remote-clear-port") != NULL) { + config_changes = true; + goto done; + } + if (crm_element_value(top, "remote-tls-port") != NULL) { + config_changes = true; + goto done; + } + } + + done: + freeXpathObject(xpathObj); + return config_changes; +} + +int +cib_process_xpath(const char *op, int options, const char *section, + const xmlNode *req, xmlNode *input, xmlNode *existing_cib, + xmlNode **result_cib, xmlNode **answer) +{ + int lpc = 0; + int max = 0; + int rc = pcmk_ok; + bool is_query = pcmk__str_eq(op, PCMK__CIB_REQUEST_QUERY, pcmk__str_none); + + xmlXPathObjectPtr xpathObj = NULL; + + crm_trace("Processing \"%s\" event", op); + + if (is_query) { + xpathObj = xpath_search(existing_cib, section); + } else { + xpathObj = xpath_search(*result_cib, section); + } + + max = numXpathResults(xpathObj); + + if ((max < 1) + && pcmk__str_eq(op, PCMK__CIB_REQUEST_DELETE, pcmk__str_none)) { + crm_debug("%s was already removed", section); + + } else if (max < 1) { + crm_debug("%s: %s does not exist", op, section); + rc = -ENXIO; + + } else if (is_query) { + if (max > 1) { + *answer = create_xml_node(NULL, "xpath-query"); + } + } + + if (pcmk_is_set(options, cib_multiple) + && pcmk__str_eq(op, PCMK__CIB_REQUEST_DELETE, pcmk__str_none)) { + dedupXpathResults(xpathObj); + } + + for (lpc = 0; lpc < max; lpc++) { + xmlChar *path = NULL; + xmlNode *match = getXpathResult(xpathObj, lpc); + + if (match == NULL) { + continue; + } + + path = xmlGetNodePath(match); + crm_debug("Processing %s op for %s with %s", op, section, path); + free(path); + + if (pcmk__str_eq(op, PCMK__CIB_REQUEST_DELETE, pcmk__str_none)) { + if (match == *result_cib) { + /* Attempting to delete the whole "/cib" */ + crm_warn("Cannot perform %s for %s: The xpath is addressing the whole /cib", op, section); + rc = -EINVAL; + break; + } + + free_xml(match); + if ((options & cib_multiple) == 0) { + break; + } + + } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_MODIFY, pcmk__str_none)) { + if (update_xml_child(match, input) == FALSE) { + rc = -ENXIO; + } else if ((options & cib_multiple) == 0) { + break; + } + + } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_CREATE, pcmk__str_none)) { + add_node_copy(match, input); + break; + + } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_QUERY, pcmk__str_none)) { + + if (options & cib_no_children) { + const char *tag = TYPE(match); + xmlNode *shallow = create_xml_node(*answer, tag); + + copy_in_properties(shallow, match); + + if (*answer == NULL) { + *answer = shallow; + } + + } else if (options & cib_xpath_address) { + char *path = NULL; + xmlNode *parent = match; + + while (parent && parent->type == XML_ELEMENT_NODE) { + const char *id = crm_element_value(parent, XML_ATTR_ID); + char *new_path = NULL; + + if (id) { + new_path = crm_strdup_printf("/%s[@" XML_ATTR_ID "='%s']" + "%s", + parent->name, id, + pcmk__s(path, "")); + } else { + new_path = crm_strdup_printf("/%s%s", parent->name, + pcmk__s(path, "")); + } + free(path); + path = new_path; + parent = parent->parent; + } + crm_trace("Got: %s", path); + + if (*answer == NULL) { + *answer = create_xml_node(NULL, "xpath-query"); + } + parent = create_xml_node(*answer, "xpath-query-path"); + crm_xml_add(parent, XML_ATTR_ID, path); + free(path); + + } else if (*answer) { + add_node_copy(*answer, match); + + } else { + *answer = match; + } + + } else if (pcmk__str_eq(op, PCMK__CIB_REQUEST_REPLACE, + pcmk__str_none)) { + xmlNode *parent = match->parent; + + free_xml(match); + if (input != NULL) { + add_node_copy(parent, input); + } + + if ((options & cib_multiple) == 0) { + break; + } + } + } + + freeXpathObject(xpathObj); + return rc; +} diff --git a/lib/cib/cib_remote.c b/lib/cib/cib_remote.c new file mode 100644 index 0000000..28095b3 --- /dev/null +++ b/lib/cib/cib_remote.c @@ -0,0 +1,638 @@ +/* + * Copyright 2008-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_GNUTLS_GNUTLS_H + +# include + +# define TLS_HANDSHAKE_TIMEOUT_MS 5000 + +static gnutls_anon_client_credentials_t anon_cred_c; +static gboolean remote_gnutls_credentials_init = FALSE; + +#endif // HAVE_GNUTLS_GNUTLS_H + +#include + +typedef struct cib_remote_opaque_s { + int port; + char *server; + char *user; + char *passwd; + gboolean encrypted; + pcmk__remote_t command; + pcmk__remote_t callback; + pcmk__output_t *out; +} cib_remote_opaque_t; + +static int +cib_remote_perform_op(cib_t *cib, const char *op, const char *host, + const char *section, xmlNode *data, + xmlNode **output_data, int call_options, const char *name) +{ + int rc; + int remaining_time = 0; + time_t start_time; + + xmlNode *op_msg = NULL; + xmlNode *op_reply = NULL; + + cib_remote_opaque_t *private = cib->variant_opaque; + + if (cib->state == cib_disconnected) { + return -ENOTCONN; + } + + if (output_data != NULL) { + *output_data = NULL; + } + + if (op == NULL) { + crm_err("No operation specified"); + return -EINVAL; + } + + cib->call_id++; + if (cib->call_id < 1) { + cib->call_id = 1; + } + + op_msg = cib_create_op(cib->call_id, op, host, section, data, call_options, + NULL); + if (op_msg == NULL) { + return -EPROTO; + } + + crm_trace("Sending %s message to the CIB manager", op); + if (!(call_options & cib_sync_call)) { + pcmk__remote_send_xml(&private->callback, op_msg); + } else { + pcmk__remote_send_xml(&private->command, op_msg); + } + free_xml(op_msg); + + if ((call_options & cib_discard_reply)) { + crm_trace("Discarding reply"); + return pcmk_ok; + + } else if (!(call_options & cib_sync_call)) { + return cib->call_id; + } + + crm_trace("Waiting for a synchronous reply"); + + start_time = time(NULL); + remaining_time = cib->call_timeout ? cib->call_timeout : 60; + + rc = pcmk_rc_ok; + while (remaining_time > 0 && (rc != ENOTCONN)) { + int reply_id = -1; + int msg_id = cib->call_id; + + rc = pcmk__read_remote_message(&private->command, + remaining_time * 1000); + op_reply = pcmk__remote_message_xml(&private->command); + + if (!op_reply) { + break; + } + + crm_element_value_int(op_reply, F_CIB_CALLID, &reply_id); + + if (reply_id == msg_id) { + break; + + } else if (reply_id < msg_id) { + crm_debug("Received old reply: %d (wanted %d)", reply_id, msg_id); + crm_log_xml_trace(op_reply, "Old reply"); + + } else if ((reply_id - 10000) > msg_id) { + /* wrap-around case */ + crm_debug("Received old reply: %d (wanted %d)", reply_id, msg_id); + crm_log_xml_trace(op_reply, "Old reply"); + } else { + crm_err("Received a __future__ reply:" " %d (wanted %d)", reply_id, msg_id); + } + + free_xml(op_reply); + op_reply = NULL; + + /* wasn't the right reply, try and read some more */ + remaining_time = time(NULL) - start_time; + } + + /* if(IPC_ISRCONN(native->command_channel) == FALSE) { */ + /* crm_err("The CIB manager disconnected: %d", */ + /* native->command_channel->ch_status); */ + /* cib->state = cib_disconnected; */ + /* } */ + + if (rc == ENOTCONN) { + crm_err("Disconnected while waiting for reply."); + return -ENOTCONN; + } else if (op_reply == NULL) { + crm_err("No reply message - empty"); + return -ENOMSG; + } + + crm_trace("Synchronous reply received"); + + /* Start processing the reply... */ + if (crm_element_value_int(op_reply, F_CIB_RC, &rc) != 0) { + rc = -EPROTO; + } + + if (rc == -pcmk_err_diff_resync) { + /* This is an internal value that clients do not and should not care about */ + rc = pcmk_ok; + } + + if (rc == pcmk_ok || rc == -EPERM) { + crm_log_xml_debug(op_reply, "passed"); + + } else { +/* } else if(rc == -ETIME) { */ + crm_err("Call failed: %s", pcmk_strerror(rc)); + crm_log_xml_warn(op_reply, "failed"); + } + + if (output_data == NULL) { + /* do nothing more */ + + } else if (!(call_options & cib_discard_reply)) { + xmlNode *tmp = get_message_xml(op_reply, F_CIB_CALLDATA); + + if (tmp == NULL) { + crm_trace("No output in reply to \"%s\" command %d", op, cib->call_id - 1); + } else { + *output_data = copy_xml(tmp); + } + } + + free_xml(op_reply); + + return rc; +} + +static int +cib_remote_callback_dispatch(gpointer user_data) +{ + int rc; + cib_t *cib = user_data; + cib_remote_opaque_t *private = cib->variant_opaque; + + xmlNode *msg = NULL; + + crm_info("Message on callback channel"); + + rc = pcmk__read_remote_message(&private->callback, -1); + + msg = pcmk__remote_message_xml(&private->callback); + while (msg) { + const char *type = crm_element_value(msg, F_TYPE); + + crm_trace("Activating %s callbacks...", type); + + if (pcmk__str_eq(type, T_CIB, pcmk__str_casei)) { + cib_native_callback(cib, msg, 0, 0); + + } else if (pcmk__str_eq(type, T_CIB_NOTIFY, pcmk__str_casei)) { + g_list_foreach(cib->notify_list, cib_native_notify, msg); + + } else { + crm_err("Unknown message type: %s", type); + } + + free_xml(msg); + msg = pcmk__remote_message_xml(&private->callback); + } + + if (rc == ENOTCONN) { + return -1; + } + + return 0; +} + +static int +cib_remote_command_dispatch(gpointer user_data) +{ + int rc; + cib_t *cib = user_data; + cib_remote_opaque_t *private = cib->variant_opaque; + + rc = pcmk__read_remote_message(&private->command, -1); + + free(private->command.buffer); + private->command.buffer = NULL; + crm_err("received late reply for remote cib connection, discarding"); + + if (rc == ENOTCONN) { + return -1; + } + return 0; +} + +static int +cib_tls_close(cib_t *cib) +{ + cib_remote_opaque_t *private = cib->variant_opaque; + +#ifdef HAVE_GNUTLS_GNUTLS_H + if (private->encrypted) { + if (private->command.tls_session) { + gnutls_bye(*(private->command.tls_session), GNUTLS_SHUT_RDWR); + gnutls_deinit(*(private->command.tls_session)); + gnutls_free(private->command.tls_session); + } + + if (private->callback.tls_session) { + gnutls_bye(*(private->callback.tls_session), GNUTLS_SHUT_RDWR); + gnutls_deinit(*(private->callback.tls_session)); + gnutls_free(private->callback.tls_session); + } + private->command.tls_session = NULL; + private->callback.tls_session = NULL; + if (remote_gnutls_credentials_init) { + gnutls_anon_free_client_credentials(anon_cred_c); + gnutls_global_deinit(); + remote_gnutls_credentials_init = FALSE; + } + } +#endif + + if (private->command.tcp_socket) { + shutdown(private->command.tcp_socket, SHUT_RDWR); /* no more receptions */ + close(private->command.tcp_socket); + } + if (private->callback.tcp_socket) { + shutdown(private->callback.tcp_socket, SHUT_RDWR); /* no more receptions */ + close(private->callback.tcp_socket); + } + private->command.tcp_socket = 0; + private->callback.tcp_socket = 0; + + free(private->command.buffer); + free(private->callback.buffer); + private->command.buffer = NULL; + private->callback.buffer = NULL; + + return 0; +} + +static void +cib_remote_connection_destroy(gpointer user_data) +{ + crm_err("Connection destroyed"); +#ifdef HAVE_GNUTLS_GNUTLS_H + cib_tls_close(user_data); +#endif +} + +static int +cib_tls_signon(cib_t *cib, pcmk__remote_t *connection, gboolean event_channel) +{ + cib_remote_opaque_t *private = cib->variant_opaque; + int rc; + + xmlNode *answer = NULL; + xmlNode *login = NULL; + + static struct mainloop_fd_callbacks cib_fd_callbacks = { 0, }; + + cib_fd_callbacks.dispatch = + event_channel ? cib_remote_callback_dispatch : cib_remote_command_dispatch; + cib_fd_callbacks.destroy = cib_remote_connection_destroy; + + connection->tcp_socket = -1; +#ifdef HAVE_GNUTLS_GNUTLS_H + connection->tls_session = NULL; +#endif + rc = pcmk__connect_remote(private->server, private->port, 0, NULL, + &(connection->tcp_socket), NULL, NULL); + if (rc != pcmk_rc_ok) { + crm_info("Remote connection to %s:%d failed: %s " CRM_XS " rc=%d", + private->server, private->port, pcmk_rc_str(rc), rc); + return -ENOTCONN; + } + + if (private->encrypted) { + /* initialize GnuTls lib */ +#ifdef HAVE_GNUTLS_GNUTLS_H + if (remote_gnutls_credentials_init == FALSE) { + crm_gnutls_global_init(); + gnutls_anon_allocate_client_credentials(&anon_cred_c); + remote_gnutls_credentials_init = TRUE; + } + + /* bind the socket to GnuTls lib */ + connection->tls_session = pcmk__new_tls_session(connection->tcp_socket, + GNUTLS_CLIENT, + GNUTLS_CRD_ANON, + anon_cred_c); + if (connection->tls_session == NULL) { + cib_tls_close(cib); + return -1; + } + + if (pcmk__tls_client_handshake(connection, TLS_HANDSHAKE_TIMEOUT_MS) + != pcmk_rc_ok) { + crm_err("Session creation for %s:%d failed", private->server, private->port); + + gnutls_deinit(*connection->tls_session); + gnutls_free(connection->tls_session); + connection->tls_session = NULL; + cib_tls_close(cib); + return -1; + } +#else + return -EPROTONOSUPPORT; +#endif + } + + /* login to server */ + login = create_xml_node(NULL, "cib_command"); + crm_xml_add(login, "op", "authenticate"); + crm_xml_add(login, "user", private->user); + crm_xml_add(login, "password", private->passwd); + crm_xml_add(login, "hidden", "password"); + + pcmk__remote_send_xml(connection, login); + free_xml(login); + + rc = pcmk_ok; + if (pcmk__read_remote_message(connection, -1) == ENOTCONN) { + rc = -ENOTCONN; + } + + answer = pcmk__remote_message_xml(connection); + + crm_log_xml_trace(answer, "Reply"); + if (answer == NULL) { + rc = -EPROTO; + + } else { + /* grab the token */ + const char *msg_type = crm_element_value(answer, F_CIB_OPERATION); + const char *tmp_ticket = crm_element_value(answer, F_CIB_CLIENTID); + + if (!pcmk__str_eq(msg_type, CRM_OP_REGISTER, pcmk__str_casei)) { + crm_err("Invalid registration message: %s", msg_type); + rc = -EPROTO; + + } else if (tmp_ticket == NULL) { + rc = -EPROTO; + + } else { + connection->token = strdup(tmp_ticket); + } + } + free_xml(answer); + answer = NULL; + + if (rc != 0) { + cib_tls_close(cib); + return rc; + } + + crm_trace("remote client connection established"); + connection->source = mainloop_add_fd("cib-remote", G_PRIORITY_HIGH, + connection->tcp_socket, cib, + &cib_fd_callbacks); + return rc; +} + +static int +cib_remote_signon(cib_t *cib, const char *name, enum cib_conn_type type) +{ + int rc = pcmk_ok; + cib_remote_opaque_t *private = cib->variant_opaque; + + if (private->passwd == NULL) { + if (private->out == NULL) { + /* If no pcmk__output_t is set, just assume that a text prompt + * is good enough. + */ + pcmk__text_prompt("Password", false, &(private->passwd)); + } else { + private->out->prompt("Password", false, &(private->passwd)); + } + } + + if (private->server == NULL || private->user == NULL) { + rc = -EINVAL; + } + + if (rc == pcmk_ok) { + rc = cib_tls_signon(cib, &(private->command), FALSE); + } + + if (rc == pcmk_ok) { + rc = cib_tls_signon(cib, &(private->callback), TRUE); + } + + if (rc == pcmk_ok) { + xmlNode *hello = cib_create_op(0, CRM_OP_REGISTER, NULL, NULL, NULL, 0, + NULL); + crm_xml_add(hello, F_CIB_CLIENTNAME, name); + pcmk__remote_send_xml(&private->command, hello); + free_xml(hello); + } + + if (rc == pcmk_ok) { + crm_info("Opened connection to %s:%d for %s", + private->server, private->port, name); + cib->state = cib_connected_command; + cib->type = cib_command; + + } else { + crm_info("Connection to %s:%d for %s failed: %s\n", + private->server, private->port, name, pcmk_strerror(rc)); + } + + return rc; +} + +static int +cib_remote_signoff(cib_t *cib) +{ + int rc = pcmk_ok; + + crm_debug("Disconnecting from the CIB manager"); +#ifdef HAVE_GNUTLS_GNUTLS_H + cib_tls_close(cib); +#endif + + cib->state = cib_disconnected; + cib->type = cib_no_connection; + + return rc; +} + +static int +cib_remote_free(cib_t *cib) +{ + int rc = pcmk_ok; + + crm_warn("Freeing CIB"); + if (cib->state != cib_disconnected) { + rc = cib_remote_signoff(cib); + if (rc == pcmk_ok) { + cib_remote_opaque_t *private = cib->variant_opaque; + + free(private->server); + free(private->user); + free(private->passwd); + free(cib->cmds); + free(private); + free(cib); + } + } + + return rc; +} + +static int +cib_remote_inputfd(cib_t * cib) +{ + cib_remote_opaque_t *private = cib->variant_opaque; + + return private->callback.tcp_socket; +} + +static int +cib_remote_register_notification(cib_t * cib, const char *callback, int enabled) +{ + xmlNode *notify_msg = create_xml_node(NULL, "cib_command"); + cib_remote_opaque_t *private = cib->variant_opaque; + + crm_xml_add(notify_msg, F_CIB_OPERATION, T_CIB_NOTIFY); + crm_xml_add(notify_msg, F_CIB_NOTIFY_TYPE, callback); + crm_xml_add_int(notify_msg, F_CIB_NOTIFY_ACTIVATE, enabled); + pcmk__remote_send_xml(&private->callback, notify_msg); + free_xml(notify_msg); + return pcmk_ok; +} + +static int +cib_remote_set_connection_dnotify(cib_t * cib, void (*dnotify) (gpointer user_data)) +{ + return -EPROTONOSUPPORT; +} + +/*! + * \internal + * \brief Get the given CIB connection's unique client identifiers + * + * These can be used to check whether this client requested the action that + * triggered a CIB notification. + * + * \param[in] cib CIB connection + * \param[out] async_id If not \p NULL, where to store asynchronous client ID + * \param[out] sync_id If not \p NULL, where to store synchronous client ID + * + * \return Legacy Pacemaker return code (specifically, \p pcmk_ok) + * + * \note This is the \p cib_remote variant implementation of + * \p cib_api_operations_t:client_id(). + * \note The client IDs are assigned during CIB sign-on. + */ +static int +cib_remote_client_id(const cib_t *cib, const char **async_id, + const char **sync_id) +{ + cib_remote_opaque_t *private = cib->variant_opaque; + + if (async_id != NULL) { + // private->callback is the channel for async requests + *async_id = private->callback.token; + } + if (sync_id != NULL) { + // private->command is the channel for sync requests + *sync_id = private->command.token; + } + return pcmk_ok; +} + +cib_t * +cib_remote_new(const char *server, const char *user, const char *passwd, int port, + gboolean encrypted) +{ + cib_remote_opaque_t *private = NULL; + cib_t *cib = cib_new_variant(); + + if (cib == NULL) { + return NULL; + } + + private = calloc(1, sizeof(cib_remote_opaque_t)); + + if (private == NULL) { + free(cib); + return NULL; + } + + cib->variant = cib_remote; + cib->variant_opaque = private; + + pcmk__str_update(&private->server, server); + pcmk__str_update(&private->user, user); + pcmk__str_update(&private->passwd, passwd); + + private->port = port; + private->encrypted = encrypted; + + /* assign variant specific ops */ + cib->delegate_fn = cib_remote_perform_op; + cib->cmds->signon = cib_remote_signon; + cib->cmds->signoff = cib_remote_signoff; + cib->cmds->free = cib_remote_free; + cib->cmds->inputfd = cib_remote_inputfd; + + cib->cmds->register_notification = cib_remote_register_notification; + cib->cmds->set_connection_dnotify = cib_remote_set_connection_dnotify; + + cib->cmds->client_id = cib_remote_client_id; + + return cib; +} + +void +cib__set_output(cib_t *cib, pcmk__output_t *out) +{ + cib_remote_opaque_t *private; + + if (cib->variant != cib_remote) { + return; + } + + private = cib->variant_opaque; + private->out = out; +} diff --git a/lib/cib/cib_utils.c b/lib/cib/cib_utils.c new file mode 100644 index 0000000..c75d844 --- /dev/null +++ b/lib/cib/cib_utils.c @@ -0,0 +1,837 @@ +/* + * Original copyright 2004 International Business Machines + * Later changes copyright 2008-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +xmlNode * +cib_get_generation(cib_t * cib) +{ + xmlNode *the_cib = NULL; + xmlNode *generation = create_xml_node(NULL, XML_CIB_TAG_GENERATION_TUPPLE); + + cib->cmds->query(cib, NULL, &the_cib, cib_scope_local | cib_sync_call); + if (the_cib != NULL) { + copy_in_properties(generation, the_cib); + free_xml(the_cib); + } + + return generation; +} + +gboolean +cib_version_details(xmlNode * cib, int *admin_epoch, int *epoch, int *updates) +{ + *epoch = -1; + *updates = -1; + *admin_epoch = -1; + + if (cib == NULL) { + return FALSE; + + } else { + crm_element_value_int(cib, XML_ATTR_GENERATION, epoch); + crm_element_value_int(cib, XML_ATTR_NUMUPDATES, updates); + crm_element_value_int(cib, XML_ATTR_GENERATION_ADMIN, admin_epoch); + } + return TRUE; +} + +gboolean +cib_diff_version_details(xmlNode * diff, int *admin_epoch, int *epoch, int *updates, + int *_admin_epoch, int *_epoch, int *_updates) +{ + int add[] = { 0, 0, 0 }; + int del[] = { 0, 0, 0 }; + + xml_patch_versions(diff, add, del); + + *admin_epoch = add[0]; + *epoch = add[1]; + *updates = add[2]; + + *_admin_epoch = del[0]; + *_epoch = del[1]; + *_updates = del[2]; + + return TRUE; +} + +/*! + * \brief Create XML for a new (empty) CIB + * + * \param[in] cib_epoch What to use as "epoch" CIB property + * + * \return Newly created XML for empty CIB + * \note It is the caller's responsibility to free the result with free_xml(). + */ +xmlNode * +createEmptyCib(int cib_epoch) +{ + xmlNode *cib_root = NULL, *config = NULL; + + cib_root = create_xml_node(NULL, XML_TAG_CIB); + crm_xml_add(cib_root, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); + crm_xml_add(cib_root, XML_ATTR_VALIDATION, xml_latest_schema()); + + crm_xml_add_int(cib_root, XML_ATTR_GENERATION, cib_epoch); + crm_xml_add_int(cib_root, XML_ATTR_NUMUPDATES, 0); + crm_xml_add_int(cib_root, XML_ATTR_GENERATION_ADMIN, 0); + + config = create_xml_node(cib_root, XML_CIB_TAG_CONFIGURATION); + create_xml_node(cib_root, XML_CIB_TAG_STATUS); + + create_xml_node(config, XML_CIB_TAG_CRMCONFIG); + create_xml_node(config, XML_CIB_TAG_NODES); + create_xml_node(config, XML_CIB_TAG_RESOURCES); + create_xml_node(config, XML_CIB_TAG_CONSTRAINTS); + +#if PCMK__RESOURCE_STICKINESS_DEFAULT != 0 + { + xmlNode *rsc_defaults = create_xml_node(config, XML_CIB_TAG_RSCCONFIG); + xmlNode *meta = create_xml_node(rsc_defaults, XML_TAG_META_SETS); + xmlNode *nvpair = create_xml_node(meta, XML_CIB_TAG_NVPAIR); + + crm_xml_add(meta, XML_ATTR_ID, "build-resource-defaults"); + crm_xml_add(nvpair, XML_ATTR_ID, "build-" XML_RSC_ATTR_STICKINESS); + crm_xml_add(nvpair, XML_NVPAIR_ATTR_NAME, XML_RSC_ATTR_STICKINESS); + crm_xml_add_int(nvpair, XML_NVPAIR_ATTR_VALUE, + PCMK__RESOURCE_STICKINESS_DEFAULT); + } +#endif + return cib_root; +} + +static bool +cib_acl_enabled(xmlNode *xml, const char *user) +{ + bool rc = FALSE; + + if(pcmk_acl_required(user)) { + const char *value = NULL; + GHashTable *options = pcmk__strkey_table(free, free); + + cib_read_config(options, xml); + value = cib_pref(options, "enable-acl"); + rc = crm_is_true(value); + g_hash_table_destroy(options); + } + + crm_trace("CIB ACL is %s", rc ? "enabled" : "disabled"); + return rc; +} + +int +cib_perform_op(const char *op, int call_options, cib_op_t * fn, gboolean is_query, + const char *section, xmlNode * req, xmlNode * input, + gboolean manage_counters, gboolean * config_changed, + xmlNode * current_cib, xmlNode ** result_cib, xmlNode ** diff, xmlNode ** output) +{ + int rc = pcmk_ok; + gboolean check_schema = TRUE; + xmlNode *top = NULL; + xmlNode *scratch = NULL; + xmlNode *local_diff = NULL; + + const char *new_version = NULL; + const char *user = crm_element_value(req, F_CIB_USER); + bool with_digest = FALSE; + + pcmk__output_t *out = NULL; + int out_rc = pcmk_rc_no_output; + + crm_trace("Begin %s%s%s op", + (pcmk_is_set(call_options, cib_dryrun)? "dry run of " : ""), + (is_query? "read-only " : ""), op); + + CRM_CHECK(output != NULL, return -ENOMSG); + CRM_CHECK(result_cib != NULL, return -ENOMSG); + CRM_CHECK(config_changed != NULL, return -ENOMSG); + + if(output) { + *output = NULL; + } + + *result_cib = NULL; + *config_changed = FALSE; + + if (fn == NULL) { + return -EINVAL; + } + + if (is_query) { + xmlNode *cib_ro = current_cib; + xmlNode *cib_filtered = NULL; + + if(cib_acl_enabled(cib_ro, user)) { + if(xml_acl_filtered_copy(user, current_cib, current_cib, &cib_filtered)) { + if (cib_filtered == NULL) { + crm_debug("Pre-filtered the entire cib"); + return -EACCES; + } + cib_ro = cib_filtered; + crm_log_xml_trace(cib_ro, "filtered"); + } + } + + rc = (*fn) (op, call_options, section, req, input, cib_ro, result_cib, output); + + if(output == NULL || *output == NULL) { + /* nothing */ + + } else if(cib_filtered == *output) { + cib_filtered = NULL; /* Let them have this copy */ + + } else if(*output == current_cib) { + /* They already know not to free it */ + + } else if(cib_filtered && (*output)->doc == cib_filtered->doc) { + /* We're about to free the document of which *output is a part */ + *output = copy_xml(*output); + + } else if((*output)->doc == current_cib->doc) { + /* Give them a copy they can free */ + *output = copy_xml(*output); + } + + free_xml(cib_filtered); + return rc; + } + + + if (pcmk_is_set(call_options, cib_zero_copy)) { + /* Conditional on v2 patch style */ + + scratch = current_cib; + + /* Create a shallow copy of current_cib for the version details */ + current_cib = create_xml_node(NULL, (const char *)scratch->name); + copy_in_properties(current_cib, scratch); + top = current_cib; + + xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user)); + rc = (*fn) (op, call_options, section, req, input, scratch, &scratch, output); + + } else { + scratch = copy_xml(current_cib); + xml_track_changes(scratch, user, NULL, cib_acl_enabled(scratch, user)); + rc = (*fn) (op, call_options, section, req, input, current_cib, &scratch, output); + + if(scratch && xml_tracking_changes(scratch) == FALSE) { + crm_trace("Inferring changes after %s op", op); + xml_track_changes(scratch, user, current_cib, cib_acl_enabled(current_cib, user)); + xml_calculate_changes(current_cib, scratch); + } + CRM_CHECK(current_cib != scratch, return -EINVAL); + } + + xml_acl_disable(scratch); /* Allow the system to make any additional changes */ + + if (rc == pcmk_ok && scratch == NULL) { + rc = -EINVAL; + goto done; + + } else if(rc == pcmk_ok && xml_acl_denied(scratch)) { + crm_trace("ACL rejected part or all of the proposed changes"); + rc = -EACCES; + goto done; + + } else if (rc != pcmk_ok) { + goto done; + } + + if (scratch) { + new_version = crm_element_value(scratch, XML_ATTR_CRM_VERSION); + + if (new_version && compare_version(new_version, CRM_FEATURE_SET) > 0) { + crm_err("Discarding update with feature set '%s' greater than our own '%s'", + new_version, CRM_FEATURE_SET); + rc = -EPROTONOSUPPORT; + goto done; + } + } + + if (current_cib) { + int old = 0; + int new = 0; + + crm_element_value_int(scratch, XML_ATTR_GENERATION_ADMIN, &new); + crm_element_value_int(current_cib, XML_ATTR_GENERATION_ADMIN, &old); + + if (old > new) { + crm_err("%s went backwards: %d -> %d (Opts: %#x)", + XML_ATTR_GENERATION_ADMIN, old, new, call_options); + crm_log_xml_warn(req, "Bad Op"); + crm_log_xml_warn(input, "Bad Data"); + rc = -pcmk_err_old_data; + + } else if (old == new) { + crm_element_value_int(scratch, XML_ATTR_GENERATION, &new); + crm_element_value_int(current_cib, XML_ATTR_GENERATION, &old); + if (old > new) { + crm_err("%s went backwards: %d -> %d (Opts: %#x)", + XML_ATTR_GENERATION, old, new, call_options); + crm_log_xml_warn(req, "Bad Op"); + crm_log_xml_warn(input, "Bad Data"); + rc = -pcmk_err_old_data; + } + } + } + + crm_trace("Massaging CIB contents"); + pcmk__strip_xml_text(scratch); + fix_plus_plus_recursive(scratch); + + if (pcmk_is_set(call_options, cib_zero_copy)) { + /* At this point, current_cib is just the 'cib' tag and its properties, + * + * The v1 format would barf on this, but we know the v2 patch + * format only needs it for the top-level version fields + */ + local_diff = xml_create_patchset(2, current_cib, scratch, (bool*)config_changed, manage_counters); + + } else { + static time_t expires = 0; + time_t tm_now = time(NULL); + + if (expires < tm_now) { + expires = tm_now + 60; /* Validate clients are correctly applying v2-style diffs at most once a minute */ + with_digest = TRUE; + } + + local_diff = xml_create_patchset(0, current_cib, scratch, (bool*)config_changed, manage_counters); + } + + // Create a log output object only if we're going to use it + pcmk__if_tracing( + { + rc = pcmk_rc2legacy(pcmk__log_output_new(&out)); + CRM_CHECK(rc == pcmk_ok, goto done); + + pcmk__output_set_log_level(out, LOG_TRACE); + out_rc = pcmk__xml_show_changes(out, scratch); + }, + {} + ); + xml_accept_changes(scratch); + + if(local_diff) { + int temp_rc = pcmk_rc_no_output; + + patchset_process_digest(local_diff, current_cib, scratch, with_digest); + + if (out == NULL) { + rc = pcmk_rc2legacy(pcmk__log_output_new(&out)); + CRM_CHECK(rc == pcmk_ok, goto done); + } + pcmk__output_set_log_level(out, LOG_INFO); + temp_rc = out->message(out, "xml-patchset", local_diff); + out_rc = pcmk__output_select_rc(rc, temp_rc); + + crm_log_xml_trace(local_diff, "raw patch"); + } + + if (out != NULL) { + out->finish(out, pcmk_rc2exitc(out_rc), true, NULL); + pcmk__output_free(out); + out = NULL; + } + + if (!pcmk_is_set(call_options, cib_zero_copy) && (local_diff != NULL)) { + // Original to compare against doesn't exist + pcmk__if_tracing( + { + // Validate the calculated patch set + int test_rc = pcmk_ok; + int format = 1; + xmlNode *cib_copy = copy_xml(current_cib); + + crm_element_value_int(local_diff, "format", &format); + test_rc = xml_apply_patchset(cib_copy, local_diff, + manage_counters); + + if (test_rc != pcmk_ok) { + save_xml_to_file(cib_copy, "PatchApply:calculated", NULL); + save_xml_to_file(current_cib, "PatchApply:input", NULL); + save_xml_to_file(scratch, "PatchApply:actual", NULL); + save_xml_to_file(local_diff, "PatchApply:diff", NULL); + crm_err("v%d patchset error, patch failed to apply: %s " + "(%d)", + format, pcmk_rc_str(pcmk_legacy2rc(test_rc)), + test_rc); + } + free_xml(cib_copy); + }, + {} + ); + } + + if (pcmk__str_eq(section, XML_CIB_TAG_STATUS, pcmk__str_casei)) { + /* Throttle the amount of costly validation we perform due to status updates + * a) we don't really care whats in the status section + * b) we don't validate any of its contents at the moment anyway + */ + check_schema = FALSE; + } + + /* === scratch must not be modified after this point === + * Exceptions, anything in: + + static filter_t filter[] = { + { 0, XML_ATTR_ORIGIN }, + { 0, XML_CIB_ATTR_WRITTEN }, + { 0, XML_ATTR_UPDATE_ORIG }, + { 0, XML_ATTR_UPDATE_CLIENT }, + { 0, XML_ATTR_UPDATE_USER }, + }; + */ + + if (*config_changed && !pcmk_is_set(call_options, cib_no_mtime)) { + const char *schema = crm_element_value(scratch, XML_ATTR_VALIDATION); + + pcmk__xe_add_last_written(scratch); + if (schema) { + static int minimum_schema = 0; + int current_schema = get_schema_version(schema); + + if (minimum_schema == 0) { + minimum_schema = get_schema_version("pacemaker-1.2"); + } + + /* Does the CIB support the "update-*" attributes... */ + if (current_schema >= minimum_schema) { + const char *origin = crm_element_value(req, F_ORIG); + + CRM_LOG_ASSERT(origin != NULL); + crm_xml_replace(scratch, XML_ATTR_UPDATE_ORIG, origin); + crm_xml_replace(scratch, XML_ATTR_UPDATE_CLIENT, + crm_element_value(req, F_CIB_CLIENTNAME)); + crm_xml_replace(scratch, XML_ATTR_UPDATE_USER, crm_element_value(req, F_CIB_USER)); + } + } + } + + crm_trace("Perform validation: %s", pcmk__btoa(check_schema)); + if ((rc == pcmk_ok) && check_schema && !validate_xml(scratch, NULL, TRUE)) { + const char *current_schema = crm_element_value(scratch, + XML_ATTR_VALIDATION); + + crm_warn("Updated CIB does not validate against %s schema", + pcmk__s(current_schema, "unspecified")); + rc = -pcmk_err_schema_validation; + } + + done: + + *result_cib = scratch; + if(rc != pcmk_ok && cib_acl_enabled(current_cib, user)) { + if(xml_acl_filtered_copy(user, current_cib, scratch, result_cib)) { + if (*result_cib == NULL) { + crm_debug("Pre-filtered the entire cib result"); + } + free_xml(scratch); + } + } + + if(diff) { + *diff = local_diff; + } else { + free_xml(local_diff); + } + + free_xml(top); + crm_trace("Done"); + return rc; +} + +xmlNode * +cib_create_op(int call_id, const char *op, const char *host, + const char *section, xmlNode *data, int call_options, + const char *user_name) +{ + xmlNode *op_msg = create_xml_node(NULL, "cib_command"); + + CRM_CHECK(op_msg != NULL, return NULL); + + crm_xml_add(op_msg, F_XML_TAGNAME, "cib_command"); + + crm_xml_add(op_msg, F_TYPE, T_CIB); + crm_xml_add(op_msg, F_CIB_OPERATION, op); + crm_xml_add(op_msg, F_CIB_HOST, host); + crm_xml_add(op_msg, F_CIB_SECTION, section); + crm_xml_add_int(op_msg, F_CIB_CALLID, call_id); + if (user_name) { + crm_xml_add(op_msg, F_CIB_USER, user_name); + } + crm_trace("Sending call options: %.8lx, %d", (long)call_options, call_options); + crm_xml_add_int(op_msg, F_CIB_CALLOPTS, call_options); + + if (data != NULL) { + add_message_xml(op_msg, F_CIB_CALLDATA, data); + } + + if (call_options & cib_inhibit_bcast) { + CRM_CHECK((call_options & cib_scope_local), return NULL); + } + return op_msg; +} + +void +cib_native_callback(cib_t * cib, xmlNode * msg, int call_id, int rc) +{ + xmlNode *output = NULL; + cib_callback_client_t *blob = NULL; + + if (msg != NULL) { + crm_element_value_int(msg, F_CIB_RC, &rc); + crm_element_value_int(msg, F_CIB_CALLID, &call_id); + output = get_message_xml(msg, F_CIB_CALLDATA); + } + + blob = cib__lookup_id(call_id); + + if (blob == NULL) { + crm_trace("No callback found for call %d", call_id); + } + + if (cib == NULL) { + crm_debug("No cib object supplied"); + } + + if (rc == -pcmk_err_diff_resync) { + /* This is an internal value that clients do not and should not care about */ + rc = pcmk_ok; + } + + if (blob && blob->callback && (rc == pcmk_ok || blob->only_success == FALSE)) { + crm_trace("Invoking callback %s for call %d", + pcmk__s(blob->id, "without ID"), call_id); + blob->callback(msg, call_id, rc, output, blob->user_data); + + } else if (cib && cib->op_callback == NULL && rc != pcmk_ok) { + crm_warn("CIB command failed: %s", pcmk_strerror(rc)); + crm_log_xml_debug(msg, "Failed CIB Update"); + } + + /* This may free user_data, so do it after the callback */ + if (blob) { + remove_cib_op_callback(call_id, FALSE); + } + + if (cib && cib->op_callback != NULL) { + crm_trace("Invoking global callback for call %d", call_id); + cib->op_callback(msg, call_id, rc, output); + } + crm_trace("OP callback activated for %d", call_id); +} + +void +cib_native_notify(gpointer data, gpointer user_data) +{ + xmlNode *msg = user_data; + cib_notify_client_t *entry = data; + const char *event = NULL; + + if (msg == NULL) { + crm_warn("Skipping callback - NULL message"); + return; + } + + event = crm_element_value(msg, F_SUBTYPE); + + if (entry == NULL) { + crm_warn("Skipping callback - NULL callback client"); + return; + + } else if (entry->callback == NULL) { + crm_warn("Skipping callback - NULL callback"); + return; + + } else if (!pcmk__str_eq(entry->event, event, pcmk__str_casei)) { + crm_trace("Skipping callback - event mismatch %p/%s vs. %s", entry, entry->event, event); + return; + } + + crm_trace("Invoking callback for %p/%s event...", entry, event); + entry->callback(event, msg); + crm_trace("Callback invoked..."); +} + +static pcmk__cluster_option_t cib_opts[] = { + /* name, legacy name, type, allowed values, + * default value, validator, + * short description, + * long description + */ + { + "enable-acl", NULL, "boolean", NULL, + "false", pcmk__valid_boolean, + N_("Enable Access Control Lists (ACLs) for the CIB"), + NULL + }, + { + "cluster-ipc-limit", NULL, "integer", NULL, + "500", pcmk__valid_positive_number, + N_("Maximum IPC message backlog before disconnecting a cluster daemon"), + N_("Raise this if log has \"Evicting client\" messages for cluster daemon" + " PIDs (a good value is the number of resources in the cluster" + " multiplied by the number of nodes).") + }, +}; + +void +cib_metadata(void) +{ + const char *desc_short = "Cluster Information Base manager options"; + const char *desc_long = "Cluster options used by Pacemaker's Cluster " + "Information Base manager"; + + gchar *s = pcmk__format_option_metadata("pacemaker-based", desc_short, + desc_long, cib_opts, + PCMK__NELEM(cib_opts)); + printf("%s", s); + g_free(s); +} + +static void +verify_cib_options(GHashTable *options) +{ + pcmk__validate_cluster_options(options, cib_opts, PCMK__NELEM(cib_opts)); +} + +const char * +cib_pref(GHashTable * options, const char *name) +{ + return pcmk__cluster_option(options, cib_opts, PCMK__NELEM(cib_opts), + name); +} + +gboolean +cib_read_config(GHashTable * options, xmlNode * current_cib) +{ + xmlNode *config = NULL; + crm_time_t *now = NULL; + + if (options == NULL || current_cib == NULL) { + return FALSE; + } + + now = crm_time_new(NULL); + + g_hash_table_remove_all(options); + + config = pcmk_find_cib_element(current_cib, XML_CIB_TAG_CRMCONFIG); + if (config) { + pe_unpack_nvpairs(current_cib, config, XML_CIB_TAG_PROPSET, NULL, + options, CIB_OPTIONS_FIRST, TRUE, now, NULL); + } + + verify_cib_options(options); + + crm_time_free(now); + + return TRUE; +} + +int +cib_internal_op(cib_t * cib, const char *op, const char *host, + const char *section, xmlNode * data, + xmlNode ** output_data, int call_options, const char *user_name) +{ + int (*delegate) (cib_t * cib, const char *op, const char *host, + const char *section, xmlNode * data, + xmlNode ** output_data, int call_options, const char *user_name) = + cib->delegate_fn; + + if(user_name == NULL) { + user_name = getenv("CIB_user"); + } + + return delegate(cib, op, host, section, data, output_data, call_options, user_name); +} + +/*! + * \brief Apply a CIB update patch to a given CIB + * + * \param[in] event CIB update patch + * \param[in] input CIB to patch + * \param[out] output Resulting CIB after patch + * \param[in] level Log the patch at this log level (unless LOG_CRIT) + * + * \return Legacy Pacemaker return code + * \note sbd calls this function + */ +int +cib_apply_patch_event(xmlNode *event, xmlNode *input, xmlNode **output, + int level) +{ + int rc = pcmk_err_generic; + + xmlNode *diff = NULL; + + CRM_ASSERT(event); + CRM_ASSERT(input); + CRM_ASSERT(output); + + crm_element_value_int(event, F_CIB_RC, &rc); + diff = get_message_xml(event, F_CIB_UPDATE_RESULT); + + if (rc < pcmk_ok || diff == NULL) { + return rc; + } + + if (level > LOG_CRIT) { + pcmk__output_t *out = NULL; + + rc = pcmk_rc2legacy(pcmk__log_output_new(&out)); + CRM_CHECK(rc == pcmk_ok, return rc); + + pcmk__output_set_log_level(out, level); + rc = out->message(out, "xml-patchset", diff); + out->finish(out, pcmk_rc2exitc(rc), true, NULL); + pcmk__output_free(out); + rc = pcmk_ok; + } + + if (input != NULL) { + rc = cib_process_diff(NULL, cib_none, NULL, event, diff, input, output, + NULL); + + if (rc != pcmk_ok) { + crm_debug("Update didn't apply: %s (%d) %p", + pcmk_strerror(rc), rc, *output); + + if (rc == -pcmk_err_old_data) { + crm_trace("Masking error, we already have the supplied update"); + return pcmk_ok; + } + free_xml(*output); + *output = NULL; + return rc; + } + } + return rc; +} + +#define log_signon_query_err(out, fmt, args...) do { \ + if (out != NULL) { \ + out->err(out, fmt, ##args); \ + } else { \ + crm_err(fmt, ##args); \ + } \ + } while (0) + +int +cib__signon_query(pcmk__output_t *out, cib_t **cib, xmlNode **cib_object) +{ + int rc = pcmk_rc_ok; + cib_t *cib_conn = NULL; + + CRM_ASSERT(cib_object != NULL); + + if (cib == NULL) { + cib_conn = cib_new(); + } else { + if (*cib == NULL) { + *cib = cib_new(); + } + cib_conn = *cib; + } + + if (cib_conn == NULL) { + return ENOMEM; + } + + if (cib_conn->state == cib_disconnected) { + rc = cib_conn->cmds->signon(cib_conn, crm_system_name, cib_command); + rc = pcmk_legacy2rc(rc); + } + + if (rc != pcmk_rc_ok) { + log_signon_query_err(out, "Could not connect to the CIB: %s", + pcmk_rc_str(rc)); + goto done; + } + + if (out != NULL) { + out->transient(out, "Querying CIB..."); + } + rc = cib_conn->cmds->query(cib_conn, NULL, cib_object, + cib_scope_local|cib_sync_call); + rc = pcmk_legacy2rc(rc); + + if (rc != pcmk_rc_ok) { + log_signon_query_err(out, "CIB query failed: %s", pcmk_rc_str(rc)); + } + +done: + if (cib == NULL) { + cib__clean_up_connection(&cib_conn); + } + + if ((rc == pcmk_rc_ok) && (*cib_object == NULL)) { + return pcmk_rc_no_input; + } + return rc; +} + +int +cib__clean_up_connection(cib_t **cib) +{ + int rc; + + if (*cib == NULL) { + return pcmk_rc_ok; + } + + rc = (*cib)->cmds->signoff(*cib); + cib_delete(*cib); + *cib = NULL; + return pcmk_legacy2rc(rc); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +const char * +get_object_path(const char *object_type) +{ + return pcmk_cib_xpath_for(object_type); +} + +const char * +get_object_parent(const char *object_type) +{ + return pcmk_cib_parent_name_for(object_type); +} + +xmlNode * +get_object_root(const char *object_type, xmlNode *the_root) +{ + return pcmk_find_cib_element(the_root, object_type); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/cluster/Makefile.am b/lib/cluster/Makefile.am new file mode 100644 index 0000000..9225f29 --- /dev/null +++ b/lib/cluster/Makefile.am @@ -0,0 +1,29 @@ +# +# Copyright 2004-2018 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# +include $(top_srcdir)/mk/common.mk + +noinst_HEADERS = crmcluster_private.h + +## libraries +lib_LTLIBRARIES = libcrmcluster.la + +libcrmcluster_la_LDFLAGS = -version-info 30:0:1 + +libcrmcluster_la_CFLAGS = $(CFLAGS_HARDENED_LIB) +libcrmcluster_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) + +libcrmcluster_la_LIBADD = $(top_builddir)/lib/common/libcrmcommon.la $(top_builddir)/lib/fencing/libstonithd.la $(CLUSTERLIBS) + +libcrmcluster_la_SOURCES = election.c cluster.c membership.c +if BUILD_CS_SUPPORT +libcrmcluster_la_SOURCES += cpg.c corosync.c +endif + +clean-generic: + rm -f *.log *.debug *.xml *~ diff --git a/lib/cluster/cluster.c b/lib/cluster/cluster.c new file mode 100644 index 0000000..011e053 --- /dev/null +++ b/lib/cluster/cluster.c @@ -0,0 +1,405 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include "crmcluster_private.h" + +CRM_TRACE_INIT_DATA(cluster); + +/*! + * \brief Get (and set if needed) a node's UUID + * + * \param[in,out] peer Node to check + * + * \return Node UUID of \p peer, or NULL if unknown + */ +const char * +crm_peer_uuid(crm_node_t *peer) +{ + char *uuid = NULL; + + // Check simple cases first, to avoid any calls that might block + if (peer == NULL) { + return NULL; + } + if (peer->uuid != NULL) { + return peer->uuid; + } + + switch (get_cluster_type()) { + case pcmk_cluster_corosync: +#if SUPPORT_COROSYNC + uuid = pcmk__corosync_uuid(peer); +#endif + break; + + case pcmk_cluster_unknown: + case pcmk_cluster_invalid: + crm_err("Unsupported cluster type"); + break; + } + + peer->uuid = uuid; + return peer->uuid; +} + +/*! + * \brief Connect to the cluster layer + * + * \param[in,out] Initialized cluster object to connect + * + * \return TRUE on success, otherwise FALSE + */ +gboolean +crm_cluster_connect(crm_cluster_t *cluster) +{ + enum cluster_type_e type = get_cluster_type(); + + crm_notice("Connecting to %s cluster infrastructure", + name_for_cluster_type(type)); + switch (type) { + case pcmk_cluster_corosync: +#if SUPPORT_COROSYNC + crm_peer_init(); + return pcmk__corosync_connect(cluster); +#else + break; +#endif // SUPPORT_COROSYNC + default: + break; + } + return FALSE; +} + +/*! + * \brief Disconnect from the cluster layer + * + * \param[in,out] cluster Cluster object to disconnect + */ +void +crm_cluster_disconnect(crm_cluster_t *cluster) +{ + enum cluster_type_e type = get_cluster_type(); + + crm_info("Disconnecting from %s cluster infrastructure", + name_for_cluster_type(type)); + switch (type) { + case pcmk_cluster_corosync: +#if SUPPORT_COROSYNC + crm_peer_destroy(); + pcmk__corosync_disconnect(cluster); +#endif // SUPPORT_COROSYNC + break; + default: + break; + } +} + +/*! + * \brief Allocate a new \p crm_cluster_t object + * + * \return A newly allocated \p crm_cluster_t object (guaranteed not \p NULL) + * \note The caller is responsible for freeing the return value using + * \p pcmk_cluster_free(). + */ +crm_cluster_t * +pcmk_cluster_new(void) +{ + crm_cluster_t *cluster = calloc(1, sizeof(crm_cluster_t)); + + CRM_ASSERT(cluster != NULL); + return cluster; +} + +/*! + * \brief Free a \p crm_cluster_t object and its dynamically allocated members + * + * \param[in,out] cluster Cluster object to free + */ +void +pcmk_cluster_free(crm_cluster_t *cluster) +{ + if (cluster == NULL) { + return; + } + free(cluster->uuid); + free(cluster->uname); + free(cluster); +} + +/*! + * \brief Send an XML message via the cluster messaging layer + * + * \param[in] node Cluster node to send message to + * \param[in] service Message type to use in message host info + * \param[in] data XML message to send + * \param[in] ordered Ignored for currently supported messaging layers + * + * \return TRUE on success, otherwise FALSE + */ +gboolean +send_cluster_message(const crm_node_t *node, enum crm_ais_msg_types service, + xmlNode *data, gboolean ordered) +{ + switch (get_cluster_type()) { + case pcmk_cluster_corosync: +#if SUPPORT_COROSYNC + return pcmk__cpg_send_xml(data, node, service); +#endif + break; + default: + break; + } + return FALSE; +} + +/*! + * \brief Get the local node's name + * + * \return Local node's name + * \note This will fatally exit if local node name cannot be known. + */ +const char * +get_local_node_name(void) +{ + static char *name = NULL; + + if (name == NULL) { + name = get_node_name(0); + } + return name; +} + +/*! + * \brief Get the node name corresponding to a cluster node ID + * + * \param[in] nodeid Node ID to check (or 0 for local node) + * + * \return Node name corresponding to \p nodeid + * \note This will fatally exit if \p nodeid is 0 and local node name cannot be + * known. + */ +char * +get_node_name(uint32_t nodeid) +{ + char *name = NULL; + enum cluster_type_e stack = get_cluster_type(); + + switch (stack) { + case pcmk_cluster_corosync: +#if SUPPORT_COROSYNC + name = pcmk__corosync_name(0, nodeid); + break; +#endif // SUPPORT_COROSYNC + + default: + crm_err("Unknown cluster type: %s (%d)", name_for_cluster_type(stack), stack); + } + + if ((name == NULL) && (nodeid == 0)) { + name = pcmk_hostname(); + if (name == NULL) { + // @TODO Maybe let the caller decide what to do + crm_err("Could not obtain the local %s node name", + name_for_cluster_type(stack)); + crm_exit(CRM_EX_FATAL); + } + crm_notice("Defaulting to uname -n for the local %s node name", + name_for_cluster_type(stack)); + } + + if (name == NULL) { + crm_notice("Could not obtain a node name for %s node with id %u", + name_for_cluster_type(stack), nodeid); + } + return name; +} + +/*! + * \brief Get the node name corresponding to a node UUID + * + * \param[in] uuid UUID of desired node + * + * \return name of desired node + * + * \note This relies on the remote peer cache being populated with all + * remote nodes in the cluster, so callers should maintain that cache. + */ +const char * +crm_peer_uname(const char *uuid) +{ + GHashTableIter iter; + crm_node_t *node = NULL; + + CRM_CHECK(uuid != NULL, return NULL); + + /* remote nodes have the same uname and uuid */ + if (g_hash_table_lookup(crm_remote_peer_cache, uuid)) { + return uuid; + } + + /* avoid blocking calls where possible */ + g_hash_table_iter_init(&iter, crm_peer_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { + if (pcmk__str_eq(node->uuid, uuid, pcmk__str_casei)) { + if (node->uname != NULL) { + return node->uname; + } + break; + } + } + node = NULL; + + if (is_corosync_cluster()) { + long long id; + + if ((pcmk__scan_ll(uuid, &id, 0LL) != pcmk_rc_ok) + || (id < 1LL) || (id > UINT32_MAX)) { + crm_err("Invalid Corosync node ID '%s'", uuid); + return NULL; + } + + node = pcmk__search_cluster_node_cache((uint32_t) id, NULL); + if (node != NULL) { + crm_info("Setting uuid for node %s[%u] to %s", + node->uname, node->id, uuid); + node->uuid = strdup(uuid); + return node->uname; + } + return NULL; + } + + return NULL; +} + +/*! + * \brief Add a node's UUID as an XML attribute + * + * \param[in,out] xml XML element to add UUID to + * \param[in] attr XML attribute name to set + * \param[in,out] node Node whose UUID should be used as attribute value + */ +void +set_uuid(xmlNode *xml, const char *attr, crm_node_t *node) +{ + crm_xml_add(xml, attr, crm_peer_uuid(node)); +} + +/*! + * \brief Get a log-friendly string equivalent of a cluster type + * + * \param[in] type Cluster type + * + * \return Log-friendly string corresponding to \p type + */ +const char * +name_for_cluster_type(enum cluster_type_e type) +{ + switch (type) { + case pcmk_cluster_corosync: + return "corosync"; + case pcmk_cluster_unknown: + return "unknown"; + case pcmk_cluster_invalid: + return "invalid"; + } + crm_err("Invalid cluster type: %d", type); + return "invalid"; +} + +/*! + * \brief Get (and validate) the local cluster type + * + * \return Local cluster type + * \note This will fatally exit if the local cluster type is invalid. + */ +enum cluster_type_e +get_cluster_type(void) +{ + bool detected = false; + const char *cluster = NULL; + static enum cluster_type_e cluster_type = pcmk_cluster_unknown; + + /* Return the previous calculation, if any */ + if (cluster_type != pcmk_cluster_unknown) { + return cluster_type; + } + + cluster = pcmk__env_option(PCMK__ENV_CLUSTER_TYPE); + +#if SUPPORT_COROSYNC + /* If nothing is defined in the environment, try corosync (if supported) */ + if (cluster == NULL) { + crm_debug("Testing with Corosync"); + cluster_type = pcmk__corosync_detect(); + if (cluster_type != pcmk_cluster_unknown) { + detected = true; + goto done; + } + } +#endif + + /* Something was defined in the environment, test it against what we support */ + crm_info("Verifying cluster type: '%s'", + ((cluster == NULL)? "-unspecified-" : cluster)); + if (cluster == NULL) { + +#if SUPPORT_COROSYNC + } else if (pcmk__str_eq(cluster, "corosync", pcmk__str_casei)) { + cluster_type = pcmk_cluster_corosync; +#endif + + } else { + cluster_type = pcmk_cluster_invalid; + goto done; /* Keep the compiler happy when no stacks are supported */ + } + + done: + if (cluster_type == pcmk_cluster_unknown) { + crm_notice("Could not determine the current cluster type"); + + } else if (cluster_type == pcmk_cluster_invalid) { + crm_notice("This installation does not support the '%s' cluster infrastructure: terminating.", + cluster); + crm_exit(CRM_EX_FATAL); + + } else { + crm_info("%s an active '%s' cluster", + (detected? "Detected" : "Assuming"), + name_for_cluster_type(cluster_type)); + } + + return cluster_type; +} + +/*! + * \brief Check whether the local cluster is a Corosync cluster + * + * \return TRUE if the local cluster is a Corosync cluster, otherwise FALSE + */ +gboolean +is_corosync_cluster(void) +{ + return get_cluster_type() == pcmk_cluster_corosync; +} diff --git a/lib/cluster/corosync.c b/lib/cluster/corosync.c new file mode 100644 index 0000000..08280ce --- /dev/null +++ b/lib/cluster/corosync.c @@ -0,0 +1,814 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include // PRIu64 + +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include /* PCMK__SPECIAL_PID* */ +#include "crmcluster_private.h" + +static quorum_handle_t pcmk_quorum_handle = 0; + +static gboolean (*quorum_app_callback)(unsigned long long seq, + gboolean quorate) = NULL; + +/*! + * \internal + * \brief Get the Corosync UUID associated with a Pacemaker node + * + * \param[in] node Pacemaker node + * + * \return Newly allocated string with node's Corosync UUID, or NULL if unknown + * \note It is the caller's responsibility to free the result with free(). + */ +char * +pcmk__corosync_uuid(const crm_node_t *node) +{ + if ((node != NULL) && is_corosync_cluster()) { + if (node->id > 0) { + return crm_strdup_printf("%u", node->id); + } else { + crm_info("Node %s is not yet known by Corosync", node->uname); + } + } + return NULL; +} + +static bool +node_name_is_valid(const char *key, const char *name) +{ + int octet; + + if (name == NULL) { + crm_trace("%s is empty", key); + return false; + + } else if (sscanf(name, "%d.%d.%d.%d", &octet, &octet, &octet, &octet) == 4) { + crm_trace("%s contains an IPv4 address (%s), ignoring", key, name); + return false; + + } else if (strstr(name, ":") != NULL) { + crm_trace("%s contains an IPv6 address (%s), ignoring", key, name); + return false; + } + crm_trace("'%s: %s' is valid", key, name); + return true; +} + +/* + * \internal + * \brief Get Corosync node name corresponding to a node ID + * + * \param[in] cmap_handle Connection to Corosync CMAP + * \param[in] nodeid Node ID to check + * + * \return Newly allocated string with name or (if no name) IP address + * associated with first address assigned to a Corosync node ID (or NULL + * if unknown) + * \note It is the caller's responsibility to free the result with free(). + */ +char * +pcmk__corosync_name(uint64_t /*cmap_handle_t */ cmap_handle, uint32_t nodeid) +{ + // Originally based on corosync-quorumtool.c:node_name() + + int lpc = 0; + cs_error_t rc = CS_OK; + int retries = 0; + char *name = NULL; + cmap_handle_t local_handle = 0; + int fd = -1; + uid_t found_uid = 0; + gid_t found_gid = 0; + pid_t found_pid = 0; + int rv; + + if (nodeid == 0) { + nodeid = get_local_nodeid(0); + } + + if (cmap_handle == 0 && local_handle == 0) { + retries = 0; + crm_trace("Initializing CMAP connection"); + do { + rc = pcmk__init_cmap(&local_handle); + if (rc != CS_OK) { + retries++; + crm_debug("API connection setup failed: %s. Retrying in %ds", cs_strerror(rc), + retries); + sleep(retries); + } + + } while (retries < 5 && rc != CS_OK); + + if (rc != CS_OK) { + crm_warn("Could not connect to Cluster Configuration Database API, error %s", + cs_strerror(rc)); + local_handle = 0; + } + } + + if (cmap_handle == 0) { + cmap_handle = local_handle; + + rc = cmap_fd_get(cmap_handle, &fd); + if (rc != CS_OK) { + crm_err("Could not obtain the CMAP API connection: %s (%d)", + cs_strerror(rc), rc); + goto bail; + } + + /* CMAP provider run as root (in given user namespace, anyway)? */ + if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid, + &found_uid, &found_gid))) { + crm_err("CMAP provider is not authentic:" + " process %lld (uid: %lld, gid: %lld)", + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + goto bail; + } else if (rv < 0) { + crm_err("Could not verify authenticity of CMAP provider: %s (%d)", + strerror(-rv), -rv); + goto bail; + } + } + + while (name == NULL && cmap_handle != 0) { + uint32_t id = 0; + char *key = NULL; + + key = crm_strdup_printf("nodelist.node.%d.nodeid", lpc); + rc = cmap_get_uint32(cmap_handle, key, &id); + crm_trace("Checking %u vs %u from %s", nodeid, id, key); + free(key); + + if (rc != CS_OK) { + break; + } + + if (nodeid == id) { + crm_trace("Searching for node name for %u in nodelist.node.%d %s", + nodeid, lpc, pcmk__s(name, "")); + if (name == NULL) { + key = crm_strdup_printf("nodelist.node.%d.name", lpc); + cmap_get_string(cmap_handle, key, &name); + crm_trace("%s = %s", key, pcmk__s(name, "")); + free(key); + } + if (name == NULL) { + key = crm_strdup_printf("nodelist.node.%d.ring0_addr", lpc); + cmap_get_string(cmap_handle, key, &name); + crm_trace("%s = %s", key, pcmk__s(name, "")); + + if (!node_name_is_valid(key, name)) { + free(name); + name = NULL; + } + free(key); + } + break; + } + + lpc++; + } + +bail: + if(local_handle) { + cmap_finalize(local_handle); + } + + if (name == NULL) { + crm_info("Unable to get node name for nodeid %u", nodeid); + } + return name; +} + +/*! + * \internal + * \brief Disconnect from Corosync cluster + * + * \param[in,out] cluster Cluster connection to disconnect + */ +void +pcmk__corosync_disconnect(crm_cluster_t *cluster) +{ + cluster_disconnect_cpg(cluster); + if (pcmk_quorum_handle) { + quorum_finalize(pcmk_quorum_handle); + pcmk_quorum_handle = 0; + } + crm_notice("Disconnected from Corosync"); +} + +/*! + * \internal + * \brief Dispatch function for quorum connection file descriptor + * + * \param[in] user_data Ignored + * + * \return 0 on success, -1 on error (per mainloop_io_t interface) + */ +static int +quorum_dispatch_cb(gpointer user_data) +{ + int rc = quorum_dispatch(pcmk_quorum_handle, CS_DISPATCH_ALL); + + if (rc < 0) { + crm_err("Connection to the Quorum API failed: %d", rc); + quorum_finalize(pcmk_quorum_handle); + pcmk_quorum_handle = 0; + return -1; + } + return 0; +} + +/*! + * \internal + * \brief Notification callback for Corosync quorum connection + * + * \param[in] handle Corosync quorum connection + * \param[in] quorate Whether cluster is quorate + * \param[in] ring_id Corosync ring ID + * \param[in] view_list_entries Number of entries in \p view_list + * \param[in] view_list Corosync node IDs in membership + */ +static void +quorum_notification_cb(quorum_handle_t handle, uint32_t quorate, + uint64_t ring_id, uint32_t view_list_entries, + uint32_t *view_list) +{ + int i; + GHashTableIter iter; + crm_node_t *node = NULL; + static gboolean init_phase = TRUE; + + if (quorate != crm_have_quorum) { + if (quorate) { + crm_notice("Quorum acquired " CRM_XS " membership=%" PRIu64 " members=%lu", + ring_id, (long unsigned int)view_list_entries); + } else { + crm_warn("Quorum lost " CRM_XS " membership=%" PRIu64 " members=%lu", + ring_id, (long unsigned int)view_list_entries); + } + crm_have_quorum = quorate; + + } else { + crm_info("Quorum %s " CRM_XS " membership=%" PRIu64 " members=%lu", + (quorate? "retained" : "still lost"), ring_id, + (long unsigned int)view_list_entries); + } + + if (view_list_entries == 0 && init_phase) { + crm_info("Corosync membership is still forming, ignoring"); + return; + } + + init_phase = FALSE; + + /* Reset last_seen for all cached nodes so we can tell which ones aren't + * in the view list */ + g_hash_table_iter_init(&iter, crm_peer_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { + node->last_seen = 0; + } + + /* Update the peer cache for each node in view list */ + for (i = 0; i < view_list_entries; i++) { + uint32_t id = view_list[i]; + + crm_debug("Member[%d] %u ", i, id); + + /* Get this node's peer cache entry (adding one if not already there) */ + node = crm_get_peer(id, NULL); + if (node->uname == NULL) { + char *name = pcmk__corosync_name(0, id); + + crm_info("Obtaining name for new node %u", id); + node = crm_get_peer(id, name); + free(name); + } + + /* Update the node state (including updating last_seen to ring_id) */ + pcmk__update_peer_state(__func__, node, CRM_NODE_MEMBER, ring_id); + } + + /* Remove any peer cache entries we didn't update */ + pcmk__reap_unseen_nodes(ring_id); + + if (quorum_app_callback) { + quorum_app_callback(ring_id, quorate); + } +} + +/*! + * \internal + * \brief Connect to Corosync quorum service + * + * \param[in] dispatch Connection dispatch callback + * \param[in] destroy Connection destroy callback + */ +void +pcmk__corosync_quorum_connect(gboolean (*dispatch)(unsigned long long, + gboolean), + void (*destroy)(gpointer)) +{ + cs_error_t rc; + int fd = 0; + int quorate = 0; + uint32_t quorum_type = 0; + struct mainloop_fd_callbacks quorum_fd_callbacks; + uid_t found_uid = 0; + gid_t found_gid = 0; + pid_t found_pid = 0; + int rv; + + quorum_fd_callbacks.dispatch = quorum_dispatch_cb; + quorum_fd_callbacks.destroy = destroy; + + crm_debug("Configuring Pacemaker to obtain quorum from Corosync"); + + { +#if 0 + // New way but not supported by all Corosync 2 versions + quorum_model_v0_data_t quorum_model_data = { + .model = QUORUM_MODEL_V0, + .quorum_notify_fn = quorum_notification_cb, + }; + + rc = quorum_model_initialize(&pcmk_quorum_handle, QUORUM_MODEL_V0, + (quorum_model_data_t *) &quorum_model_data, + &quorum_type, NULL); +#else + quorum_callbacks_t quorum_callbacks = { + .quorum_notify_fn = quorum_notification_cb, + }; + + rc = quorum_initialize(&pcmk_quorum_handle, &quorum_callbacks, + &quorum_type); +#endif + } + + if (rc != CS_OK) { + crm_err("Could not connect to the Quorum API: %s (%d)", + cs_strerror(rc), rc); + goto bail; + + } else if (quorum_type != QUORUM_SET) { + crm_err("Corosync quorum is not configured"); + goto bail; + } + + rc = quorum_fd_get(pcmk_quorum_handle, &fd); + if (rc != CS_OK) { + crm_err("Could not obtain the Quorum API connection: %s (%d)", + strerror(rc), rc); + goto bail; + } + + /* Quorum provider run as root (in given user namespace, anyway)? */ + if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid, + &found_uid, &found_gid))) { + crm_err("Quorum provider is not authentic:" + " process %lld (uid: %lld, gid: %lld)", + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + rc = CS_ERR_ACCESS; + goto bail; + } else if (rv < 0) { + crm_err("Could not verify authenticity of Quorum provider: %s (%d)", + strerror(-rv), -rv); + rc = CS_ERR_ACCESS; + goto bail; + } + + rc = quorum_getquorate(pcmk_quorum_handle, &quorate); + if (rc != CS_OK) { + crm_err("Could not obtain the current Quorum API state: %d", rc); + goto bail; + } + + if (quorate) { + crm_notice("Quorum acquired"); + } else { + crm_warn("No quorum"); + } + quorum_app_callback = dispatch; + crm_have_quorum = quorate; + + rc = quorum_trackstart(pcmk_quorum_handle, CS_TRACK_CHANGES | CS_TRACK_CURRENT); + if (rc != CS_OK) { + crm_err("Could not setup Quorum API notifications: %d", rc); + goto bail; + } + + mainloop_add_fd("quorum", G_PRIORITY_HIGH, fd, dispatch, &quorum_fd_callbacks); + + pcmk__corosync_add_nodes(NULL); + + bail: + if (rc != CS_OK) { + quorum_finalize(pcmk_quorum_handle); + } +} + +/*! + * \internal + * \brief Connect to Corosync cluster layer + * + * \param[in,out] cluster Initialized cluster object to connect + */ +gboolean +pcmk__corosync_connect(crm_cluster_t *cluster) +{ + crm_node_t *peer = NULL; + enum cluster_type_e stack = get_cluster_type(); + + crm_peer_init(); + + if (stack != pcmk_cluster_corosync) { + crm_err("Invalid cluster type: %s " CRM_XS " stack=%d", + name_for_cluster_type(stack), stack); + return FALSE; + } + + if (!cluster_connect_cpg(cluster)) { + // Error message was logged by cluster_connect_cpg() + return FALSE; + } + crm_info("Connection to %s established", name_for_cluster_type(stack)); + + cluster->nodeid = get_local_nodeid(0); + if (cluster->nodeid == 0) { + crm_err("Could not determine local node ID"); + return FALSE; + } + + cluster->uname = get_node_name(0); + if (cluster->uname == NULL) { + crm_err("Could not determine local node name"); + return FALSE; + } + + // Ensure local node always exists in peer cache + peer = crm_get_peer(cluster->nodeid, cluster->uname); + cluster->uuid = pcmk__corosync_uuid(peer); + + return TRUE; +} + +/*! + * \internal + * \brief Check whether a Corosync cluster is active + * + * \return pcmk_cluster_corosync if Corosync is found, else pcmk_cluster_unknown + */ +enum cluster_type_e +pcmk__corosync_detect(void) +{ + int rc = CS_OK; + cmap_handle_t handle; + + rc = pcmk__init_cmap(&handle); + + switch(rc) { + case CS_OK: + break; + case CS_ERR_SECURITY: + crm_debug("Failed to initialize the cmap API: Permission denied (%d)", rc); + /* It's there, we just can't talk to it. + * Good enough for us to identify as 'corosync' + */ + return pcmk_cluster_corosync; + + default: + crm_info("Failed to initialize the cmap API: %s (%d)", + pcmk__cs_err_str(rc), rc); + return pcmk_cluster_unknown; + } + + cmap_finalize(handle); + return pcmk_cluster_corosync; +} + +/*! + * \brief Check whether a Corosync cluster peer is active + * + * \param[in] node Node to check + * + * \return TRUE if \p node is an active Corosync peer, otherwise FALSE + */ +gboolean +crm_is_corosync_peer_active(const crm_node_t *node) +{ + if (node == NULL) { + crm_trace("Corosync peer inactive: NULL"); + return FALSE; + + } else if (!pcmk__str_eq(node->state, CRM_NODE_MEMBER, pcmk__str_casei)) { + crm_trace("Corosync peer %s inactive: state=%s", + node->uname, node->state); + return FALSE; + + } else if (!pcmk_is_set(node->processes, crm_proc_cpg)) { + crm_trace("Corosync peer %s inactive: processes=%.16x", + node->uname, node->processes); + return FALSE; + } + return TRUE; +} + +/*! + * \internal + * \brief Load Corosync node list (via CMAP) into peer cache and optionally XML + * + * \param[in,out] xml_parent If not NULL, add entry here for each node + * + * \return true if any nodes were found, false otherwise + */ +bool +pcmk__corosync_add_nodes(xmlNode *xml_parent) +{ + int lpc = 0; + cs_error_t rc = CS_OK; + int retries = 0; + bool any = false; + cmap_handle_t cmap_handle; + int fd = -1; + uid_t found_uid = 0; + gid_t found_gid = 0; + pid_t found_pid = 0; + int rv; + + do { + rc = pcmk__init_cmap(&cmap_handle); + if (rc != CS_OK) { + retries++; + crm_debug("API connection setup failed: %s. Retrying in %ds", cs_strerror(rc), + retries); + sleep(retries); + } + + } while (retries < 5 && rc != CS_OK); + + if (rc != CS_OK) { + crm_warn("Could not connect to Cluster Configuration Database API, error %d", rc); + return false; + } + + rc = cmap_fd_get(cmap_handle, &fd); + if (rc != CS_OK) { + crm_err("Could not obtain the CMAP API connection: %s (%d)", + cs_strerror(rc), rc); + goto bail; + } + + /* CMAP provider run as root (in given user namespace, anyway)? */ + if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid, + &found_uid, &found_gid))) { + crm_err("CMAP provider is not authentic:" + " process %lld (uid: %lld, gid: %lld)", + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + goto bail; + } else if (rv < 0) { + crm_err("Could not verify authenticity of CMAP provider: %s (%d)", + strerror(-rv), -rv); + goto bail; + } + + crm_peer_init(); + crm_trace("Initializing Corosync node list"); + for (lpc = 0; TRUE; lpc++) { + uint32_t nodeid = 0; + char *name = NULL; + char *key = NULL; + + key = crm_strdup_printf("nodelist.node.%d.nodeid", lpc); + rc = cmap_get_uint32(cmap_handle, key, &nodeid); + free(key); + + if (rc != CS_OK) { + break; + } + + name = pcmk__corosync_name(cmap_handle, nodeid); + if (name != NULL) { + GHashTableIter iter; + crm_node_t *node = NULL; + + g_hash_table_iter_init(&iter, crm_peer_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { + if(node && node->uname && strcasecmp(node->uname, name) == 0) { + if (node->id && node->id != nodeid) { + crm_crit("Nodes %u and %u share the same name '%s': shutting down", node->id, + nodeid, name); + crm_exit(CRM_EX_FATAL); + } + } + } + } + + if (nodeid > 0 || name != NULL) { + crm_trace("Initializing node[%d] %u = %s", lpc, nodeid, name); + crm_get_peer(nodeid, name); + } + + if (nodeid > 0 && name != NULL) { + any = true; + + if (xml_parent) { + xmlNode *node = create_xml_node(xml_parent, XML_CIB_TAG_NODE); + + crm_xml_set_id(node, "%u", nodeid); + crm_xml_add(node, XML_ATTR_UNAME, name); + } + } + + free(name); + } +bail: + cmap_finalize(cmap_handle); + return any; +} + +/*! + * \internal + * \brief Get cluster name from Corosync configuration (via CMAP) + * + * \return Newly allocated string with cluster name if configured, or NULL + */ +char * +pcmk__corosync_cluster_name(void) +{ + cmap_handle_t handle; + char *cluster_name = NULL; + cs_error_t rc = CS_OK; + int fd = -1; + uid_t found_uid = 0; + gid_t found_gid = 0; + pid_t found_pid = 0; + int rv; + + rc = pcmk__init_cmap(&handle); + if (rc != CS_OK) { + crm_info("Failed to initialize the cmap API: %s (%d)", + cs_strerror(rc), rc); + return NULL; + } + + rc = cmap_fd_get(handle, &fd); + if (rc != CS_OK) { + crm_err("Could not obtain the CMAP API connection: %s (%d)", + cs_strerror(rc), rc); + goto bail; + } + + /* CMAP provider run as root (in given user namespace, anyway)? */ + if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid, + &found_uid, &found_gid))) { + crm_err("CMAP provider is not authentic:" + " process %lld (uid: %lld, gid: %lld)", + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + goto bail; + } else if (rv < 0) { + crm_err("Could not verify authenticity of CMAP provider: %s (%d)", + strerror(-rv), -rv); + goto bail; + } + + rc = cmap_get_string(handle, "totem.cluster_name", &cluster_name); + if (rc != CS_OK) { + crm_info("Cannot get totem.cluster_name: %s (%d)", cs_strerror(rc), rc); + + } else { + crm_debug("cmap totem.cluster_name = '%s'", cluster_name); + } + +bail: + cmap_finalize(handle); + return cluster_name; +} + +/*! + * \internal + * \brief Check (via CMAP) whether Corosync configuration has a node list + * + * \return true if Corosync has node list, otherwise false + */ +bool +pcmk__corosync_has_nodelist(void) +{ + cs_error_t cs_rc = CS_OK; + int retries = 0; + cmap_handle_t cmap_handle; + cmap_iter_handle_t iter_handle; + char key_name[CMAP_KEYNAME_MAXLEN + 1]; + int fd = -1; + uid_t found_uid = 0; + gid_t found_gid = 0; + pid_t found_pid = 0; + int rc = pcmk_ok; + + static bool got_result = false; + static bool result = false; + + if (got_result) { + return result; + } + + // Connect to CMAP + do { + cs_rc = pcmk__init_cmap(&cmap_handle); + if (cs_rc != CS_OK) { + retries++; + crm_debug("CMAP connection failed: %s (rc=%d, retrying in %ds)", + cs_strerror(cs_rc), cs_rc, retries); + sleep(retries); + } + } while ((retries < 5) && (cs_rc != CS_OK)); + if (cs_rc != CS_OK) { + crm_warn("Assuming Corosync does not have node list: " + "CMAP connection failed (%s) " CRM_XS " rc=%d", + cs_strerror(cs_rc), cs_rc); + return false; + } + + // Get CMAP connection file descriptor + cs_rc = cmap_fd_get(cmap_handle, &fd); + if (cs_rc != CS_OK) { + crm_warn("Assuming Corosync does not have node list: " + "CMAP unusable (%s) " CRM_XS " rc=%d", + cs_strerror(cs_rc), cs_rc); + goto bail; + } + + // Check whether CMAP connection is authentic (i.e. provided by root) + rc = crm_ipc_is_authentic_process(fd, (uid_t) 0, (gid_t) 0, + &found_pid, &found_uid, &found_gid); + if (rc == 0) { + crm_warn("Assuming Corosync does not have node list: " + "CMAP provider is inauthentic " + CRM_XS " pid=%lld uid=%lld gid=%lld", + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + goto bail; + } else if (rc < 0) { + crm_warn("Assuming Corosync does not have node list: " + "Could not verify CMAP authenticity (%s) " CRM_XS " rc=%d", + pcmk_strerror(rc), rc); + goto bail; + } + + // Check whether nodelist section is presetn + cs_rc = cmap_iter_init(cmap_handle, "nodelist", &iter_handle); + if (cs_rc != CS_OK) { + crm_warn("Assuming Corosync does not have node list: " + "CMAP not readable (%s) " CRM_XS " rc=%d", + cs_strerror(cs_rc), cs_rc); + goto bail; + } + + cs_rc = cmap_iter_next(cmap_handle, iter_handle, key_name, NULL, NULL); + if (cs_rc == CS_OK) { + result = true; + } + + cmap_iter_finalize(cmap_handle, iter_handle); + got_result = true; + crm_debug("Corosync %s node list", (result? "has" : "does not have")); + +bail: + cmap_finalize(cmap_handle); + return result; +} diff --git a/lib/cluster/cpg.c b/lib/cluster/cpg.c new file mode 100644 index 0000000..2af4a50 --- /dev/null +++ b/lib/cluster/cpg.c @@ -0,0 +1,1092 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include /* PCMK__SPECIAL_PID* */ +#include "crmcluster_private.h" + +/* @TODO Once we can update the public API to require crm_cluster_t* in more + * functions, we can ditch this in favor of cluster->cpg_handle. + */ +static cpg_handle_t pcmk_cpg_handle = 0; + +// @TODO These could be moved to crm_cluster_t* at that time as well +static bool cpg_evicted = false; +static GList *cs_message_queue = NULL; +static int cs_message_timer = 0; + +struct pcmk__cpg_host_s { + uint32_t id; + uint32_t pid; + gboolean local; + enum crm_ais_msg_types type; + uint32_t size; + char uname[MAX_NAME]; +} __attribute__ ((packed)); + +typedef struct pcmk__cpg_host_s pcmk__cpg_host_t; + +struct pcmk__cpg_msg_s { + struct qb_ipc_response_header header __attribute__ ((aligned(8))); + uint32_t id; + gboolean is_compressed; + + pcmk__cpg_host_t host; + pcmk__cpg_host_t sender; + + uint32_t size; + uint32_t compressed_size; + /* 584 bytes */ + char data[0]; + +} __attribute__ ((packed)); + +typedef struct pcmk__cpg_msg_s pcmk__cpg_msg_t; + +static void crm_cs_flush(gpointer data); + +#define msg_data_len(msg) (msg->is_compressed?msg->compressed_size:msg->size) + +#define cs_repeat(rc, counter, max, code) do { \ + rc = code; \ + if ((rc == CS_ERR_TRY_AGAIN) || (rc == CS_ERR_QUEUE_FULL)) { \ + counter++; \ + crm_debug("Retrying operation after %ds", counter); \ + sleep(counter); \ + } else { \ + break; \ + } \ + } while (counter < max) + +/*! + * \brief Disconnect from Corosync CPG + * + * \param[in,out] cluster Cluster to disconnect + */ +void +cluster_disconnect_cpg(crm_cluster_t *cluster) +{ + pcmk_cpg_handle = 0; + if (cluster->cpg_handle) { + crm_trace("Disconnecting CPG"); + cpg_leave(cluster->cpg_handle, &cluster->group); + cpg_finalize(cluster->cpg_handle); + cluster->cpg_handle = 0; + + } else { + crm_info("No CPG connection"); + } +} + +/*! + * \brief Get the local Corosync node ID (via CPG) + * + * \param[in] handle CPG connection to use (or 0 to use new connection) + * + * \return Corosync ID of local node (or 0 if not known) + */ +uint32_t +get_local_nodeid(cpg_handle_t handle) +{ + cs_error_t rc = CS_OK; + int retries = 0; + static uint32_t local_nodeid = 0; + cpg_handle_t local_handle = handle; + cpg_model_v1_data_t cpg_model_info = {CPG_MODEL_V1, NULL, NULL, NULL, 0}; + int fd = -1; + uid_t found_uid = 0; + gid_t found_gid = 0; + pid_t found_pid = 0; + int rv; + + if(local_nodeid != 0) { + return local_nodeid; + } + + if(handle == 0) { + crm_trace("Creating connection"); + cs_repeat(rc, retries, 5, cpg_model_initialize(&local_handle, CPG_MODEL_V1, (cpg_model_data_t *)&cpg_model_info, NULL)); + if (rc != CS_OK) { + crm_err("Could not connect to the CPG API: %s (%d)", + cs_strerror(rc), rc); + return 0; + } + + rc = cpg_fd_get(local_handle, &fd); + if (rc != CS_OK) { + crm_err("Could not obtain the CPG API connection: %s (%d)", + cs_strerror(rc), rc); + goto bail; + } + + /* CPG provider run as root (in given user namespace, anyway)? */ + if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid, + &found_uid, &found_gid))) { + crm_err("CPG provider is not authentic:" + " process %lld (uid: %lld, gid: %lld)", + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + goto bail; + } else if (rv < 0) { + crm_err("Could not verify authenticity of CPG provider: %s (%d)", + strerror(-rv), -rv); + goto bail; + } + } + + if (rc == CS_OK) { + retries = 0; + crm_trace("Performing lookup"); + cs_repeat(rc, retries, 5, cpg_local_get(local_handle, &local_nodeid)); + } + + if (rc != CS_OK) { + crm_err("Could not get local node id from the CPG API: %s (%d)", + pcmk__cs_err_str(rc), rc); + } + +bail: + if(handle == 0) { + crm_trace("Closing connection"); + cpg_finalize(local_handle); + } + crm_debug("Local nodeid is %u", local_nodeid); + return local_nodeid; +} + +/*! + * \internal + * \brief Callback function for Corosync message queue timer + * + * \param[in] data CPG handle + * + * \return FALSE (to indicate to glib that timer should not be removed) + */ +static gboolean +crm_cs_flush_cb(gpointer data) +{ + cs_message_timer = 0; + crm_cs_flush(data); + return FALSE; +} + +// Send no more than this many CPG messages in one flush +#define CS_SEND_MAX 200 + +/*! + * \internal + * \brief Send messages in Corosync CPG message queue + * + * \param[in] data CPG handle + */ +static void +crm_cs_flush(gpointer data) +{ + unsigned int sent = 0; + guint queue_len = 0; + cs_error_t rc = 0; + cpg_handle_t *handle = (cpg_handle_t *) data; + + if (*handle == 0) { + crm_trace("Connection is dead"); + return; + } + + queue_len = g_list_length(cs_message_queue); + if (((queue_len % 1000) == 0) && (queue_len > 1)) { + crm_err("CPG queue has grown to %d", queue_len); + + } else if (queue_len == CS_SEND_MAX) { + crm_warn("CPG queue has grown to %d", queue_len); + } + + if (cs_message_timer != 0) { + /* There is already a timer, wait until it goes off */ + crm_trace("Timer active %d", cs_message_timer); + return; + } + + while ((cs_message_queue != NULL) && (sent < CS_SEND_MAX)) { + struct iovec *iov = cs_message_queue->data; + + rc = cpg_mcast_joined(*handle, CPG_TYPE_AGREED, iov, 1); + if (rc != CS_OK) { + break; + } + + sent++; + crm_trace("CPG message sent, size=%llu", + (unsigned long long) iov->iov_len); + + cs_message_queue = g_list_remove(cs_message_queue, iov); + free(iov->iov_base); + free(iov); + } + + queue_len -= sent; + do_crm_log((queue_len > 5)? LOG_INFO : LOG_TRACE, + "Sent %u CPG message%s (%d still queued): %s (rc=%d)", + sent, pcmk__plural_s(sent), queue_len, pcmk__cs_err_str(rc), + (int) rc); + + if (cs_message_queue) { + uint32_t delay_ms = 100; + if (rc != CS_OK) { + /* Proportionally more if sending failed but cap at 1s */ + delay_ms = QB_MIN(1000, CS_SEND_MAX + (10 * queue_len)); + } + cs_message_timer = g_timeout_add(delay_ms, crm_cs_flush_cb, data); + } +} + +/*! + * \internal + * \brief Dispatch function for CPG handle + * + * \param[in,out] user_data Cluster object + * + * \return 0 on success, -1 on error (per mainloop_io_t interface) + */ +static int +pcmk_cpg_dispatch(gpointer user_data) +{ + cs_error_t rc = CS_OK; + crm_cluster_t *cluster = (crm_cluster_t *) user_data; + + rc = cpg_dispatch(cluster->cpg_handle, CS_DISPATCH_ONE); + if (rc != CS_OK) { + crm_err("Connection to the CPG API failed: %s (%d)", + pcmk__cs_err_str(rc), rc); + cpg_finalize(cluster->cpg_handle); + cluster->cpg_handle = 0; + return -1; + + } else if (cpg_evicted) { + crm_err("Evicted from CPG membership"); + return -1; + } + return 0; +} + +static inline const char * +ais_dest(const pcmk__cpg_host_t *host) +{ + if (host->local) { + return "local"; + } else if (host->size > 0) { + return host->uname; + } else { + return ""; + } +} + +static inline const char * +msg_type2text(enum crm_ais_msg_types type) +{ + const char *text = "unknown"; + + switch (type) { + case crm_msg_none: + text = "unknown"; + break; + case crm_msg_ais: + text = "ais"; + break; + case crm_msg_cib: + text = "cib"; + break; + case crm_msg_crmd: + text = "crmd"; + break; + case crm_msg_pe: + text = "pengine"; + break; + case crm_msg_te: + text = "tengine"; + break; + case crm_msg_lrmd: + text = "lrmd"; + break; + case crm_msg_attrd: + text = "attrd"; + break; + case crm_msg_stonithd: + text = "stonithd"; + break; + case crm_msg_stonith_ng: + text = "stonith-ng"; + break; + } + return text; +} + +/*! + * \internal + * \brief Check whether a Corosync CPG message is valid + * + * \param[in] msg Corosync CPG message to check + * + * \return true if \p msg is valid, otherwise false + */ +static bool +check_message_sanity(const pcmk__cpg_msg_t *msg) +{ + int32_t payload_size = msg->header.size - sizeof(pcmk__cpg_msg_t); + + if (payload_size < 1) { + crm_err("%sCPG message %d from %s invalid: " + "Claimed size of %d bytes is too small " + CRM_XS " from %s[%u] to %s@%s", + (msg->is_compressed? "Compressed " : ""), + msg->id, ais_dest(&(msg->sender)), + (int) msg->header.size, + msg_type2text(msg->sender.type), msg->sender.pid, + msg_type2text(msg->host.type), ais_dest(&(msg->host))); + return false; + } + + if (msg->header.error != CS_OK) { + crm_err("%sCPG message %d from %s invalid: " + "Sender indicated error %d " + CRM_XS " from %s[%u] to %s@%s", + (msg->is_compressed? "Compressed " : ""), + msg->id, ais_dest(&(msg->sender)), + msg->header.error, + msg_type2text(msg->sender.type), msg->sender.pid, + msg_type2text(msg->host.type), ais_dest(&(msg->host))); + return false; + } + + if (msg_data_len(msg) != payload_size) { + crm_err("%sCPG message %d from %s invalid: " + "Total size %d inconsistent with payload size %d " + CRM_XS " from %s[%u] to %s@%s", + (msg->is_compressed? "Compressed " : ""), + msg->id, ais_dest(&(msg->sender)), + (int) msg->header.size, (int) msg_data_len(msg), + msg_type2text(msg->sender.type), msg->sender.pid, + msg_type2text(msg->host.type), ais_dest(&(msg->host))); + return false; + } + + if (!msg->is_compressed && + /* msg->size != (strlen(msg->data) + 1) would be a stronger check, + * but checking the last byte or two should be quick + */ + (((msg->size > 1) && (msg->data[msg->size - 2] == '\0')) + || (msg->data[msg->size - 1] != '\0'))) { + crm_err("CPG message %d from %s invalid: " + "Payload does not end at byte %llu " + CRM_XS " from %s[%u] to %s@%s", + msg->id, ais_dest(&(msg->sender)), + (unsigned long long) msg->size, + msg_type2text(msg->sender.type), msg->sender.pid, + msg_type2text(msg->host.type), ais_dest(&(msg->host))); + return false; + } + + crm_trace("Verified %d-byte %sCPG message %d from %s[%u]@%s to %s@%s", + (int) msg->header.size, (msg->is_compressed? "compressed " : ""), + msg->id, msg_type2text(msg->sender.type), msg->sender.pid, + ais_dest(&(msg->sender)), + msg_type2text(msg->host.type), ais_dest(&(msg->host))); + return true; +} + +/*! + * \brief Extract text data from a Corosync CPG message + * + * \param[in] handle CPG connection (to get local node ID if not known) + * \param[in] nodeid Corosync ID of node that sent message + * \param[in] pid Process ID of message sender (for logging only) + * \param[in,out] content CPG message + * \param[out] kind If not NULL, will be set to CPG header ID + * (which should be an enum crm_ais_msg_class value, + * currently always crm_class_cluster) + * \param[out] from If not NULL, will be set to sender uname + * (valid for the lifetime of \p content) + * + * \return Newly allocated string with message data + * \note It is the caller's responsibility to free the return value with free(). + */ +char * +pcmk_message_common_cs(cpg_handle_t handle, uint32_t nodeid, uint32_t pid, void *content, + uint32_t *kind, const char **from) +{ + char *data = NULL; + pcmk__cpg_msg_t *msg = (pcmk__cpg_msg_t *) content; + + if(handle) { + // Do filtering and field massaging + uint32_t local_nodeid = get_local_nodeid(handle); + const char *local_name = get_local_node_name(); + + if (msg->sender.id > 0 && msg->sender.id != nodeid) { + crm_err("Nodeid mismatch from %d.%d: claimed nodeid=%u", nodeid, pid, msg->sender.id); + return NULL; + + } else if (msg->host.id != 0 && (local_nodeid != msg->host.id)) { + /* Not for us */ + crm_trace("Not for us: %u != %u", msg->host.id, local_nodeid); + return NULL; + } else if (msg->host.size != 0 && !pcmk__str_eq(msg->host.uname, local_name, pcmk__str_casei)) { + /* Not for us */ + crm_trace("Not for us: %s != %s", msg->host.uname, local_name); + return NULL; + } + + msg->sender.id = nodeid; + if (msg->sender.size == 0) { + crm_node_t *peer = crm_get_peer(nodeid, NULL); + + if (peer == NULL) { + crm_err("Peer with nodeid=%u is unknown", nodeid); + + } else if (peer->uname == NULL) { + crm_err("No uname for peer with nodeid=%u", nodeid); + + } else { + crm_notice("Fixing uname for peer with nodeid=%u", nodeid); + msg->sender.size = strlen(peer->uname); + memset(msg->sender.uname, 0, MAX_NAME); + memcpy(msg->sender.uname, peer->uname, msg->sender.size); + } + } + } + + crm_trace("Got new%s message (size=%d, %d, %d)", + msg->is_compressed ? " compressed" : "", + msg_data_len(msg), msg->size, msg->compressed_size); + + if (kind != NULL) { + *kind = msg->header.id; + } + if (from != NULL) { + *from = msg->sender.uname; + } + + if (msg->is_compressed && msg->size > 0) { + int rc = BZ_OK; + char *uncompressed = NULL; + unsigned int new_size = msg->size + 1; + + if (!check_message_sanity(msg)) { + goto badmsg; + } + + crm_trace("Decompressing message data"); + uncompressed = calloc(1, new_size); + rc = BZ2_bzBuffToBuffDecompress(uncompressed, &new_size, msg->data, msg->compressed_size, 1, 0); + + if (rc != BZ_OK) { + crm_err("Decompression failed: %s " CRM_XS " bzerror=%d", + bz2_strerror(rc), rc); + free(uncompressed); + goto badmsg; + } + + CRM_ASSERT(rc == BZ_OK); + CRM_ASSERT(new_size == msg->size); + + data = uncompressed; + + } else if (!check_message_sanity(msg)) { + goto badmsg; + + } else { + data = strdup(msg->data); + } + + // Is this necessary? + crm_get_peer(msg->sender.id, msg->sender.uname); + + crm_trace("Payload: %.200s", data); + return data; + + badmsg: + crm_err("Invalid message (id=%d, dest=%s:%s, from=%s:%s.%d):" + " min=%d, total=%d, size=%d, bz2_size=%d", + msg->id, ais_dest(&(msg->host)), msg_type2text(msg->host.type), + ais_dest(&(msg->sender)), msg_type2text(msg->sender.type), + msg->sender.pid, (int)sizeof(pcmk__cpg_msg_t), + msg->header.size, msg->size, msg->compressed_size); + + free(data); + return NULL; +} + +/*! + * \internal + * \brief Compare cpg_address objects by node ID + * + * \param[in] first First cpg_address structure to compare + * \param[in] second Second cpg_address structure to compare + * + * \return Negative number if first's node ID is lower, + * positive number if first's node ID is greater, + * or 0 if both node IDs are equal + */ +static int +cmp_member_list_nodeid(const void *first, const void *second) +{ + const struct cpg_address *const a = *((const struct cpg_address **) first), + *const b = *((const struct cpg_address **) second); + if (a->nodeid < b->nodeid) { + return -1; + } else if (a->nodeid > b->nodeid) { + return 1; + } + /* don't bother with "reason" nor "pid" */ + return 0; +} + +/*! + * \internal + * \brief Get a readable string equivalent of a cpg_reason_t value + * + * \param[in] reason CPG reason value + * + * \return Readable string suitable for logging + */ +static const char * +cpgreason2str(cpg_reason_t reason) +{ + switch (reason) { + case CPG_REASON_JOIN: return " via cpg_join"; + case CPG_REASON_LEAVE: return " via cpg_leave"; + case CPG_REASON_NODEDOWN: return " via cluster exit"; + case CPG_REASON_NODEUP: return " via cluster join"; + case CPG_REASON_PROCDOWN: return " for unknown reason"; + default: break; + } + return ""; +} + +/*! + * \internal + * \brief Get a log-friendly node name + * + * \param[in] peer Node to check + * + * \return Node's uname, or readable string if not known + */ +static inline const char * +peer_name(const crm_node_t *peer) +{ + if (peer == NULL) { + return "unknown node"; + } else if (peer->uname == NULL) { + return "peer node"; + } else { + return peer->uname; + } +} + +/*! + * \internal + * \brief Process a CPG peer's leaving the cluster + * + * \param[in] cpg_group_name CPG group name (for logging) + * \param[in] event_counter Event number (for logging) + * \param[in] local_nodeid Node ID of local node + * \param[in] cpg_peer CPG peer that left + * \param[in] sorted_member_list List of remaining members, qsort()-ed by ID + * \param[in] member_list_entries Number of entries in \p sorted_member_list + */ +static void +node_left(const char *cpg_group_name, int event_counter, + uint32_t local_nodeid, const struct cpg_address *cpg_peer, + const struct cpg_address **sorted_member_list, + size_t member_list_entries) +{ + crm_node_t *peer = pcmk__search_cluster_node_cache(cpg_peer->nodeid, + NULL); + const struct cpg_address **rival = NULL; + + /* Most CPG-related Pacemaker code assumes that only one process on a node + * can be in the process group, but Corosync does not impose this + * limitation, and more than one can be a member in practice due to a + * daemon attempting to start while another instance is already running. + * + * Check for any such duplicate instances, because we don't want to process + * their leaving as if our actual peer left. If the peer that left still has + * an entry in sorted_member_list (with a different PID), we will ignore the + * leaving. + * + * @TODO Track CPG members' PIDs so we can tell exactly who left. + */ + if (peer != NULL) { + rival = bsearch(&cpg_peer, sorted_member_list, member_list_entries, + sizeof(const struct cpg_address *), + cmp_member_list_nodeid); + } + + if (rival == NULL) { + crm_info("Group %s event %d: %s (node %u pid %u) left%s", + cpg_group_name, event_counter, peer_name(peer), + cpg_peer->nodeid, cpg_peer->pid, + cpgreason2str(cpg_peer->reason)); + if (peer != NULL) { + crm_update_peer_proc(__func__, peer, crm_proc_cpg, + OFFLINESTATUS); + } + } else if (cpg_peer->nodeid == local_nodeid) { + crm_warn("Group %s event %d: duplicate local pid %u left%s", + cpg_group_name, event_counter, + cpg_peer->pid, cpgreason2str(cpg_peer->reason)); + } else { + crm_warn("Group %s event %d: " + "%s (node %u) duplicate pid %u left%s (%u remains)", + cpg_group_name, event_counter, peer_name(peer), + cpg_peer->nodeid, cpg_peer->pid, + cpgreason2str(cpg_peer->reason), (*rival)->pid); + } +} + +/*! + * \brief Handle a CPG configuration change event + * + * \param[in] handle CPG connection + * \param[in] cpg_name CPG group name + * \param[in] member_list List of current CPG members + * \param[in] member_list_entries Number of entries in \p member_list + * \param[in] left_list List of CPG members that left + * \param[in] left_list_entries Number of entries in \p left_list + * \param[in] joined_list List of CPG members that joined + * \param[in] joined_list_entries Number of entries in \p joined_list + */ +void +pcmk_cpg_membership(cpg_handle_t handle, + const struct cpg_name *groupName, + const struct cpg_address *member_list, size_t member_list_entries, + const struct cpg_address *left_list, size_t left_list_entries, + const struct cpg_address *joined_list, size_t joined_list_entries) +{ + int i; + gboolean found = FALSE; + static int counter = 0; + uint32_t local_nodeid = get_local_nodeid(handle); + const struct cpg_address **sorted; + + sorted = malloc(member_list_entries * sizeof(const struct cpg_address *)); + CRM_ASSERT(sorted != NULL); + + for (size_t iter = 0; iter < member_list_entries; iter++) { + sorted[iter] = member_list + iter; + } + /* so that the cross-matching multiply-subscribed nodes is then cheap */ + qsort(sorted, member_list_entries, sizeof(const struct cpg_address *), + cmp_member_list_nodeid); + + for (i = 0; i < left_list_entries; i++) { + node_left(groupName->value, counter, local_nodeid, &left_list[i], + sorted, member_list_entries); + } + free(sorted); + sorted = NULL; + + for (i = 0; i < joined_list_entries; i++) { + crm_info("Group %s event %d: node %u pid %u joined%s", + groupName->value, counter, joined_list[i].nodeid, + joined_list[i].pid, cpgreason2str(joined_list[i].reason)); + } + + for (i = 0; i < member_list_entries; i++) { + crm_node_t *peer = crm_get_peer(member_list[i].nodeid, NULL); + + if (member_list[i].nodeid == local_nodeid + && member_list[i].pid != getpid()) { + // See the note in node_left() + crm_warn("Group %s event %d: detected duplicate local pid %u", + groupName->value, counter, member_list[i].pid); + continue; + } + crm_info("Group %s event %d: %s (node %u pid %u) is member", + groupName->value, counter, peer_name(peer), + member_list[i].nodeid, member_list[i].pid); + + /* If the caller left auto-reaping enabled, this will also update the + * state to member. + */ + peer = crm_update_peer_proc(__func__, peer, crm_proc_cpg, + ONLINESTATUS); + + if (peer && peer->state && strcmp(peer->state, CRM_NODE_MEMBER)) { + /* The node is a CPG member, but we currently think it's not a + * cluster member. This is possible only if auto-reaping was + * disabled. The node may be joining, and we happened to get the CPG + * notification before the quorum notification; or the node may have + * just died, and we are processing its final messages; or a bug + * has affected the peer cache. + */ + time_t now = time(NULL); + + if (peer->when_lost == 0) { + // Track when we first got into this contradictory state + peer->when_lost = now; + + } else if (now > (peer->when_lost + 60)) { + // If it persists for more than a minute, update the state + crm_warn("Node %u is member of group %s but was believed offline", + member_list[i].nodeid, groupName->value); + pcmk__update_peer_state(__func__, peer, CRM_NODE_MEMBER, 0); + } + } + + if (local_nodeid == member_list[i].nodeid) { + found = TRUE; + } + } + + if (!found) { + crm_err("Local node was evicted from group %s", groupName->value); + cpg_evicted = true; + } + + counter++; +} + +/*! + * \brief Connect to Corosync CPG + * + * \param[in,out] cluster Cluster object + * + * \return TRUE on success, otherwise FALSE + */ +gboolean +cluster_connect_cpg(crm_cluster_t *cluster) +{ + cs_error_t rc; + int fd = -1; + int retries = 0; + uint32_t id = 0; + crm_node_t *peer = NULL; + cpg_handle_t handle = 0; + const char *message_name = pcmk__message_name(crm_system_name); + uid_t found_uid = 0; + gid_t found_gid = 0; + pid_t found_pid = 0; + int rv; + + struct mainloop_fd_callbacks cpg_fd_callbacks = { + .dispatch = pcmk_cpg_dispatch, + .destroy = cluster->destroy, + }; + + cpg_model_v1_data_t cpg_model_info = { + .model = CPG_MODEL_V1, + .cpg_deliver_fn = cluster->cpg.cpg_deliver_fn, + .cpg_confchg_fn = cluster->cpg.cpg_confchg_fn, + .cpg_totem_confchg_fn = NULL, + .flags = 0, + }; + + cpg_evicted = false; + cluster->group.length = 0; + cluster->group.value[0] = 0; + + /* group.value is char[128] */ + strncpy(cluster->group.value, message_name, 127); + cluster->group.value[127] = 0; + cluster->group.length = 1 + QB_MIN(127, strlen(cluster->group.value)); + + cs_repeat(rc, retries, 30, cpg_model_initialize(&handle, CPG_MODEL_V1, (cpg_model_data_t *)&cpg_model_info, NULL)); + if (rc != CS_OK) { + crm_err("Could not connect to the CPG API: %s (%d)", + cs_strerror(rc), rc); + goto bail; + } + + rc = cpg_fd_get(handle, &fd); + if (rc != CS_OK) { + crm_err("Could not obtain the CPG API connection: %s (%d)", + cs_strerror(rc), rc); + goto bail; + } + + /* CPG provider run as root (in given user namespace, anyway)? */ + if (!(rv = crm_ipc_is_authentic_process(fd, (uid_t) 0,(gid_t) 0, &found_pid, + &found_uid, &found_gid))) { + crm_err("CPG provider is not authentic:" + " process %lld (uid: %lld, gid: %lld)", + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + rc = CS_ERR_ACCESS; + goto bail; + } else if (rv < 0) { + crm_err("Could not verify authenticity of CPG provider: %s (%d)", + strerror(-rv), -rv); + rc = CS_ERR_ACCESS; + goto bail; + } + + id = get_local_nodeid(handle); + if (id == 0) { + crm_err("Could not get local node id from the CPG API"); + goto bail; + + } + cluster->nodeid = id; + + retries = 0; + cs_repeat(rc, retries, 30, cpg_join(handle, &cluster->group)); + if (rc != CS_OK) { + crm_err("Could not join the CPG group '%s': %d", message_name, rc); + goto bail; + } + + pcmk_cpg_handle = handle; + cluster->cpg_handle = handle; + mainloop_add_fd("corosync-cpg", G_PRIORITY_MEDIUM, fd, cluster, &cpg_fd_callbacks); + + bail: + if (rc != CS_OK) { + cpg_finalize(handle); + return FALSE; + } + + peer = crm_get_peer(id, NULL); + crm_update_peer_proc(__func__, peer, crm_proc_cpg, ONLINESTATUS); + return TRUE; +} + +/*! + * \internal + * \brief Send an XML message via Corosync CPG + * + * \param[in] msg XML message to send + * \param[in] node Cluster node to send message to + * \param[in] dest Type of message to send + * + * \return TRUE on success, otherwise FALSE + */ +gboolean +pcmk__cpg_send_xml(xmlNode *msg, const crm_node_t *node, + enum crm_ais_msg_types dest) +{ + gboolean rc = TRUE; + char *data = NULL; + + data = dump_xml_unformatted(msg); + rc = send_cluster_text(crm_class_cluster, data, FALSE, node, dest); + free(data); + return rc; +} + +/*! + * \internal + * \brief Send string data via Corosync CPG + * + * \param[in] msg_class Message class (to set as CPG header ID) + * \param[in] data Data to send + * \param[in] local What to set as host "local" value (which is never used) + * \param[in] node Cluster node to send message to + * \param[in] dest Type of message to send + * + * \return TRUE on success, otherwise FALSE + */ +gboolean +send_cluster_text(enum crm_ais_msg_class msg_class, const char *data, + gboolean local, const crm_node_t *node, + enum crm_ais_msg_types dest) +{ + static int msg_id = 0; + static int local_pid = 0; + static int local_name_len = 0; + static const char *local_name = NULL; + + char *target = NULL; + struct iovec *iov; + pcmk__cpg_msg_t *msg = NULL; + enum crm_ais_msg_types sender = text2msg_type(crm_system_name); + + switch (msg_class) { + case crm_class_cluster: + break; + default: + crm_err("Invalid message class: %d", msg_class); + return FALSE; + } + + CRM_CHECK(dest != crm_msg_ais, return FALSE); + + if (local_name == NULL) { + local_name = get_local_node_name(); + } + if ((local_name_len == 0) && (local_name != NULL)) { + local_name_len = strlen(local_name); + } + + if (data == NULL) { + data = ""; + } + + if (local_pid == 0) { + local_pid = getpid(); + } + + if (sender == crm_msg_none) { + sender = local_pid; + } + + msg = calloc(1, sizeof(pcmk__cpg_msg_t)); + + msg_id++; + msg->id = msg_id; + msg->header.id = msg_class; + msg->header.error = CS_OK; + + msg->host.type = dest; + msg->host.local = local; + + if (node) { + if (node->uname) { + target = strdup(node->uname); + msg->host.size = strlen(node->uname); + memset(msg->host.uname, 0, MAX_NAME); + memcpy(msg->host.uname, node->uname, msg->host.size); + } else { + target = crm_strdup_printf("%u", node->id); + } + msg->host.id = node->id; + } else { + target = strdup("all"); + } + + msg->sender.id = 0; + msg->sender.type = sender; + msg->sender.pid = local_pid; + msg->sender.size = local_name_len; + memset(msg->sender.uname, 0, MAX_NAME); + if ((local_name != NULL) && (msg->sender.size != 0)) { + memcpy(msg->sender.uname, local_name, msg->sender.size); + } + + msg->size = 1 + strlen(data); + msg->header.size = sizeof(pcmk__cpg_msg_t) + msg->size; + + if (msg->size < CRM_BZ2_THRESHOLD) { + msg = pcmk__realloc(msg, msg->header.size); + memcpy(msg->data, data, msg->size); + + } else { + char *compressed = NULL; + unsigned int new_size = 0; + char *uncompressed = strdup(data); + + if (pcmk__compress(uncompressed, (unsigned int) msg->size, 0, + &compressed, &new_size) == pcmk_rc_ok) { + + msg->header.size = sizeof(pcmk__cpg_msg_t) + new_size; + msg = pcmk__realloc(msg, msg->header.size); + memcpy(msg->data, compressed, new_size); + + msg->is_compressed = TRUE; + msg->compressed_size = new_size; + + } else { + // cppcheck seems not to understand the abort logic in pcmk__realloc + // cppcheck-suppress memleak + msg = pcmk__realloc(msg, msg->header.size); + memcpy(msg->data, data, msg->size); + } + + free(uncompressed); + free(compressed); + } + + iov = calloc(1, sizeof(struct iovec)); + iov->iov_base = msg; + iov->iov_len = msg->header.size; + + if (msg->compressed_size) { + crm_trace("Queueing CPG message %u to %s (%llu bytes, %d bytes compressed payload): %.200s", + msg->id, target, (unsigned long long) iov->iov_len, + msg->compressed_size, data); + } else { + crm_trace("Queueing CPG message %u to %s (%llu bytes, %d bytes payload): %.200s", + msg->id, target, (unsigned long long) iov->iov_len, + msg->size, data); + } + free(target); + + cs_message_queue = g_list_append(cs_message_queue, iov); + crm_cs_flush(&pcmk_cpg_handle); + + return TRUE; +} + +/*! + * \brief Get the message type equivalent of a string + * + * \param[in] text String of message type + * + * \return Message type equivalent of \p text + */ +enum crm_ais_msg_types +text2msg_type(const char *text) +{ + int type = crm_msg_none; + + CRM_CHECK(text != NULL, return type); + text = pcmk__message_name(text); + if (pcmk__str_eq(text, "ais", pcmk__str_casei)) { + type = crm_msg_ais; + } else if (pcmk__str_eq(text, CRM_SYSTEM_CIB, pcmk__str_casei)) { + type = crm_msg_cib; + } else if (pcmk__strcase_any_of(text, CRM_SYSTEM_CRMD, CRM_SYSTEM_DC, NULL)) { + type = crm_msg_crmd; + } else if (pcmk__str_eq(text, CRM_SYSTEM_TENGINE, pcmk__str_casei)) { + type = crm_msg_te; + } else if (pcmk__str_eq(text, CRM_SYSTEM_PENGINE, pcmk__str_casei)) { + type = crm_msg_pe; + } else if (pcmk__str_eq(text, CRM_SYSTEM_LRMD, pcmk__str_casei)) { + type = crm_msg_lrmd; + } else if (pcmk__str_eq(text, CRM_SYSTEM_STONITHD, pcmk__str_casei)) { + type = crm_msg_stonithd; + } else if (pcmk__str_eq(text, "stonith-ng", pcmk__str_casei)) { + type = crm_msg_stonith_ng; + } else if (pcmk__str_eq(text, "attrd", pcmk__str_casei)) { + type = crm_msg_attrd; + + } else { + /* This will normally be a transient client rather than + * a cluster daemon. Set the type to the pid of the client + */ + int scan_rc = sscanf(text, "%d", &type); + + if (scan_rc != 1 || type <= crm_msg_stonith_ng) { + /* Ensure it's sane */ + type = crm_msg_none; + } + } + return type; +} diff --git a/lib/cluster/crmcluster_private.h b/lib/cluster/crmcluster_private.h new file mode 100644 index 0000000..6933b73 --- /dev/null +++ b/lib/cluster/crmcluster_private.h @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PCMK__CRMCLUSTER_PRIVATE__H +# define PCMK__CRMCLUSTER_PRIVATE__H + +/* This header is for the sole use of libcrmcluster, so that functions can be + * declared with G_GNUC_INTERNAL for efficiency. + */ + +#include // uint32_t, uint64_t + +#include // G_GNUC_INTERNAL, gboolean +#include // xmlNode + +#include // cluster_type_e, crm_node_t + +G_GNUC_INTERNAL +enum cluster_type_e pcmk__corosync_detect(void); + +G_GNUC_INTERNAL +bool pcmk__corosync_has_nodelist(void); + +G_GNUC_INTERNAL +char *pcmk__corosync_uuid(const crm_node_t *peer); + +G_GNUC_INTERNAL +char *pcmk__corosync_name(uint64_t /*cmap_handle_t */ cmap_handle, + uint32_t nodeid); + +G_GNUC_INTERNAL +gboolean pcmk__corosync_connect(crm_cluster_t *cluster); + +G_GNUC_INTERNAL +void pcmk__corosync_disconnect(crm_cluster_t *cluster); + +G_GNUC_INTERNAL +gboolean pcmk__cpg_send_xml(xmlNode *msg, const crm_node_t *node, + enum crm_ais_msg_types dest); + +#endif // PCMK__CRMCLUSTER_PRIVATE__H diff --git a/lib/cluster/election.c b/lib/cluster/election.c new file mode 100644 index 0000000..ebbae72 --- /dev/null +++ b/lib/cluster/election.c @@ -0,0 +1,727 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include + +#define STORM_INTERVAL 2 /* in seconds */ + +struct election_s { + enum election_result state; + guint count; // How many times local node has voted + char *name; // Descriptive name for this election + char *uname; // Local node's name + GSourceFunc cb; // Function to call if election is won + GHashTable *voted; // Key = node name, value = how node voted + mainloop_timer_t *timeout; // When to abort if all votes not received + int election_wins; // Track wins, for storm detection + bool wrote_blackbox; // Write a storm blackbox at most once + time_t expires; // When storm detection period ends + time_t last_election_loss; // When dampening period ends +}; + +static void +election_complete(election_t *e) +{ + e->state = election_won; + if (e->cb != NULL) { + e->cb(e); + } + election_reset(e); +} + +static gboolean +election_timer_cb(gpointer user_data) +{ + election_t *e = user_data; + + crm_info("%s timed out, declaring local node as winner", e->name); + election_complete(e); + return FALSE; +} + +/*! + * \brief Get current state of an election + * + * \param[in] e Election object + * + * \return Current state of \e + */ +enum election_result +election_state(const election_t *e) +{ + return (e == NULL)? election_error : e->state; +} + +/*! + * \brief Create a new election object + * + * Every node that wishes to participate in an election must create an election + * object. Typically, this should be done once, at start-up. A caller should + * only create a single election object. + * + * \param[in] name Label for election (for logging) + * \param[in] uname Local node's name + * \param[in] period_ms How long to wait for all peers to vote + * \param[in] cb Function to call if local node wins election + * + * \return Newly allocated election object on success, NULL on error + * \note The caller is responsible for freeing the returned value using + * election_fini(). + */ +election_t * +election_init(const char *name, const char *uname, guint period_ms, GSourceFunc cb) +{ + election_t *e = NULL; + + static guint count = 0; + + CRM_CHECK(uname != NULL, return NULL); + + e = calloc(1, sizeof(election_t)); + if (e == NULL) { + crm_perror(LOG_CRIT, "Cannot create election"); + return NULL; + } + + e->uname = strdup(uname); + if (e->uname == NULL) { + crm_perror(LOG_CRIT, "Cannot create election"); + free(e); + return NULL; + } + + e->name = name? crm_strdup_printf("election-%s", name) + : crm_strdup_printf("election-%u", count++); + e->cb = cb; + e->timeout = mainloop_timer_add(e->name, period_ms, FALSE, + election_timer_cb, e); + crm_trace("Created %s", e->name); + return e; +} + +/*! + * \brief Disregard any previous vote by specified peer + * + * This discards any recorded vote from a specified peer. Election users should + * call this whenever a voting peer becomes inactive. + * + * \param[in,out] e Election object + * \param[in] uname Name of peer to disregard + */ +void +election_remove(election_t *e, const char *uname) +{ + if ((e != NULL) && (uname != NULL) && (e->voted != NULL)) { + crm_trace("Discarding %s (no-)vote from lost peer %s", e->name, uname); + g_hash_table_remove(e->voted, uname); + } +} + +/*! + * \brief Stop election timer and disregard all votes + * + * \param[in,out] e Election object + */ +void +election_reset(election_t *e) +{ + if (e != NULL) { + crm_trace("Resetting election %s", e->name); + mainloop_timer_stop(e->timeout); + if (e->voted) { + crm_trace("Destroying voted cache with %d members", g_hash_table_size(e->voted)); + g_hash_table_destroy(e->voted); + e->voted = NULL; + } + } +} + +/*! + * \brief Free an election object + * + * Free all memory associated with an election object, stopping its + * election timer (if running). + * + * \param[in,out] e Election object + */ +void +election_fini(election_t *e) +{ + if (e != NULL) { + election_reset(e); + crm_trace("Destroying %s", e->name); + mainloop_timer_del(e->timeout); + free(e->uname); + free(e->name); + free(e); + } +} + +static void +election_timeout_start(election_t *e) +{ + if (e != NULL) { + mainloop_timer_start(e->timeout); + } +} + +/*! + * \brief Stop an election's timer, if running + * + * \param[in,out] e Election object + */ +void +election_timeout_stop(election_t *e) +{ + if (e != NULL) { + mainloop_timer_stop(e->timeout); + } +} + +/*! + * \brief Change an election's timeout (restarting timer if running) + * + * \param[in,out] e Election object + * \param[in] period New timeout + */ +void +election_timeout_set_period(election_t *e, guint period) +{ + if (e != NULL) { + mainloop_timer_set_period(e->timeout, period); + } else { + crm_err("No election defined"); + } +} + +static int +get_uptime(struct timeval *output) +{ + static time_t expires = 0; + static struct rusage info; + + time_t tm_now = time(NULL); + + if (expires < tm_now) { + int rc = 0; + + info.ru_utime.tv_sec = 0; + info.ru_utime.tv_usec = 0; + rc = getrusage(RUSAGE_SELF, &info); + + output->tv_sec = 0; + output->tv_usec = 0; + + if (rc < 0) { + crm_perror(LOG_ERR, "Could not calculate the current uptime"); + expires = 0; + return -1; + } + + crm_debug("Current CPU usage is: %lds, %ldus", (long)info.ru_utime.tv_sec, + (long)info.ru_utime.tv_usec); + } + + expires = tm_now + STORM_INTERVAL; /* N seconds after the last _access_ */ + output->tv_sec = info.ru_utime.tv_sec; + output->tv_usec = info.ru_utime.tv_usec; + + return 1; +} + +static int +compare_age(struct timeval your_age) +{ + struct timeval our_age; + + get_uptime(&our_age); /* If an error occurred, our_age will be compared as {0,0} */ + + if (our_age.tv_sec > your_age.tv_sec) { + crm_debug("Win: %ld vs %ld (seconds)", (long)our_age.tv_sec, (long)your_age.tv_sec); + return 1; + } else if (our_age.tv_sec < your_age.tv_sec) { + crm_debug("Lose: %ld vs %ld (seconds)", (long)our_age.tv_sec, (long)your_age.tv_sec); + return -1; + } else if (our_age.tv_usec > your_age.tv_usec) { + crm_debug("Win: %ld.%06ld vs %ld.%06ld (usec)", + (long)our_age.tv_sec, (long)our_age.tv_usec, (long)your_age.tv_sec, (long)your_age.tv_usec); + return 1; + } else if (our_age.tv_usec < your_age.tv_usec) { + crm_debug("Lose: %ld.%06ld vs %ld.%06ld (usec)", + (long)our_age.tv_sec, (long)our_age.tv_usec, (long)your_age.tv_sec, (long)your_age.tv_usec); + return -1; + } + + return 0; +} + +/*! + * \brief Start a new election by offering local node's candidacy + * + * Broadcast a "vote" election message containing the local node's ID, + * (incremented) election counter, and uptime, and start the election timer. + * + * \param[in,out] e Election object + * + * \note Any nodes agreeing to the candidacy will send a "no-vote" reply, and if + * all active peers do so, or if the election times out, the local node + * wins the election. (If we lose to any peer vote, we will stop the + * timer, so a timeout means we did not lose -- either some peer did not + * vote, or we did not call election_check() in time.) + */ +void +election_vote(election_t *e) +{ + struct timeval age; + xmlNode *vote = NULL; + crm_node_t *our_node; + + if (e == NULL) { + crm_trace("Election vote requested, but no election available"); + return; + } + + our_node = crm_get_peer(0, e->uname); + if ((our_node == NULL) || (crm_is_peer_active(our_node) == FALSE)) { + crm_trace("Cannot vote in %s yet: local node not connected to cluster", + e->name); + return; + } + + election_reset(e); + e->state = election_in_progress; + vote = create_request(CRM_OP_VOTE, NULL, NULL, CRM_SYSTEM_CRMD, CRM_SYSTEM_CRMD, NULL); + + e->count++; + crm_xml_add(vote, F_CRM_ELECTION_OWNER, our_node->uuid); + crm_xml_add_int(vote, F_CRM_ELECTION_ID, e->count); + + get_uptime(&age); + crm_xml_add_timeval(vote, F_CRM_ELECTION_AGE_S, F_CRM_ELECTION_AGE_US, &age); + + send_cluster_message(NULL, crm_msg_crmd, vote, TRUE); + free_xml(vote); + + crm_debug("Started %s round %d", e->name, e->count); + election_timeout_start(e); + return; +} + +/*! + * \brief Check whether local node has won an election + * + * If all known peers have sent no-vote messages, stop the election timer, set + * the election state to won, and call any registered win callback. + * + * \param[in,out] e Election object + * + * \return TRUE if local node has won, FALSE otherwise + * \note If all known peers have sent no-vote messages, but the election owner + * does not call this function, the election will not be won (and the + * callback will not be called) until the election times out. + * \note This should be called when election_count_vote() returns + * \c election_in_progress. + */ +bool +election_check(election_t *e) +{ + int voted_size = 0; + int num_members = 0; + + if (e == NULL) { + crm_trace("Election check requested, but no election available"); + return FALSE; + } + if (e->voted == NULL) { + crm_trace("%s check requested, but no votes received yet", e->name); + return FALSE; + } + + voted_size = g_hash_table_size(e->voted); + num_members = crm_active_peers(); + + /* in the case of #voted > #members, it is better to + * wait for the timeout and give the cluster time to + * stabilize + */ + if (voted_size >= num_members) { + /* we won and everyone has voted */ + election_timeout_stop(e); + if (voted_size > num_members) { + GHashTableIter gIter; + const crm_node_t *node; + char *key = NULL; + + crm_warn("Received too many votes in %s", e->name); + g_hash_table_iter_init(&gIter, crm_peer_cache); + while (g_hash_table_iter_next(&gIter, NULL, (gpointer *) & node)) { + if (crm_is_peer_active(node)) { + crm_warn("* expected vote: %s", node->uname); + } + } + + g_hash_table_iter_init(&gIter, e->voted); + while (g_hash_table_iter_next(&gIter, (gpointer *) & key, NULL)) { + crm_warn("* actual vote: %s", key); + } + + } + + crm_info("%s won by local node", e->name); + election_complete(e); + return TRUE; + + } else { + crm_debug("%s still waiting on %d of %d votes", + e->name, num_members - voted_size, num_members); + } + + return FALSE; +} + +#define LOSS_DAMPEN 2 /* in seconds */ + +struct vote { + const char *op; + const char *from; + const char *version; + const char *election_owner; + int election_id; + struct timeval age; +}; + +/*! + * \brief Unpack an election message + * + * \param[in] e Election object (for logging only) + * \param[in] message Election message XML + * \param[out] vote Parsed fields from message + * + * \return TRUE if election message and election are valid, FALSE otherwise + * \note The parsed struct's pointer members are valid only for the lifetime of + * the message argument. + */ +static bool +parse_election_message(const election_t *e, const xmlNode *message, + struct vote *vote) +{ + CRM_CHECK(message && vote, return FALSE); + + vote->election_id = -1; + vote->age.tv_sec = -1; + vote->age.tv_usec = -1; + + vote->op = crm_element_value(message, F_CRM_TASK); + vote->from = crm_element_value(message, F_CRM_HOST_FROM); + vote->version = crm_element_value(message, F_CRM_VERSION); + vote->election_owner = crm_element_value(message, F_CRM_ELECTION_OWNER); + + crm_element_value_int(message, F_CRM_ELECTION_ID, &(vote->election_id)); + + if ((vote->op == NULL) || (vote->from == NULL) || (vote->version == NULL) + || (vote->election_owner == NULL) || (vote->election_id < 0)) { + + crm_warn("Invalid %s message from %s in %s ", + (vote->op? vote->op : "election"), + (vote->from? vote->from : "unspecified node"), + (e? e->name : "election")); + return FALSE; + } + + // Op-specific validation + + if (pcmk__str_eq(vote->op, CRM_OP_VOTE, pcmk__str_none)) { + // Only vote ops have uptime + crm_element_value_timeval(message, F_CRM_ELECTION_AGE_S, + F_CRM_ELECTION_AGE_US, &(vote->age)); + if ((vote->age.tv_sec < 0) || (vote->age.tv_usec < 0)) { + crm_warn("Cannot count %s %s from %s because it is missing uptime", + (e? e->name : "election"), vote->op, vote->from); + return FALSE; + } + + } else if (!pcmk__str_eq(vote->op, CRM_OP_NOVOTE, pcmk__str_none)) { + crm_info("Cannot process %s message from %s because %s is not a known election op", + (e? e->name : "election"), vote->from, vote->op); + return FALSE; + } + + // Election validation + + if (e == NULL) { + crm_info("Cannot count %s from %s because no election available", + vote->op, vote->from); + return FALSE; + } + + /* If the membership cache is NULL, we REALLY shouldn't be voting -- + * the question is how we managed to get here. + */ + if (crm_peer_cache == NULL) { + crm_info("Cannot count %s %s from %s because no peer information available", + e->name, vote->op, vote->from); + return FALSE; + } + return TRUE; +} + +static void +record_vote(election_t *e, struct vote *vote) +{ + char *voter_copy = NULL; + char *vote_copy = NULL; + + CRM_ASSERT(e && vote && vote->from && vote->op); + if (e->voted == NULL) { + e->voted = pcmk__strkey_table(free, free); + } + + voter_copy = strdup(vote->from); + vote_copy = strdup(vote->op); + CRM_ASSERT(voter_copy && vote_copy); + + g_hash_table_replace(e->voted, voter_copy, vote_copy); +} + +static void +send_no_vote(crm_node_t *peer, struct vote *vote) +{ + // @TODO probably shouldn't hardcode CRM_SYSTEM_CRMD and crm_msg_crmd + + xmlNode *novote = create_request(CRM_OP_NOVOTE, NULL, vote->from, + CRM_SYSTEM_CRMD, CRM_SYSTEM_CRMD, NULL); + + crm_xml_add(novote, F_CRM_ELECTION_OWNER, vote->election_owner); + crm_xml_add_int(novote, F_CRM_ELECTION_ID, vote->election_id); + + send_cluster_message(peer, crm_msg_crmd, novote, TRUE); + free_xml(novote); +} + +/*! + * \brief Process an election message (vote or no-vote) from a peer + * + * \param[in,out] e Election object + * \param[in] message Election message XML from peer + * \param[in] can_win Whether local node is eligible to win + * + * \return Election state after new vote is considered + * \note If the peer message is a vote, and we prefer the peer to win, this will + * send a no-vote reply to the peer. + * \note The situations "we lost to this vote" from "this is a late no-vote + * after we've already lost" both return election_lost. If a caller needs + * to distinguish them, it should save the current state before calling + * this function, and then compare the result. + */ +enum election_result +election_count_vote(election_t *e, const xmlNode *message, bool can_win) +{ + int log_level = LOG_INFO; + gboolean done = FALSE; + gboolean we_lose = FALSE; + const char *reason = "unknown"; + bool we_are_owner = FALSE; + crm_node_t *our_node = NULL, *your_node = NULL; + time_t tm_now = time(NULL); + struct vote vote; + + CRM_CHECK(message != NULL, return election_error); + if (parse_election_message(e, message, &vote) == FALSE) { + return election_error; + } + + your_node = crm_get_peer(0, vote.from); + our_node = crm_get_peer(0, e->uname); + we_are_owner = (our_node != NULL) + && pcmk__str_eq(our_node->uuid, vote.election_owner, + pcmk__str_none); + + if (!can_win) { + reason = "Not eligible"; + we_lose = TRUE; + + } else if (our_node == NULL || crm_is_peer_active(our_node) == FALSE) { + reason = "We are not part of the cluster"; + log_level = LOG_ERR; + we_lose = TRUE; + + } else if (we_are_owner && (vote.election_id != e->count)) { + log_level = LOG_TRACE; + reason = "Superseded"; + done = TRUE; + + } else if (your_node == NULL || crm_is_peer_active(your_node) == FALSE) { + /* Possibly we cached the message in the FSA queue at a point that it wasn't */ + reason = "Peer is not part of our cluster"; + log_level = LOG_WARNING; + done = TRUE; + + } else if (pcmk__str_eq(vote.op, CRM_OP_NOVOTE, pcmk__str_none) + || pcmk__str_eq(vote.from, e->uname, pcmk__str_none)) { + /* Receiving our own broadcast vote, or a no-vote from peer, is a vote + * for us to win + */ + if (!we_are_owner) { + crm_warn("Cannot count %s round %d %s from %s because we are not election owner (%s)", + e->name, vote.election_id, vote.op, vote.from, + vote.election_owner); + return election_error; + } + if (e->state != election_in_progress) { + // Should only happen if we already lost + crm_debug("Not counting %s round %d %s from %s because no election in progress", + e->name, vote.election_id, vote.op, vote.from); + return e->state; + } + record_vote(e, &vote); + reason = "Recorded"; + done = TRUE; + + } else { + // A peer vote requires a comparison to determine which node is better + int age_result = compare_age(vote.age); + int version_result = compare_version(vote.version, CRM_FEATURE_SET); + + if (version_result < 0) { + reason = "Version"; + we_lose = TRUE; + + } else if (version_result > 0) { + reason = "Version"; + + } else if (age_result < 0) { + reason = "Uptime"; + we_lose = TRUE; + + } else if (age_result > 0) { + reason = "Uptime"; + + } else if (strcasecmp(e->uname, vote.from) > 0) { + reason = "Host name"; + we_lose = TRUE; + + } else { + reason = "Host name"; + } + } + + if (e->expires < tm_now) { + e->election_wins = 0; + e->expires = tm_now + STORM_INTERVAL; + + } else if (done == FALSE && we_lose == FALSE) { + int peers = 1 + g_hash_table_size(crm_peer_cache); + + /* If every node has to vote down every other node, thats N*(N-1) total elections + * Allow some leeway before _really_ complaining + */ + e->election_wins++; + if (e->election_wins > (peers * peers)) { + crm_warn("%s election storm detected: %d wins in %d seconds", + e->name, e->election_wins, STORM_INTERVAL); + e->election_wins = 0; + e->expires = tm_now + STORM_INTERVAL; + if (e->wrote_blackbox == FALSE) { + /* It's questionable whether a black box (from every node in the + * cluster) would be truly helpful in diagnosing an election + * storm. It's also highly doubtful a production environment + * would get multiple election storms from distinct causes, so + * saving one blackbox per process lifetime should be + * sufficient. Alternatives would be to save a timestamp of the + * last blackbox write instead of a boolean, and write a new one + * if some amount of time has passed; or to save a storm count, + * write a blackbox on every Nth occurrence. + */ + crm_write_blackbox(0, NULL); + e->wrote_blackbox = TRUE; + } + } + } + + if (done) { + do_crm_log(log_level + 1, + "Processed %s round %d %s (current round %d) from %s (%s)", + e->name, vote.election_id, vote.op, e->count, vote.from, + reason); + return e->state; + + } else if (we_lose == FALSE) { + /* We track the time of the last election loss to implement an election + * dampening period, reducing the likelihood of an election storm. If + * this node has lost within the dampening period, don't start a new + * election, even if we win against a peer's vote -- the peer we lost to + * should win again. + * + * @TODO This has a problem case: if an election winner immediately + * leaves the cluster, and a new election is immediately called, all + * nodes could lose, with no new winner elected. The ideal solution + * would be to tie the election structure with the peer caches, which + * would allow us to clear the dampening when the previous winner + * leaves (and would allow other improvements as well). + */ + if ((e->last_election_loss == 0) + || ((tm_now - e->last_election_loss) > (time_t) LOSS_DAMPEN)) { + + do_crm_log(log_level, "%s round %d (owner node ID %s) pass: %s from %s (%s)", + e->name, vote.election_id, vote.election_owner, vote.op, + vote.from, reason); + + e->last_election_loss = 0; + election_timeout_stop(e); + + /* Start a new election by voting down this, and other, peers */ + e->state = election_start; + return e->state; + } else { + char *loss_time = ctime(&e->last_election_loss); + + if (loss_time) { + // Show only HH:MM:SS + loss_time += 11; + loss_time[8] = '\0'; + } + crm_info("Ignoring %s round %d (owner node ID %s) pass vs %s because we lost less than %ds ago at %s", + e->name, vote.election_id, vote.election_owner, vote.from, + LOSS_DAMPEN, (loss_time? loss_time : "unknown")); + } + } + + e->last_election_loss = tm_now; + + do_crm_log(log_level, "%s round %d (owner node ID %s) lost: %s from %s (%s)", + e->name, vote.election_id, vote.election_owner, vote.op, + vote.from, reason); + + election_reset(e); + send_no_vote(your_node, &vote); + e->state = election_lost; + return e->state; +} + +/*! + * \brief Reset any election dampening currently in effect + * + * \param[in,out] e Election object to clear + */ +void +election_clear_dampening(election_t *e) +{ + e->last_election_loss = 0; +} diff --git a/lib/cluster/membership.c b/lib/cluster/membership.c new file mode 100644 index 0000000..0c54f19 --- /dev/null +++ b/lib/cluster/membership.c @@ -0,0 +1,1301 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "crmcluster_private.h" + +/* The peer cache remembers cluster nodes that have been seen. + * This is managed mostly automatically by libcluster, based on + * cluster membership events. + * + * Because cluster nodes can have conflicting names or UUIDs, + * the hash table key is a uniquely generated ID. + */ +GHashTable *crm_peer_cache = NULL; + +/* + * The remote peer cache tracks pacemaker_remote nodes. While the + * value has the same type as the peer cache's, it is tracked separately for + * three reasons: pacemaker_remote nodes can't have conflicting names or UUIDs, + * so the name (which is also the UUID) is used as the hash table key; there + * is no equivalent of membership events, so management is not automatic; and + * most users of the peer cache need to exclude pacemaker_remote nodes. + * + * That said, using a single cache would be more logical and less error-prone, + * so it would be a good idea to merge them one day. + * + * libcluster provides two avenues for populating the cache: + * crm_remote_peer_get() and crm_remote_peer_cache_remove() directly manage it, + * while crm_remote_peer_cache_refresh() populates it via the CIB. + */ +GHashTable *crm_remote_peer_cache = NULL; + +/* + * The known node cache tracks cluster and remote nodes that have been seen in + * the CIB. It is useful mainly when a caller needs to know about a node that + * may no longer be in the membership, but doesn't want to add the node to the + * main peer cache tables. + */ +static GHashTable *known_node_cache = NULL; + +unsigned long long crm_peer_seq = 0; +gboolean crm_have_quorum = FALSE; +static gboolean crm_autoreap = TRUE; + +// Flag setting and clearing for crm_node_t:flags + +#define set_peer_flags(peer, flags_to_set) do { \ + (peer)->flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, \ + "Peer", (peer)->uname, \ + (peer)->flags, (flags_to_set), \ + #flags_to_set); \ + } while (0) + +#define clear_peer_flags(peer, flags_to_clear) do { \ + (peer)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ + LOG_TRACE, \ + "Peer", (peer)->uname, \ + (peer)->flags, (flags_to_clear), \ + #flags_to_clear); \ + } while (0) + +static void update_peer_uname(crm_node_t *node, const char *uname); + +int +crm_remote_peer_cache_size(void) +{ + if (crm_remote_peer_cache == NULL) { + return 0; + } + return g_hash_table_size(crm_remote_peer_cache); +} + +/*! + * \brief Get a remote node peer cache entry, creating it if necessary + * + * \param[in] node_name Name of remote node + * + * \return Cache entry for node on success, NULL (and set errno) otherwise + * + * \note When creating a new entry, this will leave the node state undetermined, + * so the caller should also call pcmk__update_peer_state() if the state + * is known. + */ +crm_node_t * +crm_remote_peer_get(const char *node_name) +{ + crm_node_t *node; + + if (node_name == NULL) { + errno = -EINVAL; + return NULL; + } + + /* Return existing cache entry if one exists */ + node = g_hash_table_lookup(crm_remote_peer_cache, node_name); + if (node) { + return node; + } + + /* Allocate a new entry */ + node = calloc(1, sizeof(crm_node_t)); + if (node == NULL) { + return NULL; + } + + /* Populate the essential information */ + set_peer_flags(node, crm_remote_node); + node->uuid = strdup(node_name); + if (node->uuid == NULL) { + free(node); + errno = -ENOMEM; + return NULL; + } + + /* Add the new entry to the cache */ + g_hash_table_replace(crm_remote_peer_cache, node->uuid, node); + crm_trace("added %s to remote cache", node_name); + + /* Update the entry's uname, ensuring peer status callbacks are called */ + update_peer_uname(node, node_name); + return node; +} + +void +crm_remote_peer_cache_remove(const char *node_name) +{ + if (g_hash_table_remove(crm_remote_peer_cache, node_name)) { + crm_trace("removed %s from remote peer cache", node_name); + } +} + +/*! + * \internal + * \brief Return node status based on a CIB status entry + * + * \param[in] node_state XML of node state + * + * \return CRM_NODE_LOST if XML_NODE_IN_CLUSTER is false in node_state, + * CRM_NODE_MEMBER otherwise + * \note Unlike most boolean XML attributes, this one defaults to true, for + * backward compatibility with older controllers that don't set it. + */ +static const char * +remote_state_from_cib(const xmlNode *node_state) +{ + bool status = false; + + if (pcmk__xe_get_bool_attr(node_state, XML_NODE_IN_CLUSTER, &status) == pcmk_rc_ok && !status) { + return CRM_NODE_LOST; + } else { + return CRM_NODE_MEMBER; + } +} + +/* user data for looping through remote node xpath searches */ +struct refresh_data { + const char *field; /* XML attribute to check for node name */ + gboolean has_state; /* whether to update node state based on XML */ +}; + +/*! + * \internal + * \brief Process one pacemaker_remote node xpath search result + * + * \param[in] result XML search result + * \param[in] user_data what to look for in the XML + */ +static void +remote_cache_refresh_helper(xmlNode *result, void *user_data) +{ + const struct refresh_data *data = user_data; + const char *remote = crm_element_value(result, data->field); + const char *state = NULL; + crm_node_t *node; + + CRM_CHECK(remote != NULL, return); + + /* Determine node's state, if the result has it */ + if (data->has_state) { + state = remote_state_from_cib(result); + } + + /* Check whether cache already has entry for node */ + node = g_hash_table_lookup(crm_remote_peer_cache, remote); + + if (node == NULL) { + /* Node is not in cache, so add a new entry for it */ + node = crm_remote_peer_get(remote); + CRM_ASSERT(node); + if (state) { + pcmk__update_peer_state(__func__, node, state, 0); + } + + } else if (pcmk_is_set(node->flags, crm_node_dirty)) { + /* Node is in cache and hasn't been updated already, so mark it clean */ + clear_peer_flags(node, crm_node_dirty); + if (state) { + pcmk__update_peer_state(__func__, node, state, 0); + } + } +} + +static void +mark_dirty(gpointer key, gpointer value, gpointer user_data) +{ + set_peer_flags((crm_node_t *) value, crm_node_dirty); +} + +static gboolean +is_dirty(gpointer key, gpointer value, gpointer user_data) +{ + return pcmk_is_set(((crm_node_t*)value)->flags, crm_node_dirty); +} + +/*! + * \brief Repopulate the remote peer cache based on CIB XML + * + * \param[in] xmlNode CIB XML to parse + */ +void +crm_remote_peer_cache_refresh(xmlNode *cib) +{ + struct refresh_data data; + + crm_peer_init(); + + /* First, we mark all existing cache entries as dirty, + * so that later we can remove any that weren't in the CIB. + * We don't empty the cache, because we need to detect changes in state. + */ + g_hash_table_foreach(crm_remote_peer_cache, mark_dirty, NULL); + + /* Look for guest nodes and remote nodes in the status section */ + data.field = "id"; + data.has_state = TRUE; + crm_foreach_xpath_result(cib, PCMK__XP_REMOTE_NODE_STATUS, + remote_cache_refresh_helper, &data); + + /* Look for guest nodes and remote nodes in the configuration section, + * because they may have just been added and not have a status entry yet. + * In that case, the cached node state will be left NULL, so that the + * peer status callback isn't called until we're sure the node started + * successfully. + */ + data.field = "value"; + data.has_state = FALSE; + crm_foreach_xpath_result(cib, PCMK__XP_GUEST_NODE_CONFIG, + remote_cache_refresh_helper, &data); + data.field = "id"; + data.has_state = FALSE; + crm_foreach_xpath_result(cib, PCMK__XP_REMOTE_NODE_CONFIG, + remote_cache_refresh_helper, &data); + + /* Remove all old cache entries that weren't seen in the CIB */ + g_hash_table_foreach_remove(crm_remote_peer_cache, is_dirty, NULL); +} + +gboolean +crm_is_peer_active(const crm_node_t * node) +{ + if(node == NULL) { + return FALSE; + } + + if (pcmk_is_set(node->flags, crm_remote_node)) { + /* remote nodes are never considered active members. This + * guarantees they will never be considered for DC membership.*/ + return FALSE; + } +#if SUPPORT_COROSYNC + if (is_corosync_cluster()) { + return crm_is_corosync_peer_active(node); + } +#endif + crm_err("Unhandled cluster type: %s", name_for_cluster_type(get_cluster_type())); + return FALSE; +} + +static gboolean +crm_reap_dead_member(gpointer key, gpointer value, gpointer user_data) +{ + crm_node_t *node = value; + crm_node_t *search = user_data; + + if (search == NULL) { + return FALSE; + + } else if (search->id && node->id != search->id) { + return FALSE; + + } else if (search->id == 0 && !pcmk__str_eq(node->uname, search->uname, pcmk__str_casei)) { + return FALSE; + + } else if (crm_is_peer_active(value) == FALSE) { + crm_info("Removing node with name %s and id %u from membership cache", + (node->uname? node->uname : "unknown"), node->id); + return TRUE; + } + return FALSE; +} + +/*! + * \brief Remove all peer cache entries matching a node ID and/or uname + * + * \param[in] id ID of node to remove (or 0 to ignore) + * \param[in] name Uname of node to remove (or NULL to ignore) + * + * \return Number of cache entries removed + */ +guint +reap_crm_member(uint32_t id, const char *name) +{ + int matches = 0; + crm_node_t search = { 0, }; + + if (crm_peer_cache == NULL) { + crm_trace("Membership cache not initialized, ignoring purge request"); + return 0; + } + + search.id = id; + pcmk__str_update(&search.uname, name); + matches = g_hash_table_foreach_remove(crm_peer_cache, crm_reap_dead_member, &search); + if(matches) { + crm_notice("Purged %d peer%s with id=%u%s%s from the membership cache", + matches, pcmk__plural_s(matches), search.id, + (search.uname? " and/or uname=" : ""), + (search.uname? search.uname : "")); + + } else { + crm_info("No peers with id=%u%s%s to purge from the membership cache", + search.id, (search.uname? " and/or uname=" : ""), + (search.uname? search.uname : "")); + } + + free(search.uname); + return matches; +} + +static void +count_peer(gpointer key, gpointer value, gpointer user_data) +{ + guint *count = user_data; + crm_node_t *node = value; + + if (crm_is_peer_active(node)) { + *count = *count + 1; + } +} + +guint +crm_active_peers(void) +{ + guint count = 0; + + if (crm_peer_cache) { + g_hash_table_foreach(crm_peer_cache, count_peer, &count); + } + return count; +} + +static void +destroy_crm_node(gpointer data) +{ + crm_node_t *node = data; + + crm_trace("Destroying entry for node %u: %s", node->id, node->uname); + + free(node->uname); + free(node->state); + free(node->uuid); + free(node->expected); + free(node->conn_host); + free(node); +} + +void +crm_peer_init(void) +{ + if (crm_peer_cache == NULL) { + crm_peer_cache = pcmk__strikey_table(free, destroy_crm_node); + } + + if (crm_remote_peer_cache == NULL) { + crm_remote_peer_cache = pcmk__strikey_table(NULL, destroy_crm_node); + } + + if (known_node_cache == NULL) { + known_node_cache = pcmk__strikey_table(free, destroy_crm_node); + } +} + +void +crm_peer_destroy(void) +{ + if (crm_peer_cache != NULL) { + crm_trace("Destroying peer cache with %d members", g_hash_table_size(crm_peer_cache)); + g_hash_table_destroy(crm_peer_cache); + crm_peer_cache = NULL; + } + + if (crm_remote_peer_cache != NULL) { + crm_trace("Destroying remote peer cache with %d members", g_hash_table_size(crm_remote_peer_cache)); + g_hash_table_destroy(crm_remote_peer_cache); + crm_remote_peer_cache = NULL; + } + + if (known_node_cache != NULL) { + crm_trace("Destroying known node cache with %d members", + g_hash_table_size(known_node_cache)); + g_hash_table_destroy(known_node_cache); + known_node_cache = NULL; + } + +} + +static void (*peer_status_callback)(enum crm_status_type, crm_node_t *, + const void *) = NULL; + +/*! + * \brief Set a client function that will be called after peer status changes + * + * \param[in] dispatch Pointer to function to use as callback + * + * \note Previously, client callbacks were responsible for peer cache + * management. This is no longer the case, and client callbacks should do + * only client-specific handling. Callbacks MUST NOT add or remove entries + * in the peer caches. + */ +void +crm_set_status_callback(void (*dispatch) (enum crm_status_type, crm_node_t *, const void *)) +{ + peer_status_callback = dispatch; +} + +/*! + * \brief Tell the library whether to automatically reap lost nodes + * + * If TRUE (the default), calling crm_update_peer_proc() will also update the + * peer state to CRM_NODE_MEMBER or CRM_NODE_LOST, and pcmk__update_peer_state() + * will reap peers whose state changes to anything other than CRM_NODE_MEMBER. + * Callers should leave this enabled unless they plan to manage the cache + * separately on their own. + * + * \param[in] autoreap TRUE to enable automatic reaping, FALSE to disable + */ +void +crm_set_autoreap(gboolean autoreap) +{ + crm_autoreap = autoreap; +} + +static void +dump_peer_hash(int level, const char *caller) +{ + GHashTableIter iter; + const char *id = NULL; + crm_node_t *node = NULL; + + g_hash_table_iter_init(&iter, crm_peer_cache); + while (g_hash_table_iter_next(&iter, (gpointer *) &id, (gpointer *) &node)) { + do_crm_log(level, "%s: Node %u/%s = %p - %s", caller, node->id, node->uname, node, id); + } +} + +static gboolean +hash_find_by_data(gpointer key, gpointer value, gpointer user_data) +{ + return value == user_data; +} + +/*! + * \internal + * \brief Search caches for a node (cluster or Pacemaker Remote) + * + * \param[in] id If not 0, cluster node ID to search for + * \param[in] uname If not NULL, node name to search for + * \param[in] flags Bitmask of enum crm_get_peer_flags + * + * \return Node cache entry if found, otherwise NULL + */ +crm_node_t * +pcmk__search_node_caches(unsigned int id, const char *uname, uint32_t flags) +{ + crm_node_t *node = NULL; + + CRM_ASSERT(id > 0 || uname != NULL); + + crm_peer_init(); + + if ((uname != NULL) && pcmk_is_set(flags, CRM_GET_PEER_REMOTE)) { + node = g_hash_table_lookup(crm_remote_peer_cache, uname); + } + + if ((node == NULL) && pcmk_is_set(flags, CRM_GET_PEER_CLUSTER)) { + node = pcmk__search_cluster_node_cache(id, uname); + } + return node; +} + +/*! + * \brief Get a node cache entry (cluster or Pacemaker Remote) + * + * \param[in] id If not 0, cluster node ID to search for + * \param[in] uname If not NULL, node name to search for + * \param[in] flags Bitmask of enum crm_get_peer_flags + * + * \return (Possibly newly created) node cache entry + */ +crm_node_t * +crm_get_peer_full(unsigned int id, const char *uname, int flags) +{ + crm_node_t *node = NULL; + + CRM_ASSERT(id > 0 || uname != NULL); + + crm_peer_init(); + + if (pcmk_is_set(flags, CRM_GET_PEER_REMOTE)) { + node = g_hash_table_lookup(crm_remote_peer_cache, uname); + } + + if ((node == NULL) && pcmk_is_set(flags, CRM_GET_PEER_CLUSTER)) { + node = crm_get_peer(id, uname); + } + return node; +} + +/*! + * \internal + * \brief Search cluster node cache + * + * \param[in] id If not 0, cluster node ID to search for + * \param[in] uname If not NULL, node name to search for + * + * \return Cluster node cache entry if found, otherwise NULL + */ +crm_node_t * +pcmk__search_cluster_node_cache(unsigned int id, const char *uname) +{ + GHashTableIter iter; + crm_node_t *node = NULL; + crm_node_t *by_id = NULL; + crm_node_t *by_name = NULL; + + CRM_ASSERT(id > 0 || uname != NULL); + + crm_peer_init(); + + if (uname != NULL) { + g_hash_table_iter_init(&iter, crm_peer_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { + if(node->uname && strcasecmp(node->uname, uname) == 0) { + crm_trace("Name match: %s = %p", node->uname, node); + by_name = node; + break; + } + } + } + + if (id > 0) { + g_hash_table_iter_init(&iter, crm_peer_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { + if(node->id == id) { + crm_trace("ID match: %u = %p", node->id, node); + by_id = node; + break; + } + } + } + + node = by_id; /* Good default */ + if(by_id == by_name) { + /* Nothing to do if they match (both NULL counts) */ + crm_trace("Consistent: %p for %u/%s", by_id, id, uname); + + } else if(by_id == NULL && by_name) { + crm_trace("Only one: %p for %u/%s", by_name, id, uname); + + if(id && by_name->id) { + dump_peer_hash(LOG_WARNING, __func__); + crm_crit("Node %u and %u share the same name '%s'", + id, by_name->id, uname); + node = NULL; /* Create a new one */ + + } else { + node = by_name; + } + + } else if(by_name == NULL && by_id) { + crm_trace("Only one: %p for %u/%s", by_id, id, uname); + + if(uname && by_id->uname) { + dump_peer_hash(LOG_WARNING, __func__); + crm_crit("Node '%s' and '%s' share the same cluster nodeid %u: assuming '%s' is correct", + uname, by_id->uname, id, uname); + } + + } else if(uname && by_id->uname) { + if(pcmk__str_eq(uname, by_id->uname, pcmk__str_casei)) { + crm_notice("Node '%s' has changed its ID from %u to %u", by_id->uname, by_name->id, by_id->id); + g_hash_table_foreach_remove(crm_peer_cache, hash_find_by_data, by_name); + + } else { + crm_warn("Node '%s' and '%s' share the same cluster nodeid: %u %s", by_id->uname, by_name->uname, id, uname); + dump_peer_hash(LOG_INFO, __func__); + crm_abort(__FILE__, __func__, __LINE__, "member weirdness", TRUE, + TRUE); + } + + } else if(id && by_name->id) { + crm_warn("Node %u and %u share the same name: '%s'", by_id->id, by_name->id, uname); + + } else { + /* Simple merge */ + + /* Only corosync-based clusters use node IDs. The functions that call + * pcmk__update_peer_state() and crm_update_peer_proc() only know + * nodeid, so 'by_id' is authoritative when merging. + */ + dump_peer_hash(LOG_DEBUG, __func__); + + crm_info("Merging %p into %p", by_name, by_id); + g_hash_table_foreach_remove(crm_peer_cache, hash_find_by_data, by_name); + } + + return node; +} + +#if SUPPORT_COROSYNC +static guint +remove_conflicting_peer(crm_node_t *node) +{ + int matches = 0; + GHashTableIter iter; + crm_node_t *existing_node = NULL; + + if (node->id == 0 || node->uname == NULL) { + return 0; + } + + if (!pcmk__corosync_has_nodelist()) { + return 0; + } + + g_hash_table_iter_init(&iter, crm_peer_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &existing_node)) { + if (existing_node->id > 0 + && existing_node->id != node->id + && existing_node->uname != NULL + && strcasecmp(existing_node->uname, node->uname) == 0) { + + if (crm_is_peer_active(existing_node)) { + continue; + } + + crm_warn("Removing cached offline node %u/%s which has conflicting uname with %u", + existing_node->id, existing_node->uname, node->id); + + g_hash_table_iter_remove(&iter); + matches++; + } + } + + return matches; +} +#endif + +/*! + * \brief Get a cluster node cache entry + * + * \param[in] id If not 0, cluster node ID to search for + * \param[in] uname If not NULL, node name to search for + * + * \return (Possibly newly created) cluster node cache entry + */ +/* coverity[-alloc] Memory is referenced in one or both hashtables */ +crm_node_t * +crm_get_peer(unsigned int id, const char *uname) +{ + crm_node_t *node = NULL; + char *uname_lookup = NULL; + + CRM_ASSERT(id > 0 || uname != NULL); + + crm_peer_init(); + + node = pcmk__search_cluster_node_cache(id, uname); + + /* if uname wasn't provided, and find_peer did not turn up a uname based on id. + * we need to do a lookup of the node name using the id in the cluster membership. */ + if ((node == NULL || node->uname == NULL) && (uname == NULL)) { + uname_lookup = get_node_name(id); + } + + if (uname_lookup) { + uname = uname_lookup; + crm_trace("Inferred a name of '%s' for node %u", uname, id); + + /* try to turn up the node one more time now that we know the uname. */ + if (node == NULL) { + node = pcmk__search_cluster_node_cache(id, uname); + } + } + + + if (node == NULL) { + char *uniqueid = crm_generate_uuid(); + + node = calloc(1, sizeof(crm_node_t)); + CRM_ASSERT(node); + + crm_info("Created entry %s/%p for node %s/%u (%d total)", + uniqueid, node, uname, id, 1 + g_hash_table_size(crm_peer_cache)); + g_hash_table_replace(crm_peer_cache, uniqueid, node); + } + + if(id > 0 && uname && (node->id == 0 || node->uname == NULL)) { + crm_info("Node %u is now known as %s", id, uname); + } + + if(id > 0 && node->id == 0) { + node->id = id; + } + + if (uname && (node->uname == NULL)) { + update_peer_uname(node, uname); + } + + if(node->uuid == NULL) { + const char *uuid = crm_peer_uuid(node); + + if (uuid) { + crm_info("Node %u has uuid %s", id, uuid); + + } else { + crm_info("Cannot obtain a UUID for node %u/%s", id, node->uname); + } + } + + free(uname_lookup); + + return node; +} + +/*! + * \internal + * \brief Update a node's uname + * + * \param[in,out] node Node object to update + * \param[in] uname New name to set + * + * \note This function should not be called within a peer cache iteration, + * because in some cases it can remove conflicting cache entries, + * which would invalidate the iterator. + */ +static void +update_peer_uname(crm_node_t *node, const char *uname) +{ + CRM_CHECK(uname != NULL, + crm_err("Bug: can't update node name without name"); return); + CRM_CHECK(node != NULL, + crm_err("Bug: can't update node name to %s without node", uname); + return); + + if (pcmk__str_eq(uname, node->uname, pcmk__str_casei)) { + crm_debug("Node uname '%s' did not change", uname); + return; + } + + for (const char *c = uname; *c; ++c) { + if ((*c >= 'A') && (*c <= 'Z')) { + crm_warn("Node names with capitals are discouraged, consider changing '%s'", + uname); + break; + } + } + + pcmk__str_update(&node->uname, uname); + + if (peer_status_callback != NULL) { + peer_status_callback(crm_status_uname, node, NULL); + } + +#if SUPPORT_COROSYNC + if (is_corosync_cluster() && !pcmk_is_set(node->flags, crm_remote_node)) { + remove_conflicting_peer(node); + } +#endif +} + +/*! + * \internal + * \brief Get log-friendly string equivalent of a process flag + * + * \param[in] proc Process flag + * + * \return Log-friendly string equivalent of \p proc + */ +static inline const char * +proc2text(enum crm_proc_flag proc) +{ + const char *text = "unknown"; + + switch (proc) { + case crm_proc_none: + text = "none"; + break; + case crm_proc_based: + text = "pacemaker-based"; + break; + case crm_proc_controld: + text = "pacemaker-controld"; + break; + case crm_proc_schedulerd: + text = "pacemaker-schedulerd"; + break; + case crm_proc_execd: + text = "pacemaker-execd"; + break; + case crm_proc_attrd: + text = "pacemaker-attrd"; + break; + case crm_proc_fenced: + text = "pacemaker-fenced"; + break; + case crm_proc_cpg: + text = "corosync-cpg"; + break; + } + return text; +} + +/*! + * \internal + * \brief Update a node's process information (and potentially state) + * + * \param[in] source Caller's function name (for log messages) + * \param[in,out] node Node object to update + * \param[in] flag Bitmask of new process information + * \param[in] status node status (online, offline, etc.) + * + * \return NULL if any node was reaped from peer caches, value of node otherwise + * + * \note If this function returns NULL, the supplied node object was likely + * freed and should not be used again. This function should not be + * called within a cache iteration if reaping is possible, otherwise + * reaping could invalidate the iterator. + */ +crm_node_t * +crm_update_peer_proc(const char *source, crm_node_t * node, uint32_t flag, const char *status) +{ + uint32_t last = 0; + gboolean changed = FALSE; + + CRM_CHECK(node != NULL, crm_err("%s: Could not set %s to %s for NULL", + source, proc2text(flag), status); + return NULL); + + /* Pacemaker doesn't spawn processes on remote nodes */ + if (pcmk_is_set(node->flags, crm_remote_node)) { + return node; + } + + last = node->processes; + if (status == NULL) { + node->processes = flag; + if (node->processes != last) { + changed = TRUE; + } + + } else if (pcmk__str_eq(status, ONLINESTATUS, pcmk__str_casei)) { + if ((node->processes & flag) != flag) { + node->processes = pcmk__set_flags_as(__func__, __LINE__, + LOG_TRACE, "Peer process", + node->uname, node->processes, + flag, "processes"); + changed = TRUE; + } + + } else if (node->processes & flag) { + node->processes = pcmk__clear_flags_as(__func__, __LINE__, + LOG_TRACE, "Peer process", + node->uname, node->processes, + flag, "processes"); + changed = TRUE; + } + + if (changed) { + if (status == NULL && flag <= crm_proc_none) { + crm_info("%s: Node %s[%u] - all processes are now offline", source, node->uname, + node->id); + } else { + crm_info("%s: Node %s[%u] - %s is now %s", source, node->uname, node->id, + proc2text(flag), status); + } + + /* Call the client callback first, then update the peer state, + * in case the node will be reaped + */ + if (peer_status_callback != NULL) { + peer_status_callback(crm_status_processes, node, &last); + } + + /* The client callback shouldn't touch the peer caches, + * but as a safety net, bail if the peer cache was destroyed. + */ + if (crm_peer_cache == NULL) { + return NULL; + } + + if (crm_autoreap) { + const char *peer_state = NULL; + + if (pcmk_is_set(node->processes, crm_get_cluster_proc())) { + peer_state = CRM_NODE_MEMBER; + } else { + peer_state = CRM_NODE_LOST; + } + node = pcmk__update_peer_state(__func__, node, peer_state, 0); + } + } else { + crm_trace("%s: Node %s[%u] - %s is unchanged (%s)", source, node->uname, node->id, + proc2text(flag), status); + } + return node; +} + +/*! + * \internal + * \brief Update a cluster node cache entry's expected join state + * + * \param[in] source Caller's function name (for logging) + * \param[in,out] node Node to update + * \param[in] expected Node's new join state + */ +void +pcmk__update_peer_expected(const char *source, crm_node_t *node, + const char *expected) +{ + char *last = NULL; + gboolean changed = FALSE; + + CRM_CHECK(node != NULL, crm_err("%s: Could not set 'expected' to %s", source, expected); + return); + + /* Remote nodes don't participate in joins */ + if (pcmk_is_set(node->flags, crm_remote_node)) { + return; + } + + last = node->expected; + if (expected != NULL && !pcmk__str_eq(node->expected, expected, pcmk__str_casei)) { + node->expected = strdup(expected); + changed = TRUE; + } + + if (changed) { + crm_info("%s: Node %s[%u] - expected state is now %s (was %s)", source, node->uname, node->id, + expected, last); + free(last); + } else { + crm_trace("%s: Node %s[%u] - expected state is unchanged (%s)", source, node->uname, + node->id, expected); + } +} + +/*! + * \internal + * \brief Update a node's state and membership information + * + * \param[in] source Caller's function name (for log messages) + * \param[in,out] node Node object to update + * \param[in] state Node's new state + * \param[in] membership Node's new membership ID + * \param[in,out] iter If not NULL, pointer to node's peer cache iterator + * + * \return NULL if any node was reaped, value of node otherwise + * + * \note If this function returns NULL, the supplied node object was likely + * freed and should not be used again. This function may be called from + * within a peer cache iteration if the iterator is supplied. + */ +static crm_node_t * +update_peer_state_iter(const char *source, crm_node_t *node, const char *state, + uint64_t membership, GHashTableIter *iter) +{ + gboolean is_member; + + CRM_CHECK(node != NULL, + crm_err("Could not set state for unknown host to %s" + CRM_XS " source=%s", state, source); + return NULL); + + is_member = pcmk__str_eq(state, CRM_NODE_MEMBER, pcmk__str_casei); + if (is_member) { + node->when_lost = 0; + if (membership) { + node->last_seen = membership; + } + } + + if (state && !pcmk__str_eq(node->state, state, pcmk__str_casei)) { + char *last = node->state; + + node->state = strdup(state); + crm_notice("Node %s state is now %s " CRM_XS + " nodeid=%u previous=%s source=%s", node->uname, state, + node->id, (last? last : "unknown"), source); + if (peer_status_callback != NULL) { + peer_status_callback(crm_status_nstate, node, last); + } + free(last); + + if (crm_autoreap && !is_member + && !pcmk_is_set(node->flags, crm_remote_node)) { + /* We only autoreap from the peer cache, not the remote peer cache, + * because the latter should be managed only by + * crm_remote_peer_cache_refresh(). + */ + if(iter) { + crm_notice("Purged 1 peer with id=%u and/or uname=%s from the membership cache", node->id, node->uname); + g_hash_table_iter_remove(iter); + + } else { + reap_crm_member(node->id, node->uname); + } + node = NULL; + } + + } else { + crm_trace("Node %s state is unchanged (%s) " CRM_XS + " nodeid=%u source=%s", node->uname, state, node->id, source); + } + return node; +} + +/*! + * \brief Update a node's state and membership information + * + * \param[in] source Caller's function name (for log messages) + * \param[in,out] node Node object to update + * \param[in] state Node's new state + * \param[in] membership Node's new membership ID + * + * \return NULL if any node was reaped, value of node otherwise + * + * \note If this function returns NULL, the supplied node object was likely + * freed and should not be used again. This function should not be + * called within a cache iteration if reaping is possible, + * otherwise reaping could invalidate the iterator. + */ +crm_node_t * +pcmk__update_peer_state(const char *source, crm_node_t *node, + const char *state, uint64_t membership) +{ + return update_peer_state_iter(source, node, state, membership, NULL); +} + +/*! + * \internal + * \brief Reap all nodes from cache whose membership information does not match + * + * \param[in] membership Membership ID of nodes to keep + */ +void +pcmk__reap_unseen_nodes(uint64_t membership) +{ + GHashTableIter iter; + crm_node_t *node = NULL; + + crm_trace("Reaping unseen nodes..."); + g_hash_table_iter_init(&iter, crm_peer_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *)&node)) { + if (node->last_seen != membership) { + if (node->state) { + /* + * Calling update_peer_state_iter() allows us to + * remove the node from crm_peer_cache without + * invalidating our iterator + */ + update_peer_state_iter(__func__, node, CRM_NODE_LOST, + membership, &iter); + + } else { + crm_info("State of node %s[%u] is still unknown", + node->uname, node->id); + } + } + } +} + +static crm_node_t * +find_known_node(const char *id, const char *uname) +{ + GHashTableIter iter; + crm_node_t *node = NULL; + crm_node_t *by_id = NULL; + crm_node_t *by_name = NULL; + + if (uname) { + g_hash_table_iter_init(&iter, known_node_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { + if (node->uname && strcasecmp(node->uname, uname) == 0) { + crm_trace("Name match: %s = %p", node->uname, node); + by_name = node; + break; + } + } + } + + if (id) { + g_hash_table_iter_init(&iter, known_node_cache); + while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &node)) { + if(strcasecmp(node->uuid, id) == 0) { + crm_trace("ID match: %s= %p", id, node); + by_id = node; + break; + } + } + } + + node = by_id; /* Good default */ + if (by_id == by_name) { + /* Nothing to do if they match (both NULL counts) */ + crm_trace("Consistent: %p for %s/%s", by_id, id, uname); + + } else if (by_id == NULL && by_name) { + crm_trace("Only one: %p for %s/%s", by_name, id, uname); + + if (id) { + node = NULL; + + } else { + node = by_name; + } + + } else if (by_name == NULL && by_id) { + crm_trace("Only one: %p for %s/%s", by_id, id, uname); + + if (uname) { + node = NULL; + } + + } else if (uname && by_id->uname + && pcmk__str_eq(uname, by_id->uname, pcmk__str_casei)) { + /* Multiple nodes have the same uname in the CIB. + * Return by_id. */ + + } else if (id && by_name->uuid + && pcmk__str_eq(id, by_name->uuid, pcmk__str_casei)) { + /* Multiple nodes have the same id in the CIB. + * Return by_name. */ + node = by_name; + + } else { + node = NULL; + } + + if (node == NULL) { + crm_debug("Couldn't find node%s%s%s%s", + id? " " : "", + id? id : "", + uname? " with name " : "", + uname? uname : ""); + } + + return node; +} + +static void +known_node_cache_refresh_helper(xmlNode *xml_node, void *user_data) +{ + const char *id = crm_element_value(xml_node, XML_ATTR_ID); + const char *uname = crm_element_value(xml_node, XML_ATTR_UNAME); + crm_node_t * node = NULL; + + CRM_CHECK(id != NULL && uname !=NULL, return); + node = find_known_node(id, uname); + + if (node == NULL) { + char *uniqueid = crm_generate_uuid(); + + node = calloc(1, sizeof(crm_node_t)); + CRM_ASSERT(node != NULL); + + node->uname = strdup(uname); + CRM_ASSERT(node->uname != NULL); + + node->uuid = strdup(id); + CRM_ASSERT(node->uuid != NULL); + + g_hash_table_replace(known_node_cache, uniqueid, node); + + } else if (pcmk_is_set(node->flags, crm_node_dirty)) { + pcmk__str_update(&node->uname, uname); + + /* Node is in cache and hasn't been updated already, so mark it clean */ + clear_peer_flags(node, crm_node_dirty); + } + +} + +static void +refresh_known_node_cache(xmlNode *cib) +{ + crm_peer_init(); + + g_hash_table_foreach(known_node_cache, mark_dirty, NULL); + + crm_foreach_xpath_result(cib, PCMK__XP_MEMBER_NODE_CONFIG, + known_node_cache_refresh_helper, NULL); + + /* Remove all old cache entries that weren't seen in the CIB */ + g_hash_table_foreach_remove(known_node_cache, is_dirty, NULL); +} + +void +pcmk__refresh_node_caches_from_cib(xmlNode *cib) +{ + crm_remote_peer_cache_refresh(cib); + refresh_known_node_cache(cib); +} + +/*! + * \internal + * \brief Search known node cache + * + * \param[in] id If not 0, cluster node ID to search for + * \param[in] uname If not NULL, node name to search for + * \param[in] flags Bitmask of enum crm_get_peer_flags + * + * \return Known node cache entry if found, otherwise NULL + */ +crm_node_t * +pcmk__search_known_node_cache(unsigned int id, const char *uname, + uint32_t flags) +{ + crm_node_t *node = NULL; + char *id_str = NULL; + + CRM_ASSERT(id > 0 || uname != NULL); + + node = pcmk__search_node_caches(id, uname, flags); + + if (node || !(flags & CRM_GET_PEER_CLUSTER)) { + return node; + } + + if (id > 0) { + id_str = crm_strdup_printf("%u", id); + } + + node = find_known_node(id_str, uname); + + free(id_str); + return node; +} + + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +int +crm_terminate_member(int nodeid, const char *uname, void *unused) +{ + return stonith_api_kick(nodeid, uname, 120, TRUE); +} + +int +crm_terminate_member_no_mainloop(int nodeid, const char *uname, int *connection) +{ + return stonith_api_kick(nodeid, uname, 120, TRUE); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am new file mode 100644 index 0000000..ef729d4 --- /dev/null +++ b/lib/common/Makefile.am @@ -0,0 +1,124 @@ +# +# Copyright 2004-2023 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# +include $(top_srcdir)/mk/common.mk + +AM_CPPFLAGS += -I$(top_builddir)/lib/gnu -I$(top_srcdir)/lib/gnu + +## libraries +lib_LTLIBRARIES = libcrmcommon.la +check_LTLIBRARIES = libcrmcommon_test.la + +# Disable -Wcast-qual if used, because we do some hacky casting, +# and because libxml2 has some signatures that should be const but aren't +# for backward compatibility reasons. + +# s390 needs -fPIC +# s390-suse-linux/bin/ld: .libs/ipc.o: relocation R_390_PC32DBL against `__stack_chk_fail@@GLIBC_2.4' can not be used when making a shared object; recompile with -fPIC + +CFLAGS = $(CFLAGS_COPY:-Wcast-qual=) -fPIC + +# Without "." here, check-recursive will run through the subdirectories first +# and then run "make check" here. This will fail, because there's things in +# the subdirectories that need check_LTLIBRARIES built first. Adding "." here +# changes the order so the subdirectories are processed afterwards. +SUBDIRS = . tests + +noinst_HEADERS = crmcommon_private.h mock_private.h + +libcrmcommon_la_LDFLAGS = -version-info 45:0:11 + +libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB) +libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) + +libcrmcommon_la_LIBADD = @LIBADD_DL@ $(top_builddir)/lib/gnu/libgnu.la + +# If configured with --with-profiling or --with-coverage, BUILD_PROFILING will +# be set and -fno-builtin will be added to the CFLAGS. However, libcrmcommon +# uses the fabs() function which is normally supplied by gcc as one of its +# builtins. Therefore we need to explicitly link against libm here or the +# tests won't link. +if BUILD_PROFILING +libcrmcommon_la_LIBADD += -lm +endif + +# Use += rather than backlashed continuation lines for parsing by bumplibs +libcrmcommon_la_SOURCES = +libcrmcommon_la_SOURCES += acl.c +libcrmcommon_la_SOURCES += agents.c +libcrmcommon_la_SOURCES += alerts.c +libcrmcommon_la_SOURCES += attrs.c +libcrmcommon_la_SOURCES += cib.c +if BUILD_CIBSECRETS +libcrmcommon_la_SOURCES += cib_secrets.c +endif +libcrmcommon_la_SOURCES += cmdline.c +libcrmcommon_la_SOURCES += digest.c +libcrmcommon_la_SOURCES += health.c +libcrmcommon_la_SOURCES += io.c +libcrmcommon_la_SOURCES += ipc_attrd.c +libcrmcommon_la_SOURCES += ipc_client.c +libcrmcommon_la_SOURCES += ipc_common.c +libcrmcommon_la_SOURCES += ipc_controld.c +libcrmcommon_la_SOURCES += ipc_pacemakerd.c +libcrmcommon_la_SOURCES += ipc_schedulerd.c +libcrmcommon_la_SOURCES += ipc_server.c +libcrmcommon_la_SOURCES += iso8601.c +libcrmcommon_la_SOURCES += lists.c +libcrmcommon_la_SOURCES += logging.c +libcrmcommon_la_SOURCES += mainloop.c +libcrmcommon_la_SOURCES += messages.c +libcrmcommon_la_SOURCES += nodes.c +libcrmcommon_la_SOURCES += nvpair.c +libcrmcommon_la_SOURCES += operations.c +libcrmcommon_la_SOURCES += options.c +libcrmcommon_la_SOURCES += output.c +libcrmcommon_la_SOURCES += output_html.c +libcrmcommon_la_SOURCES += output_log.c +libcrmcommon_la_SOURCES += output_none.c +libcrmcommon_la_SOURCES += output_text.c +libcrmcommon_la_SOURCES += output_xml.c +libcrmcommon_la_SOURCES += patchset.c +libcrmcommon_la_SOURCES += patchset_display.c +libcrmcommon_la_SOURCES += pid.c +libcrmcommon_la_SOURCES += procfs.c +libcrmcommon_la_SOURCES += remote.c +libcrmcommon_la_SOURCES += results.c +libcrmcommon_la_SOURCES += schemas.c +libcrmcommon_la_SOURCES += scores.c +libcrmcommon_la_SOURCES += strings.c +libcrmcommon_la_SOURCES += utils.c +libcrmcommon_la_SOURCES += watchdog.c +libcrmcommon_la_SOURCES += xml.c +libcrmcommon_la_SOURCES += xml_display.c +libcrmcommon_la_SOURCES += xpath.c + +# +# libcrmcommon_test is used only with unit tests, so we can mock system calls. +# See mock.c for details. +# + +include $(top_srcdir)/mk/tap.mk + +libcrmcommon_test_la_SOURCES = $(libcrmcommon_la_SOURCES) +libcrmcommon_test_la_SOURCES += mock.c +libcrmcommon_test_la_LDFLAGS = $(libcrmcommon_la_LDFLAGS) -rpath $(libdir) $(LDFLAGS_WRAP) +# If GCC emits a builtin function in place of something we've mocked up, that will +# get used instead of the mocked version which leads to unexpected test results. So +# disable all builtins. Older versions of GCC (at least, on RHEL7) will still emit +# replacement code for strdup (and possibly other functions) unless -fno-inline is +# also added. +libcrmcommon_test_la_CFLAGS = $(libcrmcommon_la_CFLAGS) -DPCMK__UNIT_TESTING -fno-builtin -fno-inline +# If -fno-builtin is used, -lm also needs to be added. See the comment at +# BUILD_PROFILING above. +libcrmcommon_test_la_LIBADD = $(libcrmcommon_la_LIBADD) -lcmocka -lm + +nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES) + +clean-generic: + rm -f *.log *.debug *.xml *~ diff --git a/lib/common/acl.c b/lib/common/acl.c new file mode 100644 index 0000000..33a4e00 --- /dev/null +++ b/lib/common/acl.c @@ -0,0 +1,860 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include "crmcommon_private.h" + +typedef struct xml_acl_s { + enum xml_private_flags mode; + char *xpath; +} xml_acl_t; + +static void +free_acl(void *data) +{ + if (data) { + xml_acl_t *acl = data; + + free(acl->xpath); + free(acl); + } +} + +void +pcmk__free_acls(GList *acls) +{ + g_list_free_full(acls, free_acl); +} + +static GList * +create_acl(const xmlNode *xml, GList *acls, enum xml_private_flags mode) +{ + xml_acl_t *acl = NULL; + + const char *tag = crm_element_value(xml, XML_ACL_ATTR_TAG); + const char *ref = crm_element_value(xml, XML_ACL_ATTR_REF); + const char *xpath = crm_element_value(xml, XML_ACL_ATTR_XPATH); + const char *attr = crm_element_value(xml, XML_ACL_ATTR_ATTRIBUTE); + + if (tag == NULL) { + // @COMPAT rolling upgrades <=1.1.11 + tag = crm_element_value(xml, XML_ACL_ATTR_TAGv1); + } + if (ref == NULL) { + // @COMPAT rolling upgrades <=1.1.11 + ref = crm_element_value(xml, XML_ACL_ATTR_REFv1); + } + + if ((tag == NULL) && (ref == NULL) && (xpath == NULL)) { + // Schema should prevent this, but to be safe ... + crm_trace("Ignoring ACL <%s> element without selection criteria", + crm_element_name(xml)); + return NULL; + } + + acl = calloc(1, sizeof (xml_acl_t)); + CRM_ASSERT(acl != NULL); + + acl->mode = mode; + if (xpath) { + acl->xpath = strdup(xpath); + CRM_ASSERT(acl->xpath != NULL); + crm_trace("Unpacked ACL <%s> element using xpath: %s", + crm_element_name(xml), acl->xpath); + + } else { + GString *buf = g_string_sized_new(128); + + if ((ref != NULL) && (attr != NULL)) { + // NOTE: schema currently does not allow this + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" XML_ATTR_ID "='", + ref, "' and @", attr, "]", NULL); + + } else if (ref != NULL) { + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@" XML_ATTR_ID "='", + ref, "']", NULL); + + } else if (attr != NULL) { + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), "[@", attr, "]", NULL); + + } else { + pcmk__g_strcat(buf, "//", pcmk__s(tag, "*"), NULL); + } + + acl->xpath = strdup((const char *) buf->str); + CRM_ASSERT(acl->xpath != NULL); + + g_string_free(buf, TRUE); + crm_trace("Unpacked ACL <%s> element as xpath: %s", + crm_element_name(xml), acl->xpath); + } + + return g_list_append(acls, acl); +} + +/*! + * \internal + * \brief Unpack a user, group, or role subtree of the ACLs section + * + * \param[in] acl_top XML of entire ACLs section + * \param[in] acl_entry XML of ACL element being unpacked + * \param[in,out] acls List of ACLs unpacked so far + * + * \return New head of (possibly modified) acls + * + * \note This function is recursive + */ +static GList * +parse_acl_entry(const xmlNode *acl_top, const xmlNode *acl_entry, GList *acls) +{ + xmlNode *child = NULL; + + for (child = pcmk__xe_first_child(acl_entry); child; + child = pcmk__xe_next(child)) { + const char *tag = crm_element_name(child); + const char *kind = crm_element_value(child, XML_ACL_ATTR_KIND); + + if (strcmp(XML_ACL_TAG_PERMISSION, tag) == 0){ + CRM_ASSERT(kind != NULL); + crm_trace("Unpacking ACL <%s> element of kind '%s'", tag, kind); + tag = kind; + } else { + crm_trace("Unpacking ACL <%s> element", tag); + } + + if (strcmp(XML_ACL_TAG_ROLE_REF, tag) == 0 + || strcmp(XML_ACL_TAG_ROLE_REFv1, tag) == 0) { + const char *ref_role = crm_element_value(child, XML_ATTR_ID); + + if (ref_role) { + xmlNode *role = NULL; + + for (role = pcmk__xe_first_child(acl_top); role; + role = pcmk__xe_next(role)) { + if (!strcmp(XML_ACL_TAG_ROLE, (const char *) role->name)) { + const char *role_id = crm_element_value(role, + XML_ATTR_ID); + + if (role_id && strcmp(ref_role, role_id) == 0) { + crm_trace("Unpacking referenced role '%s' in ACL <%s> element", + role_id, crm_element_name(acl_entry)); + acls = parse_acl_entry(acl_top, role, acls); + break; + } + } + } + } + + } else if (strcmp(XML_ACL_TAG_READ, tag) == 0) { + acls = create_acl(child, acls, pcmk__xf_acl_read); + + } else if (strcmp(XML_ACL_TAG_WRITE, tag) == 0) { + acls = create_acl(child, acls, pcmk__xf_acl_write); + + } else if (strcmp(XML_ACL_TAG_DENY, tag) == 0) { + acls = create_acl(child, acls, pcmk__xf_acl_deny); + + } else { + crm_warn("Ignoring unknown ACL %s '%s'", + (kind? "kind" : "element"), tag); + } + } + + return acls; +} + +/* + + + + + + + + + + + + + + + + + + + +*/ + +static const char * +acl_to_text(enum xml_private_flags flags) +{ + if (pcmk_is_set(flags, pcmk__xf_acl_deny)) { + return "deny"; + + } else if (pcmk_any_flags_set(flags, pcmk__xf_acl_write|pcmk__xf_acl_create)) { + return "read/write"; + + } else if (pcmk_is_set(flags, pcmk__xf_acl_read)) { + return "read"; + } + return "none"; +} + +void +pcmk__apply_acl(xmlNode *xml) +{ + GList *aIter = NULL; + xml_doc_private_t *docpriv = xml->doc->_private; + xml_node_private_t *nodepriv; + xmlXPathObjectPtr xpathObj = NULL; + + if (!xml_acl_enabled(xml)) { + crm_trace("Skipping ACLs for user '%s' because not enabled for this XML", + docpriv->user); + return; + } + + for (aIter = docpriv->acls; aIter != NULL; aIter = aIter->next) { + int max = 0, lpc = 0; + xml_acl_t *acl = aIter->data; + + xpathObj = xpath_search(xml, acl->xpath); + max = numXpathResults(xpathObj); + + for (lpc = 0; lpc < max; lpc++) { + xmlNode *match = getXpathResult(xpathObj, lpc); + + nodepriv = match->_private; + pcmk__set_xml_flags(nodepriv, acl->mode); + + // Build a GString only if tracing is enabled + pcmk__if_tracing( + { + GString *path = pcmk__element_xpath(match); + crm_trace("Applying %s ACL to %s matched by %s", + acl_to_text(acl->mode), path->str, acl->xpath); + g_string_free(path, TRUE); + }, + {} + ); + } + crm_trace("Applied %s ACL %s (%d match%s)", + acl_to_text(acl->mode), acl->xpath, max, + ((max == 1)? "" : "es")); + freeXpathObject(xpathObj); + } +} + +/*! + * \internal + * \brief Unpack ACLs for a given user into the + * metadata of the target XML tree + * + * Taking the description of ACLs from the source XML tree and + * marking up the target XML tree with access information for the + * given user by tacking it onto the relevant nodes + * + * \param[in] source XML with ACL definitions + * \param[in,out] target XML that ACLs will be applied to + * \param[in] user Username whose ACLs need to be unpacked + */ +void +pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user) +{ + xml_doc_private_t *docpriv = NULL; + + if ((target == NULL) || (target->doc == NULL) + || (target->doc->_private == NULL)) { + return; + } + + docpriv = target->doc->_private; + if (!pcmk_acl_required(user)) { + crm_trace("Not unpacking ACLs because not required for user '%s'", + user); + + } else if (docpriv->acls == NULL) { + xmlNode *acls = get_xpath_object("//" XML_CIB_TAG_ACLS, + source, LOG_NEVER); + + pcmk__str_update(&docpriv->user, user); + + if (acls) { + xmlNode *child = NULL; + + for (child = pcmk__xe_first_child(acls); child; + child = pcmk__xe_next(child)) { + const char *tag = crm_element_name(child); + + if (!strcmp(tag, XML_ACL_TAG_USER) + || !strcmp(tag, XML_ACL_TAG_USERv1)) { + const char *id = crm_element_value(child, XML_ATTR_NAME); + + if (id == NULL) { + id = crm_element_value(child, XML_ATTR_ID); + } + + if (id && strcmp(id, user) == 0) { + crm_debug("Unpacking ACLs for user '%s'", id); + docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); + } + } else if (!strcmp(tag, XML_ACL_TAG_GROUP)) { + const char *id = crm_element_value(child, XML_ATTR_NAME); + + if (id == NULL) { + id = crm_element_value(child, XML_ATTR_ID); + } + + if (id && pcmk__is_user_in_group(user,id)) { + crm_debug("Unpacking ACLs for group '%s'", id); + docpriv->acls = parse_acl_entry(acls, child, docpriv->acls); + } + } + } + } + } +} + +/*! + * \internal + * \brief Copy source to target and set xf_acl_enabled flag in target + * + * \param[in] acl_source XML with ACL definitions + * \param[in,out] target XML that ACLs will be applied to + * \param[in] user Username whose ACLs need to be set + */ +void +pcmk__enable_acl(xmlNode *acl_source, xmlNode *target, const char *user) +{ + pcmk__unpack_acl(acl_source, target, user); + pcmk__set_xml_doc_flag(target, pcmk__xf_acl_enabled); + pcmk__apply_acl(target); +} + +static inline bool +test_acl_mode(enum xml_private_flags allowed, enum xml_private_flags requested) +{ + if (pcmk_is_set(allowed, pcmk__xf_acl_deny)) { + return false; + + } else if (pcmk_all_flags_set(allowed, requested)) { + return true; + + } else if (pcmk_is_set(requested, pcmk__xf_acl_read) + && pcmk_is_set(allowed, pcmk__xf_acl_write)) { + return true; + + } else if (pcmk_is_set(requested, pcmk__xf_acl_create) + && pcmk_any_flags_set(allowed, pcmk__xf_acl_write|pcmk__xf_created)) { + return true; + } + return false; +} + +/*! + * \internal + * \brief Rid XML tree of all unreadable nodes and node properties + * + * \param[in,out] xml Root XML node to be purged of attributes + * + * \return true if this node or any of its children are readable + * if false is returned, xml will be freed + * + * \note This function is recursive + */ +static bool +purge_xml_attributes(xmlNode *xml) +{ + xmlNode *child = NULL; + xmlAttr *xIter = NULL; + bool readable_children = false; + xml_node_private_t *nodepriv = xml->_private; + + if (test_acl_mode(nodepriv->flags, pcmk__xf_acl_read)) { + crm_trace("%s[@" XML_ATTR_ID "=%s] is readable", + crm_element_name(xml), ID(xml)); + return true; + } + + xIter = xml->properties; + while (xIter != NULL) { + xmlAttr *tmp = xIter; + const char *prop_name = (const char *)xIter->name; + + xIter = xIter->next; + if (strcmp(prop_name, XML_ATTR_ID) == 0) { + continue; + } + + xmlUnsetProp(xml, tmp->name); + } + + child = pcmk__xml_first_child(xml); + while ( child != NULL ) { + xmlNode *tmp = child; + + child = pcmk__xml_next(child); + readable_children |= purge_xml_attributes(tmp); + } + + if (!readable_children) { + free_xml(xml); /* Nothing readable under here, purge completely */ + } + return readable_children; +} + +/*! + * \brief Copy ACL-allowed portions of specified XML + * + * \param[in] user Username whose ACLs should be used + * \param[in] acl_source XML containing ACLs + * \param[in] xml XML to be copied + * \param[out] result Copy of XML portions readable via ACLs + * + * \return true if xml exists and ACLs are required for user, false otherwise + * \note If this returns true, caller should use \p result rather than \p xml + */ +bool +xml_acl_filtered_copy(const char *user, xmlNode *acl_source, xmlNode *xml, + xmlNode **result) +{ + GList *aIter = NULL; + xmlNode *target = NULL; + xml_doc_private_t *docpriv = NULL; + + *result = NULL; + if ((xml == NULL) || !pcmk_acl_required(user)) { + crm_trace("Not filtering XML because ACLs not required for user '%s'", + user); + return false; + } + + crm_trace("Filtering XML copy using user '%s' ACLs", user); + target = copy_xml(xml); + if (target == NULL) { + return true; + } + + pcmk__enable_acl(acl_source, target, user); + + docpriv = target->doc->_private; + for(aIter = docpriv->acls; aIter != NULL && target; aIter = aIter->next) { + int max = 0; + xml_acl_t *acl = aIter->data; + + if (acl->mode != pcmk__xf_acl_deny) { + /* Nothing to do */ + + } else if (acl->xpath) { + int lpc = 0; + xmlXPathObjectPtr xpathObj = xpath_search(target, acl->xpath); + + max = numXpathResults(xpathObj); + for(lpc = 0; lpc < max; lpc++) { + xmlNode *match = getXpathResult(xpathObj, lpc); + + if (!purge_xml_attributes(match) && (match == target)) { + crm_trace("ACLs deny user '%s' access to entire XML document", + user); + freeXpathObject(xpathObj); + return true; + } + } + crm_trace("ACLs deny user '%s' access to %s (%d %s)", + user, acl->xpath, max, + pcmk__plural_alt(max, "match", "matches")); + freeXpathObject(xpathObj); + } + } + + if (!purge_xml_attributes(target)) { + crm_trace("ACLs deny user '%s' access to entire XML document", user); + return true; + } + + if (docpriv->acls) { + g_list_free_full(docpriv->acls, free_acl); + docpriv->acls = NULL; + + } else { + crm_trace("User '%s' without ACLs denied access to entire XML document", + user); + free_xml(target); + target = NULL; + } + + if (target) { + *result = target; + } + + return true; +} + +/*! + * \internal + * \brief Check whether creation of an XML element is implicitly allowed + * + * Check whether XML is a "scaffolding" element whose creation is implicitly + * allowed regardless of ACLs (that is, it is not in the ACL section and has + * no attributes other than "id"). + * + * \param[in] xml XML element to check + * + * \return true if XML element is implicitly allowed, false otherwise + */ +static bool +implicitly_allowed(const xmlNode *xml) +{ + GString *path = NULL; + + for (xmlAttr *prop = xml->properties; prop != NULL; prop = prop->next) { + if (strcmp((const char *) prop->name, XML_ATTR_ID) != 0) { + return false; + } + } + + path = pcmk__element_xpath(xml); + CRM_ASSERT(path != NULL); + + if (strstr((const char *) path->str, "/" XML_CIB_TAG_ACLS "/") != NULL) { + g_string_free(path, TRUE); + return false; + } + + g_string_free(path, TRUE); + return true; +} + +#define display_id(xml) (ID(xml)? ID(xml) : "") + +/*! + * \internal + * \brief Drop XML nodes created in violation of ACLs + * + * Given an XML element, free all of its descendant nodes created in violation + * of ACLs, with the exception of allowing "scaffolding" elements (i.e. those + * that aren't in the ACL section and don't have any attributes other than + * "id"). + * + * \param[in,out] xml XML to check + * \param[in] check_top Whether to apply checks to argument itself + * (if true, xml might get freed) + * + * \note This function is recursive + */ +void +pcmk__apply_creation_acl(xmlNode *xml, bool check_top) +{ + xml_node_private_t *nodepriv = xml->_private; + + if (pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { + if (implicitly_allowed(xml)) { + crm_trace("Creation of <%s> scaffolding with id=\"%s\"" + " is implicitly allowed", + crm_element_name(xml), display_id(xml)); + + } else if (pcmk__check_acl(xml, NULL, pcmk__xf_acl_write)) { + crm_trace("ACLs allow creation of <%s> with id=\"%s\"", + crm_element_name(xml), display_id(xml)); + + } else if (check_top) { + crm_trace("ACLs disallow creation of <%s> with id=\"%s\"", + crm_element_name(xml), display_id(xml)); + pcmk_free_xml_subtree(xml); + return; + + } else { + crm_notice("ACLs would disallow creation of %s<%s> with id=\"%s\"", + ((xml == xmlDocGetRootElement(xml->doc))? "root element " : ""), + crm_element_name(xml), display_id(xml)); + } + } + + for (xmlNode *cIter = pcmk__xml_first_child(xml); cIter != NULL; ) { + xmlNode *child = cIter; + cIter = pcmk__xml_next(cIter); /* In case it is free'd */ + pcmk__apply_creation_acl(child, true); + } +} + +/*! + * \brief Check whether or not an XML node is ACL-denied + * + * \param[in] xml node to check + * + * \return true if XML node exists and is ACL-denied, false otherwise + */ +bool +xml_acl_denied(const xmlNode *xml) +{ + if (xml && xml->doc && xml->doc->_private){ + xml_doc_private_t *docpriv = xml->doc->_private; + + return pcmk_is_set(docpriv->flags, pcmk__xf_acl_denied); + } + return false; +} + +void +xml_acl_disable(xmlNode *xml) +{ + if (xml_acl_enabled(xml)) { + xml_doc_private_t *docpriv = xml->doc->_private; + + /* Catch anything that was created but shouldn't have been */ + pcmk__apply_acl(xml); + pcmk__apply_creation_acl(xml, false); + pcmk__clear_xml_flags(docpriv, pcmk__xf_acl_enabled); + } +} + +/*! + * \brief Check whether or not an XML node is ACL-enabled + * + * \param[in] xml node to check + * + * \return true if XML node exists and is ACL-enabled, false otherwise + */ +bool +xml_acl_enabled(const xmlNode *xml) +{ + if (xml && xml->doc && xml->doc->_private){ + xml_doc_private_t *docpriv = xml->doc->_private; + + return pcmk_is_set(docpriv->flags, pcmk__xf_acl_enabled); + } + return false; +} + +bool +pcmk__check_acl(xmlNode *xml, const char *name, enum xml_private_flags mode) +{ + CRM_ASSERT(xml); + CRM_ASSERT(xml->doc); + CRM_ASSERT(xml->doc->_private); + + if (pcmk__tracking_xml_changes(xml, false) && xml_acl_enabled(xml)) { + xmlNode *parent = xml; + xml_doc_private_t *docpriv = xml->doc->_private; + GString *xpath = NULL; + + if (docpriv->acls == NULL) { + pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); + + pcmk__if_tracing({}, return false); + xpath = pcmk__element_xpath(xml); + if (name != NULL) { + pcmk__g_strcat(xpath, "[@", name, "]", NULL); + } + + qb_log_from_external_source(__func__, __FILE__, + "User '%s' without ACLs denied %s " + "access to %s", LOG_TRACE, __LINE__, 0, + docpriv->user, acl_to_text(mode), + (const char *) xpath->str); + g_string_free(xpath, TRUE); + return false; + } + + /* Walk the tree upwards looking for xml_acl_* flags + * - Creating an attribute requires write permissions for the node + * - Creating a child requires write permissions for the parent + */ + + if (name) { + xmlAttr *attr = xmlHasProp(xml, (pcmkXmlStr) name); + + if (attr && mode == pcmk__xf_acl_create) { + mode = pcmk__xf_acl_write; + } + } + + while (parent && parent->_private) { + xml_node_private_t *nodepriv = parent->_private; + if (test_acl_mode(nodepriv->flags, mode)) { + return true; + + } else if (pcmk_is_set(nodepriv->flags, pcmk__xf_acl_deny)) { + pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); + + pcmk__if_tracing({}, return false); + xpath = pcmk__element_xpath(xml); + if (name != NULL) { + pcmk__g_strcat(xpath, "[@", name, "]", NULL); + } + + qb_log_from_external_source(__func__, __FILE__, + "%sACL denies user '%s' %s access " + "to %s", LOG_TRACE, __LINE__, 0, + (parent != xml)? "Parent ": "", + docpriv->user, acl_to_text(mode), + (const char *) xpath->str); + g_string_free(xpath, TRUE); + return false; + } + parent = parent->parent; + } + + pcmk__set_xml_doc_flag(xml, pcmk__xf_acl_denied); + + pcmk__if_tracing({}, return false); + xpath = pcmk__element_xpath(xml); + if (name != NULL) { + pcmk__g_strcat(xpath, "[@", name, "]", NULL); + } + + qb_log_from_external_source(__func__, __FILE__, + "Default ACL denies user '%s' %s access to " + "%s", LOG_TRACE, __LINE__, 0, + docpriv->user, acl_to_text(mode), + (const char *) xpath->str); + g_string_free(xpath, TRUE); + return false; + } + + return true; +} + +/*! + * \brief Check whether ACLs are required for a given user + * + * \param[in] User name to check + * + * \return true if the user requires ACLs, false otherwise + */ +bool +pcmk_acl_required(const char *user) +{ + if (pcmk__str_empty(user)) { + crm_trace("ACLs not required because no user set"); + return false; + + } else if (!strcmp(user, CRM_DAEMON_USER) || !strcmp(user, "root")) { + crm_trace("ACLs not required for privileged user %s", user); + return false; + } + crm_trace("ACLs required for %s", user); + return true; +} + +char * +pcmk__uid2username(uid_t uid) +{ + char *result = NULL; + struct passwd *pwent = getpwuid(uid); + + if (pwent == NULL) { + crm_perror(LOG_INFO, "Cannot get user details for user ID %d", uid); + return NULL; + } + pcmk__str_update(&result, pwent->pw_name); + return result; +} + +/*! + * \internal + * \brief Set the ACL user field properly on an XML request + * + * Multiple user names are potentially involved in an XML request: the effective + * user of the current process; the user name known from an IPC client + * connection; and the user name obtained from the request itself, whether by + * the current standard XML attribute name or an older legacy attribute name. + * This function chooses the appropriate one that should be used for ACLs, sets + * it in the request (using the standard attribute name, and the legacy name if + * given), and returns it. + * + * \param[in,out] request XML request to update + * \param[in] field Alternate name for ACL user name XML attribute + * \param[in] peer_user User name as known from IPC connection + * + * \return ACL user name actually used + */ +const char * +pcmk__update_acl_user(xmlNode *request, const char *field, + const char *peer_user) +{ + static const char *effective_user = NULL; + const char *requested_user = NULL; + const char *user = NULL; + + if (effective_user == NULL) { + effective_user = pcmk__uid2username(geteuid()); + if (effective_user == NULL) { + effective_user = strdup("#unprivileged"); + CRM_CHECK(effective_user != NULL, return NULL); + crm_err("Unable to determine effective user, assuming unprivileged for ACLs"); + } + } + + requested_user = crm_element_value(request, XML_ACL_TAG_USER); + if (requested_user == NULL) { + /* @COMPAT rolling upgrades <=1.1.11 + * + * field is checked for backward compatibility with older versions that + * did not use XML_ACL_TAG_USER. + */ + requested_user = crm_element_value(request, field); + } + + if (!pcmk__is_privileged(effective_user)) { + /* We're not running as a privileged user, set or overwrite any existing + * value for $XML_ACL_TAG_USER + */ + user = effective_user; + + } else if (peer_user == NULL && requested_user == NULL) { + /* No user known or requested, use 'effective_user' and make sure one is + * set for the request + */ + user = effective_user; + + } else if (peer_user == NULL) { + /* No user known, trusting 'requested_user' */ + user = requested_user; + + } else if (!pcmk__is_privileged(peer_user)) { + /* The peer is not a privileged user, set or overwrite any existing + * value for $XML_ACL_TAG_USER + */ + user = peer_user; + + } else if (requested_user == NULL) { + /* Even if we're privileged, make sure there is always a value set */ + user = peer_user; + + } else { + /* Legal delegation to 'requested_user' */ + user = requested_user; + } + + // This requires pointer comparison, not string comparison + if (user != crm_element_value(request, XML_ACL_TAG_USER)) { + crm_xml_add(request, XML_ACL_TAG_USER, user); + } + + if (field != NULL && user != crm_element_value(request, field)) { + crm_xml_add(request, field, user); + } + + return requested_user; +} diff --git a/lib/common/agents.c b/lib/common/agents.c new file mode 100644 index 0000000..d2066c0 --- /dev/null +++ b/lib/common/agents.c @@ -0,0 +1,213 @@ +/* + * Copyright 2004-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include + +#include +#include + +/*! + * \brief Get capabilities of a resource agent standard + * + * \param[in] standard Standard name + * + * \return Bitmask of enum pcmk_ra_caps values + */ +uint32_t +pcmk_get_ra_caps(const char *standard) +{ + /* @COMPAT This should probably be case-sensitive, but isn't, + * for backward compatibility. + */ + if (standard == NULL) { + return pcmk_ra_cap_none; + + } else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_OCF)) { + return pcmk_ra_cap_provider | pcmk_ra_cap_params + | pcmk_ra_cap_unique | pcmk_ra_cap_promotable; + + } else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_STONITH)) { + /* @COMPAT Stonith resources can't really be unique clones, but we've + * allowed it in the past and have it in some scheduler regression tests + * (which were likely never used as real configurations). + * + * @TODO Remove pcmk_ra_cap_unique at the next major schema version + * bump, with a transform to remove globally-unique from the config. + */ + return pcmk_ra_cap_params | pcmk_ra_cap_unique | pcmk_ra_cap_stdin + | pcmk_ra_cap_fence_params; + + } else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_SYSTEMD) + || !strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE) + || !strcasecmp(standard, PCMK_RESOURCE_CLASS_LSB) + || !strcasecmp(standard, PCMK_RESOURCE_CLASS_UPSTART)) { + + /* Since service can map to LSB, systemd, or upstart, these should + * have identical capabilities + */ + return pcmk_ra_cap_status; + + } else if (!strcasecmp(standard, PCMK_RESOURCE_CLASS_NAGIOS)) { + return pcmk_ra_cap_params; + } + return pcmk_ra_cap_none; +} + +int +pcmk__effective_rc(int rc) +{ + int remapped_rc = rc; + + switch (rc) { + case PCMK_OCF_DEGRADED: + remapped_rc = PCMK_OCF_OK; + break; + + case PCMK_OCF_DEGRADED_PROMOTED: + remapped_rc = PCMK_OCF_RUNNING_PROMOTED; + break; + + default: + break; + } + + return remapped_rc; +} + +char * +crm_generate_ra_key(const char *standard, const char *provider, + const char *type) +{ + bool std_empty = pcmk__str_empty(standard); + bool prov_empty = pcmk__str_empty(provider); + bool ty_empty = pcmk__str_empty(type); + + if (std_empty || ty_empty) { + return NULL; + } + + return crm_strdup_printf("%s%s%s:%s", + standard, + (prov_empty ? "" : ":"), (prov_empty ? "" : provider), + type); +} + +/*! + * \brief Parse a "standard[:provider]:type" agent specification + * + * \param[in] spec Agent specification + * \param[out] standard Newly allocated memory containing agent standard (or NULL) + * \param[out] provider Newly allocated memory containing agent provider (or NULL) + * \param[put] type Newly allocated memory containing agent type (or NULL) + * + * \return pcmk_ok if the string could be parsed, -EINVAL otherwise + * + * \note It is acceptable for the type to contain a ':' if the standard supports + * that. For example, systemd supports the form "systemd:UNIT@A:B". + * \note It is the caller's responsibility to free the returned values. + */ +int +crm_parse_agent_spec(const char *spec, char **standard, char **provider, + char **type) +{ + char *colon; + + CRM_CHECK(spec && standard && provider && type, return -EINVAL); + *standard = NULL; + *provider = NULL; + *type = NULL; + + colon = strchr(spec, ':'); + if ((colon == NULL) || (colon == spec)) { + return -EINVAL; + } + + *standard = strndup(spec, colon - spec); + spec = colon + 1; + + if (pcmk_is_set(pcmk_get_ra_caps(*standard), pcmk_ra_cap_provider)) { + colon = strchr(spec, ':'); + if ((colon == NULL) || (colon == spec)) { + free(*standard); + return -EINVAL; + } + *provider = strndup(spec, colon - spec); + spec = colon + 1; + } + + if (*spec == '\0') { + free(*standard); + free(*provider); + return -EINVAL; + } + + *type = strdup(spec); + return pcmk_ok; +} + +/*! + * \brief Check whether a given stonith parameter is handled by Pacemaker + * + * Return true if a given string is the name of one of the special resource + * instance attributes interpreted directly by Pacemaker for stonith-class + * resources. + * + * \param[in] param Parameter name to check + * + * \return true if \p param is a special fencing parameter + */ +bool +pcmk_stonith_param(const char *param) +{ + if (param == NULL) { + return false; + } + if (pcmk__str_any_of(param, PCMK_STONITH_PROVIDES, + PCMK_STONITH_STONITH_TIMEOUT, NULL)) { + return true; + } + if (!pcmk__starts_with(param, "pcmk_")) { // Short-circuit common case + return false; + } + if (pcmk__str_any_of(param, + PCMK_STONITH_ACTION_LIMIT, + PCMK_STONITH_DELAY_BASE, + PCMK_STONITH_DELAY_MAX, + PCMK_STONITH_HOST_ARGUMENT, + PCMK_STONITH_HOST_CHECK, + PCMK_STONITH_HOST_LIST, + PCMK_STONITH_HOST_MAP, + NULL)) { + return true; + } + param = strchr(param + 5, '_'); // Skip past "pcmk_ACTION" + return pcmk__str_any_of(param, "_action", "_timeout", "_retries", NULL); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +bool +crm_provider_required(const char *standard) +{ + return pcmk_is_set(pcmk_get_ra_caps(standard), pcmk_ra_cap_provider); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/alerts.c b/lib/common/alerts.c new file mode 100644 index 0000000..abdadef --- /dev/null +++ b/lib/common/alerts.c @@ -0,0 +1,253 @@ +/* + * Copyright 2015-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include +#include +#include /* for F_CIB_UPDATE_RESULT */ + +/* + * to allow script compatibility we can have more than one + * set of environment variables + */ +const char *pcmk__alert_keys[PCMK__ALERT_INTERNAL_KEY_MAX][3] = +{ + [PCMK__alert_key_recipient] = { + "CRM_notify_recipient", "CRM_alert_recipient", NULL + }, + [PCMK__alert_key_node] = { + "CRM_notify_node", "CRM_alert_node", NULL + }, + [PCMK__alert_key_nodeid] = { + "CRM_notify_nodeid", "CRM_alert_nodeid", NULL + }, + [PCMK__alert_key_rsc] = { + "CRM_notify_rsc", "CRM_alert_rsc", NULL + }, + [PCMK__alert_key_task] = { + "CRM_notify_task", "CRM_alert_task", NULL + }, + [PCMK__alert_key_interval] = { + "CRM_notify_interval", "CRM_alert_interval", NULL + }, + [PCMK__alert_key_desc] = { + "CRM_notify_desc", "CRM_alert_desc", NULL + }, + [PCMK__alert_key_status] = { + "CRM_notify_status", "CRM_alert_status", NULL + }, + [PCMK__alert_key_target_rc] = { + "CRM_notify_target_rc", "CRM_alert_target_rc", NULL + }, + [PCMK__alert_key_rc] = { + "CRM_notify_rc", "CRM_alert_rc", NULL + }, + [PCMK__alert_key_kind] = { + "CRM_notify_kind", "CRM_alert_kind", NULL + }, + [PCMK__alert_key_version] = { + "CRM_notify_version", "CRM_alert_version", NULL + }, + [PCMK__alert_key_node_sequence] = { + "CRM_notify_node_sequence", PCMK__ALERT_NODE_SEQUENCE, NULL + }, + [PCMK__alert_key_timestamp] = { + "CRM_notify_timestamp", "CRM_alert_timestamp", NULL + }, + [PCMK__alert_key_attribute_name] = { + "CRM_notify_attribute_name", "CRM_alert_attribute_name", NULL + }, + [PCMK__alert_key_attribute_value] = { + "CRM_notify_attribute_value", "CRM_alert_attribute_value", NULL + }, + [PCMK__alert_key_timestamp_epoch] = { + "CRM_notify_timestamp_epoch", "CRM_alert_timestamp_epoch", NULL + }, + [PCMK__alert_key_timestamp_usec] = { + "CRM_notify_timestamp_usec", "CRM_alert_timestamp_usec", NULL + }, + [PCMK__alert_key_exec_time] = { + "CRM_notify_exec_time", "CRM_alert_exec_time", NULL + } +}; + +/*! + * \brief Create a new alert entry structure + * + * \param[in] id ID to use + * \param[in] path Path to alert agent executable + * + * \return Pointer to newly allocated alert entry + * \note Non-string fields will be filled in with defaults. + * It is the caller's responsibility to free the result, + * using pcmk__free_alert(). + */ +pcmk__alert_t * +pcmk__alert_new(const char *id, const char *path) +{ + pcmk__alert_t *entry = calloc(1, sizeof(pcmk__alert_t)); + + CRM_ASSERT(entry && id && path); + entry->id = strdup(id); + entry->path = strdup(path); + entry->timeout = PCMK__ALERT_DEFAULT_TIMEOUT_MS; + entry->flags = pcmk__alert_default; + return entry; +} + +void +pcmk__free_alert(pcmk__alert_t *entry) +{ + if (entry) { + free(entry->id); + free(entry->path); + free(entry->tstamp_format); + free(entry->recipient); + + g_strfreev(entry->select_attribute_name); + if (entry->envvars) { + g_hash_table_destroy(entry->envvars); + } + free(entry); + } +} + +/*! + * \internal + * \brief Duplicate an alert entry + * + * \param[in] entry Alert entry to duplicate + * + * \return Duplicate of alert entry + */ +pcmk__alert_t * +pcmk__dup_alert(const pcmk__alert_t *entry) +{ + pcmk__alert_t *new_entry = pcmk__alert_new(entry->id, entry->path); + + new_entry->timeout = entry->timeout; + new_entry->flags = entry->flags; + new_entry->envvars = pcmk__str_table_dup(entry->envvars); + pcmk__str_update(&new_entry->tstamp_format, entry->tstamp_format); + pcmk__str_update(&new_entry->recipient, entry->recipient); + if (entry->select_attribute_name) { + new_entry->select_attribute_name = g_strdupv(entry->select_attribute_name); + } + return new_entry; +} + +void +pcmk__add_alert_key(GHashTable *table, enum pcmk__alert_keys_e name, + const char *value) +{ + for (const char **key = pcmk__alert_keys[name]; *key; key++) { + crm_trace("Inserting alert key %s = '%s'", *key, value); + if (value) { + g_hash_table_insert(table, strdup(*key), strdup(value)); + } else { + g_hash_table_remove(table, *key); + } + } +} + +void +pcmk__add_alert_key_int(GHashTable *table, enum pcmk__alert_keys_e name, + int value) +{ + for (const char **key = pcmk__alert_keys[name]; *key; key++) { + crm_trace("Inserting alert key %s = %d", *key, value); + g_hash_table_insert(table, strdup(*key), pcmk__itoa(value)); + } +} + +#define XPATH_PATCHSET1_DIFF "//" F_CIB_UPDATE_RESULT "//" XML_TAG_DIFF_ADDED + +#define XPATH_PATCHSET1_CRMCONFIG XPATH_PATCHSET1_DIFF "//" XML_CIB_TAG_CRMCONFIG +#define XPATH_PATCHSET1_ALERTS XPATH_PATCHSET1_DIFF "//" XML_CIB_TAG_ALERTS + +#define XPATH_PATCHSET1_EITHER \ + XPATH_PATCHSET1_CRMCONFIG " | " XPATH_PATCHSET1_ALERTS + +#define XPATH_CONFIG "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION + +#define XPATH_CRMCONFIG XPATH_CONFIG "/" XML_CIB_TAG_CRMCONFIG "/" +#define XPATH_ALERTS XPATH_CONFIG "/" XML_CIB_TAG_ALERTS + +/*! + * \internal + * \brief Check whether a CIB update affects alerts + * + * \param[in] msg XML containing CIB update + * \param[in] config Whether to check for crmconfig change as well + * + * \return TRUE if update affects alerts, FALSE otherwise + */ +bool +pcmk__alert_in_patchset(xmlNode *msg, bool config) +{ + int rc = -1; + int format= 1; + xmlNode *patchset = get_message_xml(msg, F_CIB_UPDATE_RESULT); + xmlNode *change = NULL; + xmlXPathObject *xpathObj = NULL; + + CRM_CHECK(msg != NULL, return FALSE); + + crm_element_value_int(msg, F_CIB_RC, &rc); + if (rc < pcmk_ok) { + crm_trace("Ignore failed CIB update: %s (%d)", pcmk_strerror(rc), rc); + return FALSE; + } + + crm_element_value_int(patchset, "format", &format); + if (format == 1) { + const char *diff = (config? XPATH_PATCHSET1_EITHER : XPATH_PATCHSET1_ALERTS); + + if ((xpathObj = xpath_search(msg, diff)) != NULL) { + freeXpathObject(xpathObj); + return TRUE; + } + } else if (format == 2) { + for (change = pcmk__xml_first_child(patchset); change != NULL; + change = pcmk__xml_next(change)) { + const char *xpath = crm_element_value(change, XML_DIFF_PATH); + + if (xpath == NULL) { + continue; + } + + if ((!config || !strstr(xpath, XPATH_CRMCONFIG)) + && !strstr(xpath, XPATH_ALERTS)) { + + /* this is not a change to an existing section ... */ + + xmlNode *section = NULL; + const char *name = NULL; + + if ((strcmp(xpath, XPATH_CONFIG) != 0) || + ((section = pcmk__xml_first_child(change)) == NULL) || + ((name = crm_element_name(section)) == NULL) || + (strcmp(name, XML_CIB_TAG_ALERTS) != 0)) { + + /* ... nor is it a newly added alerts section */ + continue; + } + } + + return TRUE; + } + + } else { + crm_warn("Unknown patch format: %d", format); + } + return FALSE; +} diff --git a/lib/common/attrs.c b/lib/common/attrs.c new file mode 100644 index 0000000..2be03b4 --- /dev/null +++ b/lib/common/attrs.c @@ -0,0 +1,89 @@ +/* + * Copyright 2011-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include + +#include + +#include +#include + +#define LRM_TARGET_ENV "OCF_RESKEY_" CRM_META "_" XML_LRM_ATTR_TARGET + +/*! + * \internal + * \brief Get the node name that should be used to set node attributes + * + * If given NULL, "auto", or "localhost" as an argument, check the environment + * to detect the node name that should be used to set node attributes. (The + * caller might not know the correct name, for example if the target is part of + * a bundle with container-attribute-target set to "host".) + * + * \param[in] name NULL, "auto" or "localhost" to check environment variables, + * or anything else to return NULL + * + * \return Node name that should be used for node attributes based on the + * environment if known, otherwise NULL + */ +const char * +pcmk__node_attr_target(const char *name) +{ + if (name == NULL || pcmk__strcase_any_of(name, "auto", "localhost", NULL)) { + char *target_var = crm_meta_name(XML_RSC_ATTR_TARGET); + char *phys_var = crm_meta_name(PCMK__ENV_PHYSICAL_HOST); + const char *target = getenv(target_var); + const char *host_physical = getenv(phys_var); + + // It is important to use the name by which the scheduler knows us + if (host_physical && pcmk__str_eq(target, "host", pcmk__str_casei)) { + name = host_physical; + + } else { + const char *host_pcmk = getenv(LRM_TARGET_ENV); + + if (host_pcmk) { + name = host_pcmk; + } + } + free(target_var); + free(phys_var); + + // TODO? Call get_local_node_name() if name == NULL + // (currently would require linkage against libcrmcluster) + return name; + } else { + return NULL; + } +} + +/*! + * \brief Return the name of the node attribute used as a promotion score + * + * \param[in] rsc_id Resource ID that promotion score is for (or NULL to + * check the OCF_RESOURCE_INSTANCE environment variable) + * + * \return Newly allocated string with the node attribute name (or NULL on + * error, including no ID or environment variable specified) + * \note It is the caller's responsibility to free() the result. + */ +char * +pcmk_promotion_score_name(const char *rsc_id) +{ + if (pcmk__str_empty(rsc_id)) { + rsc_id = getenv("OCF_RESOURCE_INSTANCE"); + if (pcmk__str_empty(rsc_id)) { + return NULL; + } + } + return crm_strdup_printf("master-%s", rsc_id); +} diff --git a/lib/common/cib.c b/lib/common/cib.c new file mode 100644 index 0000000..b84c5e8 --- /dev/null +++ b/lib/common/cib.c @@ -0,0 +1,156 @@ +/* + * Original copyright 2004 International Business Machines + * Later changes copyright 2008-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include // xmlNode + +#include + +/* + * Functions to help find particular sections of the CIB + */ + +// Map CIB element names to their parent elements and XPath searches +static struct { + const char *name; // Name of this CIB element + const char *parent; // CIB element that this element is a child of + const char *path; // XPath to find this CIB element +} cib_sections[] = { + { + // This first entry is also the default if a NULL is compared + XML_TAG_CIB, + NULL, + "//" XML_TAG_CIB + }, + { + XML_CIB_TAG_STATUS, + "/" XML_TAG_CIB, + "//" XML_TAG_CIB "/" XML_CIB_TAG_STATUS + }, + { + XML_CIB_TAG_CONFIGURATION, + "/" XML_TAG_CIB, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION + }, + { + XML_CIB_TAG_CRMCONFIG, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_CRMCONFIG + }, + { + XML_CIB_TAG_NODES, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_NODES + }, + { + XML_CIB_TAG_RESOURCES, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RESOURCES + }, + { + XML_CIB_TAG_CONSTRAINTS, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_CONSTRAINTS + }, + { + XML_CIB_TAG_OPCONFIG, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_OPCONFIG + }, + { + XML_CIB_TAG_RSCCONFIG, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RSCCONFIG + }, + { + XML_CIB_TAG_ACLS, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_ACLS + }, + { + XML_TAG_FENCING_TOPOLOGY, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_TAG_FENCING_TOPOLOGY + }, + { + XML_CIB_TAG_TAGS, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_TAGS + }, + { + XML_CIB_TAG_ALERTS, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION, + "//" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_ALERTS + }, + { + XML_CIB_TAG_SECTION_ALL, + NULL, + "//" XML_TAG_CIB + }, +}; + +/*! + * \brief Get the XPath needed to find a specified CIB element name + * + * \param[in] element_name Name of CIB element + * + * \return XPath for finding \p element_name in CIB XML (or NULL if unknown) + * \note The return value is constant and should not be freed. + */ +const char * +pcmk_cib_xpath_for(const char *element_name) +{ + for (int lpc = 0; lpc < PCMK__NELEM(cib_sections); lpc++) { + // A NULL element_name will match the first entry + if (pcmk__str_eq(element_name, cib_sections[lpc].name, + pcmk__str_null_matches)) { + return cib_sections[lpc].path; + } + } + return NULL; +} + +/*! + * \brief Get the parent element name of a given CIB element name + * + * \param[in] element_name Name of CIB element + * + * \return Parent element of \p element_name (or NULL if none or unknown) + * \note The return value is constant and should not be freed. + */ +const char * +pcmk_cib_parent_name_for(const char *element_name) +{ + for (int lpc = 0; lpc < PCMK__NELEM(cib_sections); lpc++) { + // A NULL element_name will match the first entry + if (pcmk__str_eq(element_name, cib_sections[lpc].name, + pcmk__str_null_matches)) { + return cib_sections[lpc].parent; + } + } + return NULL; +} + +/*! + * \brief Find an element in the CIB + * + * \param[in,out] cib Top-level CIB XML to search + * \param[in] element_name Name of CIB element to search for + * + * \return XML element in \p cib corresponding to \p element_name + * (or \p cib itself if element is unknown or not found) + */ +xmlNode * +pcmk_find_cib_element(xmlNode *cib, const char *element_name) +{ + return get_xpath_object(pcmk_cib_xpath_for(element_name), cib, LOG_TRACE); +} diff --git a/lib/common/cib_secrets.c b/lib/common/cib_secrets.c new file mode 100644 index 0000000..2f71c98 --- /dev/null +++ b/lib/common/cib_secrets.c @@ -0,0 +1,192 @@ +/* + * Copyright 2011-2020 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +static int is_magic_value(char *p); +static bool check_md5_hash(char *hash, char *value); +static void add_secret_params(gpointer key, gpointer value, gpointer user_data); +static char *read_local_file(char *local_file); + +#define MAX_VALUE_LEN 255 +#define MAGIC "lrm://" + +static int +is_magic_value(char *p) +{ + return !strcmp(p, MAGIC); +} + +static bool +check_md5_hash(char *hash, char *value) +{ + bool rc = false; + char *hash2 = NULL; + + hash2 = crm_md5sum(value); + crm_debug("hash: %s, calculated hash: %s", hash, hash2); + if (pcmk__str_eq(hash, hash2, pcmk__str_casei)) { + rc = true; + } + free(hash2); + return rc; +} + +static char * +read_local_file(char *local_file) +{ + FILE *fp = fopen(local_file, "r"); + char buf[MAX_VALUE_LEN+1]; + char *p; + + if (!fp) { + if (errno != ENOENT) { + crm_perror(LOG_ERR, "cannot open %s" , local_file); + } + return NULL; + } + + if (!fgets(buf, MAX_VALUE_LEN, fp)) { + crm_perror(LOG_ERR, "cannot read %s", local_file); + fclose(fp); + return NULL; + } + fclose(fp); + + // Strip trailing white space + for (p = buf + strlen(buf) - 1; (p >= buf) && isspace(*p); p--); + *(p+1) = '\0'; + return strdup(buf); +} + +/*! + * \internal + * \brief Read secret parameter values from file + * + * Given a table of resource parameters, if any of their values are the + * magic string indicating a CIB secret, replace that string with the + * secret read from the file appropriate to the given resource. + * + * \param[in] rsc_id Resource whose parameters are being checked + * \param[in,out] params Resource parameters to check + * + * \return Standard Pacemaker return code + */ +int +pcmk__substitute_secrets(const char *rsc_id, GHashTable *params) +{ + char local_file[FILENAME_MAX+1], *start_pname; + char hash_file[FILENAME_MAX+1], *hash; + GList *secret_params = NULL, *l; + char *key, *pvalue, *secret_value; + int rc = pcmk_rc_ok; + + if (params == NULL) { + return pcmk_rc_ok; + } + + /* secret_params could be cached with the resource; + * there are also parameters sent with operations + * which cannot be cached + */ + g_hash_table_foreach(params, add_secret_params, &secret_params); + if (secret_params == NULL) { // No secret parameters found + return pcmk_rc_ok; + } + + crm_debug("Replace secret parameters for resource %s", rsc_id); + + if (snprintf(local_file, FILENAME_MAX, LRM_CIBSECRETS_DIR "/%s/", rsc_id) + > FILENAME_MAX) { + crm_err("Can't replace secret parameters for %s: file name size exceeded", + rsc_id); + return ENAMETOOLONG; + } + start_pname = local_file + strlen(local_file); + + for (l = g_list_first(secret_params); l; l = g_list_next(l)) { + key = (char *)(l->data); + pvalue = g_hash_table_lookup(params, key); + if (!pvalue) { /* this cannot really happen */ + crm_err("odd, no parameter %s for rsc %s found now", key, rsc_id); + continue; + } + + if ((strlen(key) + strlen(local_file)) >= FILENAME_MAX-2) { + crm_err("%s: parameter name %s too big", rsc_id, key); + rc = ENAMETOOLONG; + continue; + } + + strcpy(start_pname, key); + secret_value = read_local_file(local_file); + if (!secret_value) { + crm_err("secret for rsc %s parameter %s not found in %s", + rsc_id, key, LRM_CIBSECRETS_DIR); + rc = ENOENT; + continue; + } + + strcpy(hash_file, local_file); + if (strlen(hash_file) + 5 > FILENAME_MAX) { + crm_err("cannot build such a long name " + "for the sign file: %s.sign", hash_file); + free(secret_value); + rc = ENAMETOOLONG; + continue; + + } else { + strcat(hash_file, ".sign"); + hash = read_local_file(hash_file); + if (hash == NULL) { + crm_err("md5 sum for rsc %s parameter %s " + "cannot be read from %s", rsc_id, key, hash_file); + free(secret_value); + rc = ENOENT; + continue; + + } else if (!check_md5_hash(hash, secret_value)) { + crm_err("md5 sum for rsc %s parameter %s " + "does not match", rsc_id, key); + free(secret_value); + free(hash); + rc = pcmk_rc_cib_corrupt; + continue; + } + free(hash); + } + g_hash_table_replace(params, strdup(key), secret_value); + } + g_list_free(secret_params); + return rc; +} + +static void +add_secret_params(gpointer key, gpointer value, gpointer user_data) +{ + GList **lp = (GList **)user_data; + + if (is_magic_value((char *)value)) { + *lp = g_list_append(*lp, (char *)key); + } +} diff --git a/lib/common/cmdline.c b/lib/common/cmdline.c new file mode 100644 index 0000000..08c43f7 --- /dev/null +++ b/lib/common/cmdline.c @@ -0,0 +1,379 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include +#include +#include + +static gboolean +bump_verbosity(const gchar *option_name, const gchar *optarg, gpointer data, GError **error) { + pcmk__common_args_t *common_args = (pcmk__common_args_t *) data; + common_args->verbosity++; + return TRUE; +} + +pcmk__common_args_t * +pcmk__new_common_args(const char *summary) +{ + pcmk__common_args_t *args = NULL; + + args = calloc(1, sizeof(pcmk__common_args_t)); + if (args == NULL) { + crm_exit(CRM_EX_OSERR); + } + + args->summary = strdup(summary); + if (args->summary == NULL) { + free(args); + args = NULL; + crm_exit(CRM_EX_OSERR); + } + + return args; +} + +static void +free_common_args(gpointer data) { + pcmk__common_args_t *common_args = (pcmk__common_args_t *) data; + + free(common_args->summary); + free(common_args->output_ty); + free(common_args->output_dest); + + if (common_args->output_as_descr != NULL) { + free(common_args->output_as_descr); + } + + free(common_args); +} + +GOptionContext * +pcmk__build_arg_context(pcmk__common_args_t *common_args, const char *fmts, + GOptionGroup **output_group, const char *param_string) { + GOptionContext *context; + GOptionGroup *main_group; + + GOptionEntry main_entries[3] = { + { "version", '$', 0, G_OPTION_ARG_NONE, &(common_args->version), + N_("Display software version and exit"), + NULL }, + { "verbose", 'V', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, bump_verbosity, + N_("Increase debug output (may be specified multiple times)"), + NULL }, + + { NULL } + }; + + main_group = g_option_group_new(NULL, "Application Options:", NULL, common_args, free_common_args); + g_option_group_add_entries(main_group, main_entries); + + context = g_option_context_new(param_string); + g_option_context_set_summary(context, common_args->summary); + g_option_context_set_description(context, + "Report bugs to " PCMK__BUG_URL "\n"); + g_option_context_set_main_group(context, main_group); + + if (fmts != NULL) { + GOptionEntry output_entries[3] = { + { "output-as", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_ty), + NULL, + N_("FORMAT") }, + { "output-to", 0, 0, G_OPTION_ARG_STRING, &(common_args->output_dest), + N_( "Specify file name for output (or \"-\" for stdout)"), N_("DEST") }, + + { NULL } + }; + + if (*output_group == NULL) { + *output_group = g_option_group_new("output", N_("Output Options:"), N_("Show output help"), NULL, NULL); + } + + common_args->output_as_descr = crm_strdup_printf("Specify output format as one of: %s", fmts); + output_entries[0].description = common_args->output_as_descr; + g_option_group_add_entries(*output_group, output_entries); + g_option_context_add_group(context, *output_group); + } + + // main_group is now owned by context, we don't free it here + // cppcheck-suppress memleak + return context; +} + +void +pcmk__free_arg_context(GOptionContext *context) { + if (context == NULL) { + return; + } + + g_option_context_free(context); +} + +void +pcmk__add_main_args(GOptionContext *context, const GOptionEntry entries[]) +{ + GOptionGroup *main_group = g_option_context_get_main_group(context); + + g_option_group_add_entries(main_group, entries); +} + +void +pcmk__add_arg_group(GOptionContext *context, const char *name, + const char *header, const char *desc, + const GOptionEntry entries[]) +{ + GOptionGroup *group = NULL; + + group = g_option_group_new(name, header, desc, NULL, NULL); + g_option_group_add_entries(group, entries); + g_option_context_add_group(context, group); + // group is now owned by context, we don't free it here + // cppcheck-suppress memleak +} + +static gchar * +string_replace(gchar *str, const gchar *sub, const gchar *repl) +{ + /* This function just replaces all occurrences of a substring + * with some other string. It doesn't handle cases like overlapping, + * so don't get clever with it. + * + * FIXME: When glib >= 2.68 is supported, we can get rid of this + * function and use g_string_replace instead. + */ + gchar **split = g_strsplit(str, sub, 0); + gchar *retval = g_strjoinv(repl, split); + + g_strfreev(split); + return retval; +} + +gchar * +pcmk__quote_cmdline(gchar **argv) +{ + GString *gs = NULL; + + if (argv == NULL || argv[0] == NULL) { + return NULL; + } + + gs = g_string_sized_new(100); + + for (int i = 0; argv[i] != NULL; i++) { + if (i > 0) { + g_string_append_c(gs, ' '); + } + + if (strchr(argv[i], ' ') == NULL) { + /* The arg does not contain a space. */ + g_string_append(gs, argv[i]); + } else if (strchr(argv[i], '\'') == NULL) { + /* The arg contains a space, but not a single quote. */ + pcmk__g_strcat(gs, "'", argv[i], "'", NULL); + } else { + /* The arg contains both a space and a single quote, which needs to + * be replaced with an escaped version. We do this instead of counting + * on libxml to handle the escaping for various reasons: + * + * (1) This keeps the string as valid shell. + * (2) We don't want to use XML entities in formats besides XML and HTML. + * (3) The string we are feeding to libxml is something like: "a b 'c d' e". + * It won't escape the single quotes around 'c d' here because there is + * no need to escape quotes inside a different form of quote. If we + * change the string to "a b 'c'd' e", we haven't changed anything - it's + * still single quotes inside double quotes. + * + * On the other hand, if we replace the single quote with "'", then + * we have introduced an ampersand which libxml will escape. This leaves + * us with "&apos;" which is not what we want. + * + * It's simplest to just escape with a backslash. + */ + gchar *repl = string_replace(argv[i], "'", "\\\'"); + pcmk__g_strcat(gs, "'", repl, "'", NULL); + g_free(repl); + } + } + + return g_string_free(gs, FALSE); +} + +gchar ** +pcmk__cmdline_preproc(char *const *argv, const char *special) { + GPtrArray *arr = NULL; + bool saw_dash_dash = false; + bool copy_option = false; + + if (argv == NULL) { + return NULL; + } + + if (g_get_prgname() == NULL && argv && *argv) { + gchar *basename = g_path_get_basename(*argv); + + g_set_prgname(basename); + g_free(basename); + } + + arr = g_ptr_array_new(); + + for (int i = 0; argv[i] != NULL; i++) { + /* If this is the first time we saw "--" in the command line, set + * a flag so we know to just copy everything after it over. We also + * want to copy the "--" over so whatever actually parses the command + * line when we're done knows where arguments end. + */ + if (saw_dash_dash == false && strcmp(argv[i], "--") == 0) { + saw_dash_dash = true; + } + + if (saw_dash_dash == true) { + g_ptr_array_add(arr, g_strdup(argv[i])); + continue; + } + + if (copy_option == true) { + g_ptr_array_add(arr, g_strdup(argv[i])); + copy_option = false; + continue; + } + + /* This is just a dash by itself. That could indicate stdin/stdout, or + * it could be user error. Copy it over and let glib figure it out. + */ + if (pcmk__str_eq(argv[i], "-", pcmk__str_casei)) { + g_ptr_array_add(arr, g_strdup(argv[i])); + continue; + } + + /* "-INFINITY" is almost certainly meant as a string, not as an option + * list + */ + if (strcmp(argv[i], "-INFINITY") == 0) { + g_ptr_array_add(arr, g_strdup(argv[i])); + continue; + } + + /* This is a short argument, or perhaps several. Iterate over it + * and explode them out into individual arguments. + */ + if (g_str_has_prefix(argv[i], "-") && !g_str_has_prefix(argv[i], "--")) { + /* Skip over leading dash */ + const char *ch = argv[i]+1; + + /* This looks like the start of a number, which means it is a negative + * number. It's probably the argument to the preceeding option, but + * we can't know that here. Copy it over and let whatever handles + * arguments next figure it out. + */ + if (*ch != '\0' && *ch >= '1' && *ch <= '9') { + bool is_numeric = true; + + while (*ch != '\0') { + if (!isdigit(*ch)) { + is_numeric = false; + break; + } + + ch++; + } + + if (is_numeric) { + g_ptr_array_add(arr, g_strdup_printf("%s", argv[i])); + continue; + } else { + /* This argument wasn't entirely numeric. Reset ch to the + * beginning so we can process it one character at a time. + */ + ch = argv[i]+1; + } + } + + while (*ch != '\0') { + /* This is a special short argument that takes an option. getopt + * allows values to be interspersed with a list of arguments, but + * glib does not. Grab both the argument and its value and + * separate them into a new argument. + */ + if (special != NULL && strchr(special, *ch) != NULL) { + /* The argument does not occur at the end of this string of + * arguments. Take everything through the end as its value. + */ + if (*(ch+1) != '\0') { + fprintf(stderr, "Deprecated argument format '-%c%s' used.\n", *ch, ch+1); + fprintf(stderr, "Please use '-%c %s' instead. " + "Support will be removed in a future release.\n", + *ch, ch+1); + + g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); + g_ptr_array_add(arr, g_strdup(ch+1)); + break; + + /* The argument occurs at the end of this string. Hopefully + * whatever comes next in argv is its value. It may not be, + * but that is not for us to decide. + */ + } else { + g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); + copy_option = true; + ch++; + } + + /* This is a regular short argument. Just copy it over. */ + } else { + g_ptr_array_add(arr, g_strdup_printf("-%c", *ch)); + ch++; + } + } + + /* This is a long argument, or an option, or something else. + * Copy it over - everything else is copied, so this keeps it easy for + * the caller to know what to do with the memory when it's done. + */ + } else { + g_ptr_array_add(arr, g_strdup(argv[i])); + } + } + + g_ptr_array_add(arr, NULL); + + return (char **) g_ptr_array_free(arr, FALSE); +} + +G_GNUC_PRINTF(3, 4) +gboolean +pcmk__force_args(GOptionContext *context, GError **error, const char *format, ...) { + int len = 0; + char *buf = NULL; + gchar **extra_args = NULL; + va_list ap; + gboolean retval = TRUE; + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + + if (!g_shell_parse_argv(buf, NULL, &extra_args, error)) { + g_strfreev(extra_args); + free(buf); + return FALSE; + } + + retval = g_option_context_parse_strv(context, &extra_args, error); + + g_strfreev(extra_args); + free(buf); + return retval; +} diff --git a/lib/common/crmcommon_private.h b/lib/common/crmcommon_private.h new file mode 100644 index 0000000..7faccb6 --- /dev/null +++ b/lib/common/crmcommon_private.h @@ -0,0 +1,325 @@ +/* + * Copyright 2018-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef CRMCOMMON_PRIVATE__H +# define CRMCOMMON_PRIVATE__H + +/* This header is for the sole use of libcrmcommon, so that functions can be + * declared with G_GNUC_INTERNAL for efficiency. + */ + +#include // uint8_t, uint32_t +#include // bool +#include // size_t +#include // GList +#include // xmlNode, xmlAttr +#include // struct qb_ipc_response_header + +// Decent chunk size for processing large amounts of data +#define PCMK__BUFFER_SIZE 4096 + +#if defined(PCMK__UNIT_TESTING) +#undef G_GNUC_INTERNAL +#define G_GNUC_INTERNAL +#endif + +/* When deleting portions of an XML tree, we keep a record so we can know later + * (e.g. when checking differences) that something was deleted. + */ +typedef struct pcmk__deleted_xml_s { + char *path; + int position; +} pcmk__deleted_xml_t; + +typedef struct xml_node_private_s { + long check; + uint32_t flags; +} xml_node_private_t; + +typedef struct xml_doc_private_s { + long check; + uint32_t flags; + char *user; + GList *acls; + GList *deleted_objs; // List of pcmk__deleted_xml_t +} xml_doc_private_t; + +#define pcmk__set_xml_flags(xml_priv, flags_to_set) do { \ + (xml_priv)->flags = pcmk__set_flags_as(__func__, __LINE__, \ + LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ + (flags_to_set), #flags_to_set); \ + } while (0) + +#define pcmk__clear_xml_flags(xml_priv, flags_to_clear) do { \ + (xml_priv)->flags = pcmk__clear_flags_as(__func__, __LINE__, \ + LOG_NEVER, "XML", "XML node", (xml_priv)->flags, \ + (flags_to_clear), #flags_to_clear); \ + } while (0) + +G_GNUC_INTERNAL +void pcmk__xml2text(xmlNodePtr data, uint32_t options, GString *buffer, + int depth); + +G_GNUC_INTERNAL +bool pcmk__tracking_xml_changes(xmlNode *xml, bool lazy); + +G_GNUC_INTERNAL +void pcmk__mark_xml_created(xmlNode *xml); + +G_GNUC_INTERNAL +int pcmk__xml_position(const xmlNode *xml, + enum xml_private_flags ignore_if_set); + +G_GNUC_INTERNAL +xmlNode *pcmk__xml_match(const xmlNode *haystack, const xmlNode *needle, + bool exact); + +G_GNUC_INTERNAL +void pcmk__xml_update(xmlNode *parent, xmlNode *target, xmlNode *update, + bool as_diff); + +G_GNUC_INTERNAL +xmlNode *pcmk__xc_match(const xmlNode *root, const xmlNode *search_comment, + bool exact); + +G_GNUC_INTERNAL +void pcmk__xc_update(xmlNode *parent, xmlNode *target, xmlNode *update); + +G_GNUC_INTERNAL +void pcmk__free_acls(GList *acls); + +G_GNUC_INTERNAL +void pcmk__unpack_acl(xmlNode *source, xmlNode *target, const char *user); + +G_GNUC_INTERNAL +bool pcmk__is_user_in_group(const char *user, const char *group); + +G_GNUC_INTERNAL +void pcmk__apply_acl(xmlNode *xml); + +G_GNUC_INTERNAL +void pcmk__apply_creation_acl(xmlNode *xml, bool check_top); + +G_GNUC_INTERNAL +void pcmk__mark_xml_attr_dirty(xmlAttr *a); + +G_GNUC_INTERNAL +bool pcmk__xa_filterable(const char *name); + +G_GNUC_INTERNAL +void pcmk__log_xmllib_err(void *ctx, const char *fmt, ...) +G_GNUC_PRINTF(2, 3); + +static inline const char * +pcmk__xml_attr_value(const xmlAttr *attr) +{ + return ((attr == NULL) || (attr->children == NULL))? NULL + : (const char *) attr->children->content; +} + +/* + * IPC + */ + +#define PCMK__IPC_VERSION 1 + +#define PCMK__CONTROLD_API_MAJOR "1" +#define PCMK__CONTROLD_API_MINOR "0" + +// IPC behavior that varies by daemon +typedef struct pcmk__ipc_methods_s { + /*! + * \internal + * \brief Allocate any private data needed by daemon IPC + * + * \param[in,out] api IPC API connection + * + * \return Standard Pacemaker return code + */ + int (*new_data)(pcmk_ipc_api_t *api); + + /*! + * \internal + * \brief Free any private data used by daemon IPC + * + * \param[in,out] api_data Data allocated by new_data() method + */ + void (*free_data)(void *api_data); + + /*! + * \internal + * \brief Perform daemon-specific handling after successful connection + * + * Some daemons require clients to register before sending any other + * commands. The controller requires a CRM_OP_HELLO (with no reply), and + * the CIB manager, executor, and fencer require a CRM_OP_REGISTER (with a + * reply). Ideally this would be consistent across all daemons, but for now + * this allows each to do its own authorization. + * + * \param[in,out] api IPC API connection + * + * \return Standard Pacemaker return code + */ + int (*post_connect)(pcmk_ipc_api_t *api); + + /*! + * \internal + * \brief Check whether an IPC request results in a reply + * + * \param[in,out] api IPC API connection + * \param[in,out] request IPC request XML + * + * \return true if request would result in an IPC reply, false otherwise + */ + bool (*reply_expected)(pcmk_ipc_api_t *api, xmlNode *request); + + /*! + * \internal + * \brief Perform daemon-specific handling of an IPC message + * + * \param[in,out] api IPC API connection + * \param[in,out] msg Message read from IPC connection + * + * \return true if more IPC reply messages should be expected + */ + bool (*dispatch)(pcmk_ipc_api_t *api, xmlNode *msg); + + /*! + * \internal + * \brief Perform daemon-specific handling of an IPC disconnect + * + * \param[in,out] api IPC API connection + */ + void (*post_disconnect)(pcmk_ipc_api_t *api); +} pcmk__ipc_methods_t; + +// Implementation of pcmk_ipc_api_t +struct pcmk_ipc_api_s { + enum pcmk_ipc_server server; // Daemon this IPC API instance is for + enum pcmk_ipc_dispatch dispatch_type; // How replies should be dispatched + size_t ipc_size_max; // maximum IPC buffer size + crm_ipc_t *ipc; // IPC connection + mainloop_io_t *mainloop_io; // If using mainloop, I/O source for IPC + bool free_on_disconnect; // Whether disconnect should free object + pcmk_ipc_callback_t cb; // Caller-registered callback (if any) + void *user_data; // Caller-registered data (if any) + void *api_data; // For daemon-specific use + pcmk__ipc_methods_t *cmds; // Behavior that varies by daemon +}; + +typedef struct pcmk__ipc_header_s { + struct qb_ipc_response_header qb; + uint32_t size_uncompressed; + uint32_t size_compressed; + uint32_t flags; + uint8_t version; +} pcmk__ipc_header_t; + +G_GNUC_INTERNAL +int pcmk__send_ipc_request(pcmk_ipc_api_t *api, xmlNode *request); + +G_GNUC_INTERNAL +void pcmk__call_ipc_callback(pcmk_ipc_api_t *api, + enum pcmk_ipc_event event_type, + crm_exit_t status, void *event_data); + +G_GNUC_INTERNAL +unsigned int pcmk__ipc_buffer_size(unsigned int max); + +G_GNUC_INTERNAL +bool pcmk__valid_ipc_header(const pcmk__ipc_header_t *header); + +G_GNUC_INTERNAL +pcmk__ipc_methods_t *pcmk__attrd_api_methods(void); + +G_GNUC_INTERNAL +pcmk__ipc_methods_t *pcmk__controld_api_methods(void); + +G_GNUC_INTERNAL +pcmk__ipc_methods_t *pcmk__pacemakerd_api_methods(void); + +G_GNUC_INTERNAL +pcmk__ipc_methods_t *pcmk__schedulerd_api_methods(void); + + +/* + * Logging + */ + +//! XML is newly created +#define PCMK__XML_PREFIX_CREATED "++" + +//! XML has been deleted +#define PCMK__XML_PREFIX_DELETED "--" + +//! XML has been modified +#define PCMK__XML_PREFIX_MODIFIED "+ " + +//! XML has been moved +#define PCMK__XML_PREFIX_MOVED "+~" + +/*! + * \brief Check the authenticity of the IPC socket peer process + * + * If everything goes well, peer's authenticity is verified by the means + * of comparing against provided referential UID and GID (either satisfies), + * and the result of this check can be deduced from the return value. + * As an exception, detected UID of 0 ("root") satisfies arbitrary + * provided referential daemon's credentials. + * + * \param[in] qb_ipc libqb client connection if available + * \param[in] sock IPC related, connected Unix socket to check peer of + * \param[in] refuid referential UID to check against + * \param[in] refgid referential GID to check against + * \param[out] gotpid to optionally store obtained PID of the peer + * (not available on FreeBSD, special value of 1 + * used instead, and the caller is required to + * special case this value respectively) + * \param[out] gotuid to optionally store obtained UID of the peer + * \param[out] gotgid to optionally store obtained GID of the peer + * + * \return Standard Pacemaker return code + * ie: 0 if it the connection is authentic + * pcmk_rc_ipc_unauthorized if the connection is not authentic, + * standard errors. + * + * \note While this function is tolerant on what constitutes authorized + * IPC daemon process (its effective user matches UID=0 or \p refuid, + * or at least its group matches \p refgid), either or both (in case + * of UID=0) mismatches on the expected credentials of such peer + * process \e shall be investigated at the caller when value of 1 + * gets returned there, since higher-than-expected privileges in + * respect to the expected/intended credentials possibly violate + * the least privilege principle and may pose an additional risk + * (i.e. such accidental inconsistency shall be eventually fixed). + */ +int pcmk__crm_ipc_is_authentic_process(qb_ipcc_connection_t *qb_ipc, int sock, + uid_t refuid, gid_t refgid, + pid_t *gotpid, uid_t *gotuid, + gid_t *gotgid); + + +/* + * Output + */ +G_GNUC_INTERNAL +int pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, + const char *filename, char **argv); + +G_GNUC_INTERNAL +void pcmk__register_patchset_messages(pcmk__output_t *out); + + +/* + * Utils + */ +#define PCMK__PW_BUFFER_LEN 500 + + +#endif // CRMCOMMON_PRIVATE__H diff --git a/lib/common/digest.c b/lib/common/digest.c new file mode 100644 index 0000000..3bf04bf --- /dev/null +++ b/lib/common/digest.c @@ -0,0 +1,278 @@ +/* + * Copyright 2015-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include "crmcommon_private.h" + +#define BEST_EFFORT_STATUS 0 + +/*! + * \internal + * \brief Dump XML in a format used with v1 digests + * + * \param[in] xml Root of XML to dump + * + * \return Newly allocated buffer containing dumped XML + */ +static GString * +dump_xml_for_digest(xmlNodePtr xml) +{ + GString *buffer = g_string_sized_new(1024); + + /* for compatibility with the old result which is used for v1 digests */ + g_string_append_c(buffer, ' '); + pcmk__xml2text(xml, 0, buffer, 0); + g_string_append_c(buffer, '\n'); + + return buffer; +} + +/*! + * \brief Calculate and return v1 digest of XML tree + * + * \param[in] input Root of XML to digest + * \param[in] sort Whether to sort the XML before calculating digest + * \param[in] ignored Not used + * + * \return Newly allocated string containing digest + * \note Example return value: "c048eae664dba840e1d2060f00299e9d" + */ +static char * +calculate_xml_digest_v1(xmlNode *input, gboolean sort, gboolean ignored) +{ + char *digest = NULL; + GString *buffer = NULL; + xmlNode *copy = NULL; + + if (sort) { + crm_trace("Sorting xml..."); + copy = sorted_xml(input, NULL, TRUE); + crm_trace("Done"); + input = copy; + } + + buffer = dump_xml_for_digest(input); + CRM_CHECK(buffer->len > 0, free_xml(copy); + g_string_free(buffer, TRUE); + return NULL); + + digest = crm_md5sum((const char *) buffer->str); + crm_log_xml_trace(input, "digest:source"); + + g_string_free(buffer, TRUE); + free_xml(copy); + return digest; +} + +/*! + * \brief Calculate and return v2 digest of XML tree + * + * \param[in] source Root of XML to digest + * \param[in] do_filter Whether to filter certain XML attributes + * + * \return Newly allocated string containing digest + */ +static char * +calculate_xml_digest_v2(xmlNode *source, gboolean do_filter) +{ + char *digest = NULL; + GString *buffer = g_string_sized_new(1024); + + crm_trace("Begin digest %s", do_filter?"filtered":""); + pcmk__xml2text(source, (do_filter? pcmk__xml_fmt_filtered : 0), buffer, 0); + + CRM_ASSERT(buffer != NULL); + digest = crm_md5sum((const char *) buffer->str); + + pcmk__if_tracing( + { + char *trace_file = crm_strdup_printf("%s/digest-%s", + pcmk__get_tmpdir(), digest); + + crm_trace("Saving %s.%s.%s to %s", + crm_element_value(source, XML_ATTR_GENERATION_ADMIN), + crm_element_value(source, XML_ATTR_GENERATION), + crm_element_value(source, XML_ATTR_NUMUPDATES), + trace_file); + save_xml_to_file(source, "digest input", trace_file); + free(trace_file); + }, + {} + ); + g_string_free(buffer, TRUE); + crm_trace("End digest"); + return digest; +} + +/*! + * \brief Calculate and return digest of XML tree, suitable for storing on disk + * + * \param[in] input Root of XML to digest + * + * \return Newly allocated string containing digest + */ +char * +calculate_on_disk_digest(xmlNode *input) +{ + /* Always use the v1 format for on-disk digests + * a) it's a compatibility nightmare + * b) we only use this once at startup, all other + * invocations are in a separate child process + */ + return calculate_xml_digest_v1(input, FALSE, FALSE); +} + +/*! + * \brief Calculate and return digest of XML operation + * + * \param[in] input Root of XML to digest + * \param[in] version Unused + * + * \return Newly allocated string containing digest + */ +char * +calculate_operation_digest(xmlNode *input, const char *version) +{ + /* We still need the sorting for operation digests */ + return calculate_xml_digest_v1(input, TRUE, FALSE); +} + +/*! + * \brief Calculate and return digest of XML tree + * + * \param[in] input Root of XML to digest + * \param[in] sort Whether to sort XML before calculating digest + * \param[in] do_filter Whether to filter certain XML attributes + * \param[in] version CRM feature set version (used to select v1/v2 digest) + * + * \return Newly allocated string containing digest + */ +char * +calculate_xml_versioned_digest(xmlNode *input, gboolean sort, + gboolean do_filter, const char *version) +{ + /* + * @COMPAT digests (on-disk or in diffs/patchsets) created <1.1.4; + * removing this affects even full-restart upgrades from old versions + * + * The sorting associated with v1 digest creation accounted for 23% of + * the CIB manager's CPU usage on the server. v2 drops this. + * + * The filtering accounts for an additional 2.5% and we may want to + * remove it in future. + * + * v2 also uses the xmlBuffer contents directly to avoid additional copying + */ + if (version == NULL || compare_version("3.0.5", version) > 0) { + crm_trace("Using v1 digest algorithm for %s", + pcmk__s(version, "unknown feature set")); + return calculate_xml_digest_v1(input, sort, do_filter); + } + crm_trace("Using v2 digest algorithm for %s", + pcmk__s(version, "unknown feature set")); + return calculate_xml_digest_v2(input, do_filter); +} + +/*! + * \internal + * \brief Check whether calculated digest of given XML matches expected digest + * + * \param[in] input Root of XML tree to digest + * \param[in] expected Expected digest in on-disk format + * + * \return true if digests match, false on mismatch or error + */ +bool +pcmk__verify_digest(xmlNode *input, const char *expected) +{ + char *calculated = NULL; + bool passed; + + if (input != NULL) { + calculated = calculate_on_disk_digest(input); + if (calculated == NULL) { + crm_perror(LOG_ERR, "Could not calculate digest for comparison"); + return false; + } + } + passed = pcmk__str_eq(expected, calculated, pcmk__str_casei); + if (passed) { + crm_trace("Digest comparison passed: %s", calculated); + } else { + crm_err("Digest comparison failed: expected %s, calculated %s", + expected, calculated); + } + free(calculated); + return passed; +} + +/*! + * \internal + * \brief Check whether an XML attribute should be excluded from CIB digests + * + * \param[in] name XML attribute name + * + * \return true if XML attribute should be excluded from CIB digest calculation + */ +bool +pcmk__xa_filterable(const char *name) +{ + static const char *filter[] = { + XML_ATTR_ORIGIN, + XML_CIB_ATTR_WRITTEN, + XML_ATTR_UPDATE_ORIG, + XML_ATTR_UPDATE_CLIENT, + XML_ATTR_UPDATE_USER, + }; + + for (int i = 0; i < PCMK__NELEM(filter); i++) { + if (strcmp(name, filter[i]) == 0) { + return true; + } + } + return false; +} + +char * +crm_md5sum(const char *buffer) +{ + int lpc = 0, len = 0; + char *digest = NULL; + unsigned char raw_digest[MD5_DIGEST_SIZE]; + + if (buffer == NULL) { + buffer = ""; + } + len = strlen(buffer); + + crm_trace("Beginning digest of %d bytes", len); + digest = malloc(2 * MD5_DIGEST_SIZE + 1); + if (digest) { + md5_buffer(buffer, len, raw_digest); + for (lpc = 0; lpc < MD5_DIGEST_SIZE; lpc++) { + sprintf(digest + (2 * lpc), "%02x", raw_digest[lpc]); + } + digest[(2 * MD5_DIGEST_SIZE)] = 0; + crm_trace("Digest %s.", digest); + + } else { + crm_err("Could not create digest"); + } + return digest; +} diff --git a/lib/common/health.c b/lib/common/health.c new file mode 100644 index 0000000..ee412be --- /dev/null +++ b/lib/common/health.c @@ -0,0 +1,70 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +/*! + * \internal + * \brief Ensure a health strategy value is allowed + * + * \param[in] value Configured health strategy + * + * \return true if \p value is an allowed health strategy value, otherwise false + */ +bool +pcmk__validate_health_strategy(const char *value) +{ + return pcmk__strcase_any_of(value, + PCMK__VALUE_NONE, + PCMK__VALUE_CUSTOM, + PCMK__VALUE_ONLY_GREEN, + PCMK__VALUE_PROGRESSIVE, + PCMK__VALUE_MIGRATE_ON_RED, + NULL); +} + +/*! + * \internal + * \brief Parse node health strategy from a user-provided string + * + * \param[in] value User-provided configuration value for node-health-strategy + * + * \return Node health strategy corresponding to \p value + */ +enum pcmk__health_strategy +pcmk__parse_health_strategy(const char *value) +{ + if (pcmk__str_eq(value, PCMK__VALUE_NONE, + pcmk__str_null_matches|pcmk__str_casei)) { + return pcmk__health_strategy_none; + + } else if (pcmk__str_eq(value, PCMK__VALUE_MIGRATE_ON_RED, + pcmk__str_casei)) { + return pcmk__health_strategy_no_red; + + } else if (pcmk__str_eq(value, PCMK__VALUE_ONLY_GREEN, + pcmk__str_casei)) { + return pcmk__health_strategy_only_green; + + } else if (pcmk__str_eq(value, PCMK__VALUE_PROGRESSIVE, + pcmk__str_casei)) { + return pcmk__health_strategy_progressive; + + } else if (pcmk__str_eq(value, PCMK__VALUE_CUSTOM, + pcmk__str_casei)) { + return pcmk__health_strategy_custom; + + } else { + pcmk__config_err("Using default of \"" PCMK__VALUE_NONE "\" for " + PCMK__OPT_NODE_HEALTH_STRATEGY + " because '%s' is not a valid value", + value); + return pcmk__health_strategy_none; + } +} diff --git a/lib/common/io.c b/lib/common/io.c new file mode 100644 index 0000000..2264e16 --- /dev/null +++ b/lib/common/io.c @@ -0,0 +1,663 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +/*! + * \internal + * \brief Create a directory, including any parent directories needed + * + * \param[in] path_c Pathname of the directory to create + * \param[in] mode Permissions to be used (with current umask) when creating + * + * \return Standard Pacemaker return code + */ +int +pcmk__build_path(const char *path_c, mode_t mode) +{ + int offset = 1, len = 0; + int rc = pcmk_rc_ok; + char *path = strdup(path_c); + + // cppcheck seems not to understand the abort logic in CRM_CHECK + // cppcheck-suppress memleak + CRM_CHECK(path != NULL, return -ENOMEM); + for (len = strlen(path); offset < len; offset++) { + if (path[offset] == '/') { + path[offset] = 0; + if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { + rc = errno; + goto done; + } + path[offset] = '/'; + } + } + if ((mkdir(path, mode) < 0) && (errno != EEXIST)) { + rc = errno; + } +done: + free(path); + return rc; +} + +/*! + * \internal + * \brief Return canonicalized form of a path name + * + * \param[in] path Pathname to canonicalize + * \param[out] resolved_path Where to store canonicalized pathname + * + * \return Standard Pacemaker return code + * \note The caller is responsible for freeing \p resolved_path on success. + * \note This function exists because not all C library versions of + * realpath(path, resolved_path) support a NULL resolved_path. + */ +int +pcmk__real_path(const char *path, char **resolved_path) +{ + CRM_CHECK((path != NULL) && (resolved_path != NULL), return EINVAL); + +#if _POSIX_VERSION >= 200809L + /* Recent C libraries can dynamically allocate memory as needed */ + *resolved_path = realpath(path, NULL); + return (*resolved_path == NULL)? errno : pcmk_rc_ok; + +#elif defined(PATH_MAX) + /* Older implementations require pre-allocated memory */ + /* (this is less desirable because PATH_MAX may be huge or not defined) */ + *resolved_path = malloc(PATH_MAX); + if ((*resolved_path == NULL) || (realpath(path, *resolved_path) == NULL)) { + return errno; + } + return pcmk_rc_ok; +#else + *resolved_path = NULL; + return ENOTSUP; +#endif +} + +/*! + * \internal + * \brief Create a file name using a sequence number + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[in] sequence Sequence number + * \param[in] bzip Whether to use ".bz2" instead of ".raw" as extension + * + * \return Newly allocated file path (asserts on error, so always non-NULL) + * \note The caller is responsible for freeing the return value. + */ +char * +pcmk__series_filename(const char *directory, const char *series, int sequence, + bool bzip) +{ + CRM_ASSERT((directory != NULL) && (series != NULL)); + return crm_strdup_printf("%s/%s-%d.%s", directory, series, sequence, + (bzip? "bz2" : "raw")); +} + +/*! + * \internal + * \brief Read sequence number stored in a file series' .last file + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[out] seq Where to store the sequence number + * + * \return Standard Pacemaker return code + */ +int +pcmk__read_series_sequence(const char *directory, const char *series, + unsigned int *seq) +{ + int rc; + FILE *fp = NULL; + char *series_file = NULL; + + if ((directory == NULL) || (series == NULL) || (seq == NULL)) { + return EINVAL; + } + + series_file = crm_strdup_printf("%s/%s.last", directory, series); + fp = fopen(series_file, "r"); + if (fp == NULL) { + rc = errno; + crm_debug("Could not open series file %s: %s", + series_file, strerror(rc)); + free(series_file); + return rc; + } + errno = 0; + if (fscanf(fp, "%u", seq) != 1) { + rc = (errno == 0)? ENODATA : errno; + crm_debug("Could not read sequence number from series file %s: %s", + series_file, pcmk_rc_str(rc)); + fclose(fp); + return rc; + } + fclose(fp); + crm_trace("Found last sequence number %u in series file %s", + *seq, series_file); + free(series_file); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Write sequence number to a file series' .last file + * + * \param[in] directory Directory that contains the file series + * \param[in] series Start of file name + * \param[in] sequence Sequence number to write + * \param[in] max Maximum sequence value, after which it is reset to 0 + * + * \note This function logs some errors but does not return any to the caller + */ +void +pcmk__write_series_sequence(const char *directory, const char *series, + unsigned int sequence, int max) +{ + int rc = 0; + FILE *file_strm = NULL; + char *series_file = NULL; + + CRM_CHECK(directory != NULL, return); + CRM_CHECK(series != NULL, return); + + if (max == 0) { + return; + } + if (max > 0 && sequence >= max) { + sequence = 0; + } + + series_file = crm_strdup_printf("%s/%s.last", directory, series); + file_strm = fopen(series_file, "w"); + if (file_strm != NULL) { + rc = fprintf(file_strm, "%u", sequence); + if (rc < 0) { + crm_perror(LOG_ERR, "Cannot write to series file %s", series_file); + } + + } else { + crm_err("Cannot open series file %s for writing", series_file); + } + + if (file_strm != NULL) { + fflush(file_strm); + fclose(file_strm); + } + + crm_trace("Wrote %d to %s", sequence, series_file); + free(series_file); +} + +/*! + * \internal + * \brief Change the owner and group of a file series' .last file + * + * \param[in] directory Directory that contains series + * \param[in] series Series to change + * \param[in] uid User ID of desired file owner + * \param[in] gid Group ID of desired file group + * + * \return Standard Pacemaker return code + * \note The caller must have the appropriate privileges. + */ +int +pcmk__chown_series_sequence(const char *directory, const char *series, + uid_t uid, gid_t gid) +{ + char *series_file = NULL; + int rc = pcmk_rc_ok; + + if ((directory == NULL) || (series == NULL)) { + return EINVAL; + } + series_file = crm_strdup_printf("%s/%s.last", directory, series); + if (chown(series_file, uid, gid) < 0) { + rc = errno; + } + free(series_file); + return rc; +} + +static bool +pcmk__daemon_user_can_write(const char *target_name, struct stat *target_stat) +{ + struct passwd *sys_user = NULL; + + errno = 0; + sys_user = getpwnam(CRM_DAEMON_USER); + if (sys_user == NULL) { + crm_notice("Could not find user %s: %s", + CRM_DAEMON_USER, pcmk_rc_str(errno)); + return FALSE; + } + if (target_stat->st_uid != sys_user->pw_uid) { + crm_notice("%s is not owned by user %s " CRM_XS " uid %d != %d", + target_name, CRM_DAEMON_USER, sys_user->pw_uid, + target_stat->st_uid); + return FALSE; + } + if ((target_stat->st_mode & (S_IRUSR | S_IWUSR)) == 0) { + crm_notice("%s is not readable and writable by user %s " + CRM_XS " st_mode=0%lo", + target_name, CRM_DAEMON_USER, + (unsigned long) target_stat->st_mode); + return FALSE; + } + return TRUE; +} + +static bool +pcmk__daemon_group_can_write(const char *target_name, struct stat *target_stat) +{ + struct group *sys_grp = NULL; + + errno = 0; + sys_grp = getgrnam(CRM_DAEMON_GROUP); + if (sys_grp == NULL) { + crm_notice("Could not find group %s: %s", + CRM_DAEMON_GROUP, pcmk_rc_str(errno)); + return FALSE; + } + + if (target_stat->st_gid != sys_grp->gr_gid) { + crm_notice("%s is not owned by group %s " CRM_XS " uid %d != %d", + target_name, CRM_DAEMON_GROUP, + sys_grp->gr_gid, target_stat->st_gid); + return FALSE; + } + + if ((target_stat->st_mode & (S_IRGRP | S_IWGRP)) == 0) { + crm_notice("%s is not readable and writable by group %s " + CRM_XS " st_mode=0%lo", + target_name, CRM_DAEMON_GROUP, + (unsigned long) target_stat->st_mode); + return FALSE; + } + return TRUE; +} + +/*! + * \internal + * \brief Check whether a directory or file is writable by the cluster daemon + * + * Return true if either the cluster daemon user or cluster daemon group has + * write permission on a specified file or directory. + * + * \param[in] dir Directory to check (this argument must be specified, and + * the directory must exist) + * \param[in] file File to check (only the directory will be checked if this + * argument is not specified or the file does not exist) + * + * \return true if target is writable by cluster daemon, false otherwise + */ +bool +pcmk__daemon_can_write(const char *dir, const char *file) +{ + int s_res = 0; + struct stat buf; + char *full_file = NULL; + const char *target = NULL; + + // Caller must supply directory + CRM_ASSERT(dir != NULL); + + // If file is given, check whether it exists as a regular file + if (file != NULL) { + full_file = crm_strdup_printf("%s/%s", dir, file); + target = full_file; + + s_res = stat(full_file, &buf); + if (s_res < 0) { + crm_notice("%s not found: %s", target, pcmk_rc_str(errno)); + free(full_file); + full_file = NULL; + target = NULL; + + } else if (S_ISREG(buf.st_mode) == FALSE) { + crm_err("%s must be a regular file " CRM_XS " st_mode=0%lo", + target, (unsigned long) buf.st_mode); + free(full_file); + return false; + } + } + + // If file is not given, ensure dir exists as directory + if (target == NULL) { + target = dir; + s_res = stat(dir, &buf); + if (s_res < 0) { + crm_err("%s not found: %s", dir, pcmk_rc_str(errno)); + return false; + + } else if (S_ISDIR(buf.st_mode) == FALSE) { + crm_err("%s must be a directory " CRM_XS " st_mode=0%lo", + dir, (unsigned long) buf.st_mode); + return false; + } + } + + if (!pcmk__daemon_user_can_write(target, &buf) + && !pcmk__daemon_group_can_write(target, &buf)) { + + crm_err("%s must be owned and writable by either user %s or group %s " + CRM_XS " st_mode=0%lo", + target, CRM_DAEMON_USER, CRM_DAEMON_GROUP, + (unsigned long) buf.st_mode); + free(full_file); + return false; + } + + free(full_file); + return true; +} + +/*! + * \internal + * \brief Flush and sync a directory to disk + * + * \param[in] name Directory to flush and sync + * \note This function logs errors but does not return them to the caller + */ +void +pcmk__sync_directory(const char *name) +{ + int fd; + DIR *directory; + + directory = opendir(name); + if (directory == NULL) { + crm_perror(LOG_ERR, "Could not open %s for syncing", name); + return; + } + + fd = dirfd(directory); + if (fd < 0) { + crm_perror(LOG_ERR, "Could not obtain file descriptor for %s", name); + return; + } + + if (fsync(fd) < 0) { + crm_perror(LOG_ERR, "Could not sync %s", name); + } + if (closedir(directory) < 0) { + crm_perror(LOG_ERR, "Could not close %s after fsync", name); + } +} + +/*! + * \internal + * \brief Read the contents of a file + * + * \param[in] filename Name of file to read + * \param[out] contents Where to store file contents + * + * \return Standard Pacemaker return code + * \note On success, the caller is responsible for freeing contents. + */ +int +pcmk__file_contents(const char *filename, char **contents) +{ + FILE *fp; + int length, read_len; + int rc = pcmk_rc_ok; + + if ((filename == NULL) || (contents == NULL)) { + return EINVAL; + } + + fp = fopen(filename, "r"); + if ((fp == NULL) || (fseek(fp, 0L, SEEK_END) < 0)) { + rc = errno; + goto bail; + } + + length = ftell(fp); + if (length < 0) { + rc = errno; + goto bail; + } + + if (length == 0) { + *contents = NULL; + } else { + *contents = calloc(length + 1, sizeof(char)); + if (*contents == NULL) { + rc = errno; + goto bail; + } + rewind(fp); + read_len = fread(*contents, 1, length, fp); /* Coverity: False positive */ + if (read_len != length) { + free(*contents); + *contents = NULL; + rc = EIO; + } + } + +bail: + if (fp != NULL) { + fclose(fp); + } + return rc; +} + +/*! + * \internal + * \brief Write text to a file, flush and sync it to disk, then close the file + * + * \param[in] fd File descriptor opened for writing + * \param[in] contents String to write to file + * + * \return Standard Pacemaker return code + */ +int +pcmk__write_sync(int fd, const char *contents) +{ + int rc = 0; + FILE *fp = fdopen(fd, "w"); + + if (fp == NULL) { + return errno; + } + if ((contents != NULL) && (fprintf(fp, "%s", contents) < 0)) { + rc = EIO; + } + if (fflush(fp) != 0) { + rc = errno; + } + if (fsync(fileno(fp)) < 0) { + rc = errno; + } + fclose(fp); + return rc; +} + +/*! + * \internal + * \brief Set a file descriptor to non-blocking + * + * \param[in] fd File descriptor to use + * + * \return Standard Pacemaker return code + */ +int +pcmk__set_nonblocking(int fd) +{ + int flag = fcntl(fd, F_GETFL); + + if (flag < 0) { + return errno; + } + if (fcntl(fd, F_SETFL, flag | O_NONBLOCK) < 0) { + return errno; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Get directory name for temporary files + * + * Return the value of the TMPDIR environment variable if it is set to a + * full path, otherwise return "/tmp". + * + * \return Name of directory to be used for temporary files + */ +const char * +pcmk__get_tmpdir(void) +{ + const char *dir = getenv("TMPDIR"); + + return (dir && (*dir == '/'))? dir : "/tmp"; +} + +/*! + * \internal + * \brief Close open file descriptors + * + * Close all file descriptors (except optionally stdin, stdout, and stderr), + * which is a best practice for a new child process forked for the purpose of + * executing an external program. + * + * \param[in] bool If true, close stdin, stdout, and stderr as well + */ +void +pcmk__close_fds_in_child(bool all) +{ + DIR *dir; + struct rlimit rlim; + rlim_t max_fd; + int min_fd = (all? 0 : (STDERR_FILENO + 1)); + + /* Find the current process's (soft) limit for open files. getrlimit() + * should always work, but have a fallback just in case. + */ + if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) { + max_fd = rlim.rlim_cur - 1; + } else { + long conf_max = sysconf(_SC_OPEN_MAX); + + max_fd = (conf_max > 0)? conf_max : 1024; + } + + /* /proc/self/fd (on Linux) or /dev/fd (on most OSes) contains symlinks to + * all open files for the current process, named as the file descriptor. + * Use this if available, because it's more efficient than a shotgun + * approach to closing descriptors. + */ +#if HAVE_LINUX_PROCFS + dir = opendir("/proc/self/fd"); + if (dir == NULL) { + dir = opendir("/dev/fd"); + } +#else + dir = opendir("/dev/fd"); +#endif // HAVE_LINUX_PROCFS + if (dir != NULL) { + struct dirent *entry; + int dir_fd = dirfd(dir); + + while ((entry = readdir(dir)) != NULL) { + int lpc = atoi(entry->d_name); + + /* How could one of these entries be higher than max_fd, you ask? + * It isn't possible in normal operation, but when run under + * valgrind, valgrind can open high-numbered file descriptors for + * its own use that are higher than the process's soft limit. + * These will show up in the fd directory but aren't closable. + */ + if ((lpc >= min_fd) && (lpc <= max_fd) && (lpc != dir_fd)) { + close(lpc); + } + } + closedir(dir); + return; + } + + /* If no fd directory is available, iterate over all possible descriptors. + * This is less efficient due to the overhead of many system calls. + */ + for (int lpc = max_fd; lpc >= min_fd; lpc--) { + close(lpc); + } +} + +/*! + * \brief Duplicate a file path, inserting a prefix if not absolute + * + * \param[in] filename File path to duplicate + * \param[in] dirname If filename is not absolute, prefix to add + * + * \return Newly allocated memory with full path (guaranteed non-NULL) + */ +char * +pcmk__full_path(const char *filename, const char *dirname) +{ + char *path = NULL; + + CRM_ASSERT(filename != NULL); + + if (filename[0] == '/') { + path = strdup(filename); + CRM_ASSERT(path != NULL); + + } else { + CRM_ASSERT(dirname != NULL); + path = crm_strdup_printf("%s/%s", dirname, filename); + } + + return path; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +void +crm_build_path(const char *path_c, mode_t mode) +{ + int rc = pcmk__build_path(path_c, mode); + + if (rc != pcmk_rc_ok) { + crm_err("Could not create directory '%s': %s", + path_c, pcmk_rc_str(rc)); + } +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/ipc_attrd.c b/lib/common/ipc_attrd.c new file mode 100644 index 0000000..7c40aa7 --- /dev/null +++ b/lib/common/ipc_attrd.c @@ -0,0 +1,590 @@ +/* + * Copyright 2011-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include + +#include + +#include +#include +#include +#include +#include +#include "crmcommon_private.h" + +static void +set_pairs_data(pcmk__attrd_api_reply_t *data, xmlNode *msg_data) +{ + const char *name = NULL; + pcmk__attrd_query_pair_t *pair; + + name = crm_element_value(msg_data, PCMK__XA_ATTR_NAME); + + for (xmlNode *node = first_named_child(msg_data, XML_CIB_TAG_NODE); + node != NULL; node = crm_next_same_xml(node)) { + pair = calloc(1, sizeof(pcmk__attrd_query_pair_t)); + + CRM_ASSERT(pair != NULL); + + pair->node = crm_element_value(node, PCMK__XA_ATTR_NODE_NAME); + pair->name = name; + pair->value = crm_element_value(node, PCMK__XA_ATTR_VALUE); + data->data.pairs = g_list_prepend(data->data.pairs, pair); + } +} + +static bool +reply_expected(pcmk_ipc_api_t *api, xmlNode *request) +{ + const char *command = crm_element_value(request, PCMK__XA_TASK); + + return pcmk__str_any_of(command, + PCMK__ATTRD_CMD_CLEAR_FAILURE, + PCMK__ATTRD_CMD_QUERY, + PCMK__ATTRD_CMD_REFRESH, + PCMK__ATTRD_CMD_UPDATE, + PCMK__ATTRD_CMD_UPDATE_BOTH, + PCMK__ATTRD_CMD_UPDATE_DELAY, + NULL); +} + +static bool +dispatch(pcmk_ipc_api_t *api, xmlNode *reply) +{ + const char *value = NULL; + crm_exit_t status = CRM_EX_OK; + + pcmk__attrd_api_reply_t reply_data = { + pcmk__attrd_reply_unknown + }; + + if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_none)) { + return false; + } + + /* Do some basic validation of the reply */ + value = crm_element_value(reply, F_TYPE); + if (pcmk__str_empty(value) + || !pcmk__str_eq(value, T_ATTRD, pcmk__str_none)) { + crm_info("Unrecognizable message from attribute manager: " + "message type '%s' not '" T_ATTRD "'", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + + value = crm_element_value(reply, F_SUBTYPE); + + /* Only the query command gets a reply for now. NULL counts as query for + * backward compatibility with attribute managers <2.1.3 that didn't set it. + */ + if (pcmk__str_eq(value, PCMK__ATTRD_CMD_QUERY, pcmk__str_null_matches)) { + if (!xmlHasProp(reply, (pcmkXmlStr) PCMK__XA_ATTR_NAME)) { + status = ENXIO; // Most likely, the attribute doesn't exist + goto done; + } + reply_data.reply_type = pcmk__attrd_reply_query; + set_pairs_data(&reply_data, reply); + + } else { + crm_info("Unrecognizable message from attribute manager: " + "message subtype '%s' unknown", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + +done: + pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + + /* Free any reply data that was allocated */ + if (reply_data.data.pairs) { + g_list_free_full(reply_data.data.pairs, free); + } + + return false; +} + +pcmk__ipc_methods_t * +pcmk__attrd_api_methods(void) +{ + pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); + + if (cmds != NULL) { + cmds->new_data = NULL; + cmds->free_data = NULL; + cmds->post_connect = NULL; + cmds->reply_expected = reply_expected; + cmds->dispatch = dispatch; + } + return cmds; +} + +/*! + * \internal + * \brief Create a generic pacemaker-attrd operation + * + * \param[in] user_name If not NULL, ACL user to set for operation + * + * \return XML of pacemaker-attrd operation + */ +static xmlNode * +create_attrd_op(const char *user_name) +{ + xmlNode *attrd_op = create_xml_node(NULL, __func__); + + crm_xml_add(attrd_op, F_TYPE, T_ATTRD); + crm_xml_add(attrd_op, F_ORIG, (crm_system_name? crm_system_name: "unknown")); + crm_xml_add(attrd_op, PCMK__XA_ATTR_USER, user_name); + + return attrd_op; +} + +static int +create_api(pcmk_ipc_api_t **api) +{ + int rc = pcmk_new_ipc_api(api, pcmk_ipc_attrd); + + if (rc != pcmk_rc_ok) { + crm_err("Could not connect to attrd: %s", pcmk_rc_str(rc)); + } + + return rc; +} + +static void +destroy_api(pcmk_ipc_api_t *api) +{ + pcmk_disconnect_ipc(api); + pcmk_free_ipc_api(api); + api = NULL; +} + +static int +connect_and_send_attrd_request(pcmk_ipc_api_t *api, xmlNode *request) +{ + int rc = pcmk_rc_ok; + int max = 5; + + while (max > 0) { + crm_info("Connecting to cluster... %d retries remaining", max); + rc = pcmk_connect_ipc(api, pcmk_ipc_dispatch_sync); + + if (rc == pcmk_rc_ok) { + rc = pcmk__send_ipc_request(api, request); + break; + } else if (rc == EAGAIN || rc == EALREADY) { + sleep(5 - max); + max--; + } else { + crm_err("Could not connect to attrd: %s", pcmk_rc_str(rc)); + break; + } + } + + return rc; +} + +static int +send_attrd_request(pcmk_ipc_api_t *api, xmlNode *request) +{ + return pcmk__send_ipc_request(api, request); +} + +int +pcmk__attrd_api_clear_failures(pcmk_ipc_api_t *api, const char *node, + const char *resource, const char *operation, + const char *interval_spec, const char *user_name, + uint32_t options) +{ + int rc = pcmk_rc_ok; + xmlNode *request = create_attrd_op(user_name); + const char *interval_desc = NULL; + const char *op_desc = NULL; + const char *target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + + crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_CLEAR_FAILURE); + pcmk__xe_add_node(request, node, 0); + crm_xml_add(request, PCMK__XA_ATTR_RESOURCE, resource); + crm_xml_add(request, PCMK__XA_ATTR_OPERATION, operation); + crm_xml_add(request, PCMK__XA_ATTR_INTERVAL, interval_spec); + crm_xml_add_int(request, PCMK__XA_ATTR_IS_REMOTE, + pcmk_is_set(options, pcmk__node_attr_remote)); + + if (api == NULL) { + rc = create_api(&api); + if (rc != pcmk_rc_ok) { + return rc; + } + + rc = connect_and_send_attrd_request(api, request); + destroy_api(api); + + } else if (!pcmk_ipc_is_connected(api)) { + rc = connect_and_send_attrd_request(api, request); + + } else { + rc = send_attrd_request(api, request); + } + + free_xml(request); + + if (operation) { + interval_desc = interval_spec? interval_spec : "nonrecurring"; + op_desc = operation; + } else { + interval_desc = "all"; + op_desc = "operations"; + } + + crm_debug("Asked pacemaker-attrd to clear failure of %s %s for %s on %s: %s (%d)", + interval_desc, op_desc, (resource? resource : "all resources"), + (node? node : "all nodes"), pcmk_rc_str(rc), rc); + + return rc; +} + +int +pcmk__attrd_api_delete(pcmk_ipc_api_t *api, const char *node, const char *name, + uint32_t options) +{ + const char *target = NULL; + + if (name == NULL) { + return EINVAL; + } + + target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + + /* Make sure the right update option is set. */ + options &= ~pcmk__node_attr_delay; + options |= pcmk__node_attr_value; + + return pcmk__attrd_api_update(api, node, name, NULL, NULL, NULL, NULL, options); +} + +int +pcmk__attrd_api_purge(pcmk_ipc_api_t *api, const char *node) +{ + int rc = pcmk_rc_ok; + xmlNode *request = NULL; + const char *display_host = (node ? node : "localhost"); + const char *target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + + request = create_attrd_op(NULL); + + crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE); + pcmk__xe_add_node(request, node, 0); + + if (api == NULL) { + rc = create_api(&api); + if (rc != pcmk_rc_ok) { + return rc; + } + + rc = connect_and_send_attrd_request(api, request); + destroy_api(api); + + } else if (!pcmk_ipc_is_connected(api)) { + rc = connect_and_send_attrd_request(api, request); + + } else { + rc = send_attrd_request(api, request); + } + + free_xml(request); + + crm_debug("Asked pacemaker-attrd to purge %s: %s (%d)", + display_host, pcmk_rc_str(rc), rc); + + return rc; +} + +int +pcmk__attrd_api_query(pcmk_ipc_api_t *api, const char *node, const char *name, + uint32_t options) +{ + int rc = pcmk_rc_ok; + xmlNode *request = NULL; + const char *target = NULL; + + if (name == NULL) { + return EINVAL; + } + + if (pcmk_is_set(options, pcmk__node_attr_query_all)) { + node = NULL; + } else { + target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + } + + request = create_attrd_op(NULL); + + crm_xml_add(request, PCMK__XA_ATTR_NAME, name); + crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_QUERY); + pcmk__xe_add_node(request, node, 0); + + rc = send_attrd_request(api, request); + free_xml(request); + + if (node) { + crm_debug("Queried pacemaker-attrd for %s on %s: %s (%d)", + name, node, pcmk_rc_str(rc), rc); + } else { + crm_debug("Queried pacemaker-attrd for %s: %s (%d)", + name, pcmk_rc_str(rc), rc); + } + + return rc; +} + +int +pcmk__attrd_api_refresh(pcmk_ipc_api_t *api, const char *node) +{ + int rc = pcmk_rc_ok; + xmlNode *request = NULL; + const char *display_host = (node ? node : "localhost"); + const char *target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + + request = create_attrd_op(NULL); + + crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_REFRESH); + pcmk__xe_add_node(request, node, 0); + + if (api == NULL) { + rc = create_api(&api); + if (rc != pcmk_rc_ok) { + return rc; + } + + rc = connect_and_send_attrd_request(api, request); + destroy_api(api); + + } else if (!pcmk_ipc_is_connected(api)) { + rc = connect_and_send_attrd_request(api, request); + + } else { + rc = send_attrd_request(api, request); + } + + free_xml(request); + + crm_debug("Asked pacemaker-attrd to refresh %s: %s (%d)", + display_host, pcmk_rc_str(rc), rc); + + return rc; +} + +static void +add_op_attr(xmlNode *op, uint32_t options) +{ + if (pcmk_all_flags_set(options, pcmk__node_attr_value | pcmk__node_attr_delay)) { + crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE_BOTH); + } else if (pcmk_is_set(options, pcmk__node_attr_value)) { + crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE); + } else if (pcmk_is_set(options, pcmk__node_attr_delay)) { + crm_xml_add(op, PCMK__XA_TASK, PCMK__ATTRD_CMD_UPDATE_DELAY); + } +} + +static void +populate_update_op(xmlNode *op, const char *node, const char *name, const char *value, + const char *dampen, const char *set, uint32_t options) +{ + if (pcmk_is_set(options, pcmk__node_attr_pattern)) { + crm_xml_add(op, PCMK__XA_ATTR_PATTERN, name); + } else { + crm_xml_add(op, PCMK__XA_ATTR_NAME, name); + } + + if (pcmk_is_set(options, pcmk__node_attr_utilization)) { + crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, XML_TAG_UTILIZATION); + } else { + crm_xml_add(op, PCMK__XA_ATTR_SET_TYPE, XML_TAG_ATTR_SETS); + } + + add_op_attr(op, options); + + crm_xml_add(op, PCMK__XA_ATTR_VALUE, value); + crm_xml_add(op, PCMK__XA_ATTR_DAMPENING, dampen); + pcmk__xe_add_node(op, node, 0); + crm_xml_add(op, PCMK__XA_ATTR_SET, set); + crm_xml_add_int(op, PCMK__XA_ATTR_IS_REMOTE, + pcmk_is_set(options, pcmk__node_attr_remote)); + crm_xml_add_int(op, PCMK__XA_ATTR_IS_PRIVATE, + pcmk_is_set(options, pcmk__node_attr_private)); + + if (pcmk_is_set(options, pcmk__node_attr_sync_local)) { + crm_xml_add(op, PCMK__XA_ATTR_SYNC_POINT, PCMK__VALUE_LOCAL); + } else if (pcmk_is_set(options, pcmk__node_attr_sync_cluster)) { + crm_xml_add(op, PCMK__XA_ATTR_SYNC_POINT, PCMK__VALUE_CLUSTER); + } +} + +int +pcmk__attrd_api_update(pcmk_ipc_api_t *api, const char *node, const char *name, + const char *value, const char *dampen, const char *set, + const char *user_name, uint32_t options) +{ + int rc = pcmk_rc_ok; + xmlNode *request = NULL; + const char *display_host = (node ? node : "localhost"); + const char *target = NULL; + + if (name == NULL) { + return EINVAL; + } + + target = pcmk__node_attr_target(node); + + if (target != NULL) { + node = target; + } + + request = create_attrd_op(user_name); + populate_update_op(request, node, name, value, dampen, set, options); + + if (api == NULL) { + rc = create_api(&api); + if (rc != pcmk_rc_ok) { + return rc; + } + + rc = connect_and_send_attrd_request(api, request); + destroy_api(api); + + } else if (!pcmk_ipc_is_connected(api)) { + rc = connect_and_send_attrd_request(api, request); + + } else { + rc = send_attrd_request(api, request); + } + + free_xml(request); + + crm_debug("Asked pacemaker-attrd to update %s on %s: %s (%d)", + name, display_host, pcmk_rc_str(rc), rc); + + return rc; +} + +int +pcmk__attrd_api_update_list(pcmk_ipc_api_t *api, GList *attrs, const char *dampen, + const char *set, const char *user_name, + uint32_t options) +{ + int rc = pcmk_rc_ok; + xmlNode *request = NULL; + + if (attrs == NULL) { + return EINVAL; + } + + /* There are two different ways of handling a list of attributes: + * + * (1) For messages originating from some command line tool, we have to send + * them one at a time. In this loop, we just call pcmk__attrd_api_update + * for each, letting it deal with creating the API object if it doesn't + * already exist. + * + * The reason we can't use a single message in this case is that we can't + * trust that the server supports it. Remote nodes could be involved + * here, and there's no guarantee that a newer client running on a remote + * node is talking to (or proxied through) a cluster node with a newer + * attrd. We also can't just try sending a single message and then falling + * back on multiple. There's no handshake with the attrd server to + * determine its version. And then we would need to do that fallback in the + * dispatch function for this to work for all connection types (mainloop in + * particular), and at that point we won't know what the original message + * was in order to break it apart and resend as individual messages. + * + * (2) For messages between daemons, we can be assured that the local attrd + * will support the new message and that it can send to the other attrds + * as one request or split up according to the minimum supported version. + */ + for (GList *iter = attrs; iter != NULL; iter = iter->next) { + pcmk__attrd_query_pair_t *pair = (pcmk__attrd_query_pair_t *) iter->data; + + if (pcmk__is_daemon) { + const char *target = NULL; + xmlNode *child = NULL; + + /* First time through this loop - create the basic request. */ + if (request == NULL) { + request = create_attrd_op(user_name); + add_op_attr(request, options); + } + + /* Add a child node for this operation. We add the task to the top + * level XML node so attrd_ipc_dispatch doesn't need changes. And + * then we also add the task to each child node in populate_update_op + * so attrd_client_update knows what form of update is taking place. + */ + child = create_xml_node(request, XML_ATTR_OP); + target = pcmk__node_attr_target(pair->node); + + if (target != NULL) { + pair->node = target; + } + + populate_update_op(child, pair->node, pair->name, pair->value, dampen, + set, options); + } else { + rc = pcmk__attrd_api_update(api, pair->node, pair->name, pair->value, + dampen, set, user_name, options); + } + } + + /* If we were doing multiple attributes at once, we still need to send the + * request. Do that now, creating and destroying the API object if needed. + */ + if (pcmk__is_daemon) { + bool created_api = false; + + if (api == NULL) { + rc = create_api(&api); + if (rc != pcmk_rc_ok) { + return rc; + } + + created_api = true; + } + + rc = connect_and_send_attrd_request(api, request); + free_xml(request); + + if (created_api) { + destroy_api(api); + } + } + + return rc; +} diff --git a/lib/common/ipc_client.c b/lib/common/ipc_client.c new file mode 100644 index 0000000..c6d1645 --- /dev/null +++ b/lib/common/ipc_client.c @@ -0,0 +1,1576 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#if defined(HAVE_UCRED) || defined(HAVE_SOCKPEERCRED) +# ifdef HAVE_UCRED +# ifndef _GNU_SOURCE +# define _GNU_SOURCE +# endif +# endif +# include +#elif defined(HAVE_GETPEERUCRED) +# include +#endif + +#include +#include +#include +#include + +#include /* indirectly: pcmk_err_generic */ +#include +#include +#include +#include "crmcommon_private.h" + +/*! + * \brief Create a new object for using Pacemaker daemon IPC + * + * \param[out] api Where to store new IPC object + * \param[in] server Which Pacemaker daemon the object is for + * + * \return Standard Pacemaker result code + * + * \note The caller is responsible for freeing *api using pcmk_free_ipc_api(). + * \note This is intended to supersede crm_ipc_new() but currently only + * supports the controller, pacemakerd, and schedulerd IPC API. + */ +int +pcmk_new_ipc_api(pcmk_ipc_api_t **api, enum pcmk_ipc_server server) +{ + if (api == NULL) { + return EINVAL; + } + + *api = calloc(1, sizeof(pcmk_ipc_api_t)); + if (*api == NULL) { + return errno; + } + + (*api)->server = server; + if (pcmk_ipc_name(*api, false) == NULL) { + pcmk_free_ipc_api(*api); + *api = NULL; + return EOPNOTSUPP; + } + + (*api)->ipc_size_max = 0; + + // Set server methods and max_size (if not default) + switch (server) { + case pcmk_ipc_attrd: + (*api)->cmds = pcmk__attrd_api_methods(); + break; + + case pcmk_ipc_based: + (*api)->ipc_size_max = 512 * 1024; // 512KB + break; + + case pcmk_ipc_controld: + (*api)->cmds = pcmk__controld_api_methods(); + break; + + case pcmk_ipc_execd: + break; + + case pcmk_ipc_fenced: + break; + + case pcmk_ipc_pacemakerd: + (*api)->cmds = pcmk__pacemakerd_api_methods(); + break; + + case pcmk_ipc_schedulerd: + (*api)->cmds = pcmk__schedulerd_api_methods(); + // @TODO max_size could vary by client, maybe take as argument? + (*api)->ipc_size_max = 5 * 1024 * 1024; // 5MB + break; + } + if ((*api)->cmds == NULL) { + pcmk_free_ipc_api(*api); + *api = NULL; + return ENOMEM; + } + + (*api)->ipc = crm_ipc_new(pcmk_ipc_name(*api, false), + (*api)->ipc_size_max); + if ((*api)->ipc == NULL) { + pcmk_free_ipc_api(*api); + *api = NULL; + return ENOMEM; + } + + // If daemon API has its own data to track, allocate it + if ((*api)->cmds->new_data != NULL) { + if ((*api)->cmds->new_data(*api) != pcmk_rc_ok) { + pcmk_free_ipc_api(*api); + *api = NULL; + return ENOMEM; + } + } + crm_trace("Created %s API IPC object", pcmk_ipc_name(*api, true)); + return pcmk_rc_ok; +} + +static void +free_daemon_specific_data(pcmk_ipc_api_t *api) +{ + if ((api != NULL) && (api->cmds != NULL)) { + if ((api->cmds->free_data != NULL) && (api->api_data != NULL)) { + api->cmds->free_data(api->api_data); + api->api_data = NULL; + } + free(api->cmds); + api->cmds = NULL; + } +} + +/*! + * \internal + * \brief Call an IPC API event callback, if one is registed + * + * \param[in,out] api IPC API connection + * \param[in] event_type The type of event that occurred + * \param[in] status Event status + * \param[in,out] event_data Event-specific data + */ +void +pcmk__call_ipc_callback(pcmk_ipc_api_t *api, enum pcmk_ipc_event event_type, + crm_exit_t status, void *event_data) +{ + if ((api != NULL) && (api->cb != NULL)) { + api->cb(api, event_type, status, event_data, api->user_data); + } +} + +/*! + * \internal + * \brief Clean up after an IPC disconnect + * + * \param[in,out] user_data IPC API connection that disconnected + * + * \note This function can be used as a main loop IPC destroy callback. + */ +static void +ipc_post_disconnect(gpointer user_data) +{ + pcmk_ipc_api_t *api = user_data; + + crm_info("Disconnected from %s IPC API", pcmk_ipc_name(api, true)); + + // Perform any daemon-specific handling needed + if ((api->cmds != NULL) && (api->cmds->post_disconnect != NULL)) { + api->cmds->post_disconnect(api); + } + + // Call client's registered event callback + pcmk__call_ipc_callback(api, pcmk_ipc_event_disconnect, CRM_EX_DISCONNECT, + NULL); + + /* If this is being called from a running main loop, mainloop_gio_destroy() + * will free ipc and mainloop_io immediately after calling this function. + * If this is called from a stopped main loop, these will leak, so the best + * practice is to close the connection before stopping the main loop. + */ + api->ipc = NULL; + api->mainloop_io = NULL; + + if (api->free_on_disconnect) { + /* pcmk_free_ipc_api() has already been called, but did not free api + * or api->cmds because this function needed them. Do that now. + */ + free_daemon_specific_data(api); + crm_trace("Freeing IPC API object after disconnect"); + free(api); + } +} + +/*! + * \brief Free the contents of an IPC API object + * + * \param[in,out] api IPC API object to free + */ +void +pcmk_free_ipc_api(pcmk_ipc_api_t *api) +{ + bool free_on_disconnect = false; + + if (api == NULL) { + return; + } + crm_debug("Releasing %s IPC API", pcmk_ipc_name(api, true)); + + if (api->ipc != NULL) { + if (api->mainloop_io != NULL) { + /* We need to keep the api pointer itself around, because it is the + * user data for the IPC client destroy callback. That will be + * triggered by the pcmk_disconnect_ipc() call below, but it might + * happen later in the main loop (if still running). + * + * This flag tells the destroy callback to free the object. It can't + * do that unconditionally, because the application might call this + * function after a disconnect that happened by other means. + */ + free_on_disconnect = api->free_on_disconnect = true; + } + pcmk_disconnect_ipc(api); // Frees api if free_on_disconnect is true + } + if (!free_on_disconnect) { + free_daemon_specific_data(api); + crm_trace("Freeing IPC API object"); + free(api); + } +} + +/*! + * \brief Get the IPC name used with an IPC API connection + * + * \param[in] api IPC API connection + * \param[in] for_log If true, return human-friendly name instead of IPC name + * + * \return IPC API's human-friendly or connection name, or if none is available, + * "Pacemaker" if for_log is true and NULL if for_log is false + */ +const char * +pcmk_ipc_name(const pcmk_ipc_api_t *api, bool for_log) +{ + if (api == NULL) { + return for_log? "Pacemaker" : NULL; + } + switch (api->server) { + case pcmk_ipc_attrd: + return for_log? "attribute manager" : T_ATTRD; + + case pcmk_ipc_based: + return for_log? "CIB manager" : NULL /* PCMK__SERVER_BASED_RW */; + + case pcmk_ipc_controld: + return for_log? "controller" : CRM_SYSTEM_CRMD; + + case pcmk_ipc_execd: + return for_log? "executor" : NULL /* CRM_SYSTEM_LRMD */; + + case pcmk_ipc_fenced: + return for_log? "fencer" : NULL /* "stonith-ng" */; + + case pcmk_ipc_pacemakerd: + return for_log? "launcher" : CRM_SYSTEM_MCP; + + case pcmk_ipc_schedulerd: + return for_log? "scheduler" : CRM_SYSTEM_PENGINE; + + default: + return for_log? "Pacemaker" : NULL; + } +} + +/*! + * \brief Check whether an IPC API connection is active + * + * \param[in,out] api IPC API connection + * + * \return true if IPC is connected, false otherwise + */ +bool +pcmk_ipc_is_connected(pcmk_ipc_api_t *api) +{ + return (api != NULL) && crm_ipc_connected(api->ipc); +} + +/*! + * \internal + * \brief Call the daemon-specific API's dispatch function + * + * Perform daemon-specific handling of IPC reply dispatch. It is the daemon + * method's responsibility to call the client's registered event callback, as + * well as allocate and free any event data. + * + * \param[in,out] api IPC API connection + * \param[in,out] message IPC reply XML to dispatch + */ +static bool +call_api_dispatch(pcmk_ipc_api_t *api, xmlNode *message) +{ + crm_log_xml_trace(message, "ipc-received"); + if ((api->cmds != NULL) && (api->cmds->dispatch != NULL)) { + return api->cmds->dispatch(api, message); + } + + return false; +} + +/*! + * \internal + * \brief Dispatch previously read IPC data + * + * \param[in] buffer Data read from IPC + * \param[in,out] api IPC object + * + * \return Standard Pacemaker return code. In particular: + * + * pcmk_rc_ok: There are no more messages expected from the server. Quit + * reading. + * EINPROGRESS: There are more messages expected from the server. Keep reading. + * + * All other values indicate an error. + */ +static int +dispatch_ipc_data(const char *buffer, pcmk_ipc_api_t *api) +{ + bool more = false; + xmlNode *msg; + + if (buffer == NULL) { + crm_warn("Empty message received from %s IPC", + pcmk_ipc_name(api, true)); + return ENOMSG; + } + + msg = string2xml(buffer); + if (msg == NULL) { + crm_warn("Malformed message received from %s IPC", + pcmk_ipc_name(api, true)); + return EPROTO; + } + + more = call_api_dispatch(api, msg); + free_xml(msg); + + if (more) { + return EINPROGRESS; + } else { + return pcmk_rc_ok; + } +} + +/*! + * \internal + * \brief Dispatch data read from IPC source + * + * \param[in] buffer Data read from IPC + * \param[in] length Number of bytes of data in buffer (ignored) + * \param[in,out] user_data IPC object + * + * \return Always 0 (meaning connection is still required) + * + * \note This function can be used as a main loop IPC dispatch callback. + */ +static int +dispatch_ipc_source_data(const char *buffer, ssize_t length, gpointer user_data) +{ + pcmk_ipc_api_t *api = user_data; + + CRM_CHECK(api != NULL, return 0); + dispatch_ipc_data(buffer, api); + return 0; +} + +/*! + * \brief Check whether an IPC connection has data available (without main loop) + * + * \param[in] api IPC API connection + * \param[in] timeout_ms If less than 0, poll indefinitely; if 0, poll once + * and return immediately; otherwise, poll for up to + * this many milliseconds + * + * \return Standard Pacemaker return code + * + * \note Callers of pcmk_connect_ipc() using pcmk_ipc_dispatch_poll should call + * this function to check whether IPC data is available. Return values of + * interest include pcmk_rc_ok meaning data is available, and EAGAIN + * meaning no data is available; all other values indicate errors. + * \todo This does not allow the caller to poll multiple file descriptors at + * once. If there is demand for that, we could add a wrapper for + * crm_ipc_get_fd(api->ipc), so the caller can call poll() themselves. + */ +int +pcmk_poll_ipc(const pcmk_ipc_api_t *api, int timeout_ms) +{ + int rc; + struct pollfd pollfd = { 0, }; + + if ((api == NULL) || (api->dispatch_type != pcmk_ipc_dispatch_poll)) { + return EINVAL; + } + pollfd.fd = crm_ipc_get_fd(api->ipc); + pollfd.events = POLLIN; + rc = poll(&pollfd, 1, timeout_ms); + if (rc < 0) { + /* Some UNIX systems return negative and set EAGAIN for failure to + * allocate memory; standardize the return code in that case + */ + return (errno == EAGAIN)? ENOMEM : errno; + } else if (rc == 0) { + return EAGAIN; + } + return pcmk_rc_ok; +} + +/*! + * \brief Dispatch available messages on an IPC connection (without main loop) + * + * \param[in,out] api IPC API connection + * + * \return Standard Pacemaker return code + * + * \note Callers of pcmk_connect_ipc() using pcmk_ipc_dispatch_poll should call + * this function when IPC data is available. + */ +void +pcmk_dispatch_ipc(pcmk_ipc_api_t *api) +{ + if (api == NULL) { + return; + } + while (crm_ipc_ready(api->ipc) > 0) { + if (crm_ipc_read(api->ipc) > 0) { + dispatch_ipc_data(crm_ipc_buffer(api->ipc), api); + } + } +} + +// \return Standard Pacemaker return code +static int +connect_with_main_loop(pcmk_ipc_api_t *api) +{ + int rc; + + struct ipc_client_callbacks callbacks = { + .dispatch = dispatch_ipc_source_data, + .destroy = ipc_post_disconnect, + }; + + rc = pcmk__add_mainloop_ipc(api->ipc, G_PRIORITY_DEFAULT, api, + &callbacks, &(api->mainloop_io)); + if (rc != pcmk_rc_ok) { + return rc; + } + crm_debug("Connected to %s IPC (attached to main loop)", + pcmk_ipc_name(api, true)); + /* After this point, api->mainloop_io owns api->ipc, so api->ipc + * should not be explicitly freed. + */ + return pcmk_rc_ok; +} + +// \return Standard Pacemaker return code +static int +connect_without_main_loop(pcmk_ipc_api_t *api) +{ + int rc; + + if (!crm_ipc_connect(api->ipc)) { + rc = errno; + crm_ipc_close(api->ipc); + return rc; + } + crm_debug("Connected to %s IPC (without main loop)", + pcmk_ipc_name(api, true)); + return pcmk_rc_ok; +} + +/*! + * \brief Connect to a Pacemaker daemon via IPC + * + * \param[in,out] api IPC API instance + * \param[in] dispatch_type How IPC replies should be dispatched + * + * \return Standard Pacemaker return code + */ +int +pcmk_connect_ipc(pcmk_ipc_api_t *api, enum pcmk_ipc_dispatch dispatch_type) +{ + const int n_attempts = 2; + int rc = pcmk_rc_ok; + + if (api == NULL) { + crm_err("Cannot connect to uninitialized API object"); + return EINVAL; + } + + if (api->ipc == NULL) { + api->ipc = crm_ipc_new(pcmk_ipc_name(api, false), + api->ipc_size_max); + if (api->ipc == NULL) { + crm_err("Failed to re-create IPC API"); + return ENOMEM; + } + } + + if (crm_ipc_connected(api->ipc)) { + crm_trace("Already connected to %s IPC API", pcmk_ipc_name(api, true)); + return pcmk_rc_ok; + } + + api->dispatch_type = dispatch_type; + + for (int i = 0; i < n_attempts; i++) { + switch (dispatch_type) { + case pcmk_ipc_dispatch_main: + rc = connect_with_main_loop(api); + break; + + case pcmk_ipc_dispatch_sync: + case pcmk_ipc_dispatch_poll: + rc = connect_without_main_loop(api); + break; + } + + if (rc != EAGAIN) { + break; + } + + /* EAGAIN may occur due to interruption by a signal or due to some + * transient issue. Try one more time to be more resilient. + */ + if (i < (n_attempts - 1)) { + crm_trace("Connection to %s IPC API failed with EAGAIN, retrying", + pcmk_ipc_name(api, true)); + } + } + + if (rc != pcmk_rc_ok) { + return rc; + } + + if ((api->cmds != NULL) && (api->cmds->post_connect != NULL)) { + rc = api->cmds->post_connect(api); + if (rc != pcmk_rc_ok) { + crm_ipc_close(api->ipc); + } + } + return rc; +} + +/*! + * \brief Disconnect an IPC API instance + * + * \param[in,out] api IPC API connection + * + * \return Standard Pacemaker return code + * + * \note If the connection is attached to a main loop, this function should be + * called before quitting the main loop, to ensure that all memory is + * freed. + */ +void +pcmk_disconnect_ipc(pcmk_ipc_api_t *api) +{ + if ((api == NULL) || (api->ipc == NULL)) { + return; + } + switch (api->dispatch_type) { + case pcmk_ipc_dispatch_main: + { + mainloop_io_t *mainloop_io = api->mainloop_io; + + // Make sure no code with access to api can use these again + api->mainloop_io = NULL; + api->ipc = NULL; + + mainloop_del_ipc_client(mainloop_io); + // After this point api might have already been freed + } + break; + + case pcmk_ipc_dispatch_poll: + case pcmk_ipc_dispatch_sync: + { + crm_ipc_t *ipc = api->ipc; + + // Make sure no code with access to api can use ipc again + api->ipc = NULL; + + // This should always be the case already, but to be safe + api->free_on_disconnect = false; + + crm_ipc_close(ipc); + crm_ipc_destroy(ipc); + ipc_post_disconnect(api); + } + break; + } +} + +/*! + * \brief Register a callback for IPC API events + * + * \param[in,out] api IPC API connection + * \param[in] callback Callback to register + * \param[in] userdata Caller data to pass to callback + * + * \note This function may be called multiple times to update the callback + * and/or user data. The caller remains responsible for freeing + * userdata in any case (after the IPC is disconnected, if the + * user data is still registered with the IPC). + */ +void +pcmk_register_ipc_callback(pcmk_ipc_api_t *api, pcmk_ipc_callback_t cb, + void *user_data) +{ + if (api == NULL) { + return; + } + api->cb = cb; + api->user_data = user_data; +} + +/*! + * \internal + * \brief Send an XML request across an IPC API connection + * + * \param[in,out] api IPC API connection + * \param[in,out] request XML request to send + * + * \return Standard Pacemaker return code + * + * \note Daemon-specific IPC API functions should call this function to send + * requests, because it handles different dispatch types appropriately. + */ +int +pcmk__send_ipc_request(pcmk_ipc_api_t *api, xmlNode *request) +{ + int rc; + xmlNode *reply = NULL; + enum crm_ipc_flags flags = crm_ipc_flags_none; + + if ((api == NULL) || (api->ipc == NULL) || (request == NULL)) { + return EINVAL; + } + crm_log_xml_trace(request, "ipc-sent"); + + // Synchronous dispatch requires waiting for a reply + if ((api->dispatch_type == pcmk_ipc_dispatch_sync) + && (api->cmds != NULL) + && (api->cmds->reply_expected != NULL) + && (api->cmds->reply_expected(api, request))) { + flags = crm_ipc_client_response; + } + + // The 0 here means a default timeout of 5 seconds + rc = crm_ipc_send(api->ipc, request, flags, 0, &reply); + + if (rc < 0) { + return pcmk_legacy2rc(rc); + } else if (rc == 0) { + return ENODATA; + } + + // With synchronous dispatch, we dispatch any reply now + if (reply != NULL) { + bool more = call_api_dispatch(api, reply); + + free_xml(reply); + + while (more) { + rc = crm_ipc_read(api->ipc); + + if (rc == -EAGAIN) { + continue; + } else if (rc == -ENOMSG || rc == pcmk_ok) { + return pcmk_rc_ok; + } else if (rc < 0) { + return -rc; + } + + rc = dispatch_ipc_data(crm_ipc_buffer(api->ipc), api); + + if (rc == pcmk_rc_ok) { + more = false; + } else if (rc == EINPROGRESS) { + more = true; + } else { + continue; + } + } + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Create the XML for an IPC request to purge a node from the peer cache + * + * \param[in] api IPC API connection + * \param[in] node_name If not NULL, name of node to purge + * \param[in] nodeid If not 0, node ID of node to purge + * + * \return Newly allocated IPC request XML + * + * \note The controller, fencer, and pacemakerd use the same request syntax, but + * the attribute manager uses a different one. The CIB manager doesn't + * have any syntax for it. The executor and scheduler don't connect to the + * cluster layer and thus don't have or need any syntax for it. + * + * \todo Modify the attribute manager to accept the common syntax (as well + * as its current one, for compatibility with older clients). Modify + * the CIB manager to accept and honor the common syntax. Modify the + * executor and scheduler to accept the syntax (immediately returning + * success), just for consistency. Modify this function to use the + * common syntax with all daemons if their version supports it. + */ +static xmlNode * +create_purge_node_request(const pcmk_ipc_api_t *api, const char *node_name, + uint32_t nodeid) +{ + xmlNode *request = NULL; + const char *client = crm_system_name? crm_system_name : "client"; + + switch (api->server) { + case pcmk_ipc_attrd: + request = create_xml_node(NULL, __func__); + crm_xml_add(request, F_TYPE, T_ATTRD); + crm_xml_add(request, F_ORIG, crm_system_name); + crm_xml_add(request, PCMK__XA_TASK, PCMK__ATTRD_CMD_PEER_REMOVE); + pcmk__xe_add_node(request, node_name, nodeid); + break; + + case pcmk_ipc_controld: + case pcmk_ipc_fenced: + case pcmk_ipc_pacemakerd: + request = create_request(CRM_OP_RM_NODE_CACHE, NULL, NULL, + pcmk_ipc_name(api, false), client, NULL); + if (nodeid > 0) { + crm_xml_set_id(request, "%lu", (unsigned long) nodeid); + } + crm_xml_add(request, XML_ATTR_UNAME, node_name); + break; + + case pcmk_ipc_based: + case pcmk_ipc_execd: + case pcmk_ipc_schedulerd: + break; + } + return request; +} + +/*! + * \brief Ask a Pacemaker daemon to purge a node from its peer cache + * + * \param[in,out] api IPC API connection + * \param[in] node_name If not NULL, name of node to purge + * \param[in] nodeid If not 0, node ID of node to purge + * + * \return Standard Pacemaker return code + * + * \note At least one of node_name or nodeid must be specified. + */ +int +pcmk_ipc_purge_node(pcmk_ipc_api_t *api, const char *node_name, uint32_t nodeid) +{ + int rc = 0; + xmlNode *request = NULL; + + if (api == NULL) { + return EINVAL; + } + if ((node_name == NULL) && (nodeid == 0)) { + return EINVAL; + } + + request = create_purge_node_request(api, node_name, nodeid); + if (request == NULL) { + return EOPNOTSUPP; + } + rc = pcmk__send_ipc_request(api, request); + free_xml(request); + + crm_debug("%s peer cache purge of node %s[%lu]: rc=%d", + pcmk_ipc_name(api, true), node_name, (unsigned long) nodeid, rc); + return rc; +} + +/* + * Generic IPC API (to eventually be deprecated as public API and made internal) + */ + +struct crm_ipc_s { + struct pollfd pfd; + unsigned int max_buf_size; // maximum bytes we can send or receive over IPC + unsigned int buf_size; // size of allocated buffer + int msg_size; + int need_reply; + char *buffer; + char *server_name; // server IPC name being connected to + qb_ipcc_connection_t *ipc; +}; + +/*! + * \brief Create a new (legacy) object for using Pacemaker daemon IPC + * + * \param[in] name IPC system name to connect to + * \param[in] max_size Use a maximum IPC buffer size of at least this size + * + * \return Newly allocated IPC object on success, NULL otherwise + * + * \note The caller is responsible for freeing the result using + * crm_ipc_destroy(). + * \note This should be considered deprecated for use with daemons supported by + * pcmk_new_ipc_api(). + */ +crm_ipc_t * +crm_ipc_new(const char *name, size_t max_size) +{ + crm_ipc_t *client = NULL; + + client = calloc(1, sizeof(crm_ipc_t)); + if (client == NULL) { + crm_err("Could not create IPC connection: %s", strerror(errno)); + return NULL; + } + + client->server_name = strdup(name); + if (client->server_name == NULL) { + crm_err("Could not create %s IPC connection: %s", + name, strerror(errno)); + free(client); + return NULL; + } + client->buf_size = pcmk__ipc_buffer_size(max_size); + client->buffer = malloc(client->buf_size); + if (client->buffer == NULL) { + crm_err("Could not create %s IPC connection: %s", + name, strerror(errno)); + free(client->server_name); + free(client); + return NULL; + } + + /* Clients initiating connection pick the max buf size */ + client->max_buf_size = client->buf_size; + + client->pfd.fd = -1; + client->pfd.events = POLLIN; + client->pfd.revents = 0; + + return client; +} + +/*! + * \brief Establish an IPC connection to a Pacemaker component + * + * \param[in,out] client Connection instance obtained from crm_ipc_new() + * + * \return true on success, false otherwise (in which case errno will be set; + * specifically, in case of discovering the remote side is not + * authentic, its value is set to ECONNABORTED). + */ +bool +crm_ipc_connect(crm_ipc_t *client) +{ + uid_t cl_uid = 0; + gid_t cl_gid = 0; + pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; + int rv; + + if (client == NULL) { + errno = EINVAL; + return false; + } + + client->need_reply = FALSE; + client->ipc = qb_ipcc_connect(client->server_name, client->buf_size); + + if (client->ipc == NULL) { + crm_debug("Could not establish %s IPC connection: %s (%d)", + client->server_name, pcmk_rc_str(errno), errno); + return false; + } + + client->pfd.fd = crm_ipc_get_fd(client); + if (client->pfd.fd < 0) { + rv = errno; + /* message already omitted */ + crm_ipc_close(client); + errno = rv; + return false; + } + + rv = pcmk_daemon_user(&cl_uid, &cl_gid); + if (rv < 0) { + /* message already omitted */ + crm_ipc_close(client); + errno = -rv; + return false; + } + + if ((rv = pcmk__crm_ipc_is_authentic_process(client->ipc, client->pfd.fd, cl_uid, cl_gid, + &found_pid, &found_uid, + &found_gid)) == pcmk_rc_ipc_unauthorized) { + crm_err("%s IPC provider authentication failed: process %lld has " + "uid %lld (expected %lld) and gid %lld (expected %lld)", + client->server_name, + (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) cl_uid, + (long long) found_gid, (long long) cl_gid); + crm_ipc_close(client); + errno = ECONNABORTED; + return false; + + } else if (rv != pcmk_rc_ok) { + crm_perror(LOG_ERR, "Could not verify authenticity of %s IPC provider", + client->server_name); + crm_ipc_close(client); + if (rv > 0) { + errno = rv; + } else { + errno = ENOTCONN; + } + return false; + } + + qb_ipcc_context_set(client->ipc, client); + + client->max_buf_size = qb_ipcc_get_buffer_size(client->ipc); + if (client->max_buf_size > client->buf_size) { + free(client->buffer); + client->buffer = calloc(1, client->max_buf_size); + client->buf_size = client->max_buf_size; + } + return true; +} + +void +crm_ipc_close(crm_ipc_t * client) +{ + if (client) { + if (client->ipc) { + qb_ipcc_connection_t *ipc = client->ipc; + + client->ipc = NULL; + qb_ipcc_disconnect(ipc); + } + } +} + +void +crm_ipc_destroy(crm_ipc_t * client) +{ + if (client) { + if (client->ipc && qb_ipcc_is_connected(client->ipc)) { + crm_notice("Destroying active %s IPC connection", + client->server_name); + /* The next line is basically unsafe + * + * If this connection was attached to mainloop and mainloop is active, + * the 'disconnected' callback will end up back here and we'll end + * up free'ing the memory twice - something that can still happen + * even without this if we destroy a connection and it closes before + * we call exit + */ + /* crm_ipc_close(client); */ + } else { + crm_trace("Destroying inactive %s IPC connection", + client->server_name); + } + free(client->buffer); + free(client->server_name); + free(client); + } +} + +int +crm_ipc_get_fd(crm_ipc_t * client) +{ + int fd = 0; + + if (client && client->ipc && (qb_ipcc_fd_get(client->ipc, &fd) == 0)) { + return fd; + } + errno = EINVAL; + crm_perror(LOG_ERR, "Could not obtain file descriptor for %s IPC", + (client? client->server_name : "unspecified")); + return -errno; +} + +bool +crm_ipc_connected(crm_ipc_t * client) +{ + bool rc = FALSE; + + if (client == NULL) { + crm_trace("No client"); + return FALSE; + + } else if (client->ipc == NULL) { + crm_trace("No connection"); + return FALSE; + + } else if (client->pfd.fd < 0) { + crm_trace("Bad descriptor"); + return FALSE; + } + + rc = qb_ipcc_is_connected(client->ipc); + if (rc == FALSE) { + client->pfd.fd = -EINVAL; + } + return rc; +} + +/*! + * \brief Check whether an IPC connection is ready to be read + * + * \param[in,out] client Connection to check + * + * \return Positive value if ready to be read, 0 if not ready, -errno on error + */ +int +crm_ipc_ready(crm_ipc_t *client) +{ + int rc; + + CRM_ASSERT(client != NULL); + + if (!crm_ipc_connected(client)) { + return -ENOTCONN; + } + + client->pfd.revents = 0; + rc = poll(&(client->pfd), 1, 0); + return (rc < 0)? -errno : rc; +} + +// \return Standard Pacemaker return code +static int +crm_ipc_decompress(crm_ipc_t * client) +{ + pcmk__ipc_header_t *header = (pcmk__ipc_header_t *)(void*)client->buffer; + + if (header->size_compressed) { + int rc = 0; + unsigned int size_u = 1 + header->size_uncompressed; + /* never let buf size fall below our max size required for ipc reads. */ + unsigned int new_buf_size = QB_MAX((sizeof(pcmk__ipc_header_t) + size_u), client->max_buf_size); + char *uncompressed = calloc(1, new_buf_size); + + crm_trace("Decompressing message data %u bytes into %u bytes", + header->size_compressed, size_u); + + rc = BZ2_bzBuffToBuffDecompress(uncompressed + sizeof(pcmk__ipc_header_t), &size_u, + client->buffer + sizeof(pcmk__ipc_header_t), header->size_compressed, 1, 0); + + if (rc != BZ_OK) { + crm_err("Decompression failed: %s " CRM_XS " bzerror=%d", + bz2_strerror(rc), rc); + free(uncompressed); + return EILSEQ; + } + + /* + * This assert no longer holds true. For an identical msg, some clients may + * require compression, and others may not. If that same msg (event) is sent + * to multiple clients, it could result in some clients receiving a compressed + * msg even though compression was not explicitly required for them. + * + * CRM_ASSERT((header->size_uncompressed + sizeof(pcmk__ipc_header_t)) >= ipc_buffer_max); + */ + CRM_ASSERT(size_u == header->size_uncompressed); + + memcpy(uncompressed, client->buffer, sizeof(pcmk__ipc_header_t)); /* Preserve the header */ + header = (pcmk__ipc_header_t *)(void*)uncompressed; + + free(client->buffer); + client->buf_size = new_buf_size; + client->buffer = uncompressed; + } + + CRM_ASSERT(client->buffer[sizeof(pcmk__ipc_header_t) + header->size_uncompressed - 1] == 0); + return pcmk_rc_ok; +} + +long +crm_ipc_read(crm_ipc_t * client) +{ + pcmk__ipc_header_t *header = NULL; + + CRM_ASSERT(client != NULL); + CRM_ASSERT(client->ipc != NULL); + CRM_ASSERT(client->buffer != NULL); + + client->buffer[0] = 0; + client->msg_size = qb_ipcc_event_recv(client->ipc, client->buffer, + client->buf_size, 0); + if (client->msg_size >= 0) { + int rc = crm_ipc_decompress(client); + + if (rc != pcmk_rc_ok) { + return pcmk_rc2legacy(rc); + } + + header = (pcmk__ipc_header_t *)(void*)client->buffer; + if (!pcmk__valid_ipc_header(header)) { + return -EBADMSG; + } + + crm_trace("Received %s IPC event %d size=%u rc=%d text='%.100s'", + client->server_name, header->qb.id, header->qb.size, + client->msg_size, + client->buffer + sizeof(pcmk__ipc_header_t)); + + } else { + crm_trace("No message received from %s IPC: %s", + client->server_name, pcmk_strerror(client->msg_size)); + + if (client->msg_size == -EAGAIN) { + return -EAGAIN; + } + } + + if (!crm_ipc_connected(client) || client->msg_size == -ENOTCONN) { + crm_err("Connection to %s IPC failed", client->server_name); + } + + if (header) { + /* Data excluding the header */ + return header->size_uncompressed; + } + return -ENOMSG; +} + +const char * +crm_ipc_buffer(crm_ipc_t * client) +{ + CRM_ASSERT(client != NULL); + return client->buffer + sizeof(pcmk__ipc_header_t); +} + +uint32_t +crm_ipc_buffer_flags(crm_ipc_t * client) +{ + pcmk__ipc_header_t *header = NULL; + + CRM_ASSERT(client != NULL); + if (client->buffer == NULL) { + return 0; + } + + header = (pcmk__ipc_header_t *)(void*)client->buffer; + return header->flags; +} + +const char * +crm_ipc_name(crm_ipc_t * client) +{ + CRM_ASSERT(client != NULL); + return client->server_name; +} + +// \return Standard Pacemaker return code +static int +internal_ipc_get_reply(crm_ipc_t *client, int request_id, int ms_timeout, + ssize_t *bytes) +{ + time_t timeout = time(NULL) + 1 + (ms_timeout / 1000); + int rc = pcmk_rc_ok; + + /* get the reply */ + crm_trace("Waiting on reply to %s IPC message %d", + client->server_name, request_id); + do { + + *bytes = qb_ipcc_recv(client->ipc, client->buffer, client->buf_size, 1000); + if (*bytes > 0) { + pcmk__ipc_header_t *hdr = NULL; + + rc = crm_ipc_decompress(client); + if (rc != pcmk_rc_ok) { + return rc; + } + + hdr = (pcmk__ipc_header_t *)(void*)client->buffer; + if (hdr->qb.id == request_id) { + /* Got it */ + break; + } else if (hdr->qb.id < request_id) { + xmlNode *bad = string2xml(crm_ipc_buffer(client)); + + crm_err("Discarding old reply %d (need %d)", hdr->qb.id, request_id); + crm_log_xml_notice(bad, "OldIpcReply"); + + } else { + xmlNode *bad = string2xml(crm_ipc_buffer(client)); + + crm_err("Discarding newer reply %d (need %d)", hdr->qb.id, request_id); + crm_log_xml_notice(bad, "ImpossibleReply"); + CRM_ASSERT(hdr->qb.id <= request_id); + } + } else if (!crm_ipc_connected(client)) { + crm_err("%s IPC provider disconnected while waiting for message %d", + client->server_name, request_id); + break; + } + + } while (time(NULL) < timeout); + + if (*bytes < 0) { + rc = (int) -*bytes; // System errno + } + return rc; +} + +/*! + * \brief Send an IPC XML message + * + * \param[in,out] client Connection to IPC server + * \param[in,out] message XML message to send + * \param[in] flags Bitmask of crm_ipc_flags + * \param[in] ms_timeout Give up if not sent within this much time + * (5 seconds if 0, or no timeout if negative) + * \param[out] reply Reply from server (or NULL if none) + * + * \return Negative errno on error, otherwise size of reply received in bytes + * if reply was needed, otherwise number of bytes sent + */ +int +crm_ipc_send(crm_ipc_t * client, xmlNode * message, enum crm_ipc_flags flags, int32_t ms_timeout, + xmlNode ** reply) +{ + int rc = 0; + ssize_t qb_rc = 0; + ssize_t bytes = 0; + struct iovec *iov; + static uint32_t id = 0; + static int factor = 8; + pcmk__ipc_header_t *header; + + if (client == NULL) { + crm_notice("Can't send IPC request without connection (bug?): %.100s", + message); + return -ENOTCONN; + + } else if (!crm_ipc_connected(client)) { + /* Don't even bother */ + crm_notice("Can't send %s IPC requests: Connection closed", + client->server_name); + return -ENOTCONN; + } + + if (ms_timeout == 0) { + ms_timeout = 5000; + } + + if (client->need_reply) { + qb_rc = qb_ipcc_recv(client->ipc, client->buffer, client->buf_size, ms_timeout); + if (qb_rc < 0) { + crm_warn("Sending %s IPC disabled until pending reply received", + client->server_name); + return -EALREADY; + + } else { + crm_notice("Sending %s IPC re-enabled after pending reply received", + client->server_name); + client->need_reply = FALSE; + } + } + + id++; + CRM_LOG_ASSERT(id != 0); /* Crude wrap-around detection */ + rc = pcmk__ipc_prepare_iov(id, message, client->max_buf_size, &iov, &bytes); + if (rc != pcmk_rc_ok) { + crm_warn("Couldn't prepare %s IPC request: %s " CRM_XS " rc=%d", + client->server_name, pcmk_rc_str(rc), rc); + return pcmk_rc2legacy(rc); + } + + header = iov[0].iov_base; + pcmk__set_ipc_flags(header->flags, client->server_name, flags); + + if (pcmk_is_set(flags, crm_ipc_proxied)) { + /* Don't look for a synchronous response */ + pcmk__clear_ipc_flags(flags, "client", crm_ipc_client_response); + } + + if(header->size_compressed) { + if(factor < 10 && (client->max_buf_size / 10) < (bytes / factor)) { + crm_notice("Compressed message exceeds %d0%% of configured IPC " + "limit (%u bytes); consider setting PCMK_ipc_buffer to " + "%u or higher", + factor, client->max_buf_size, 2 * client->max_buf_size); + factor++; + } + } + + crm_trace("Sending %s IPC request %d of %u bytes using %dms timeout", + client->server_name, header->qb.id, header->qb.size, ms_timeout); + + if ((ms_timeout > 0) || !pcmk_is_set(flags, crm_ipc_client_response)) { + + time_t timeout = time(NULL) + 1 + (ms_timeout / 1000); + + do { + /* @TODO Is this check really needed? Won't qb_ipcc_sendv() return + * an error if it's not connected? + */ + if (!crm_ipc_connected(client)) { + goto send_cleanup; + } + + qb_rc = qb_ipcc_sendv(client->ipc, iov, 2); + } while ((qb_rc == -EAGAIN) && (time(NULL) < timeout)); + + rc = (int) qb_rc; // Negative of system errno, or bytes sent + if (qb_rc <= 0) { + goto send_cleanup; + + } else if (!pcmk_is_set(flags, crm_ipc_client_response)) { + crm_trace("Not waiting for reply to %s IPC request %d", + client->server_name, header->qb.id); + goto send_cleanup; + } + + rc = internal_ipc_get_reply(client, header->qb.id, ms_timeout, &bytes); + if (rc != pcmk_rc_ok) { + /* We didn't get the reply in time, so disable future sends for now. + * The only alternative would be to close the connection since we + * don't know how to detect and discard out-of-sequence replies. + * + * @TODO Implement out-of-sequence detection + */ + client->need_reply = TRUE; + } + rc = (int) bytes; // Negative system errno, or size of reply received + + } else { + // No timeout, and client response needed + do { + qb_rc = qb_ipcc_sendv_recv(client->ipc, iov, 2, client->buffer, + client->buf_size, -1); + } while ((qb_rc == -EAGAIN) && crm_ipc_connected(client)); + rc = (int) qb_rc; // Negative system errno, or size of reply received + } + + if (rc > 0) { + pcmk__ipc_header_t *hdr = (pcmk__ipc_header_t *)(void*)client->buffer; + + crm_trace("Received %d-byte reply %d to %s IPC %d: %.100s", + rc, hdr->qb.id, client->server_name, header->qb.id, + crm_ipc_buffer(client)); + + if (reply) { + *reply = string2xml(crm_ipc_buffer(client)); + } + + } else { + crm_trace("No reply to %s IPC %d: rc=%d", + client->server_name, header->qb.id, rc); + } + + send_cleanup: + if (!crm_ipc_connected(client)) { + crm_notice("Couldn't send %s IPC request %d: Connection closed " + CRM_XS " rc=%d", client->server_name, header->qb.id, rc); + + } else if (rc == -ETIMEDOUT) { + crm_warn("%s IPC request %d failed: %s after %dms " CRM_XS " rc=%d", + client->server_name, header->qb.id, pcmk_strerror(rc), + ms_timeout, rc); + crm_write_blackbox(0, NULL); + + } else if (rc <= 0) { + crm_warn("%s IPC request %d failed: %s " CRM_XS " rc=%d", + client->server_name, header->qb.id, + ((rc == 0)? "No bytes sent" : pcmk_strerror(rc)), rc); + } + + pcmk_free_ipc_event(iov); + return rc; +} + +int +pcmk__crm_ipc_is_authentic_process(qb_ipcc_connection_t *qb_ipc, int sock, uid_t refuid, gid_t refgid, + pid_t *gotpid, uid_t *gotuid, gid_t *gotgid) +{ + int ret = 0; + pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; +#if defined(HAVE_UCRED) + struct ucred ucred; + socklen_t ucred_len = sizeof(ucred); +#endif + +#ifdef HAVE_QB_IPCC_AUTH_GET + if (qb_ipc && !qb_ipcc_auth_get(qb_ipc, &found_pid, &found_uid, &found_gid)) { + goto do_checks; + } +#endif + +#if defined(HAVE_UCRED) + if (!getsockopt(sock, SOL_SOCKET, SO_PEERCRED, + &ucred, &ucred_len) + && ucred_len == sizeof(ucred)) { + found_pid = ucred.pid; found_uid = ucred.uid; found_gid = ucred.gid; + +#elif defined(HAVE_SOCKPEERCRED) + struct sockpeercred sockpeercred; + socklen_t sockpeercred_len = sizeof(sockpeercred); + + if (!getsockopt(sock, SOL_SOCKET, SO_PEERCRED, + &sockpeercred, &sockpeercred_len) + && sockpeercred_len == sizeof(sockpeercred_len)) { + found_pid = sockpeercred.pid; + found_uid = sockpeercred.uid; found_gid = sockpeercred.gid; + +#elif defined(HAVE_GETPEEREID) + if (!getpeereid(sock, &found_uid, &found_gid)) { + found_pid = PCMK__SPECIAL_PID; /* cannot obtain PID (FreeBSD) */ + +#elif defined(HAVE_GETPEERUCRED) + ucred_t *ucred; + if (!getpeerucred(sock, &ucred)) { + errno = 0; + found_pid = ucred_getpid(ucred); + found_uid = ucred_geteuid(ucred); found_gid = ucred_getegid(ucred); + ret = -errno; + ucred_free(ucred); + if (ret) { + return (ret < 0) ? ret : -pcmk_err_generic; + } + +#else +# error "No way to authenticate a Unix socket peer" + errno = 0; + if (0) { +#endif +#ifdef HAVE_QB_IPCC_AUTH_GET + do_checks: +#endif + if (gotpid != NULL) { + *gotpid = found_pid; + } + if (gotuid != NULL) { + *gotuid = found_uid; + } + if (gotgid != NULL) { + *gotgid = found_gid; + } + if (found_uid == 0 || found_uid == refuid || found_gid == refgid) { + ret = 0; + } else { + ret = pcmk_rc_ipc_unauthorized; + } + } else { + ret = (errno > 0) ? errno : pcmk_rc_error; + } + return ret; +} + +int +crm_ipc_is_authentic_process(int sock, uid_t refuid, gid_t refgid, + pid_t *gotpid, uid_t *gotuid, gid_t *gotgid) +{ + int ret = pcmk__crm_ipc_is_authentic_process(NULL, sock, refuid, refgid, + gotpid, gotuid, gotgid); + + /* The old function had some very odd return codes*/ + if (ret == 0) { + return 1; + } else if (ret == pcmk_rc_ipc_unauthorized) { + return 0; + } else { + return pcmk_rc2legacy(ret); + } +} + +int +pcmk__ipc_is_authentic_process_active(const char *name, uid_t refuid, + gid_t refgid, pid_t *gotpid) +{ + static char last_asked_name[PATH_MAX / 2] = ""; /* log spam prevention */ + int fd; + int rc = pcmk_rc_ipc_unresponsive; + int auth_rc = 0; + int32_t qb_rc; + pid_t found_pid = 0; uid_t found_uid = 0; gid_t found_gid = 0; + qb_ipcc_connection_t *c; +#ifdef HAVE_QB_IPCC_CONNECT_ASYNC + struct pollfd pollfd = { 0, }; + int poll_rc; + + c = qb_ipcc_connect_async(name, 0, + &(pollfd.fd)); +#else + c = qb_ipcc_connect(name, 0); +#endif + if (c == NULL) { + crm_info("Could not connect to %s IPC: %s", name, strerror(errno)); + rc = pcmk_rc_ipc_unresponsive; + goto bail; + } +#ifdef HAVE_QB_IPCC_CONNECT_ASYNC + pollfd.events = POLLIN; + do { + poll_rc = poll(&pollfd, 1, 2000); + } while ((poll_rc == -1) && (errno == EINTR)); + if ((poll_rc <= 0) || (qb_ipcc_connect_continue(c) != 0)) { + crm_info("Could not connect to %s IPC: %s", name, + (poll_rc == 0)?"timeout":strerror(errno)); + rc = pcmk_rc_ipc_unresponsive; + if (poll_rc > 0) { + c = NULL; // qb_ipcc_connect_continue cleaned up for us + } + goto bail; + } +#endif + + qb_rc = qb_ipcc_fd_get(c, &fd); + if (qb_rc != 0) { + rc = (int) -qb_rc; // System errno + crm_err("Could not get fd from %s IPC: %s " CRM_XS " rc=%d", + name, pcmk_rc_str(rc), rc); + goto bail; + } + + auth_rc = pcmk__crm_ipc_is_authentic_process(c, fd, refuid, refgid, &found_pid, + &found_uid, &found_gid); + if (auth_rc == pcmk_rc_ipc_unauthorized) { + crm_err("Daemon (IPC %s) effectively blocked with unauthorized" + " process %lld (uid: %lld, gid: %lld)", + name, (long long) PCMK__SPECIAL_PID_AS_0(found_pid), + (long long) found_uid, (long long) found_gid); + rc = pcmk_rc_ipc_unauthorized; + goto bail; + } + + if (auth_rc != pcmk_rc_ok) { + rc = auth_rc; + crm_err("Could not get peer credentials from %s IPC: %s " + CRM_XS " rc=%d", name, pcmk_rc_str(rc), rc); + goto bail; + } + + if (gotpid != NULL) { + *gotpid = found_pid; + } + + rc = pcmk_rc_ok; + if ((found_uid != refuid || found_gid != refgid) + && strncmp(last_asked_name, name, sizeof(last_asked_name))) { + if ((found_uid == 0) && (refuid != 0)) { + crm_warn("Daemon (IPC %s) runs as root, whereas the expected" + " credentials are %lld:%lld, hazard of violating" + " the least privilege principle", + name, (long long) refuid, (long long) refgid); + } else { + crm_notice("Daemon (IPC %s) runs as %lld:%lld, whereas the" + " expected credentials are %lld:%lld, which may" + " mean a different set of privileges than expected", + name, (long long) found_uid, (long long) found_gid, + (long long) refuid, (long long) refgid); + } + memccpy(last_asked_name, name, '\0', sizeof(last_asked_name)); + } + +bail: + if (c != NULL) { + qb_ipcc_disconnect(c); + } + return rc; +} diff --git a/lib/common/ipc_common.c b/lib/common/ipc_common.c new file mode 100644 index 0000000..d0c0636 --- /dev/null +++ b/lib/common/ipc_common.c @@ -0,0 +1,110 @@ +/* + * Copyright 2004-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include // uint64_t +#include + +#include +#include "crmcommon_private.h" + +#define MIN_MSG_SIZE 12336 // sizeof(struct qb_ipc_connection_response) +#define MAX_MSG_SIZE 128*1024 // 128k default + +/*! + * \internal + * \brief Choose an IPC buffer size in bytes + * + * \param[in] max Use this value if environment/default is lower + * + * \return Maximum of max and value of PCMK_ipc_buffer (default 128KB) + */ +unsigned int +pcmk__ipc_buffer_size(unsigned int max) +{ + static unsigned int global_max = 0; + + if (global_max == 0) { + long long global_ll; + + if ((pcmk__scan_ll(getenv("PCMK_ipc_buffer"), &global_ll, + 0LL) != pcmk_rc_ok) + || (global_ll <= 0)) { + global_max = MAX_MSG_SIZE; // Default for unset or invalid + + } else if (global_ll < MIN_MSG_SIZE) { + global_max = MIN_MSG_SIZE; + + } else if (global_ll > UINT_MAX) { + global_max = UINT_MAX; + + } else { + global_max = (unsigned int) global_ll; + } + } + return QB_MAX(max, global_max); +} + +/*! + * \brief Return pacemaker's default IPC buffer size + * + * \return IPC buffer size in bytes + */ +unsigned int +crm_ipc_default_buffer_size(void) +{ + static unsigned int default_size = 0; + + if (default_size == 0) { + default_size = pcmk__ipc_buffer_size(0); + } + return default_size; +} + +/*! + * \internal + * \brief Check whether an IPC header is valid + * + * \param[in] header IPC header to check + * + * \return true if IPC header has a supported version, false otherwise + */ +bool +pcmk__valid_ipc_header(const pcmk__ipc_header_t *header) +{ + if (header == NULL) { + crm_err("IPC message without header"); + return false; + + } else if (header->version > PCMK__IPC_VERSION) { + crm_err("Filtering incompatible v%d IPC message (only versions <= %d supported)", + header->version, PCMK__IPC_VERSION); + return false; + } + return true; +} + +const char * +pcmk__client_type_str(uint64_t client_type) +{ + switch (client_type) { + case pcmk__client_ipc: + return "IPC"; + case pcmk__client_tcp: + return "TCP"; +#ifdef HAVE_GNUTLS_GNUTLS_H + case pcmk__client_tls: + return "TLS"; +#endif + default: + return "unknown"; + } +} diff --git a/lib/common/ipc_controld.c b/lib/common/ipc_controld.c new file mode 100644 index 0000000..9303afd --- /dev/null +++ b/lib/common/ipc_controld.c @@ -0,0 +1,671 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include "crmcommon_private.h" + +struct controld_api_private_s { + char *client_uuid; + unsigned int replies_expected; +}; + +/*! + * \internal + * \brief Get a string representation of a controller API reply type + * + * \param[in] reply Controller API reply type + * + * \return String representation of a controller API reply type + */ +const char * +pcmk__controld_api_reply2str(enum pcmk_controld_api_reply reply) +{ + switch (reply) { + case pcmk_controld_reply_reprobe: + return "reprobe"; + case pcmk_controld_reply_info: + return "info"; + case pcmk_controld_reply_resource: + return "resource"; + case pcmk_controld_reply_ping: + return "ping"; + case pcmk_controld_reply_nodes: + return "nodes"; + default: + return "unknown"; + } +} + +// \return Standard Pacemaker return code +static int +new_data(pcmk_ipc_api_t *api) +{ + struct controld_api_private_s *private = NULL; + + api->api_data = calloc(1, sizeof(struct controld_api_private_s)); + + if (api->api_data == NULL) { + return errno; + } + + private = api->api_data; + + /* This is set to the PID because that's how it was always done, but PIDs + * are not unique because clients can be remote. The value appears to be + * unused other than as part of F_CRM_SYS_FROM in IPC requests, which is + * only compared against the internal system names (CRM_SYSTEM_TENGINE, + * etc.), so it shouldn't be a problem. + */ + private->client_uuid = pcmk__getpid_s(); + + /* @TODO Implement a call ID model similar to the CIB, executor, and fencer + * IPC APIs, so that requests and replies can be matched, and + * duplicate replies can be discarded. + */ + return pcmk_rc_ok; +} + +static void +free_data(void *data) +{ + free(((struct controld_api_private_s *) data)->client_uuid); + free(data); +} + +// \return Standard Pacemaker return code +static int +post_connect(pcmk_ipc_api_t *api) +{ + /* The controller currently requires clients to register via a hello + * request, but does not reply back. + */ + struct controld_api_private_s *private = api->api_data; + const char *client_name = crm_system_name? crm_system_name : "client"; + xmlNode *hello; + int rc; + + hello = create_hello_message(private->client_uuid, client_name, + PCMK__CONTROLD_API_MAJOR, + PCMK__CONTROLD_API_MINOR); + rc = pcmk__send_ipc_request(api, hello); + free_xml(hello); + if (rc != pcmk_rc_ok) { + crm_info("Could not send IPC hello to %s: %s " CRM_XS " rc=%s", + pcmk_ipc_name(api, true), pcmk_rc_str(rc), rc); + } else { + crm_debug("Sent IPC hello to %s", pcmk_ipc_name(api, true)); + } + return rc; +} + +static void +set_node_info_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) +{ + data->reply_type = pcmk_controld_reply_info; + if (msg_data == NULL) { + return; + } + data->data.node_info.have_quorum = pcmk__xe_attr_is_true(msg_data, XML_ATTR_HAVE_QUORUM); + data->data.node_info.is_remote = pcmk__xe_attr_is_true(msg_data, XML_NODE_IS_REMOTE); + + /* Integer node_info.id is currently valid only for Corosync nodes. + * + * @TODO: Improve handling after crm_node_t is refactored to handle layer- + * specific data better. + */ + crm_element_value_int(msg_data, XML_ATTR_ID, &(data->data.node_info.id)); + + data->data.node_info.uuid = crm_element_value(msg_data, XML_ATTR_ID); + data->data.node_info.uname = crm_element_value(msg_data, XML_ATTR_UNAME); + data->data.node_info.state = crm_element_value(msg_data, XML_NODE_IS_PEER); +} + +static void +set_ping_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) +{ + data->reply_type = pcmk_controld_reply_ping; + if (msg_data == NULL) { + return; + } + data->data.ping.sys_from = crm_element_value(msg_data, + XML_PING_ATTR_SYSFROM); + data->data.ping.fsa_state = crm_element_value(msg_data, + XML_PING_ATTR_CRMDSTATE); + data->data.ping.result = crm_element_value(msg_data, XML_PING_ATTR_STATUS); +} + +static void +set_nodes_data(pcmk_controld_api_reply_t *data, xmlNode *msg_data) +{ + pcmk_controld_api_node_t *node_info; + + data->reply_type = pcmk_controld_reply_nodes; + for (xmlNode *node = first_named_child(msg_data, XML_CIB_TAG_NODE); + node != NULL; node = crm_next_same_xml(node)) { + + long long id_ll = 0; + + node_info = calloc(1, sizeof(pcmk_controld_api_node_t)); + crm_element_value_ll(node, XML_ATTR_ID, &id_ll); + if (id_ll > 0) { + node_info->id = id_ll; + } + node_info->uname = crm_element_value(node, XML_ATTR_UNAME); + node_info->state = crm_element_value(node, XML_NODE_IN_CLUSTER); + data->data.nodes = g_list_prepend(data->data.nodes, node_info); + } +} + +static bool +reply_expected(pcmk_ipc_api_t *api, xmlNode *request) +{ + const char *command = crm_element_value(request, F_CRM_TASK); + + if (command == NULL) { + return false; + } + + // We only need to handle commands that functions in this file can send + return !strcmp(command, CRM_OP_REPROBE) + || !strcmp(command, CRM_OP_NODE_INFO) + || !strcmp(command, CRM_OP_PING) + || !strcmp(command, CRM_OP_LRM_FAIL) + || !strcmp(command, CRM_OP_LRM_DELETE); +} + +static bool +dispatch(pcmk_ipc_api_t *api, xmlNode *reply) +{ + struct controld_api_private_s *private = api->api_data; + crm_exit_t status = CRM_EX_OK; + xmlNode *msg_data = NULL; + const char *value = NULL; + pcmk_controld_api_reply_t reply_data = { + pcmk_controld_reply_unknown, NULL, NULL, + }; + + /* If we got an ACK, return true so the caller knows to expect more responses + * from the IPC server. We do this before decrementing replies_expected because + * ACKs are not going to be included in that value. + * + * Note that we cannot do the same kind of status checking here that we do in + * ipc_pacemakerd.c. The ACK message we receive does not necessarily contain + * a status attribute. That is, we may receive this: + * + * + * + * Instead of this: + * + * + */ + if (pcmk__str_eq(crm_element_name(reply), "ack", pcmk__str_none)) { + return true; // More replies needed + } + + if (private->replies_expected > 0) { + private->replies_expected--; + } + + // Do some basic validation of the reply + + /* @TODO We should be able to verify that value is always a response, but + * currently the controller doesn't always properly set the type. Even + * if we fix the controller, we'll still need to handle replies from + * old versions (feature set could be used to differentiate). + */ + value = crm_element_value(reply, F_CRM_MSG_TYPE); + if (pcmk__str_empty(value) + || !pcmk__str_any_of(value, XML_ATTR_REQUEST, XML_ATTR_RESPONSE, NULL)) { + crm_info("Unrecognizable message from controller: " + "invalid message type '%s'", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + + if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) { + crm_info("Unrecognizable message from controller: no reference"); + status = CRM_EX_PROTOCOL; + goto done; + } + + value = crm_element_value(reply, F_CRM_TASK); + if (pcmk__str_empty(value)) { + crm_info("Unrecognizable message from controller: no command name"); + status = CRM_EX_PROTOCOL; + goto done; + } + + // Parse useful info from reply + + reply_data.feature_set = crm_element_value(reply, XML_ATTR_VERSION); + reply_data.host_from = crm_element_value(reply, F_CRM_HOST_FROM); + msg_data = get_message_xml(reply, F_CRM_DATA); + + if (!strcmp(value, CRM_OP_REPROBE)) { + reply_data.reply_type = pcmk_controld_reply_reprobe; + + } else if (!strcmp(value, CRM_OP_NODE_INFO)) { + set_node_info_data(&reply_data, msg_data); + + } else if (!strcmp(value, CRM_OP_INVOKE_LRM)) { + reply_data.reply_type = pcmk_controld_reply_resource; + reply_data.data.resource.node_state = msg_data; + + } else if (!strcmp(value, CRM_OP_PING)) { + set_ping_data(&reply_data, msg_data); + + } else if (!strcmp(value, PCMK__CONTROLD_CMD_NODES)) { + set_nodes_data(&reply_data, msg_data); + + } else { + crm_info("Unrecognizable message from controller: unknown command '%s'", + value); + status = CRM_EX_PROTOCOL; + } + +done: + pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + + // Free any reply data that was allocated + if (pcmk__str_eq(value, PCMK__CONTROLD_CMD_NODES, pcmk__str_casei)) { + g_list_free_full(reply_data.data.nodes, free); + } + + return false; // No further replies needed +} + +pcmk__ipc_methods_t * +pcmk__controld_api_methods(void) +{ + pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); + + if (cmds != NULL) { + cmds->new_data = new_data; + cmds->free_data = free_data; + cmds->post_connect = post_connect; + cmds->reply_expected = reply_expected; + cmds->dispatch = dispatch; + } + return cmds; +} + +/*! + * \internal + * \brief Create XML for a controller IPC request + * + * \param[in] api Controller connection + * \param[in] op Controller IPC command name + * \param[in] node Node name to set as destination host + * \param[in] msg_data XML to attach to request as message data + * + * \return Newly allocated XML for request + */ +static xmlNode * +create_controller_request(const pcmk_ipc_api_t *api, const char *op, + const char *node, xmlNode *msg_data) +{ + struct controld_api_private_s *private = NULL; + const char *sys_to = NULL; + + if (api == NULL) { + return NULL; + } + private = api->api_data; + if ((node == NULL) && !strcmp(op, CRM_OP_PING)) { + sys_to = CRM_SYSTEM_DC; + } else { + sys_to = CRM_SYSTEM_CRMD; + } + return create_request(op, msg_data, node, sys_to, + (crm_system_name? crm_system_name : "client"), + private->client_uuid); +} + +// \return Standard Pacemaker return code +static int +send_controller_request(pcmk_ipc_api_t *api, xmlNode *request, + bool reply_is_expected) +{ + int rc; + + if (crm_element_value(request, XML_ATTR_REFERENCE) == NULL) { + return EINVAL; + } + rc = pcmk__send_ipc_request(api, request); + if ((rc == pcmk_rc_ok) && reply_is_expected) { + struct controld_api_private_s *private = api->api_data; + + private->replies_expected++; + } + return rc; +} + +static xmlNode * +create_reprobe_message_data(const char *target_node, const char *router_node) +{ + xmlNode *msg_data; + + msg_data = create_xml_node(NULL, "data_for_" CRM_OP_REPROBE); + crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node); + if ((router_node != NULL) && !pcmk__str_eq(router_node, target_node, pcmk__str_casei)) { + crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node); + } + return msg_data; +} + +/*! + * \brief Send a reprobe controller operation + * + * \param[in,out] api Controller connection + * \param[in] target_node Name of node to reprobe + * \param[in] router_node Router node for host + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_reprobe. + */ +int +pcmk_controld_api_reprobe(pcmk_ipc_api_t *api, const char *target_node, + const char *router_node) +{ + xmlNode *request; + xmlNode *msg_data; + int rc = pcmk_rc_ok; + + if (api == NULL) { + return EINVAL; + } + if (router_node == NULL) { + router_node = target_node; + } + crm_debug("Sending %s IPC request to reprobe %s via %s", + pcmk_ipc_name(api, true), pcmk__s(target_node, "local node"), + pcmk__s(router_node, "local node")); + msg_data = create_reprobe_message_data(target_node, router_node); + request = create_controller_request(api, CRM_OP_REPROBE, router_node, + msg_data); + rc = send_controller_request(api, request, true); + free_xml(msg_data); + free_xml(request); + return rc; +} + +/*! + * \brief Send a "node info" controller operation + * + * \param[in,out] api Controller connection + * \param[in] nodeid ID of node to get info for (or 0 for local node) + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_info. + */ +int +pcmk_controld_api_node_info(pcmk_ipc_api_t *api, uint32_t nodeid) +{ + xmlNode *request; + int rc = pcmk_rc_ok; + + request = create_controller_request(api, CRM_OP_NODE_INFO, NULL, NULL); + if (request == NULL) { + return EINVAL; + } + if (nodeid > 0) { + crm_xml_set_id(request, "%lu", (unsigned long) nodeid); + } + + rc = send_controller_request(api, request, true); + free_xml(request); + return rc; +} + +/*! + * \brief Ask the controller for status + * + * \param[in,out] api Controller connection + * \param[in] node_name Name of node whose status is desired (NULL for DC) + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_ping. + */ +int +pcmk_controld_api_ping(pcmk_ipc_api_t *api, const char *node_name) +{ + xmlNode *request; + int rc = pcmk_rc_ok; + + request = create_controller_request(api, CRM_OP_PING, node_name, NULL); + if (request == NULL) { + return EINVAL; + } + rc = send_controller_request(api, request, true); + free_xml(request); + return rc; +} + +/*! + * \brief Ask the controller for cluster information + * + * \param[in,out] api Controller connection + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_nodes. + */ +int +pcmk_controld_api_list_nodes(pcmk_ipc_api_t *api) +{ + xmlNode *request; + int rc = EINVAL; + + request = create_controller_request(api, PCMK__CONTROLD_CMD_NODES, NULL, + NULL); + if (request != NULL) { + rc = send_controller_request(api, request, true); + free_xml(request); + } + return rc; +} + +// \return Standard Pacemaker return code +static int +controller_resource_op(pcmk_ipc_api_t *api, const char *op, + const char *target_node, const char *router_node, + bool cib_only, const char *rsc_id, + const char *rsc_long_id, const char *standard, + const char *provider, const char *type) +{ + int rc = pcmk_rc_ok; + char *key; + xmlNode *request, *msg_data, *xml_rsc, *params; + + if (api == NULL) { + return EINVAL; + } + if (router_node == NULL) { + router_node = target_node; + } + + msg_data = create_xml_node(NULL, XML_GRAPH_TAG_RSC_OP); + + /* The controller logs the transition key from resource op requests, so we + * need to have *something* for it. + * @TODO don't use "crm-resource" + */ + key = pcmk__transition_key(0, getpid(), 0, + "xxxxxxxx-xrsc-opxx-xcrm-resourcexxxx"); + crm_xml_add(msg_data, XML_ATTR_TRANSITION_KEY, key); + free(key); + + crm_xml_add(msg_data, XML_LRM_ATTR_TARGET, target_node); + if (!pcmk__str_eq(router_node, target_node, pcmk__str_casei)) { + crm_xml_add(msg_data, XML_LRM_ATTR_ROUTER_NODE, router_node); + } + + if (cib_only) { + // Indicate that only the CIB needs to be cleaned + crm_xml_add(msg_data, PCMK__XA_MODE, XML_TAG_CIB); + } + + xml_rsc = create_xml_node(msg_data, XML_CIB_TAG_RESOURCE); + crm_xml_add(xml_rsc, XML_ATTR_ID, rsc_id); + crm_xml_add(xml_rsc, XML_ATTR_ID_LONG, rsc_long_id); + crm_xml_add(xml_rsc, XML_AGENT_ATTR_CLASS, standard); + crm_xml_add(xml_rsc, XML_AGENT_ATTR_PROVIDER, provider); + crm_xml_add(xml_rsc, XML_ATTR_TYPE, type); + + params = create_xml_node(msg_data, XML_TAG_ATTRS); + crm_xml_add(params, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); + + // The controller parses the timeout from the request + key = crm_meta_name(XML_ATTR_TIMEOUT); + crm_xml_add(params, key, "60000"); /* 1 minute */ //@TODO pass as arg + free(key); + + request = create_controller_request(api, op, router_node, msg_data); + rc = send_controller_request(api, request, true); + free_xml(msg_data); + free_xml(request); + return rc; +} + +/*! + * \brief Ask the controller to fail a resource + * + * \param[in,out] api Controller connection + * \param[in] target_node Name of node resource is on + * \param[in] router_node Router node for target + * \param[in] rsc_id ID of resource to fail + * \param[in] rsc_long_id Long ID of resource (if any) + * \param[in] standard Standard of resource + * \param[in] provider Provider of resource (if any) + * \param[in] type Type of resource to fail + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_resource. + */ +int +pcmk_controld_api_fail(pcmk_ipc_api_t *api, + const char *target_node, const char *router_node, + const char *rsc_id, const char *rsc_long_id, + const char *standard, const char *provider, + const char *type) +{ + crm_debug("Sending %s IPC request to fail %s (a.k.a. %s) on %s via %s", + pcmk_ipc_name(api, true), pcmk__s(rsc_id, "unknown resource"), + pcmk__s(rsc_long_id, "no other names"), + pcmk__s(target_node, "unspecified node"), + pcmk__s(router_node, "unspecified node")); + return controller_resource_op(api, CRM_OP_LRM_FAIL, target_node, + router_node, false, rsc_id, rsc_long_id, + standard, provider, type); +} + +/*! + * \brief Ask the controller to refresh a resource + * + * \param[in,out] api Controller connection + * \param[in] target_node Name of node resource is on + * \param[in] router_node Router node for target + * \param[in] rsc_id ID of resource to refresh + * \param[in] rsc_long_id Long ID of resource (if any) + * \param[in] standard Standard of resource + * \param[in] provider Provider of resource (if any) + * \param[in] type Type of resource + * \param[in] cib_only If true, clean resource from CIB only + * + * \return Standard Pacemaker return code + * \note Event callback will get a reply of type pcmk_controld_reply_resource. + */ +int +pcmk_controld_api_refresh(pcmk_ipc_api_t *api, const char *target_node, + const char *router_node, + const char *rsc_id, const char *rsc_long_id, + const char *standard, const char *provider, + const char *type, bool cib_only) +{ + crm_debug("Sending %s IPC request to refresh %s (a.k.a. %s) on %s via %s", + pcmk_ipc_name(api, true), pcmk__s(rsc_id, "unknown resource"), + pcmk__s(rsc_long_id, "no other names"), + pcmk__s(target_node, "unspecified node"), + pcmk__s(router_node, "unspecified node")); + return controller_resource_op(api, CRM_OP_LRM_DELETE, target_node, + router_node, cib_only, rsc_id, rsc_long_id, + standard, provider, type); +} + +/*! + * \brief Get the number of IPC replies currently expected from the controller + * + * \param[in] api Controller IPC API connection + * + * \return Number of replies expected + */ +unsigned int +pcmk_controld_api_replies_expected(const pcmk_ipc_api_t *api) +{ + struct controld_api_private_s *private = api->api_data; + + return private->replies_expected; +} + +/*! + * \brief Create XML for a controller IPC "hello" message + * + * \deprecated This function is deprecated as part of the public C API. + */ +// \todo make this static to this file when breaking API backward compatibility +xmlNode * +create_hello_message(const char *uuid, const char *client_name, + const char *major_version, const char *minor_version) +{ + xmlNode *hello_node = NULL; + xmlNode *hello = NULL; + + if (pcmk__str_empty(uuid) || pcmk__str_empty(client_name) + || pcmk__str_empty(major_version) || pcmk__str_empty(minor_version)) { + crm_err("Could not create IPC hello message from %s (UUID %s): " + "missing information", + client_name? client_name : "unknown client", + uuid? uuid : "unknown"); + return NULL; + } + + hello_node = create_xml_node(NULL, XML_TAG_OPTIONS); + if (hello_node == NULL) { + crm_err("Could not create IPC hello message from %s (UUID %s): " + "Message data creation failed", client_name, uuid); + return NULL; + } + + crm_xml_add(hello_node, "major_version", major_version); + crm_xml_add(hello_node, "minor_version", minor_version); + crm_xml_add(hello_node, "client_name", client_name); + crm_xml_add(hello_node, "client_uuid", uuid); + + hello = create_request(CRM_OP_HELLO, hello_node, NULL, NULL, client_name, uuid); + if (hello == NULL) { + crm_err("Could not create IPC hello message from %s (UUID %s): " + "Request creation failed", client_name, uuid); + return NULL; + } + free_xml(hello_node); + + crm_trace("Created hello message from %s (UUID %s)", client_name, uuid); + return hello; +} diff --git a/lib/common/ipc_pacemakerd.c b/lib/common/ipc_pacemakerd.c new file mode 100644 index 0000000..91a3143 --- /dev/null +++ b/lib/common/ipc_pacemakerd.c @@ -0,0 +1,316 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include "crmcommon_private.h" + +typedef struct pacemakerd_api_private_s { + enum pcmk_pacemakerd_state state; + char *client_uuid; +} pacemakerd_api_private_t; + +static const char *pacemakerd_state_str[] = { + XML_PING_ATTR_PACEMAKERDSTATE_INIT, + XML_PING_ATTR_PACEMAKERDSTATE_STARTINGDAEMONS, + XML_PING_ATTR_PACEMAKERDSTATE_WAITPING, + XML_PING_ATTR_PACEMAKERDSTATE_RUNNING, + XML_PING_ATTR_PACEMAKERDSTATE_SHUTTINGDOWN, + XML_PING_ATTR_PACEMAKERDSTATE_SHUTDOWNCOMPLETE, + XML_PING_ATTR_PACEMAKERDSTATE_REMOTE, +}; + +enum pcmk_pacemakerd_state +pcmk_pacemakerd_api_daemon_state_text2enum(const char *state) +{ + int i; + + if (state == NULL) { + return pcmk_pacemakerd_state_invalid; + } + for (i=pcmk_pacemakerd_state_init; i <= pcmk_pacemakerd_state_max; + i++) { + if (pcmk__str_eq(state, pacemakerd_state_str[i], pcmk__str_none)) { + return i; + } + } + return pcmk_pacemakerd_state_invalid; +} + +const char * +pcmk_pacemakerd_api_daemon_state_enum2text( + enum pcmk_pacemakerd_state state) +{ + if ((state >= pcmk_pacemakerd_state_init) && + (state <= pcmk_pacemakerd_state_max)) { + return pacemakerd_state_str[state]; + } + return "invalid"; +} + +/*! + * \internal + * \brief Return a friendly string representation of a \p pacemakerd state + * + * \param[in] state \p pacemakerd state + * + * \return A user-friendly string representation of \p state, or + * "Invalid pacemakerd state" + */ +const char * +pcmk__pcmkd_state_enum2friendly(enum pcmk_pacemakerd_state state) +{ + switch (state) { + case pcmk_pacemakerd_state_init: + return "Initializing pacemaker"; + case pcmk_pacemakerd_state_starting_daemons: + return "Pacemaker daemons are starting"; + case pcmk_pacemakerd_state_wait_for_ping: + return "Waiting for startup trigger from SBD"; + case pcmk_pacemakerd_state_running: + return "Pacemaker is running"; + case pcmk_pacemakerd_state_shutting_down: + return "Pacemaker daemons are shutting down"; + case pcmk_pacemakerd_state_shutdown_complete: + /* Assuming pacemakerd won't process messages while in + * shutdown_complete state unless reporting to SBD + */ + return "Pacemaker daemons are shut down (reporting to SBD)"; + case pcmk_pacemakerd_state_remote: + return "pacemaker-remoted is running (on a Pacemaker Remote node)"; + default: + return "Invalid pacemakerd state"; + } +} + +/*! + * \internal + * \brief Get a string representation of a \p pacemakerd API reply type + * + * \param[in] reply \p pacemakerd API reply type + * + * \return String representation of a \p pacemakerd API reply type + */ +const char * +pcmk__pcmkd_api_reply2str(enum pcmk_pacemakerd_api_reply reply) +{ + switch (reply) { + case pcmk_pacemakerd_reply_ping: + return "ping"; + case pcmk_pacemakerd_reply_shutdown: + return "shutdown"; + default: + return "unknown"; + } +} + +// \return Standard Pacemaker return code +static int +new_data(pcmk_ipc_api_t *api) +{ + struct pacemakerd_api_private_s *private = NULL; + + api->api_data = calloc(1, sizeof(struct pacemakerd_api_private_s)); + + if (api->api_data == NULL) { + return errno; + } + + private = api->api_data; + private->state = pcmk_pacemakerd_state_invalid; + /* other as with cib, controld, ... we are addressing pacemakerd just + from the local node -> pid is unique and thus sufficient as an ID + */ + private->client_uuid = pcmk__getpid_s(); + + return pcmk_rc_ok; +} + +static void +free_data(void *data) +{ + free(((struct pacemakerd_api_private_s *) data)->client_uuid); + free(data); +} + +// \return Standard Pacemaker return code +static int +post_connect(pcmk_ipc_api_t *api) +{ + struct pacemakerd_api_private_s *private = NULL; + + if (api->api_data == NULL) { + return EINVAL; + } + private = api->api_data; + private->state = pcmk_pacemakerd_state_invalid; + + return pcmk_rc_ok; +} + +static void +post_disconnect(pcmk_ipc_api_t *api) +{ + struct pacemakerd_api_private_s *private = NULL; + + if (api->api_data == NULL) { + return; + } + private = api->api_data; + private->state = pcmk_pacemakerd_state_invalid; + + return; +} + +static bool +reply_expected(pcmk_ipc_api_t *api, xmlNode *request) +{ + const char *command = crm_element_value(request, F_CRM_TASK); + + if (command == NULL) { + return false; + } + + // We only need to handle commands that functions in this file can send + return pcmk__str_any_of(command, CRM_OP_PING, CRM_OP_QUIT, NULL); +} + +static bool +dispatch(pcmk_ipc_api_t *api, xmlNode *reply) +{ + crm_exit_t status = CRM_EX_OK; + xmlNode *msg_data = NULL; + pcmk_pacemakerd_api_reply_t reply_data = { + pcmk_pacemakerd_reply_unknown + }; + const char *value = NULL; + long long value_ll = 0; + + if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_none)) { + long long int ack_status = 0; + pcmk__scan_ll(crm_element_value(reply, "status"), &ack_status, CRM_EX_OK); + return ack_status == CRM_EX_INDETERMINATE; + } + + value = crm_element_value(reply, F_CRM_MSG_TYPE); + if (pcmk__str_empty(value) + || !pcmk__str_eq(value, XML_ATTR_RESPONSE, pcmk__str_none)) { + crm_info("Unrecognizable message from pacemakerd: " + "message type '%s' not '" XML_ATTR_RESPONSE "'", + pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + + if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) { + crm_info("Unrecognizable message from pacemakerd: no reference"); + status = CRM_EX_PROTOCOL; + goto done; + } + + value = crm_element_value(reply, F_CRM_TASK); + + // Parse useful info from reply + msg_data = get_message_xml(reply, F_CRM_DATA); + crm_element_value_ll(msg_data, XML_ATTR_TSTAMP, &value_ll); + + if (pcmk__str_eq(value, CRM_OP_PING, pcmk__str_none)) { + reply_data.reply_type = pcmk_pacemakerd_reply_ping; + reply_data.data.ping.state = + pcmk_pacemakerd_api_daemon_state_text2enum( + crm_element_value(msg_data, XML_PING_ATTR_PACEMAKERDSTATE)); + reply_data.data.ping.status = + pcmk__str_eq(crm_element_value(msg_data, XML_PING_ATTR_STATUS), "ok", + pcmk__str_casei)?pcmk_rc_ok:pcmk_rc_error; + reply_data.data.ping.last_good = (value_ll < 0)? 0 : (time_t) value_ll; + reply_data.data.ping.sys_from = crm_element_value(msg_data, + XML_PING_ATTR_SYSFROM); + } else if (pcmk__str_eq(value, CRM_OP_QUIT, pcmk__str_none)) { + reply_data.reply_type = pcmk_pacemakerd_reply_shutdown; + reply_data.data.shutdown.status = atoi(crm_element_value(msg_data, XML_LRM_ATTR_OPSTATUS)); + } else { + crm_info("Unrecognizable message from pacemakerd: " + "unknown command '%s'", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + +done: + pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + return false; +} + +pcmk__ipc_methods_t * +pcmk__pacemakerd_api_methods(void) +{ + pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); + + if (cmds != NULL) { + cmds->new_data = new_data; + cmds->free_data = free_data; + cmds->post_connect = post_connect; + cmds->reply_expected = reply_expected; + cmds->dispatch = dispatch; + cmds->post_disconnect = post_disconnect; + } + return cmds; +} + +static int +do_pacemakerd_api_call(pcmk_ipc_api_t *api, const char *ipc_name, const char *task) +{ + pacemakerd_api_private_t *private; + xmlNode *cmd; + int rc; + + if (api == NULL) { + return EINVAL; + } + + private = api->api_data; + CRM_ASSERT(private != NULL); + + cmd = create_request(task, NULL, NULL, CRM_SYSTEM_MCP, + pcmk__ipc_sys_name(ipc_name, "client"), + private->client_uuid); + + if (cmd) { + rc = pcmk__send_ipc_request(api, cmd); + if (rc != pcmk_rc_ok) { + crm_debug("Couldn't send request to pacemakerd: %s rc=%d", + pcmk_rc_str(rc), rc); + } + free_xml(cmd); + } else { + rc = ENOMSG; + } + + return rc; +} + +int +pcmk_pacemakerd_api_ping(pcmk_ipc_api_t *api, const char *ipc_name) +{ + return do_pacemakerd_api_call(api, ipc_name, CRM_OP_PING); +} + +int +pcmk_pacemakerd_api_shutdown(pcmk_ipc_api_t *api, const char *ipc_name) +{ + return do_pacemakerd_api_call(api, ipc_name, CRM_OP_QUIT); +} diff --git a/lib/common/ipc_schedulerd.c b/lib/common/ipc_schedulerd.c new file mode 100644 index 0000000..c1b81a4 --- /dev/null +++ b/lib/common/ipc_schedulerd.c @@ -0,0 +1,180 @@ +/* + * Copyright 2021-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include "crmcommon_private.h" + +typedef struct schedulerd_api_private_s { + char *client_uuid; +} schedulerd_api_private_t; + +// \return Standard Pacemaker return code +static int +new_data(pcmk_ipc_api_t *api) +{ + struct schedulerd_api_private_s *private = NULL; + + api->api_data = calloc(1, sizeof(struct schedulerd_api_private_s)); + + if (api->api_data == NULL) { + return errno; + } + + private = api->api_data; + /* See comments in ipc_pacemakerd.c. */ + private->client_uuid = pcmk__getpid_s(); + + return pcmk_rc_ok; +} + +static void +free_data(void *data) +{ + free(((struct schedulerd_api_private_s *) data)->client_uuid); + free(data); +} + +// \return Standard Pacemaker return code +static int +post_connect(pcmk_ipc_api_t *api) +{ + if (api->api_data == NULL) { + return EINVAL; + } + + return pcmk_rc_ok; +} + +static bool +reply_expected(pcmk_ipc_api_t *api, xmlNode *request) +{ + const char *command = crm_element_value(request, F_CRM_TASK); + + if (command == NULL) { + return false; + } + + // We only need to handle commands that functions in this file can send + return pcmk__str_any_of(command, CRM_OP_PECALC, NULL); +} + +static bool +dispatch(pcmk_ipc_api_t *api, xmlNode *reply) +{ + crm_exit_t status = CRM_EX_OK; + xmlNode *msg_data = NULL; + pcmk_schedulerd_api_reply_t reply_data = { + pcmk_schedulerd_reply_unknown + }; + const char *value = NULL; + + if (pcmk__str_eq((const char *) reply->name, "ack", pcmk__str_casei)) { + return false; + } + + value = crm_element_value(reply, F_CRM_MSG_TYPE); + if (!pcmk__str_eq(value, XML_ATTR_RESPONSE, pcmk__str_none)) { + crm_info("Unrecognizable message from schedulerd: " + "message type '%s' not '" XML_ATTR_RESPONSE "'", + pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + + if (pcmk__str_empty(crm_element_value(reply, XML_ATTR_REFERENCE))) { + crm_info("Unrecognizable message from schedulerd: no reference"); + status = CRM_EX_PROTOCOL; + goto done; + } + + // Parse useful info from reply + msg_data = get_message_xml(reply, F_CRM_DATA); + value = crm_element_value(reply, F_CRM_TASK); + + if (pcmk__str_eq(value, CRM_OP_PECALC, pcmk__str_none)) { + reply_data.reply_type = pcmk_schedulerd_reply_graph; + reply_data.data.graph.reference = crm_element_value(reply, XML_ATTR_REFERENCE); + reply_data.data.graph.input = crm_element_value(reply, F_CRM_TGRAPH_INPUT); + reply_data.data.graph.tgraph = msg_data; + } else { + crm_info("Unrecognizable message from schedulerd: " + "unknown command '%s'", pcmk__s(value, "")); + status = CRM_EX_PROTOCOL; + goto done; + } + +done: + pcmk__call_ipc_callback(api, pcmk_ipc_event_reply, status, &reply_data); + return false; +} + +pcmk__ipc_methods_t * +pcmk__schedulerd_api_methods(void) +{ + pcmk__ipc_methods_t *cmds = calloc(1, sizeof(pcmk__ipc_methods_t)); + + if (cmds != NULL) { + cmds->new_data = new_data; + cmds->free_data = free_data; + cmds->post_connect = post_connect; + cmds->reply_expected = reply_expected; + cmds->dispatch = dispatch; + } + return cmds; +} + +static int +do_schedulerd_api_call(pcmk_ipc_api_t *api, const char *task, xmlNode *cib, char **ref) +{ + schedulerd_api_private_t *private; + xmlNode *cmd = NULL; + int rc; + + if (!pcmk_ipc_is_connected(api)) { + return ENOTCONN; + } + + private = api->api_data; + CRM_ASSERT(private != NULL); + + cmd = create_request(task, cib, NULL, CRM_SYSTEM_PENGINE, + crm_system_name? crm_system_name : "client", + private->client_uuid); + + if (cmd) { + rc = pcmk__send_ipc_request(api, cmd); + if (rc != pcmk_rc_ok) { + crm_debug("Couldn't send request to schedulerd: %s rc=%d", + pcmk_rc_str(rc), rc); + } + + *ref = strdup(crm_element_value(cmd, F_CRM_REFERENCE)); + free_xml(cmd); + } else { + rc = ENOMSG; + } + + return rc; +} + +int +pcmk_schedulerd_api_graph(pcmk_ipc_api_t *api, xmlNode *cib, char **ref) +{ + return do_schedulerd_api_call(api, CRM_OP_PECALC, cib, ref); +} diff --git a/lib/common/ipc_server.c b/lib/common/ipc_server.c new file mode 100644 index 0000000..60f20fb --- /dev/null +++ b/lib/common/ipc_server.c @@ -0,0 +1,1008 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "crmcommon_private.h" + +/* Evict clients whose event queue grows this large (by default) */ +#define PCMK_IPC_DEFAULT_QUEUE_MAX 500 + +static GHashTable *client_connections = NULL; + +/*! + * \internal + * \brief Count IPC clients + * + * \return Number of active IPC client connections + */ +guint +pcmk__ipc_client_count(void) +{ + return client_connections? g_hash_table_size(client_connections) : 0; +} + +/*! + * \internal + * \brief Execute a function for each active IPC client connection + * + * \param[in] func Function to call + * \param[in,out] user_data Pointer to pass to function + * + * \note The parameters are the same as for g_hash_table_foreach(). + */ +void +pcmk__foreach_ipc_client(GHFunc func, gpointer user_data) +{ + if ((func != NULL) && (client_connections != NULL)) { + g_hash_table_foreach(client_connections, func, user_data); + } +} + +pcmk__client_t * +pcmk__find_client(const qb_ipcs_connection_t *c) +{ + if (client_connections) { + return g_hash_table_lookup(client_connections, c); + } + + crm_trace("No client found for %p", c); + return NULL; +} + +pcmk__client_t * +pcmk__find_client_by_id(const char *id) +{ + if ((client_connections != NULL) && (id != NULL)) { + gpointer key; + pcmk__client_t *client = NULL; + GHashTableIter iter; + + g_hash_table_iter_init(&iter, client_connections); + while (g_hash_table_iter_next(&iter, &key, (gpointer *) & client)) { + if (strcmp(client->id, id) == 0) { + return client; + } + } + } + crm_trace("No client found with id='%s'", pcmk__s(id, "")); + return NULL; +} + +/*! + * \internal + * \brief Get a client identifier for use in log messages + * + * \param[in] c Client + * + * \return Client's name, client's ID, or a string literal, as available + * \note This is intended to be used in format strings like "client %s". + */ +const char * +pcmk__client_name(const pcmk__client_t *c) +{ + if (c == NULL) { + return "(unspecified)"; + + } else if (c->name != NULL) { + return c->name; + + } else if (c->id != NULL) { + return c->id; + + } else { + return "(unidentified)"; + } +} + +void +pcmk__client_cleanup(void) +{ + if (client_connections != NULL) { + int active = g_hash_table_size(client_connections); + + if (active > 0) { + crm_warn("Exiting with %d active IPC client%s", + active, pcmk__plural_s(active)); + } + g_hash_table_destroy(client_connections); + client_connections = NULL; + } +} + +void +pcmk__drop_all_clients(qb_ipcs_service_t *service) +{ + qb_ipcs_connection_t *c = NULL; + + if (service == NULL) { + return; + } + + c = qb_ipcs_connection_first_get(service); + + while (c != NULL) { + qb_ipcs_connection_t *last = c; + + c = qb_ipcs_connection_next_get(service, last); + + /* There really shouldn't be anyone connected at this point */ + crm_notice("Disconnecting client %p, pid=%d...", + last, pcmk__client_pid(last)); + qb_ipcs_disconnect(last); + qb_ipcs_connection_unref(last); + } +} + +/*! + * \internal + * \brief Allocate a new pcmk__client_t object based on an IPC connection + * + * \param[in] c IPC connection (NULL to allocate generic client) + * \param[in] key Connection table key (NULL to use sane default) + * \param[in] uid_client UID corresponding to c (ignored if c is NULL) + * + * \return Pointer to new pcmk__client_t (or NULL on error) + */ +static pcmk__client_t * +client_from_connection(qb_ipcs_connection_t *c, void *key, uid_t uid_client) +{ + pcmk__client_t *client = calloc(1, sizeof(pcmk__client_t)); + + if (client == NULL) { + crm_perror(LOG_ERR, "Allocating client"); + return NULL; + } + + if (c) { + client->user = pcmk__uid2username(uid_client); + if (client->user == NULL) { + client->user = strdup("#unprivileged"); + CRM_CHECK(client->user != NULL, free(client); return NULL); + crm_err("Unable to enforce ACLs for user ID %d, assuming unprivileged", + uid_client); + } + client->ipcs = c; + pcmk__set_client_flags(client, pcmk__client_ipc); + client->pid = pcmk__client_pid(c); + if (key == NULL) { + key = c; + } + } + + client->id = crm_generate_uuid(); + if (key == NULL) { + key = client->id; + } + if (client_connections == NULL) { + crm_trace("Creating IPC client table"); + client_connections = g_hash_table_new(g_direct_hash, g_direct_equal); + } + g_hash_table_insert(client_connections, key, client); + return client; +} + +/*! + * \brief Allocate a new pcmk__client_t object and generate its ID + * + * \param[in] key What to use as connections hash table key (NULL to use ID) + * + * \return Pointer to new pcmk__client_t (asserts on failure) + */ +pcmk__client_t * +pcmk__new_unauth_client(void *key) +{ + pcmk__client_t *client = client_from_connection(NULL, key, 0); + + CRM_ASSERT(client != NULL); + return client; +} + +pcmk__client_t * +pcmk__new_client(qb_ipcs_connection_t *c, uid_t uid_client, gid_t gid_client) +{ + gid_t uid_cluster = 0; + gid_t gid_cluster = 0; + + pcmk__client_t *client = NULL; + + CRM_CHECK(c != NULL, return NULL); + + if (pcmk_daemon_user(&uid_cluster, &gid_cluster) < 0) { + static bool need_log = TRUE; + + if (need_log) { + crm_warn("Could not find user and group IDs for user %s", + CRM_DAEMON_USER); + need_log = FALSE; + } + } + + if (uid_client != 0) { + crm_trace("Giving group %u access to new IPC connection", gid_cluster); + /* Passing -1 to chown(2) means don't change */ + qb_ipcs_connection_auth_set(c, -1, gid_cluster, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); + } + + /* TODO: Do our own auth checking, return NULL if unauthorized */ + client = client_from_connection(c, NULL, uid_client); + if (client == NULL) { + return NULL; + } + + if ((uid_client == 0) || (uid_client == uid_cluster)) { + /* Remember when a connection came from root or hacluster */ + pcmk__set_client_flags(client, pcmk__client_privileged); + } + + crm_debug("New IPC client %s for PID %u with uid %d and gid %d", + client->id, client->pid, uid_client, gid_client); + return client; +} + +static struct iovec * +pcmk__new_ipc_event(void) +{ + struct iovec *iov = calloc(2, sizeof(struct iovec)); + + CRM_ASSERT(iov != NULL); + return iov; +} + +/*! + * \brief Free an I/O vector created by pcmk__ipc_prepare_iov() + * + * \param[in,out] event I/O vector to free + */ +void +pcmk_free_ipc_event(struct iovec *event) +{ + if (event != NULL) { + free(event[0].iov_base); + free(event[1].iov_base); + free(event); + } +} + +static void +free_event(gpointer data) +{ + pcmk_free_ipc_event((struct iovec *) data); +} + +static void +add_event(pcmk__client_t *c, struct iovec *iov) +{ + if (c->event_queue == NULL) { + c->event_queue = g_queue_new(); + } + g_queue_push_tail(c->event_queue, iov); +} + +void +pcmk__free_client(pcmk__client_t *c) +{ + if (c == NULL) { + return; + } + + if (client_connections) { + if (c->ipcs) { + crm_trace("Destroying %p/%p (%d remaining)", + c, c->ipcs, g_hash_table_size(client_connections) - 1); + g_hash_table_remove(client_connections, c->ipcs); + + } else { + crm_trace("Destroying remote connection %p (%d remaining)", + c, g_hash_table_size(client_connections) - 1); + g_hash_table_remove(client_connections, c->id); + } + } + + if (c->event_timer) { + g_source_remove(c->event_timer); + } + + if (c->event_queue) { + crm_debug("Destroying %d events", g_queue_get_length(c->event_queue)); + g_queue_free_full(c->event_queue, free_event); + } + + free(c->id); + free(c->name); + free(c->user); + if (c->remote) { + if (c->remote->auth_timeout) { + g_source_remove(c->remote->auth_timeout); + } + free(c->remote->buffer); + free(c->remote); + } + free(c); +} + +/*! + * \internal + * \brief Raise IPC eviction threshold for a client, if allowed + * + * \param[in,out] client Client to modify + * \param[in] qmax New threshold (as non-NULL string) + * + * \return true if change was allowed, false otherwise + */ +bool +pcmk__set_client_queue_max(pcmk__client_t *client, const char *qmax) +{ + if (pcmk_is_set(client->flags, pcmk__client_privileged)) { + long long qmax_ll; + + if ((pcmk__scan_ll(qmax, &qmax_ll, 0LL) == pcmk_rc_ok) + && (qmax_ll > 0LL) && (qmax_ll <= UINT_MAX)) { + client->queue_max = (unsigned int) qmax_ll; + return true; + } + } + return false; +} + +int +pcmk__client_pid(qb_ipcs_connection_t *c) +{ + struct qb_ipcs_connection_stats stats; + + stats.client_pid = 0; + qb_ipcs_connection_stats_get(c, &stats, 0); + return stats.client_pid; +} + +/*! + * \internal + * \brief Retrieve message XML from data read from client IPC + * + * \param[in,out] c IPC client connection + * \param[in] data Data read from client connection + * \param[out] id Where to store message ID from libqb header + * \param[out] flags Where to store flags from libqb header + * + * \return Message XML on success, NULL otherwise + */ +xmlNode * +pcmk__client_data2xml(pcmk__client_t *c, void *data, uint32_t *id, + uint32_t *flags) +{ + xmlNode *xml = NULL; + char *uncompressed = NULL; + char *text = ((char *)data) + sizeof(pcmk__ipc_header_t); + pcmk__ipc_header_t *header = data; + + if (!pcmk__valid_ipc_header(header)) { + return NULL; + } + + if (id) { + *id = ((struct qb_ipc_response_header *)data)->id; + } + if (flags) { + *flags = header->flags; + } + + if (pcmk_is_set(header->flags, crm_ipc_proxied)) { + /* Mark this client as being the endpoint of a proxy connection. + * Proxy connections responses are sent on the event channel, to avoid + * blocking the controller serving as proxy. + */ + pcmk__set_client_flags(c, pcmk__client_proxied); + } + + if (header->size_compressed) { + int rc = 0; + unsigned int size_u = 1 + header->size_uncompressed; + uncompressed = calloc(1, size_u); + + crm_trace("Decompressing message data %u bytes into %u bytes", + header->size_compressed, size_u); + + rc = BZ2_bzBuffToBuffDecompress(uncompressed, &size_u, text, header->size_compressed, 1, 0); + text = uncompressed; + + if (rc != BZ_OK) { + crm_err("Decompression failed: %s " CRM_XS " bzerror=%d", + bz2_strerror(rc), rc); + free(uncompressed); + return NULL; + } + } + + CRM_ASSERT(text[header->size_uncompressed - 1] == 0); + + xml = string2xml(text); + crm_log_xml_trace(xml, "[IPC received]"); + + free(uncompressed); + return xml; +} + +static int crm_ipcs_flush_events(pcmk__client_t *c); + +static gboolean +crm_ipcs_flush_events_cb(gpointer data) +{ + pcmk__client_t *c = data; + + c->event_timer = 0; + crm_ipcs_flush_events(c); + return FALSE; +} + +/*! + * \internal + * \brief Add progressive delay before next event queue flush + * + * \param[in,out] c Client connection to add delay to + * \param[in] queue_len Current event queue length + */ +static inline void +delay_next_flush(pcmk__client_t *c, unsigned int queue_len) +{ + /* Delay a maximum of 1.5 seconds */ + guint delay = (queue_len < 5)? (1000 + 100 * queue_len) : 1500; + + c->event_timer = g_timeout_add(delay, crm_ipcs_flush_events_cb, c); +} + +/*! + * \internal + * \brief Send client any messages in its queue + * + * \param[in,out] c Client to flush + * + * \return Standard Pacemaker return value + */ +static int +crm_ipcs_flush_events(pcmk__client_t *c) +{ + int rc = pcmk_rc_ok; + ssize_t qb_rc = 0; + unsigned int sent = 0; + unsigned int queue_len = 0; + + if (c == NULL) { + return rc; + + } else if (c->event_timer) { + /* There is already a timer, wait until it goes off */ + crm_trace("Timer active for %p - %d", c->ipcs, c->event_timer); + return rc; + } + + if (c->event_queue) { + queue_len = g_queue_get_length(c->event_queue); + } + while (sent < 100) { + pcmk__ipc_header_t *header = NULL; + struct iovec *event = NULL; + + if (c->event_queue) { + // We don't pop unless send is successful + event = g_queue_peek_head(c->event_queue); + } + if (event == NULL) { // Queue is empty + break; + } + + qb_rc = qb_ipcs_event_sendv(c->ipcs, event, 2); + if (qb_rc < 0) { + rc = (int) -qb_rc; + break; + } + event = g_queue_pop_head(c->event_queue); + + sent++; + header = event[0].iov_base; + if (header->size_compressed) { + crm_trace("Event %d to %p[%d] (%lld compressed bytes) sent", + header->qb.id, c->ipcs, c->pid, (long long) qb_rc); + } else { + crm_trace("Event %d to %p[%d] (%lld bytes) sent: %.120s", + header->qb.id, c->ipcs, c->pid, (long long) qb_rc, + (char *) (event[1].iov_base)); + } + pcmk_free_ipc_event(event); + } + + queue_len -= sent; + if (sent > 0 || queue_len) { + crm_trace("Sent %d events (%d remaining) for %p[%d]: %s (%lld)", + sent, queue_len, c->ipcs, c->pid, + pcmk_rc_str(rc), (long long) qb_rc); + } + + if (queue_len) { + + /* Allow clients to briefly fall behind on processing incoming messages, + * but drop completely unresponsive clients so the connection doesn't + * consume resources indefinitely. + */ + if (queue_len > QB_MAX(c->queue_max, PCMK_IPC_DEFAULT_QUEUE_MAX)) { + if ((c->queue_backlog <= 1) || (queue_len < c->queue_backlog)) { + /* Don't evict for a new or shrinking backlog */ + crm_warn("Client with process ID %u has a backlog of %u messages " + CRM_XS " %p", c->pid, queue_len, c->ipcs); + } else { + crm_err("Evicting client with process ID %u due to backlog of %u messages " + CRM_XS " %p", c->pid, queue_len, c->ipcs); + c->queue_backlog = 0; + qb_ipcs_disconnect(c->ipcs); + return rc; + } + } + + c->queue_backlog = queue_len; + delay_next_flush(c, queue_len); + + } else { + /* Event queue is empty, there is no backlog */ + c->queue_backlog = 0; + } + + return rc; +} + +/*! + * \internal + * \brief Create an I/O vector for sending an IPC XML message + * + * \param[in] request Identifier for libqb response header + * \param[in,out] message XML message to send + * \param[in] max_send_size If 0, default IPC buffer size is used + * \param[out] result Where to store prepared I/O vector + * \param[out] bytes Size of prepared data in bytes + * + * \return Standard Pacemaker return code + */ +int +pcmk__ipc_prepare_iov(uint32_t request, xmlNode *message, + uint32_t max_send_size, struct iovec **result, + ssize_t *bytes) +{ + static unsigned int biggest = 0; + struct iovec *iov; + unsigned int total = 0; + char *compressed = NULL; + char *buffer = NULL; + pcmk__ipc_header_t *header = NULL; + + if ((message == NULL) || (result == NULL)) { + return EINVAL; + } + + header = calloc(1, sizeof(pcmk__ipc_header_t)); + if (header == NULL) { + return ENOMEM; /* errno mightn't be set by allocator */ + } + + buffer = dump_xml_unformatted(message); + + if (max_send_size == 0) { + max_send_size = crm_ipc_default_buffer_size(); + } + CRM_LOG_ASSERT(max_send_size != 0); + + *result = NULL; + iov = pcmk__new_ipc_event(); + iov[0].iov_len = sizeof(pcmk__ipc_header_t); + iov[0].iov_base = header; + + header->version = PCMK__IPC_VERSION; + header->size_uncompressed = 1 + strlen(buffer); + total = iov[0].iov_len + header->size_uncompressed; + + if (total < max_send_size) { + iov[1].iov_base = buffer; + iov[1].iov_len = header->size_uncompressed; + + } else { + unsigned int new_size = 0; + + if (pcmk__compress(buffer, (unsigned int) header->size_uncompressed, + (unsigned int) max_send_size, &compressed, + &new_size) == pcmk_rc_ok) { + + pcmk__set_ipc_flags(header->flags, "send data", crm_ipc_compressed); + header->size_compressed = new_size; + + iov[1].iov_len = header->size_compressed; + iov[1].iov_base = compressed; + + free(buffer); + + biggest = QB_MAX(header->size_compressed, biggest); + + } else { + crm_log_xml_trace(message, "EMSGSIZE"); + biggest = QB_MAX(header->size_uncompressed, biggest); + + crm_err("Could not compress %u-byte message into less than IPC " + "limit of %u bytes; set PCMK_ipc_buffer to higher value " + "(%u bytes suggested)", + header->size_uncompressed, max_send_size, 4 * biggest); + + free(compressed); + free(buffer); + pcmk_free_ipc_event(iov); + return EMSGSIZE; + } + } + + header->qb.size = iov[0].iov_len + iov[1].iov_len; + header->qb.id = (int32_t)request; /* Replying to a specific request */ + + *result = iov; + CRM_ASSERT(header->qb.size > 0); + if (bytes != NULL) { + *bytes = header->qb.size; + } + return pcmk_rc_ok; +} + +int +pcmk__ipc_send_iov(pcmk__client_t *c, struct iovec *iov, uint32_t flags) +{ + int rc = pcmk_rc_ok; + static uint32_t id = 1; + pcmk__ipc_header_t *header = iov[0].iov_base; + + if (c->flags & pcmk__client_proxied) { + /* _ALL_ replies to proxied connections need to be sent as events */ + if (!pcmk_is_set(flags, crm_ipc_server_event)) { + /* The proxied flag lets us know this was originally meant to be a + * response, even though we're sending it over the event channel. + */ + pcmk__set_ipc_flags(flags, "server event", + crm_ipc_server_event + |crm_ipc_proxied_relay_response); + } + } + + pcmk__set_ipc_flags(header->flags, "server event", flags); + if (flags & crm_ipc_server_event) { + header->qb.id = id++; /* We don't really use it, but doesn't hurt to set one */ + + if (flags & crm_ipc_server_free) { + crm_trace("Sending the original to %p[%d]", c->ipcs, c->pid); + add_event(c, iov); + + } else { + struct iovec *iov_copy = pcmk__new_ipc_event(); + + crm_trace("Sending a copy to %p[%d]", c->ipcs, c->pid); + iov_copy[0].iov_len = iov[0].iov_len; + iov_copy[0].iov_base = malloc(iov[0].iov_len); + memcpy(iov_copy[0].iov_base, iov[0].iov_base, iov[0].iov_len); + + iov_copy[1].iov_len = iov[1].iov_len; + iov_copy[1].iov_base = malloc(iov[1].iov_len); + memcpy(iov_copy[1].iov_base, iov[1].iov_base, iov[1].iov_len); + + add_event(c, iov_copy); + } + + } else { + ssize_t qb_rc; + + CRM_LOG_ASSERT(header->qb.id != 0); /* Replying to a specific request */ + + qb_rc = qb_ipcs_response_sendv(c->ipcs, iov, 2); + if (qb_rc < header->qb.size) { + if (qb_rc < 0) { + rc = (int) -qb_rc; + } + crm_notice("Response %d to pid %d failed: %s " + CRM_XS " bytes=%u rc=%lld ipcs=%p", + header->qb.id, c->pid, pcmk_rc_str(rc), + header->qb.size, (long long) qb_rc, c->ipcs); + + } else { + crm_trace("Response %d sent, %lld bytes to %p[%d]", + header->qb.id, (long long) qb_rc, c->ipcs, c->pid); + } + + if (flags & crm_ipc_server_free) { + pcmk_free_ipc_event(iov); + } + } + + if (flags & crm_ipc_server_event) { + rc = crm_ipcs_flush_events(c); + } else { + crm_ipcs_flush_events(c); + } + + if ((rc == EPIPE) || (rc == ENOTCONN)) { + crm_trace("Client %p disconnected", c->ipcs); + } + return rc; +} + +int +pcmk__ipc_send_xml(pcmk__client_t *c, uint32_t request, xmlNode *message, + uint32_t flags) +{ + struct iovec *iov = NULL; + int rc = pcmk_rc_ok; + + if (c == NULL) { + return EINVAL; + } + rc = pcmk__ipc_prepare_iov(request, message, crm_ipc_default_buffer_size(), + &iov, NULL); + if (rc == pcmk_rc_ok) { + pcmk__set_ipc_flags(flags, "send data", crm_ipc_server_free); + rc = pcmk__ipc_send_iov(c, iov, flags); + } else { + pcmk_free_ipc_event(iov); + crm_notice("IPC message to pid %d failed: %s " CRM_XS " rc=%d", + c->pid, pcmk_rc_str(rc), rc); + } + return rc; +} + +/*! + * \internal + * \brief Create an acknowledgement with a status code to send to a client + * + * \param[in] function Calling function + * \param[in] line Source file line within calling function + * \param[in] flags IPC flags to use when sending + * \param[in] tag Element name to use for acknowledgement + * \param[in] ver IPC protocol version (can be NULL) + * \param[in] status Exit status code to add to ack + * + * \return Newly created XML for ack + * \note The caller is responsible for freeing the return value with free_xml(). + */ +xmlNode * +pcmk__ipc_create_ack_as(const char *function, int line, uint32_t flags, + const char *tag, const char *ver, crm_exit_t status) +{ + xmlNode *ack = NULL; + + if (pcmk_is_set(flags, crm_ipc_client_response)) { + ack = create_xml_node(NULL, tag); + crm_xml_add(ack, "function", function); + crm_xml_add_int(ack, "line", line); + crm_xml_add_int(ack, "status", (int) status); + crm_xml_add(ack, PCMK__XA_IPC_PROTO_VERSION, ver); + } + return ack; +} + +/*! + * \internal + * \brief Send an acknowledgement with a status code to a client + * + * \param[in] function Calling function + * \param[in] line Source file line within calling function + * \param[in] c Client to send ack to + * \param[in] request Request ID being replied to + * \param[in] flags IPC flags to use when sending + * \param[in] tag Element name to use for acknowledgement + * \param[in] ver IPC protocol version (can be NULL) + * \param[in] status Status code to send with acknowledgement + * + * \return Standard Pacemaker return code + */ +int +pcmk__ipc_send_ack_as(const char *function, int line, pcmk__client_t *c, + uint32_t request, uint32_t flags, const char *tag, + const char *ver, crm_exit_t status) +{ + int rc = pcmk_rc_ok; + xmlNode *ack = pcmk__ipc_create_ack_as(function, line, flags, tag, ver, status); + + if (ack != NULL) { + crm_trace("Ack'ing IPC message from client %s as <%s status=%d>", + pcmk__client_name(c), tag, status); + c->request_id = 0; + rc = pcmk__ipc_send_xml(c, request, ack, flags); + free_xml(ack); + } + return rc; +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-based API + * + * \param[out] ipcs_ro New IPC server for read-only pacemaker-based API + * \param[out] ipcs_rw New IPC server for read/write pacemaker-based API + * \param[out] ipcs_shm New IPC server for shared-memory pacemaker-based API + * \param[in] ro_cb IPC callbacks for read-only API + * \param[in] rw_cb IPC callbacks for read/write and shared-memory APIs + * + * \note This function exits fatally if unable to create the servers. + */ +void pcmk__serve_based_ipc(qb_ipcs_service_t **ipcs_ro, + qb_ipcs_service_t **ipcs_rw, + qb_ipcs_service_t **ipcs_shm, + struct qb_ipcs_service_handlers *ro_cb, + struct qb_ipcs_service_handlers *rw_cb) +{ + *ipcs_ro = mainloop_add_ipc_server(PCMK__SERVER_BASED_RO, + QB_IPC_NATIVE, ro_cb); + + *ipcs_rw = mainloop_add_ipc_server(PCMK__SERVER_BASED_RW, + QB_IPC_NATIVE, rw_cb); + + *ipcs_shm = mainloop_add_ipc_server(PCMK__SERVER_BASED_SHM, + QB_IPC_SHM, rw_cb); + + if (*ipcs_ro == NULL || *ipcs_rw == NULL || *ipcs_shm == NULL) { + crm_err("Failed to create the CIB manager: exiting and inhibiting respawn"); + crm_warn("Verify pacemaker and pacemaker_remote are not both enabled"); + crm_exit(CRM_EX_FATAL); + } +} + +/*! + * \internal + * \brief Destroy IPC servers for pacemaker-based API + * + * \param[out] ipcs_ro IPC server for read-only pacemaker-based API + * \param[out] ipcs_rw IPC server for read/write pacemaker-based API + * \param[out] ipcs_shm IPC server for shared-memory pacemaker-based API + * + * \note This is a convenience function for calling qb_ipcs_destroy() for each + * argument. + */ +void +pcmk__stop_based_ipc(qb_ipcs_service_t *ipcs_ro, + qb_ipcs_service_t *ipcs_rw, + qb_ipcs_service_t *ipcs_shm) +{ + qb_ipcs_destroy(ipcs_ro); + qb_ipcs_destroy(ipcs_rw); + qb_ipcs_destroy(ipcs_shm); +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-controld API + * + * \param[in] cb IPC callbacks + * + * \return Newly created IPC server + */ +qb_ipcs_service_t * +pcmk__serve_controld_ipc(struct qb_ipcs_service_handlers *cb) +{ + return mainloop_add_ipc_server(CRM_SYSTEM_CRMD, QB_IPC_NATIVE, cb); +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-attrd API + * + * \param[out] ipcs Where to store newly created IPC server + * \param[in] cb IPC callbacks + * + * \note This function exits fatally if unable to create the servers. + */ +void +pcmk__serve_attrd_ipc(qb_ipcs_service_t **ipcs, + struct qb_ipcs_service_handlers *cb) +{ + *ipcs = mainloop_add_ipc_server(T_ATTRD, QB_IPC_NATIVE, cb); + + if (*ipcs == NULL) { + crm_err("Failed to create pacemaker-attrd server: exiting and inhibiting respawn"); + crm_warn("Verify pacemaker and pacemaker_remote are not both enabled."); + crm_exit(CRM_EX_FATAL); + } +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-fenced API + * + * \param[out] ipcs Where to store newly created IPC server + * \param[in] cb IPC callbacks + * + * \note This function exits fatally if unable to create the servers. + */ +void +pcmk__serve_fenced_ipc(qb_ipcs_service_t **ipcs, + struct qb_ipcs_service_handlers *cb) +{ + *ipcs = mainloop_add_ipc_server_with_prio("stonith-ng", QB_IPC_NATIVE, cb, + QB_LOOP_HIGH); + + if (*ipcs == NULL) { + crm_err("Failed to create fencer: exiting and inhibiting respawn."); + crm_warn("Verify pacemaker and pacemaker_remote are not both enabled."); + crm_exit(CRM_EX_FATAL); + } +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemakerd API + * + * \param[out] ipcs Where to store newly created IPC server + * \param[in] cb IPC callbacks + * + * \note This function exits with CRM_EX_OSERR if unable to create the servers. + */ +void +pcmk__serve_pacemakerd_ipc(qb_ipcs_service_t **ipcs, + struct qb_ipcs_service_handlers *cb) +{ + *ipcs = mainloop_add_ipc_server(CRM_SYSTEM_MCP, QB_IPC_NATIVE, cb); + + if (*ipcs == NULL) { + crm_err("Couldn't start pacemakerd IPC server"); + crm_warn("Verify pacemaker and pacemaker_remote are not both enabled."); + /* sub-daemons are observed by pacemakerd. Thus we exit CRM_EX_FATAL + * if we want to prevent pacemakerd from restarting them. + * With pacemakerd we leave the exit-code shown to e.g. systemd + * to what it was prior to moving the code here from pacemakerd.c + */ + crm_exit(CRM_EX_OSERR); + } +} + +/*! + * \internal + * \brief Add an IPC server to the main loop for the pacemaker-schedulerd API + * + * \param[in] cb IPC callbacks + * + * \return Newly created IPC server + * \note This function exits fatally if unable to create the servers. + */ +qb_ipcs_service_t * +pcmk__serve_schedulerd_ipc(struct qb_ipcs_service_handlers *cb) +{ + return mainloop_add_ipc_server(CRM_SYSTEM_PENGINE, QB_IPC_NATIVE, cb); +} + +/*! + * \brief Check whether string represents a client name used by cluster daemons + * + * \param[in] name String to check + * + * \return true if name is standard client name used by daemons, false otherwise + * + * \note This is provided by the client, and so cannot be used by itself as a + * secure means of authentication. + */ +bool +crm_is_daemon_name(const char *name) +{ + name = pcmk__message_name(name); + return (!strcmp(name, CRM_SYSTEM_CRMD) + || !strcmp(name, CRM_SYSTEM_STONITHD) + || !strcmp(name, "stonith-ng") + || !strcmp(name, "attrd") + || !strcmp(name, CRM_SYSTEM_CIB) + || !strcmp(name, CRM_SYSTEM_MCP) + || !strcmp(name, CRM_SYSTEM_DC) + || !strcmp(name, CRM_SYSTEM_TENGINE) + || !strcmp(name, CRM_SYSTEM_LRMD)); +} diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c new file mode 100644 index 0000000..3e000e1 --- /dev/null +++ b/lib/common/iso8601.c @@ -0,0 +1,1970 @@ +/* + * Copyright 2005-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +/* + * References: + * https://en.wikipedia.org/wiki/ISO_8601 + * http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Andrew's code was originally written for OSes whose "struct tm" contains: + * long tm_gmtoff; :: Seconds east of UTC + * const char *tm_zone; :: Timezone abbreviation + * Some OSes lack these, instead having: + * time_t (or long) timezone; + :: "difference between UTC and local standard time" + * char *tzname[2] = { "...", "..." }; + * I (David Lee) confess to not understanding the details. So my attempted + * generalisations for where their use is necessary may be flawed. + * + * 1. Does "difference between ..." subtract the same or opposite way? + * 2. Should it use "altzone" instead of "timezone"? + * 3. Should it use tzname[0] or tzname[1]? Interaction with timezone/altzone? + */ +#if defined(HAVE_STRUCT_TM_TM_GMTOFF) +# define GMTOFF(tm) ((tm)->tm_gmtoff) +#else +/* Note: extern variable; macro argument not actually used. */ +# define GMTOFF(tm) (-timezone+daylight) +#endif + +#define HOUR_SECONDS (60 * 60) +#define DAY_SECONDS (HOUR_SECONDS * 24) + +/*! + * \internal + * \brief Validate a seconds/microseconds tuple + * + * The microseconds value must be in the correct range, and if both are nonzero + * they must have the same sign. + * + * \param[in] sec Seconds + * \param[in] usec Microseconds + * + * \return true if the seconds/microseconds tuple is valid, or false otherwise + */ +#define valid_sec_usec(sec, usec) \ + ((QB_ABS(usec) < QB_TIME_US_IN_SEC) \ + && (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0)))) + +// A date/time or duration +struct crm_time_s { + int years; // Calendar year (date/time) or number of years (duration) + int months; // Number of months (duration only) + int days; // Ordinal day of year (date/time) or number of days (duration) + int seconds; // Seconds of day (date/time) or number of seconds (duration) + int offset; // Seconds offset from UTC (date/time only) + bool duration; // True if duration +}; + +static crm_time_t *parse_date(const char *date_str); + +static crm_time_t * +crm_get_utc_time(const crm_time_t *dt) +{ + crm_time_t *utc = NULL; + + if (dt == NULL) { + errno = EINVAL; + return NULL; + } + + utc = crm_time_new_undefined(); + utc->years = dt->years; + utc->days = dt->days; + utc->seconds = dt->seconds; + utc->offset = 0; + + if (dt->offset) { + crm_time_add_seconds(utc, -dt->offset); + } else { + /* Durations (which are the only things that can include months, never have a timezone */ + utc->months = dt->months; + } + + crm_time_log(LOG_TRACE, "utc-source", dt, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); + crm_time_log(LOG_TRACE, "utc-target", utc, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); + return utc; +} + +crm_time_t * +crm_time_new(const char *date_time) +{ + tzset(); + if (date_time == NULL) { + return pcmk__copy_timet(time(NULL)); + } + return parse_date(date_time); +} + +/*! + * \brief Allocate memory for an uninitialized time object + * + * \return Newly allocated time object + * \note The caller is responsible for freeing the return value using + * crm_time_free(). + */ +crm_time_t * +crm_time_new_undefined(void) +{ + crm_time_t *result = calloc(1, sizeof(crm_time_t)); + + CRM_ASSERT(result != NULL); + return result; +} + +/*! + * \brief Check whether a time object has been initialized yet + * + * \param[in] t Time object to check + * + * \return TRUE if time object has been initialized, FALSE otherwise + */ +bool +crm_time_is_defined(const crm_time_t *t) +{ + // Any nonzero member indicates something has been done to t + return (t != NULL) && (t->years || t->months || t->days || t->seconds + || t->offset || t->duration); +} + +void +crm_time_free(crm_time_t * dt) +{ + if (dt == NULL) { + return; + } + free(dt); +} + +static int +year_days(int year) +{ + int d = 365; + + if (crm_time_leapyear(year)) { + d++; + } + return d; +} + +/* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt : + * + * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7) + * YY = (Y-1) % 100 + * C = (Y-1) - YY + * G = YY + YY/4 + * Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7) + */ +int +crm_time_january1_weekday(int year) +{ + int YY = (year - 1) % 100; + int C = (year - 1) - YY; + int G = YY + YY / 4; + int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7); + + crm_trace("YY=%d, C=%d, G=%d", YY, C, G); + crm_trace("January 1 %.4d: %d", year, jan1); + return jan1; +} + +int +crm_time_weeks_in_year(int year) +{ + int weeks = 52; + int jan1 = crm_time_january1_weekday(year); + + /* if jan1 == thursday */ + if (jan1 == 4) { + weeks++; + } else { + jan1 = crm_time_january1_weekday(year + 1); + /* if dec31 == thursday aka. jan1 of next year is a friday */ + if (jan1 == 5) { + weeks++; + } + + } + return weeks; +} + +// Jan-Dec plus Feb of leap years +static int month_days[13] = { + 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29 +}; + +/*! + * \brief Return number of days in given month of given year + * + * \param[in] Ordinal month (1-12) + * \param[in] Gregorian year + * + * \return Number of days in given month (0 if given month is invalid) + */ +int +crm_time_days_in_month(int month, int year) +{ + if ((month < 1) || (month > 12)) { + return 0; + } + if ((month == 2) && crm_time_leapyear(year)) { + month = 13; + } + return month_days[month - 1]; +} + +bool +crm_time_leapyear(int year) +{ + gboolean is_leap = FALSE; + + if (year % 4 == 0) { + is_leap = TRUE; + } + if (year % 100 == 0 && year % 400 != 0) { + is_leap = FALSE; + } + return is_leap; +} + +static uint32_t +get_ordinal_days(uint32_t y, uint32_t m, uint32_t d) +{ + int lpc; + + for (lpc = 1; lpc < m; lpc++) { + d += crm_time_days_in_month(lpc, y); + } + return d; +} + +void +crm_time_log_alias(int log_level, const char *file, const char *function, + int line, const char *prefix, const crm_time_t *date_time, + int flags) +{ + char *date_s = crm_time_as_string(date_time, flags); + + if (log_level == LOG_STDOUT) { + printf("%s%s%s\n", + (prefix? prefix : ""), (prefix? ": " : ""), date_s); + } else { + do_crm_log_alias(log_level, file, function, line, "%s%s%s", + (prefix? prefix : ""), (prefix? ": " : ""), date_s); + } + free(date_s); +} + +static void +crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s) +{ + uint32_t hours, minutes, seconds; + + seconds = QB_ABS(sec); + + hours = seconds / HOUR_SECONDS; + seconds -= HOUR_SECONDS * hours; + + minutes = seconds / 60; + seconds -= 60 * minutes; + + crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + sec, hours, minutes, seconds); + + *h = hours; + *m = minutes; + *s = seconds; +} + +int +crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m, + uint32_t *s) +{ + crm_time_get_sec(dt->seconds, h, m, s); + return TRUE; +} + +int +crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m) +{ + uint32_t s; + + crm_time_get_sec(dt->seconds, h, m, &s); + return TRUE; +} + +long long +crm_time_get_seconds(const crm_time_t *dt) +{ + int lpc; + crm_time_t *utc = NULL; + long long in_seconds = 0; + + if (dt == NULL) { + return 0; + } + + utc = crm_get_utc_time(dt); + if (utc == NULL) { + return 0; + } + + for (lpc = 1; lpc < utc->years; lpc++) { + long long dmax = year_days(lpc); + + in_seconds += DAY_SECONDS * dmax; + } + + /* utc->months is an offset that can only be set for a duration. + * By definition, the value is variable depending on the date to + * which it is applied. + * + * Force 30-day months so that something vaguely sane happens + * for anyone that tries to use a month in this way. + */ + if (utc->months > 0) { + in_seconds += DAY_SECONDS * 30 * (long long) (utc->months); + } + + if (utc->days > 0) { + in_seconds += DAY_SECONDS * (long long) (utc->days - 1); + } + in_seconds += utc->seconds; + + crm_time_free(utc); + return in_seconds; +} + +#define EPOCH_SECONDS 62135596800ULL /* Calculated using crm_time_get_seconds() */ +long long +crm_time_get_seconds_since_epoch(const crm_time_t *dt) +{ + return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS); +} + +int +crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m, + uint32_t *d) +{ + int months = 0; + int days = dt->days; + + if(dt->years != 0) { + for (months = 1; months <= 12 && days > 0; months++) { + int mdays = crm_time_days_in_month(months, dt->years); + + if (mdays >= days) { + break; + } else { + days -= mdays; + } + } + + } else if (dt->months) { + /* This is a duration including months, don't convert the days field */ + months = dt->months; + + } else { + /* This is a duration not including months, still don't convert the days field */ + } + + *y = dt->years; + *m = months; + *d = days; + crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days); + return TRUE; +} + +int +crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d) +{ + *y = dt->years; + *d = dt->days; + return TRUE; +} + +int +crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w, + uint32_t *d) +{ + /* + * Monday 29 December 2008 is written "2009-W01-1" + * Sunday 3 January 2010 is written "2009-W53-7" + */ + int year_num = 0; + int jan1 = crm_time_january1_weekday(dt->years); + int h = -1; + + CRM_CHECK(dt->days > 0, return FALSE); + +/* 6. Find the Weekday for Y M D */ + h = dt->days + jan1 - 1; + *d = 1 + ((h - 1) % 7); + +/* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */ + if (dt->days <= (8 - jan1) && jan1 > 4) { + crm_trace("year--, jan1=%d", jan1); + year_num = dt->years - 1; + *w = crm_time_weeks_in_year(year_num); + + } else { + year_num = dt->years; + } + +/* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */ + if (year_num == dt->years) { + int dmax = year_days(year_num); + int correction = 4 - *d; + + if ((dmax - dt->days) < correction) { + crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction); + year_num = dt->years + 1; + *w = 1; + } + } + +/* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */ + if (year_num == dt->years) { + int j = dt->days + (7 - *d) + (jan1 - 1); + + *w = j / 7; + if (jan1 > 4) { + *w -= 1; + } + } + + *y = year_num; + crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, + dt->years, dt->days, *y, *w, *d); + return TRUE; +} + +#define DATE_MAX 128 + +/*! + * \internal + * \brief Print "." to a buffer + * + * \param[in] sec Seconds + * \param[in] usec Microseconds (must be of same sign as \p sec and of + * absolute value less than \p QB_TIME_US_IN_SEC) + * \param[in,out] buf Result buffer + * \param[in,out] offset Current offset within \p buf + */ +static inline void +sec_usec_as_string(long long sec, int usec, char *buf, size_t *offset) +{ + *offset += snprintf(buf + *offset, DATE_MAX - *offset, "%s%lld.%06d", + ((sec == 0) && (usec < 0))? "-" : "", + sec, QB_ABS(usec)); +} + +/*! + * \internal + * \brief Get a string representation of a duration + * + * \param[in] dt Time object to interpret as a duration + * \param[in] usec Microseconds to add to \p dt + * \param[in] show_usec Whether to include microseconds in \p result + * \param[out] result Where to store the result string + */ +static void +crm_duration_as_string(const crm_time_t *dt, int usec, bool show_usec, + char *result) +{ + size_t offset = 0; + + CRM_ASSERT(valid_sec_usec(dt->seconds, usec)); + + if (dt->years) { + offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ", + dt->years, pcmk__plural_s(dt->years)); + } + if (dt->months) { + offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ", + dt->months, pcmk__plural_s(dt->months)); + } + if (dt->days) { + offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ", + dt->days, pcmk__plural_s(dt->days)); + } + + // At least print seconds (and optionally usecs) + if ((offset == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) { + if (show_usec) { + sec_usec_as_string(dt->seconds, usec, result, &offset); + } else { + offset += snprintf(result + offset, DATE_MAX - offset, "%d", + dt->seconds); + } + offset += snprintf(result + offset, DATE_MAX - offset, " second%s", + pcmk__plural_s(dt->seconds)); + } + + // More than one minute, so provide a more readable breakdown into units + if (QB_ABS(dt->seconds) >= 60) { + uint32_t h = 0; + uint32_t m = 0; + uint32_t s = 0; + uint32_t u = QB_ABS(usec); + bool print_sec_component = false; + + crm_time_get_sec(dt->seconds, &h, &m, &s); + print_sec_component = ((s != 0) || (show_usec && (u != 0))); + + offset += snprintf(result + offset, DATE_MAX - offset, " ("); + + if (h) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32 " hour%s%s", h, pcmk__plural_s(h), + ((m != 0) || print_sec_component)? " " : ""); + } + + if (m) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32 " minute%s%s", m, pcmk__plural_s(m), + print_sec_component? " " : ""); + } + + if (print_sec_component) { + if (show_usec) { + sec_usec_as_string(s, u, result, &offset); + } else { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32, s); + } + offset += snprintf(result + offset, DATE_MAX - offset, " second%s", + pcmk__plural_s(dt->seconds)); + } + + offset += snprintf(result + offset, DATE_MAX - offset, ")"); + } +} + +/*! + * \internal + * \brief Get a string representation of a time object + * + * \param[in] dt Time to convert to string + * \param[in] usec Microseconds to add to \p dt + * \param[in] flags Group of \p crm_time_* string format options + * \param[out] result Where to store the result string + * + * \note \p result must be of size \p DATE_MAX or larger. + */ +static void +time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags, + char *result) +{ + crm_time_t *utc = NULL; + size_t offset = 0; + + if (!crm_time_is_defined(dt)) { + strcpy(result, ""); + return; + } + + CRM_ASSERT(valid_sec_usec(dt->seconds, usec)); + + /* Simple cases: as duration, seconds, or seconds since epoch. + * These never depend on time zone. + */ + + if (pcmk_is_set(flags, crm_time_log_duration)) { + crm_duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs), + result); + return; + } + + if (pcmk_any_flags_set(flags, crm_time_seconds|crm_time_epoch)) { + long long seconds = 0; + + if (pcmk_is_set(flags, crm_time_seconds)) { + seconds = crm_time_get_seconds(dt); + } else { + seconds = crm_time_get_seconds_since_epoch(dt); + } + + if (pcmk_is_set(flags, crm_time_usecs)) { + sec_usec_as_string(seconds, usec, result, &offset); + } else { + snprintf(result, DATE_MAX, "%lld", seconds); + } + return; + } + + // Convert to UTC if local timezone was not requested + if ((dt->offset != 0) && !pcmk_is_set(flags, crm_time_log_with_timezone)) { + crm_trace("UTC conversion"); + utc = crm_get_utc_time(dt); + dt = utc; + } + + // As readable string + + if (pcmk_is_set(flags, crm_time_log_date)) { + if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D + uint32_t y, w, d; + + if (crm_time_get_isoweek(dt, &y, &w, &d)) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32, + y, w, d); + } + + } else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD + uint32_t y, d; + + if (crm_time_get_ordinal(dt, &y, &d)) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%" PRIu32 "-%.3" PRIu32, y, d); + } + + } else { // YYYY-MM-DD + uint32_t y, m, d; + + if (crm_time_get_gregorian(dt, &y, &m, &d)) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, + y, m, d); + } + } + } + + if (pcmk_is_set(flags, crm_time_log_timeofday)) { + uint32_t h = 0, m = 0, s = 0; + + if (offset > 0) { + offset += snprintf(result + offset, DATE_MAX - offset, " "); + } + + if (crm_time_get_timeofday(dt, &h, &m, &s)) { + offset += snprintf(result + offset, DATE_MAX - offset, + "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + h, m, s); + + if (pcmk_is_set(flags, crm_time_usecs)) { + offset += snprintf(result + offset, DATE_MAX - offset, + ".%06" PRIu32, QB_ABS(usec)); + } + } + + if (pcmk_is_set(flags, crm_time_log_with_timezone) + && (dt->offset != 0)) { + crm_time_get_sec(dt->offset, &h, &m, &s); + offset += snprintf(result + offset, DATE_MAX - offset, + " %c%.2" PRIu32 ":%.2" PRIu32, + ((dt->offset < 0)? '-' : '+'), h, m); + } else { + offset += snprintf(result + offset, DATE_MAX - offset, "Z"); + } + } + + crm_time_free(utc); +} + +/*! + * \brief Get a string representation of a \p crm_time_t object + * + * \param[in] dt Time to convert to string + * \param[in] flags Group of \p crm_time_* string format options + * + * \note The caller is responsible for freeing the return value using \p free(). + */ +char * +crm_time_as_string(const crm_time_t *dt, int flags) +{ + char result[DATE_MAX] = { '\0', }; + char *result_copy = NULL; + + time_as_string_common(dt, 0, flags, result); + + pcmk__str_update(&result_copy, result); + return result_copy; +} + +/*! + * \internal + * \brief Determine number of seconds from an hour:minute:second string + * + * \param[in] time_str Time specification string + * \param[out] result Number of seconds equivalent to time_str + * + * \return TRUE if specification was valid, FALSE (and set errno) otherwise + * \note This may return the number of seconds in a day (which is out of bounds + * for a time object) if given 24:00:00. + */ +static bool +crm_time_parse_sec(const char *time_str, int *result) +{ + int rc; + uint32_t hour = 0; + uint32_t minute = 0; + uint32_t second = 0; + + *result = 0; + + // Must have at least hour, but minutes and seconds are optional + rc = sscanf(time_str, "%" SCNu32 ":%" SCNu32 ":%" SCNu32, + &hour, &minute, &second); + if (rc == 1) { + rc = sscanf(time_str, "%2" SCNu32 "%2" SCNu32 "%2" SCNu32, + &hour, &minute, &second); + } + if (rc == 0) { + crm_err("%s is not a valid ISO 8601 time specification", time_str); + errno = EINVAL; + return FALSE; + } + + crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32, + hour, minute, second); + + if ((hour == 24) && (minute == 0) && (second == 0)) { + // Equivalent to 00:00:00 of next day, return number of seconds in day + } else if (hour >= 24) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid hour", time_str, hour); + errno = EINVAL; + return FALSE; + } + if (minute >= 60) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid minute", time_str, minute); + errno = EINVAL; + return FALSE; + } + if (second >= 60) { + crm_err("%s is not a valid ISO 8601 time specification " + "because %" PRIu32 " is not a valid second", time_str, second); + errno = EINVAL; + return FALSE; + } + + *result = (hour * HOUR_SECONDS) + (minute * 60) + second; + return TRUE; +} + +static bool +crm_time_parse_offset(const char *offset_str, int *offset) +{ + tzset(); + + if (offset_str == NULL) { + // Use local offset +#if defined(HAVE_STRUCT_TM_TM_GMTOFF) + time_t now = time(NULL); + struct tm *now_tm = localtime(&now); +#endif + int h_offset = GMTOFF(now_tm) / HOUR_SECONDS; + int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60; + + if (h_offset < 0 && m_offset < 0) { + m_offset = 0 - m_offset; + } + *offset = (HOUR_SECONDS * h_offset) + (60 * m_offset); + return TRUE; + } + + if (offset_str[0] == 'Z') { // @TODO invalid if anything after? + *offset = 0; + return TRUE; + } + + *offset = 0; + if ((offset_str[0] == '+') || (offset_str[0] == '-') + || isdigit((int)offset_str[0])) { + + gboolean negate = FALSE; + + if (offset_str[0] == '+') { + offset_str++; + } else if (offset_str[0] == '-') { + negate = TRUE; + offset_str++; + } + if (crm_time_parse_sec(offset_str, offset) == FALSE) { + return FALSE; + } + if (negate) { + *offset = 0 - *offset; + } + } // @TODO else invalid? + return TRUE; +} + +/*! + * \internal + * \brief Parse the time portion of an ISO 8601 date/time string + * + * \param[in] time_str Time portion of specification (after any 'T') + * \param[in,out] a_time Time object to parse into + * + * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise + * \note This may add a day to a_time (if the time is 24:00:00). + */ +static bool +crm_time_parse(const char *time_str, crm_time_t *a_time) +{ + uint32_t h, m, s; + char *offset_s = NULL; + + tzset(); + + if (time_str) { + if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) { + return FALSE; + } + offset_s = strstr(time_str, "Z"); + if (offset_s == NULL) { + offset_s = strstr(time_str, " "); + if (offset_s) { + while (isspace(offset_s[0])) { + offset_s++; + } + } + } + } + + if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) { + return FALSE; + } + crm_time_get_sec(a_time->offset, &h, &m, &s); + crm_trace("Got tz: %c%2." PRIu32 ":%.2" PRIu32, + (a_time->offset < 0)? '-' : '+', h, m); + + if (a_time->seconds == DAY_SECONDS) { + // 24:00:00 == 00:00:00 of next day + a_time->seconds = 0; + crm_time_add_days(a_time, 1); + } + return TRUE; +} + +/* + * \internal + * \brief Parse a time object from an ISO 8601 date/time specification + * + * \param[in] date_str ISO 8601 date/time specification (or "epoch") + * + * \return New time object on success, NULL (and set errno) otherwise + */ +static crm_time_t * +parse_date(const char *date_str) +{ + const char *time_s = NULL; + crm_time_t *dt = NULL; + + int year = 0; + int month = 0; + int week = 0; + int day = 0; + int rc = 0; + + if (pcmk__str_empty(date_str)) { + crm_err("No ISO 8601 date/time specification given"); + goto invalid; + } + + if ((date_str[0] == 'T') || (date_str[2] == ':')) { + /* Just a time supplied - Infer current date */ + dt = crm_time_new(NULL); + if (date_str[0] == 'T') { + time_s = date_str + 1; + } else { + time_s = date_str; + } + goto parse_time; + } + + dt = crm_time_new_undefined(); + + if (!strncasecmp("epoch", date_str, 5) + && ((date_str[5] == '\0') || (date_str[5] == '/') || isspace(date_str[5]))) { + dt->days = 1; + dt->years = 1970; + crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); + return dt; + } + + /* YYYY-MM-DD */ + rc = sscanf(date_str, "%d-%d-%d", &year, &month, &day); + if (rc == 1) { + /* YYYYMMDD */ + rc = sscanf(date_str, "%4d%2d%2d", &year, &month, &day); + } + if (rc == 3) { + if (month > 12) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid month", date_str, month); + goto invalid; + } else if (day > crm_time_days_in_month(month, year)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid day of the month", + date_str, day); + goto invalid; + } else { + dt->years = year; + dt->days = get_ordinal_days(year, month, day); + crm_trace("Parsed Gregorian date '%.4d-%.3d' from date string '%s'", + year, dt->days, date_str); + } + goto parse_time; + } + + /* YYYY-DDD */ + rc = sscanf(date_str, "%d-%d", &year, &day); + if (rc == 2) { + if (day > year_days(year)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid day of the year (max %d)", + date_str, day, year_days(year)); + goto invalid; + } + crm_trace("Parsed ordinal year %d and days %d from date string '%s'", + year, day, date_str); + dt->days = day; + dt->years = year; + goto parse_time; + } + + /* YYYY-Www-D */ + rc = sscanf(date_str, "%d-W%d-%d", &year, &week, &day); + if (rc == 3) { + if (week > crm_time_weeks_in_year(year)) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid week of the year (max %d)", + date_str, week, crm_time_weeks_in_year(year)); + goto invalid; + } else if (day < 1 || day > 7) { + crm_err("'%s' is not a valid ISO 8601 date/time specification " + "because '%d' is not a valid day of the week", + date_str, day); + goto invalid; + } else { + /* + * See https://en.wikipedia.org/wiki/ISO_week_date + * + * Monday 29 December 2008 is written "2009-W01-1" + * Sunday 3 January 2010 is written "2009-W53-7" + * Saturday 27 September 2008 is written "2008-W37-6" + * + * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01. + * If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year. + */ + int jan1 = crm_time_january1_weekday(year); + + crm_trace("Got year %d (Jan 1 = %d), week %d, and day %d from date string '%s'", + year, jan1, week, day, date_str); + + dt->years = year; + crm_time_add_days(dt, (week - 1) * 7); + + if (jan1 <= 4) { + crm_time_add_days(dt, 1 - jan1); + } else { + crm_time_add_days(dt, 8 - jan1); + } + + crm_time_add_days(dt, day); + } + goto parse_time; + } + + crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str); + goto invalid; + + parse_time: + + if (time_s == NULL) { + time_s = date_str + strspn(date_str, "0123456789-W"); + if ((time_s[0] == ' ') || (time_s[0] == 'T')) { + ++time_s; + } else { + time_s = NULL; + } + } + if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) { + goto invalid; + } + + crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday); + if (crm_time_check(dt) == FALSE) { + crm_err("'%s' is not a valid ISO 8601 date/time specification", + date_str); + goto invalid; + } + return dt; + +invalid: + crm_time_free(dt); + errno = EINVAL; + return NULL; +} + +// Parse an ISO 8601 numeric value and return number of characters consumed +// @TODO This cannot handle >INT_MAX int values +// @TODO Fractions appear to be not working +// @TODO Error out on invalid specifications +static int +parse_int(const char *str, int field_width, int upper_bound, int *result) +{ + int lpc = 0; + int offset = 0; + int intermediate = 0; + gboolean fraction = FALSE; + gboolean negate = FALSE; + + *result = 0; + if (*str == '\0') { + return 0; + } + + if (str[offset] == 'T') { + offset++; + } + + if (str[offset] == '.' || str[offset] == ',') { + fraction = TRUE; + field_width = -1; + offset++; + } else if (str[offset] == '-') { + negate = TRUE; + offset++; + } else if (str[offset] == '+' || str[offset] == ':') { + offset++; + } + + for (; (fraction || lpc < field_width) && isdigit((int)str[offset]); lpc++) { + if (fraction) { + intermediate = (str[offset] - '0') / (10 ^ lpc); + } else { + *result *= 10; + intermediate = str[offset] - '0'; + } + *result += intermediate; + offset++; + } + if (fraction) { + *result = (int)(*result * upper_bound); + + } else if (upper_bound > 0 && *result > upper_bound) { + *result = upper_bound; + } + if (negate) { + *result = 0 - *result; + } + if (lpc > 0) { + crm_trace("Found int: %d. Stopped at str[%d]='%c'", *result, lpc, str[lpc]); + return offset; + } + return 0; +} + +/*! + * \brief Parse a time duration from an ISO 8601 duration specification + * + * \param[in] period_s ISO 8601 duration specification (optionally followed by + * whitespace, after which the rest of the string will be + * ignored) + * + * \return New time object on success, NULL (and set errno) otherwise + * \note It is the caller's responsibility to return the result using + * crm_time_free(). + */ +crm_time_t * +crm_time_parse_duration(const char *period_s) +{ + gboolean is_time = FALSE; + crm_time_t *diff = NULL; + + if (pcmk__str_empty(period_s)) { + crm_err("No ISO 8601 time duration given"); + goto invalid; + } + if (period_s[0] != 'P') { + crm_err("'%s' is not a valid ISO 8601 time duration " + "because it does not start with a 'P'", period_s); + goto invalid; + } + if ((period_s[1] == '\0') || isspace(period_s[1])) { + crm_err("'%s' is not a valid ISO 8601 time duration " + "because nothing follows 'P'", period_s); + goto invalid; + } + + diff = crm_time_new_undefined(); + diff->duration = TRUE; + + for (const char *current = period_s + 1; + current[0] && (current[0] != '/') && !isspace(current[0]); + ++current) { + + int an_int = 0, rc; + + if (current[0] == 'T') { + /* A 'T' separates year/month/day from hour/minute/seconds. We don't + * require it strictly, but just use it to differentiate month from + * minutes. + */ + is_time = TRUE; + continue; + } + + // An integer must be next + rc = parse_int(current, 10, 0, &an_int); + if (rc == 0) { + crm_err("'%s' is not a valid ISO 8601 time duration " + "because no integer at '%s'", period_s, current); + goto invalid; + } + current += rc; + + // A time unit must be next (we're not strict about the order) + switch (current[0]) { + case 'Y': + diff->years = an_int; + break; + case 'M': + if (is_time) { + /* Minutes */ + diff->seconds += an_int * 60; + } else { + diff->months = an_int; + } + break; + case 'W': + diff->days += an_int * 7; + break; + case 'D': + diff->days += an_int; + break; + case 'H': + diff->seconds += an_int * HOUR_SECONDS; + break; + case 'S': + diff->seconds += an_int; + break; + case '\0': + crm_err("'%s' is not a valid ISO 8601 time duration " + "because no units after %d", period_s, an_int); + goto invalid; + default: + crm_err("'%s' is not a valid ISO 8601 time duration " + "because '%c' is not a valid time unit", + period_s, current[0]); + goto invalid; + } + } + + if (!crm_time_is_defined(diff)) { + crm_err("'%s' is not a valid ISO 8601 time duration " + "because no amounts and units given", period_s); + goto invalid; + } + return diff; + +invalid: + crm_time_free(diff); + errno = EINVAL; + return NULL; +} + +/*! + * \brief Parse a time period from an ISO 8601 interval specification + * + * \param[in] period_str ISO 8601 interval specification (start/end, + * start/duration, or duration/end) + * + * \return New time period object on success, NULL (and set errno) otherwise + * \note The caller is responsible for freeing the result using + * crm_time_free_period(). + */ +crm_time_period_t * +crm_time_parse_period(const char *period_str) +{ + const char *original = period_str; + crm_time_period_t *period = NULL; + + if (pcmk__str_empty(period_str)) { + crm_err("No ISO 8601 time period given"); + goto invalid; + } + + tzset(); + period = calloc(1, sizeof(crm_time_period_t)); + CRM_ASSERT(period != NULL); + + if (period_str[0] == 'P') { + period->diff = crm_time_parse_duration(period_str); + if (period->diff == NULL) { + goto error; + } + } else { + period->start = parse_date(period_str); + if (period->start == NULL) { + goto error; + } + } + + period_str = strstr(original, "/"); + if (period_str) { + ++period_str; + if (period_str[0] == 'P') { + if (period->diff != NULL) { + crm_err("'%s' is not a valid ISO 8601 time period " + "because it has two durations", + original); + goto invalid; + } + period->diff = crm_time_parse_duration(period_str); + if (period->diff == NULL) { + goto error; + } + } else { + period->end = parse_date(period_str); + if (period->end == NULL) { + goto error; + } + } + + } else if (period->diff != NULL) { + // Only duration given, assume start is now + period->start = crm_time_new(NULL); + + } else { + // Only start given + crm_err("'%s' is not a valid ISO 8601 time period " + "because it has no duration or ending time", + original); + goto invalid; + } + + if (period->start == NULL) { + period->start = crm_time_subtract(period->end, period->diff); + + } else if (period->end == NULL) { + period->end = crm_time_add(period->start, period->diff); + } + + if (crm_time_check(period->start) == FALSE) { + crm_err("'%s' is not a valid ISO 8601 time period " + "because the start is invalid", period_str); + goto invalid; + } + if (crm_time_check(period->end) == FALSE) { + crm_err("'%s' is not a valid ISO 8601 time period " + "because the end is invalid", period_str); + goto invalid; + } + return period; + +invalid: + errno = EINVAL; +error: + crm_time_free_period(period); + return NULL; +} + +/*! + * \brief Free a dynamically allocated time period object + * + * \param[in,out] period Time period to free + */ +void +crm_time_free_period(crm_time_period_t *period) +{ + if (period) { + crm_time_free(period->start); + crm_time_free(period->end); + crm_time_free(period->diff); + free(period); + } +} + +void +crm_time_set(crm_time_t *target, const crm_time_t *source) +{ + crm_trace("target=%p, source=%p", target, source); + + CRM_CHECK(target != NULL && source != NULL, return); + + target->years = source->years; + target->days = source->days; + target->months = source->months; /* Only for durations */ + target->seconds = source->seconds; + target->offset = source->offset; + + crm_time_log(LOG_TRACE, "source", source, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); + crm_time_log(LOG_TRACE, "target", target, + crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone); +} + +static void +ha_set_tm_time(crm_time_t *target, const struct tm *source) +{ + int h_offset = 0; + int m_offset = 0; + + /* Ensure target is fully initialized */ + target->years = 0; + target->months = 0; + target->days = 0; + target->seconds = 0; + target->offset = 0; + target->duration = FALSE; + + if (source->tm_year > 0) { + /* years since 1900 */ + target->years = 1900 + source->tm_year; + } + + if (source->tm_yday >= 0) { + /* days since January 1 [0-365] */ + target->days = 1 + source->tm_yday; + } + + if (source->tm_hour >= 0) { + target->seconds += HOUR_SECONDS * source->tm_hour; + } + if (source->tm_min >= 0) { + target->seconds += 60 * source->tm_min; + } + if (source->tm_sec >= 0) { + target->seconds += source->tm_sec; + } + + /* tm_gmtoff == offset from UTC in seconds */ + h_offset = GMTOFF(source) / HOUR_SECONDS; + m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60; + crm_trace("Time offset is %lds (%.2d:%.2d)", + GMTOFF(source), h_offset, m_offset); + + target->offset += HOUR_SECONDS * h_offset; + target->offset += 60 * m_offset; +} + +void +crm_time_set_timet(crm_time_t *target, const time_t *source) +{ + ha_set_tm_time(target, localtime(source)); +} + +crm_time_t * +pcmk_copy_time(const crm_time_t *source) +{ + crm_time_t *target = crm_time_new_undefined(); + + crm_time_set(target, source); + return target; +} + +/*! + * \internal + * \brief Convert a \p time_t time to a \p crm_time_t time + * + * \param[in] source Time to convert + * + * \return A \p crm_time_t object representing \p source + */ +crm_time_t * +pcmk__copy_timet(time_t source) +{ + crm_time_t *target = crm_time_new_undefined(); + + crm_time_set_timet(target, &source); + return target; +} + +crm_time_t * +crm_time_add(const crm_time_t *dt, const crm_time_t *value) +{ + crm_time_t *utc = NULL; + crm_time_t *answer = NULL; + + if ((dt == NULL) || (value == NULL)) { + errno = EINVAL; + return NULL; + } + + answer = pcmk_copy_time(dt); + + utc = crm_get_utc_time(value); + if (utc == NULL) { + crm_time_free(answer); + return NULL; + } + + answer->years += utc->years; + crm_time_add_months(answer, utc->months); + crm_time_add_days(answer, utc->days); + crm_time_add_seconds(answer, utc->seconds); + + crm_time_free(utc); + return answer; +} + +crm_time_t * +crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value) +{ + crm_time_t *utc = NULL; + crm_time_t *answer = NULL; + + if ((dt == NULL) || (value == NULL)) { + errno = EINVAL; + return NULL; + } + + utc = crm_get_utc_time(value); + if (utc == NULL) { + return NULL; + } + + answer = crm_get_utc_time(dt); + if (answer == NULL) { + crm_time_free(utc); + return NULL; + } + answer->duration = TRUE; + + answer->years -= utc->years; + if(utc->months != 0) { + crm_time_add_months(answer, -utc->months); + } + crm_time_add_days(answer, -utc->days); + crm_time_add_seconds(answer, -utc->seconds); + + crm_time_free(utc); + return answer; +} + +crm_time_t * +crm_time_subtract(const crm_time_t *dt, const crm_time_t *value) +{ + crm_time_t *utc = NULL; + crm_time_t *answer = NULL; + + if ((dt == NULL) || (value == NULL)) { + errno = EINVAL; + return NULL; + } + + utc = crm_get_utc_time(value); + if (utc == NULL) { + return NULL; + } + + answer = pcmk_copy_time(dt); + answer->years -= utc->years; + if(utc->months != 0) { + crm_time_add_months(answer, -utc->months); + } + crm_time_add_days(answer, -utc->days); + crm_time_add_seconds(answer, -utc->seconds); + crm_time_free(utc); + + return answer; +} + +/*! + * \brief Check whether a time object represents a sensible date/time + * + * \param[in] dt Date/time object to check + * + * \return \c true if years, days, and seconds are sensible, \c false otherwise + */ +bool +crm_time_check(const crm_time_t *dt) +{ + return (dt != NULL) + && (dt->days > 0) && (dt->days <= year_days(dt->years)) + && (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS); +} + +#define do_cmp_field(l, r, field) \ + if(rc == 0) { \ + if(l->field > r->field) { \ + crm_trace("%s: %d > %d", \ + #field, l->field, r->field); \ + rc = 1; \ + } else if(l->field < r->field) { \ + crm_trace("%s: %d < %d", \ + #field, l->field, r->field); \ + rc = -1; \ + } \ + } + +int +crm_time_compare(const crm_time_t *a, const crm_time_t *b) +{ + int rc = 0; + crm_time_t *t1 = crm_get_utc_time(a); + crm_time_t *t2 = crm_get_utc_time(b); + + if ((t1 == NULL) && (t2 == NULL)) { + rc = 0; + } else if (t1 == NULL) { + rc = -1; + } else if (t2 == NULL) { + rc = 1; + } else { + do_cmp_field(t1, t2, years); + do_cmp_field(t1, t2, days); + do_cmp_field(t1, t2, seconds); + } + + crm_time_free(t1); + crm_time_free(t2); + return rc; +} + +/*! + * \brief Add a given number of seconds to a date/time or duration + * + * \param[in,out] a_time Date/time or duration to add seconds to + * \param[in] extra Number of seconds to add + */ +void +crm_time_add_seconds(crm_time_t *a_time, int extra) +{ + int days = 0; + + crm_trace("Adding %d seconds to %d (max=%d)", + extra, a_time->seconds, DAY_SECONDS); + a_time->seconds += extra; + days = a_time->seconds / DAY_SECONDS; + a_time->seconds %= DAY_SECONDS; + + // Don't have negative seconds + if (a_time->seconds < 0) { + a_time->seconds += DAY_SECONDS; + --days; + } + + crm_time_add_days(a_time, days); +} + +void +crm_time_add_days(crm_time_t * a_time, int extra) +{ + int lower_bound = 1; + int ydays = crm_time_leapyear(a_time->years) ? 366 : 365; + + crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days); + + a_time->days += extra; + while (a_time->days > ydays) { + a_time->years++; + a_time->days -= ydays; + ydays = crm_time_leapyear(a_time->years) ? 366 : 365; + } + + if(a_time->duration) { + lower_bound = 0; + } + + while (a_time->days < lower_bound) { + a_time->years--; + a_time->days += crm_time_leapyear(a_time->years) ? 366 : 365; + } +} + +void +crm_time_add_months(crm_time_t * a_time, int extra) +{ + int lpc; + uint32_t y, m, d, dmax; + + crm_time_get_gregorian(a_time, &y, &m, &d); + crm_trace("Adding %d months to %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, + extra, y, m, d); + + if (extra > 0) { + for (lpc = extra; lpc > 0; lpc--) { + m++; + if (m == 13) { + m = 1; + y++; + } + } + } else { + for (lpc = -extra; lpc > 0; lpc--) { + m--; + if (m == 0) { + m = 12; + y--; + } + } + } + + dmax = crm_time_days_in_month(m, y); + if (dmax < d) { + /* Preserve day-of-month unless the month doesn't have enough days */ + d = dmax; + } + + crm_trace("Calculated %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); + + a_time->years = y; + a_time->days = get_ordinal_days(y, m, d); + + crm_time_get_gregorian(a_time, &y, &m, &d); + crm_trace("Got %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d); +} + +void +crm_time_add_minutes(crm_time_t * a_time, int extra) +{ + crm_time_add_seconds(a_time, extra * 60); +} + +void +crm_time_add_hours(crm_time_t * a_time, int extra) +{ + crm_time_add_seconds(a_time, extra * HOUR_SECONDS); +} + +void +crm_time_add_weeks(crm_time_t * a_time, int extra) +{ + crm_time_add_days(a_time, extra * 7); +} + +void +crm_time_add_years(crm_time_t * a_time, int extra) +{ + a_time->years += extra; +} + +static void +ha_get_tm_time(struct tm *target, const crm_time_t *source) +{ + *target = (struct tm) { + .tm_year = source->years - 1900, + .tm_mday = source->days, + .tm_sec = source->seconds % 60, + .tm_min = ( source->seconds / 60 ) % 60, + .tm_hour = source->seconds / HOUR_SECONDS, + .tm_isdst = -1, /* don't adjust */ + +#if defined(HAVE_STRUCT_TM_TM_GMTOFF) + .tm_gmtoff = source->offset +#endif + }; + mktime(target); +} + +/* The high-resolution variant of time object was added to meet an immediate + * need, and is kept internal API. + * + * @TODO The long-term goal is to come up with a clean, unified design for a + * time type (or types) that meets all the various needs, to replace + * crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t). + * Using glib's GDateTime is a possibility (if we are willing to require + * glib >= 2.26). + */ + +pcmk__time_hr_t * +pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt) +{ + pcmk__time_hr_t *hr_dt = NULL; + + if (dt) { + hr_dt = target?target:calloc(1, sizeof(pcmk__time_hr_t)); + CRM_ASSERT(hr_dt != NULL); + *hr_dt = (pcmk__time_hr_t) { + .years = dt->years, + .months = dt->months, + .days = dt->days, + .seconds = dt->seconds, + .offset = dt->offset, + .duration = dt->duration + }; + } + + return hr_dt; +} + +void +pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt) +{ + CRM_ASSERT((hr_dt) && (target)); + *target = (crm_time_t) { + .years = hr_dt->years, + .months = hr_dt->months, + .days = hr_dt->days, + .seconds = hr_dt->seconds, + .offset = hr_dt->offset, + .duration = hr_dt->duration + }; +} + +/*! + * \internal + * \brief Return the current time as a high-resolution time + * + * \param[out] epoch If not NULL, this will be set to seconds since epoch + * + * \return Newly allocated high-resolution time set to the current time + */ +pcmk__time_hr_t * +pcmk__time_hr_now(time_t *epoch) +{ + struct timespec tv; + crm_time_t dt; + pcmk__time_hr_t *hr; + + qb_util_timespec_from_epoch_get(&tv); + if (epoch != NULL) { + *epoch = tv.tv_sec; + } + crm_time_set_timet(&dt, &(tv.tv_sec)); + hr = pcmk__time_hr_convert(NULL, &dt); + if (hr != NULL) { + hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC; + } + return hr; +} + +pcmk__time_hr_t * +pcmk__time_hr_new(const char *date_time) +{ + pcmk__time_hr_t *hr_dt = NULL; + + if (date_time == NULL) { + hr_dt = pcmk__time_hr_now(NULL); + } else { + crm_time_t *dt; + + dt = parse_date(date_time); + hr_dt = pcmk__time_hr_convert(NULL, dt); + crm_time_free(dt); + } + return hr_dt; +} + +void +pcmk__time_hr_free(pcmk__time_hr_t * hr_dt) +{ + free(hr_dt); +} + +char * +pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt) +{ + const char *mark_s; + int max = 128, scanned_pos = 0, printed_pos = 0, fmt_pos = 0, + date_len = 0, nano_digits = 0; + char nano_s[10], date_s[max+1], nanofmt_s[5] = "%", *tmp_fmt_s; + struct tm tm; + crm_time_t dt; + + if (!format) { + return NULL; + } + pcmk__time_set_hr_dt(&dt, hr_dt); + ha_get_tm_time(&tm, &dt); + sprintf(nano_s, "%06d000", hr_dt->useconds); + + while ((format[scanned_pos]) != '\0') { + mark_s = strchr(&format[scanned_pos], '%'); + if (mark_s) { + int fmt_len = 1; + + fmt_pos = mark_s - format; + while ((format[fmt_pos+fmt_len] != '\0') && + (format[fmt_pos+fmt_len] >= '0') && + (format[fmt_pos+fmt_len] <= '9')) { + fmt_len++; + } + scanned_pos = fmt_pos + fmt_len + 1; + if (format[fmt_pos+fmt_len] == 'N') { + nano_digits = atoi(&format[fmt_pos+1]); + nano_digits = (nano_digits > 6)?6:nano_digits; + nano_digits = (nano_digits < 0)?0:nano_digits; + sprintf(&nanofmt_s[1], ".%ds", nano_digits); + } else { + if (format[scanned_pos] != '\0') { + continue; + } + fmt_pos = scanned_pos; /* print till end */ + } + } else { + scanned_pos = strlen(format); + fmt_pos = scanned_pos; /* print till end */ + } + tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos); +#ifdef HAVE_FORMAT_NONLITERAL +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +#endif + date_len += strftime(&date_s[date_len], max-date_len, tmp_fmt_s, &tm); +#ifdef HAVE_FORMAT_NONLITERAL +#pragma GCC diagnostic pop +#endif + printed_pos = scanned_pos; + free(tmp_fmt_s); + if (nano_digits) { +#ifdef HAVE_FORMAT_NONLITERAL +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" +#endif + date_len += snprintf(&date_s[date_len], max-date_len, + nanofmt_s, nano_s); +#ifdef HAVE_FORMAT_NONLITERAL +#pragma GCC diagnostic pop +#endif + nano_digits = 0; + } + } + + return (date_len == 0)?NULL:strdup(date_s); +} + +/*! + * \internal + * \brief Return a human-friendly string corresponding to an epoch time value + * + * \param[in] source Pointer to epoch time value (or \p NULL for current time) + * \param[in] flags Group of \p crm_time_* flags controlling display format + * (0 to use \p ctime() with newline removed) + * + * \return String representation of \p source on success (may be empty depending + * on \p flags; guaranteed not to be \p NULL) + * + * \note The caller is responsible for freeing the return value using \p free(). + */ +char * +pcmk__epoch2str(const time_t *source, uint32_t flags) +{ + time_t epoch_time = (source == NULL)? time(NULL) : *source; + char *result = NULL; + + if (flags == 0) { + const char *buf = pcmk__trim(ctime(&epoch_time)); + + if (buf != NULL) { + result = strdup(buf); + CRM_ASSERT(result != NULL); + } + } else { + crm_time_t dt; + + crm_time_set_timet(&dt, &epoch_time); + result = crm_time_as_string(&dt, flags); + } + return result; +} + +/*! + * \internal + * \brief Return a human-friendly string corresponding to seconds-and- + * nanoseconds value + * + * Time is shown with microsecond resolution if \p crm_time_usecs is in \p + * flags. + * + * \param[in] ts Time in seconds and nanoseconds (or \p NULL for current + * time) + * \param[in] flags Group of \p crm_time_* flags controlling display format + * + * \return String representation of \p ts on success (may be empty depending on + * \p flags; guaranteed not to be \p NULL) + * + * \note The caller is responsible for freeing the return value using \p free(). + */ +char * +pcmk__timespec2str(const struct timespec *ts, uint32_t flags) +{ + struct timespec tmp_ts; + crm_time_t dt; + char result[DATE_MAX] = { 0 }; + char *result_copy = NULL; + + if (ts == NULL) { + qb_util_timespec_from_epoch_get(&tmp_ts); + ts = &tmp_ts; + } + crm_time_set_timet(&dt, &ts->tv_sec); + time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags, result); + pcmk__str_update(&result_copy, result); + return result_copy; +} + +/*! + * \internal + * \brief Given a millisecond interval, return a log-friendly string + * + * \param[in] interval_ms Interval in milliseconds + * + * \return Readable version of \p interval_ms + * + * \note The return value is a pointer to static memory that will be + * overwritten by later calls to this function. + */ +const char * +pcmk__readable_interval(guint interval_ms) +{ +#define MS_IN_S (1000) +#define MS_IN_M (MS_IN_S * 60) +#define MS_IN_H (MS_IN_M * 60) +#define MS_IN_D (MS_IN_H * 24) +#define MAXSTR sizeof("..d..h..m..s...ms") + static char str[MAXSTR] = { '\0', }; + int offset = 0; + + if (interval_ms > MS_IN_D) { + offset += snprintf(str + offset, MAXSTR - offset, "%ud", + interval_ms / MS_IN_D); + interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D; + } + if (interval_ms > MS_IN_H) { + offset += snprintf(str + offset, MAXSTR - offset, "%uh", + interval_ms / MS_IN_H); + interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H; + } + if (interval_ms > MS_IN_M) { + offset += snprintf(str + offset, MAXSTR - offset, "%um", + interval_ms / MS_IN_M); + interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M; + } + + // Ns, N.NNNs, or NNNms + if (interval_ms > MS_IN_S) { + offset += snprintf(str + offset, MAXSTR - offset, "%u", + interval_ms / MS_IN_S); + interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S; + if (interval_ms > 0) { + offset += snprintf(str + offset, MAXSTR - offset, ".%03u", + interval_ms); + } + (void) snprintf(str + offset, MAXSTR - offset, "s"); + + } else if (interval_ms > 0) { + (void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms); + + } else if (str[0] == '\0') { + strcpy(str, "0s"); + } + return str; +} diff --git a/lib/common/lists.c b/lib/common/lists.c new file mode 100644 index 0000000..9cd88d7 --- /dev/null +++ b/lib/common/lists.c @@ -0,0 +1,27 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include + +GList* +pcmk__subtract_lists(GList *from, const GList *items, GCompareFunc cmp) +{ + GList *result = g_list_copy(from); + + for (const GList *item = items; item != NULL; item = item->next) { + GList *match = g_list_find_custom(result, item->data, cmp); + + if (match != NULL) { + result = g_list_remove(result, match->data); + } + } + + return result; +} diff --git a/lib/common/logging.c b/lib/common/logging.c new file mode 100644 index 0000000..dded873 --- /dev/null +++ b/lib/common/logging.c @@ -0,0 +1,1192 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +// Use high-resolution (millisecond) timestamps if libqb supports them +#ifdef QB_FEATURE_LOG_HIRES_TIMESTAMPS +#define TIMESTAMP_FORMAT_SPEC "%%T" +typedef struct timespec *log_time_t; +#else +#define TIMESTAMP_FORMAT_SPEC "%%t" +typedef time_t log_time_t; +#endif + +unsigned int crm_log_level = LOG_INFO; +unsigned int crm_trace_nonlog = 0; +bool pcmk__is_daemon = false; +char *pcmk__our_nodename = NULL; + +static unsigned int crm_log_priority = LOG_NOTICE; +static GLogFunc glib_log_default = NULL; +static pcmk__output_t *logger_out = NULL; + +static gboolean crm_tracing_enabled(void); + +static void +crm_glib_handler(const gchar * log_domain, GLogLevelFlags flags, const gchar * message, + gpointer user_data) +{ + int log_level = LOG_WARNING; + GLogLevelFlags msg_level = (flags & G_LOG_LEVEL_MASK); + static struct qb_log_callsite *glib_cs = NULL; + + if (glib_cs == NULL) { + glib_cs = qb_log_callsite_get(__func__, __FILE__, "glib-handler", + LOG_DEBUG, __LINE__, crm_trace_nonlog); + } + + switch (msg_level) { + case G_LOG_LEVEL_CRITICAL: + log_level = LOG_CRIT; + + if (!crm_is_callsite_active(glib_cs, LOG_DEBUG, crm_trace_nonlog)) { + /* log and record how we got here */ + crm_abort(__FILE__, __func__, __LINE__, message, TRUE, TRUE); + } + break; + + case G_LOG_LEVEL_ERROR: + log_level = LOG_ERR; + break; + case G_LOG_LEVEL_MESSAGE: + log_level = LOG_NOTICE; + break; + case G_LOG_LEVEL_INFO: + log_level = LOG_INFO; + break; + case G_LOG_LEVEL_DEBUG: + log_level = LOG_DEBUG; + break; + + case G_LOG_LEVEL_WARNING: + case G_LOG_FLAG_RECURSION: + case G_LOG_FLAG_FATAL: + case G_LOG_LEVEL_MASK: + log_level = LOG_WARNING; + break; + } + + do_crm_log(log_level, "%s: %s", log_domain, message); +} + +#ifndef NAME_MAX +# define NAME_MAX 256 +#endif + +/*! + * \internal + * \brief Write out a blackbox (enabling blackboxes if needed) + * + * \param[in] nsig Signal number that was received + * + * \note This is a true signal handler, and so must be async-safe. + */ +static void +crm_trigger_blackbox(int nsig) +{ + if(nsig == SIGTRAP) { + /* Turn it on if it wasn't already */ + crm_enable_blackbox(nsig); + } + crm_write_blackbox(nsig, NULL); +} + +void +crm_log_deinit(void) +{ + if (glib_log_default != NULL) { + g_log_set_default_handler(glib_log_default, NULL); + } +} + +#define FMT_MAX 256 + +/*! + * \internal + * \brief Set the log format string based on the passed-in method + * + * \param[in] method The detail level of the log output + * \param[in] daemon The daemon ID included in error messages + * \param[in] use_pid Cached result of getpid() call, for efficiency + * \param[in] use_nodename Cached result of uname() call, for efficiency + * + */ + +/* XXX __attribute__((nonnull)) for use_nodename parameter */ +static void +set_format_string(int method, const char *daemon, pid_t use_pid, + const char *use_nodename) +{ + if (method == QB_LOG_SYSLOG) { + // The system log gets a simplified, user-friendly format + crm_extended_logging(method, QB_FALSE); + qb_log_format_set(method, "%g %p: %b"); + + } else { + // Everything else gets more detail, for advanced troubleshooting + + int offset = 0; + char fmt[FMT_MAX]; + + if (method > QB_LOG_STDERR) { + // If logging to file, prefix with timestamp, node name, daemon ID + offset += snprintf(fmt + offset, FMT_MAX - offset, + TIMESTAMP_FORMAT_SPEC " %s %-20s[%lu] ", + use_nodename, daemon, (unsigned long) use_pid); + } + + // Add function name (in parentheses) + offset += snprintf(fmt + offset, FMT_MAX - offset, "(%%n"); + if (crm_tracing_enabled()) { + // When tracing, add file and line number + offset += snprintf(fmt + offset, FMT_MAX - offset, "@%%f:%%l"); + } + offset += snprintf(fmt + offset, FMT_MAX - offset, ")"); + + // Add tag (if any), severity, and actual message + offset += snprintf(fmt + offset, FMT_MAX - offset, " %%g\t%%p: %%b"); + + CRM_LOG_ASSERT(offset > 0); + qb_log_format_set(method, fmt); + } +} + +#define DEFAULT_LOG_FILE CRM_LOG_DIR "/pacemaker.log" + +static bool +logfile_disabled(const char *filename) +{ + return pcmk__str_eq(filename, PCMK__VALUE_NONE, pcmk__str_casei) + || pcmk__str_eq(filename, "/dev/null", pcmk__str_none); +} + +/*! + * \internal + * \brief Fix log file ownership if group is wrong or doesn't have access + * + * \param[in] filename Log file name (for logging only) + * \param[in] logfd Log file descriptor + * + * \return Standard Pacemaker return code + */ +static int +chown_logfile(const char *filename, int logfd) +{ + uid_t pcmk_uid = 0; + gid_t pcmk_gid = 0; + struct stat st; + int rc; + + // Get the log file's current ownership and permissions + if (fstat(logfd, &st) < 0) { + return errno; + } + + // Any other errors don't prevent file from being used as log + + rc = pcmk_daemon_user(&pcmk_uid, &pcmk_gid); + if (rc != pcmk_ok) { + rc = pcmk_legacy2rc(rc); + crm_warn("Not changing '%s' ownership because user information " + "unavailable: %s", filename, pcmk_rc_str(rc)); + return pcmk_rc_ok; + } + if ((st.st_gid == pcmk_gid) + && ((st.st_mode & S_IRWXG) == (S_IRGRP|S_IWGRP))) { + return pcmk_rc_ok; + } + if (fchown(logfd, pcmk_uid, pcmk_gid) < 0) { + crm_warn("Couldn't change '%s' ownership to user %s gid %d: %s", + filename, CRM_DAEMON_USER, pcmk_gid, strerror(errno)); + } + return pcmk_rc_ok; +} + +// Reset log file permissions (using environment variable if set) +static void +chmod_logfile(const char *filename, int logfd) +{ + const char *modestr = getenv("PCMK_logfile_mode"); + mode_t filemode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP; + + if (modestr != NULL) { + long filemode_l = strtol(modestr, NULL, 8); + + if ((filemode_l != LONG_MIN) && (filemode_l != LONG_MAX)) { + filemode = (mode_t) filemode_l; + } + } + if ((filemode != 0) && (fchmod(logfd, filemode) < 0)) { + crm_warn("Couldn't change '%s' mode to %04o: %s", + filename, filemode, strerror(errno)); + } +} + +// If we're root, correct a log file's permissions if needed +static int +set_logfile_permissions(const char *filename, FILE *logfile) +{ + if (geteuid() == 0) { + int logfd = fileno(logfile); + int rc = chown_logfile(filename, logfd); + + if (rc != pcmk_rc_ok) { + return rc; + } + chmod_logfile(filename, logfd); + } + return pcmk_rc_ok; +} + +// Enable libqb logging to a new log file +static void +enable_logfile(int fd) +{ + qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_TRUE); +#if 0 + qb_log_ctl(fd, QB_LOG_CONF_FILE_SYNC, 1); // Turn on synchronous writes +#endif + +#ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN + // Longer than default, for logging long XML lines + qb_log_ctl(fd, QB_LOG_CONF_MAX_LINE_LEN, 800); +#endif + + crm_update_callsites(); +} + +static inline void +disable_logfile(int fd) +{ + qb_log_ctl(fd, QB_LOG_CONF_ENABLED, QB_FALSE); +} + +static void +setenv_logfile(const char *filename) +{ + // Some resource agents will log only if environment variable is set + if (pcmk__env_option(PCMK__ENV_LOGFILE) == NULL) { + pcmk__set_env_option(PCMK__ENV_LOGFILE, filename); + } +} + +/*! + * \brief Add a file to be used as a Pacemaker detail log + * + * \param[in] filename Name of log file to use + * + * \return Standard Pacemaker return code + */ +int +pcmk__add_logfile(const char *filename) +{ + /* No log messages from this function will be logged to the new log! + * If another target such as syslog has already been added, the messages + * should show up there. + */ + + int fd = 0; + int rc = pcmk_rc_ok; + FILE *logfile = NULL; + bool is_default = false; + + static int default_fd = -1; + static bool have_logfile = false; + + // Use default if caller didn't specify (and we don't already have one) + if (filename == NULL) { + if (have_logfile) { + return pcmk_rc_ok; + } + filename = DEFAULT_LOG_FILE; + } + + // If the user doesn't want logging, we're done + if (logfile_disabled(filename)) { + return pcmk_rc_ok; + } + + // If the caller wants the default and we already have it, we're done + is_default = pcmk__str_eq(filename, DEFAULT_LOG_FILE, pcmk__str_none); + if (is_default && (default_fd >= 0)) { + return pcmk_rc_ok; + } + + // Check whether we have write access to the file + logfile = fopen(filename, "a"); + if (logfile == NULL) { + rc = errno; + crm_warn("Logging to '%s' is disabled: %s " CRM_XS " uid=%u gid=%u", + filename, strerror(rc), geteuid(), getegid()); + return rc; + } + + rc = set_logfile_permissions(filename, logfile); + if (rc != pcmk_rc_ok) { + crm_warn("Logging to '%s' is disabled: %s " CRM_XS " permissions", + filename, strerror(rc)); + fclose(logfile); + return rc; + } + + // Close and reopen as libqb logging target + fclose(logfile); + fd = qb_log_file_open(filename); + if (fd < 0) { + crm_warn("Logging to '%s' is disabled: %s " CRM_XS " qb_log_file_open", + filename, strerror(-fd)); + return -fd; // == +errno + } + + if (is_default) { + default_fd = fd; + setenv_logfile(filename); + + } else if (default_fd >= 0) { + crm_notice("Switching logging to %s", filename); + disable_logfile(default_fd); + } + + crm_notice("Additional logging available in %s", filename); + enable_logfile(fd); + have_logfile = true; + return pcmk_rc_ok; +} + +/*! + * \brief Add multiple additional log files + * + * \param[in] log_files Array of log files to add + * \param[in] out Output object to use for error reporting + * + * \return Standard Pacemaker return code + */ +void +pcmk__add_logfiles(gchar **log_files, pcmk__output_t *out) +{ + if (log_files == NULL) { + return; + } + + for (gchar **fname = log_files; *fname != NULL; fname++) { + int rc = pcmk__add_logfile(*fname); + + if (rc != pcmk_rc_ok) { + out->err(out, "Logging to %s is disabled: %s", + *fname, pcmk_rc_str(rc)); + } + } +} + +static int blackbox_trigger = 0; +static volatile char *blackbox_file_prefix = NULL; + +static void +blackbox_logger(int32_t t, struct qb_log_callsite *cs, log_time_t timestamp, + const char *msg) +{ + if(cs && cs->priority < LOG_ERR) { + crm_write_blackbox(SIGTRAP, cs); /* Bypass the over-dumping logic */ + } else { + crm_write_blackbox(0, cs); + } +} + +static void +crm_control_blackbox(int nsig, bool enable) +{ + int lpc = 0; + + if (blackbox_file_prefix == NULL) { + pid_t pid = getpid(); + + blackbox_file_prefix = crm_strdup_printf("%s/%s-%lu", + CRM_BLACKBOX_DIR, + crm_system_name, + (unsigned long) pid); + } + + if (enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_SIZE, 5 * 1024 * 1024); /* Any size change drops existing entries */ + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); /* Setting the size seems to disable it */ + + /* Enable synchronous logging */ + for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { + qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_TRUE); + } + + crm_notice("Initiated blackbox recorder: %s", blackbox_file_prefix); + + /* Save to disk on abnormal termination */ + crm_signal_handler(SIGSEGV, crm_trigger_blackbox); + crm_signal_handler(SIGABRT, crm_trigger_blackbox); + crm_signal_handler(SIGILL, crm_trigger_blackbox); + crm_signal_handler(SIGBUS, crm_trigger_blackbox); + crm_signal_handler(SIGFPE, crm_trigger_blackbox); + + crm_update_callsites(); + + blackbox_trigger = qb_log_custom_open(blackbox_logger, NULL, NULL, NULL); + qb_log_ctl(blackbox_trigger, QB_LOG_CONF_ENABLED, QB_TRUE); + crm_trace("Trigger: %d is %d %d", blackbox_trigger, + qb_log_ctl(blackbox_trigger, QB_LOG_CONF_STATE_GET, 0), QB_LOG_STATE_ENABLED); + + crm_update_callsites(); + + } else if (!enable && qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_STATE_GET, 0) == QB_LOG_STATE_ENABLED) { + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); + + /* Disable synchronous logging again when the blackbox is disabled */ + for (lpc = QB_LOG_BLACKBOX; lpc < QB_LOG_TARGET_MAX; lpc++) { + qb_log_ctl(lpc, QB_LOG_CONF_FILE_SYNC, QB_FALSE); + } + } +} + +void +crm_enable_blackbox(int nsig) +{ + crm_control_blackbox(nsig, TRUE); +} + +void +crm_disable_blackbox(int nsig) +{ + crm_control_blackbox(nsig, FALSE); +} + +/*! + * \internal + * \brief Write out a blackbox, if blackboxes are enabled + * + * \param[in] nsig Signal that was received + * \param[in] cs libqb callsite + * + * \note This may be called via a true signal handler and so must be async-safe. + * @TODO actually make this async-safe + */ +void +crm_write_blackbox(int nsig, const struct qb_log_callsite *cs) +{ + static volatile int counter = 1; + static volatile time_t last = 0; + + char buffer[NAME_MAX]; + time_t now = time(NULL); + + if (blackbox_file_prefix == NULL) { + return; + } + + switch (nsig) { + case 0: + case SIGTRAP: + /* The graceful case - such as assertion failure or user request */ + + if (nsig == 0 && now == last) { + /* Prevent over-dumping */ + return; + } + + snprintf(buffer, NAME_MAX, "%s.%d", blackbox_file_prefix, counter++); + if (nsig == SIGTRAP) { + crm_notice("Blackbox dump requested, please see %s for contents", buffer); + + } else if (cs) { + syslog(LOG_NOTICE, + "Problem detected at %s:%d (%s), please see %s for additional details", + cs->function, cs->lineno, cs->filename, buffer); + } else { + crm_notice("Problem detected, please see %s for additional details", buffer); + } + + last = now; + qb_log_blackbox_write_to_file(buffer); + + /* Flush the existing contents + * A size change would also work + */ + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_TRUE); + break; + + default: + /* Do as little as possible, just try to get what we have out + * We logged the filename when the blackbox was enabled + */ + crm_signal_handler(nsig, SIG_DFL); + qb_log_blackbox_write_to_file((const char *)blackbox_file_prefix); + qb_log_ctl(QB_LOG_BLACKBOX, QB_LOG_CONF_ENABLED, QB_FALSE); + raise(nsig); + break; + } +} + +static const char * +crm_quark_to_string(uint32_t tag) +{ + const char *text = g_quark_to_string(tag); + + if (text) { + return text; + } + return ""; +} + +static void +crm_log_filter_source(int source, const char *trace_files, const char *trace_fns, + const char *trace_fmts, const char *trace_tags, const char *trace_blackbox, + struct qb_log_callsite *cs) +{ + if (qb_log_ctl(source, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { + return; + } else if (cs->tags != crm_trace_nonlog && source == QB_LOG_BLACKBOX) { + /* Blackbox gets everything if enabled */ + qb_bit_set(cs->targets, source); + + } else if (source == blackbox_trigger && blackbox_trigger > 0) { + /* Should this log message result in the blackbox being dumped */ + if (cs->priority <= LOG_ERR) { + qb_bit_set(cs->targets, source); + + } else if (trace_blackbox) { + char *key = crm_strdup_printf("%s:%d", cs->function, cs->lineno); + + if (strstr(trace_blackbox, key) != NULL) { + qb_bit_set(cs->targets, source); + } + free(key); + } + + } else if (source == QB_LOG_SYSLOG) { /* No tracing to syslog */ + if (cs->priority <= crm_log_priority && cs->priority <= crm_log_level) { + qb_bit_set(cs->targets, source); + } + /* Log file tracing options... */ + } else if (cs->priority <= crm_log_level) { + qb_bit_set(cs->targets, source); + } else if (trace_files && strstr(trace_files, cs->filename) != NULL) { + qb_bit_set(cs->targets, source); + } else if (trace_fns && strstr(trace_fns, cs->function) != NULL) { + qb_bit_set(cs->targets, source); + } else if (trace_fmts && strstr(trace_fmts, cs->format) != NULL) { + qb_bit_set(cs->targets, source); + } else if (trace_tags + && cs->tags != 0 + && cs->tags != crm_trace_nonlog && g_quark_to_string(cs->tags) != NULL) { + qb_bit_set(cs->targets, source); + } +} + +static void +crm_log_filter(struct qb_log_callsite *cs) +{ + int lpc = 0; + static int need_init = 1; + static const char *trace_fns = NULL; + static const char *trace_tags = NULL; + static const char *trace_fmts = NULL; + static const char *trace_files = NULL; + static const char *trace_blackbox = NULL; + + if (need_init) { + need_init = 0; + trace_fns = getenv("PCMK_trace_functions"); + trace_fmts = getenv("PCMK_trace_formats"); + trace_tags = getenv("PCMK_trace_tags"); + trace_files = getenv("PCMK_trace_files"); + trace_blackbox = getenv("PCMK_trace_blackbox"); + + if (trace_tags != NULL) { + uint32_t tag; + char token[500]; + const char *offset = NULL; + const char *next = trace_tags; + + do { + offset = next; + next = strchrnul(offset, ','); + snprintf(token, sizeof(token), "%.*s", (int)(next - offset), offset); + + tag = g_quark_from_string(token); + crm_info("Created GQuark %u from token '%s' in '%s'", tag, token, trace_tags); + + if (next[0] != 0) { + next++; + } + + } while (next != NULL && next[0] != 0); + } + } + + cs->targets = 0; /* Reset then find targets to enable */ + for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { + crm_log_filter_source(lpc, trace_files, trace_fns, trace_fmts, trace_tags, trace_blackbox, + cs); + } +} + +gboolean +crm_is_callsite_active(struct qb_log_callsite *cs, uint8_t level, uint32_t tags) +{ + gboolean refilter = FALSE; + + if (cs == NULL) { + return FALSE; + } + + if (cs->priority != level) { + cs->priority = level; + refilter = TRUE; + } + + if (cs->tags != tags) { + cs->tags = tags; + refilter = TRUE; + } + + if (refilter) { + crm_log_filter(cs); + } + + if (cs->targets == 0) { + return FALSE; + } + return TRUE; +} + +void +crm_update_callsites(void) +{ + static gboolean log = TRUE; + + if (log) { + log = FALSE; + crm_debug + ("Enabling callsites based on priority=%d, files=%s, functions=%s, formats=%s, tags=%s", + crm_log_level, getenv("PCMK_trace_files"), getenv("PCMK_trace_functions"), + getenv("PCMK_trace_formats"), getenv("PCMK_trace_tags")); + } + qb_log_filter_fn_set(crm_log_filter); +} + +static gboolean +crm_tracing_enabled(void) +{ + if (crm_log_level == LOG_TRACE) { + return TRUE; + } else if (getenv("PCMK_trace_files") || getenv("PCMK_trace_functions") + || getenv("PCMK_trace_formats") || getenv("PCMK_trace_tags")) { + return TRUE; + } + return FALSE; +} + +static int +crm_priority2int(const char *name) +{ + struct syslog_names { + const char *name; + int priority; + }; + static struct syslog_names p_names[] = { + {"emerg", LOG_EMERG}, + {"alert", LOG_ALERT}, + {"crit", LOG_CRIT}, + {"error", LOG_ERR}, + {"warning", LOG_WARNING}, + {"notice", LOG_NOTICE}, + {"info", LOG_INFO}, + {"debug", LOG_DEBUG}, + {NULL, -1} + }; + int lpc; + + for (lpc = 0; name != NULL && p_names[lpc].name != NULL; lpc++) { + if (pcmk__str_eq(p_names[lpc].name, name, pcmk__str_none)) { + return p_names[lpc].priority; + } + } + return crm_log_priority; +} + + +/*! + * \internal + * \brief Set the identifier for the current process + * + * If the identifier crm_system_name is not already set, then it is set as follows: + * - it is passed to the function via the "entity" parameter, or + * - it is derived from the executable name + * + * The identifier can be used in logs, IPC, and more. + * + * This method also sets the PCMK_service environment variable. + * + * \param[in] entity If not NULL, will be assigned to the identifier + * \param[in] argc The number of command line parameters + * \param[in] argv The command line parameter values + */ +static void +set_identity(const char *entity, int argc, char *const *argv) +{ + if (crm_system_name != NULL) { + return; // Already set, don't overwrite + } + + if (entity != NULL) { + crm_system_name = strdup(entity); + + } else if ((argc > 0) && (argv != NULL)) { + char *mutable = strdup(argv[0]); + char *modified = basename(mutable); + + if (strstr(modified, "lt-") == modified) { + modified += 3; + } + crm_system_name = strdup(modified); + free(mutable); + + } else { + crm_system_name = strdup("Unknown"); + } + + CRM_ASSERT(crm_system_name != NULL); + + setenv("PCMK_service", crm_system_name, 1); +} + +void +crm_log_preinit(const char *entity, int argc, char *const *argv) +{ + /* Configure libqb logging with nothing turned on */ + + struct utsname res; + int lpc = 0; + int32_t qb_facility = 0; + pid_t pid = getpid(); + const char *nodename = "localhost"; + static bool have_logging = false; + + if (have_logging) { + return; + } + + have_logging = true; + + crm_xml_init(); /* Sets buffer allocation strategy */ + + if (crm_trace_nonlog == 0) { + crm_trace_nonlog = g_quark_from_static_string("Pacemaker non-logging tracepoint"); + } + + umask(S_IWGRP | S_IWOTH | S_IROTH); + + /* Redirect messages from glib functions to our handler */ + glib_log_default = g_log_set_default_handler(crm_glib_handler, NULL); + + /* and for good measure... - this enum is a bit field (!) */ + g_log_set_always_fatal((GLogLevelFlags) 0); /*value out of range */ + + /* Set crm_system_name, which is used as the logging name. It may also + * be used for other purposes such as an IPC client name. + */ + set_identity(entity, argc, argv); + + qb_facility = qb_log_facility2int("local0"); + qb_log_init(crm_system_name, qb_facility, LOG_ERR); + crm_log_level = LOG_CRIT; + + /* Nuke any syslog activity until it's asked for */ + qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_FALSE); +#ifdef HAVE_qb_log_conf_QB_LOG_CONF_MAX_LINE_LEN + // Shorter than default, generous for what we *should* send to syslog + qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_MAX_LINE_LEN, 256); +#endif + if (uname(memset(&res, 0, sizeof(res))) == 0 && *res.nodename != '\0') { + nodename = res.nodename; + } + + /* Set format strings and disable threading + * Pacemaker and threads do not mix well (due to the amount of forking) + */ + qb_log_tags_stringify_fn_set(crm_quark_to_string); + for (lpc = QB_LOG_SYSLOG; lpc < QB_LOG_TARGET_MAX; lpc++) { + qb_log_ctl(lpc, QB_LOG_CONF_THREADED, QB_FALSE); +#ifdef HAVE_qb_log_conf_QB_LOG_CONF_ELLIPSIS + // End truncated lines with '...' + qb_log_ctl(lpc, QB_LOG_CONF_ELLIPSIS, QB_TRUE); +#endif + set_format_string(lpc, crm_system_name, pid, nodename); + } + +#ifdef ENABLE_NLS + /* Enable translations (experimental). Currently we only have a few + * proof-of-concept translations for some option help. The goal would be to + * offer translations for option help and man pages rather than logs or + * documentation, to reduce the burden of maintaining them. + */ + + // Load locale information for the local host from the environment + setlocale(LC_ALL, ""); + + // Tell gettext where to find Pacemaker message catalogs + CRM_ASSERT(bindtextdomain(PACKAGE, PCMK__LOCALE_DIR) != NULL); + + // Tell gettext to use the Pacemaker message catalogs + CRM_ASSERT(textdomain(PACKAGE) != NULL); + + // Tell gettext that the translated strings are stored in UTF-8 + bind_textdomain_codeset(PACKAGE, "UTF-8"); +#endif +} + +gboolean +crm_log_init(const char *entity, uint8_t level, gboolean daemon, gboolean to_stderr, + int argc, char **argv, gboolean quiet) +{ + const char *syslog_priority = NULL; + const char *facility = pcmk__env_option(PCMK__ENV_LOGFACILITY); + const char *f_copy = facility; + + pcmk__is_daemon = daemon; + crm_log_preinit(entity, argc, argv); + + if (level > LOG_TRACE) { + level = LOG_TRACE; + } + if(level > crm_log_level) { + crm_log_level = level; + } + + /* Should we log to syslog */ + if (facility == NULL) { + if (pcmk__is_daemon) { + facility = "daemon"; + } else { + facility = PCMK__VALUE_NONE; + } + pcmk__set_env_option(PCMK__ENV_LOGFACILITY, facility); + } + + if (pcmk__str_eq(facility, PCMK__VALUE_NONE, pcmk__str_casei)) { + quiet = TRUE; + + + } else { + qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_FACILITY, qb_log_facility2int(facility)); + } + + if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_DEBUG)) { + /* Override the default setting */ + crm_log_level = LOG_DEBUG; + } + + /* What lower threshold do we have for sending to syslog */ + syslog_priority = pcmk__env_option(PCMK__ENV_LOGPRIORITY); + if (syslog_priority) { + crm_log_priority = crm_priority2int(syslog_priority); + } + qb_log_filter_ctl(QB_LOG_SYSLOG, QB_LOG_FILTER_ADD, QB_LOG_FILTER_FILE, "*", + crm_log_priority); + + // Log to syslog unless requested to be quiet + if (!quiet) { + qb_log_ctl(QB_LOG_SYSLOG, QB_LOG_CONF_ENABLED, QB_TRUE); + } + + /* Should we log to stderr */ + if (pcmk__env_option_enabled(crm_system_name, PCMK__ENV_STDERR)) { + /* Override the default setting */ + to_stderr = TRUE; + } + crm_enable_stderr(to_stderr); + + // Log to a file if we're a daemon or user asked for one + { + const char *logfile = pcmk__env_option(PCMK__ENV_LOGFILE); + + if (!pcmk__str_eq(PCMK__VALUE_NONE, logfile, pcmk__str_casei) + && (pcmk__is_daemon || (logfile != NULL))) { + // Daemons always get a log file, unless explicitly set to "none" + pcmk__add_logfile(logfile); + } + } + + if (pcmk__is_daemon + && pcmk__env_option_enabled(crm_system_name, PCMK__ENV_BLACKBOX)) { + crm_enable_blackbox(0); + } + + /* Summary */ + crm_trace("Quiet: %d, facility %s", quiet, f_copy); + pcmk__env_option(PCMK__ENV_LOGFILE); + pcmk__env_option(PCMK__ENV_LOGFACILITY); + + crm_update_callsites(); + + /* Ok, now we can start logging... */ + + // Disable daemon request if user isn't root or Pacemaker daemon user + if (pcmk__is_daemon) { + const char *user = getenv("USER"); + + if (user != NULL && !pcmk__strcase_any_of(user, "root", CRM_DAEMON_USER, NULL)) { + crm_trace("Not switching to corefile directory for %s", user); + pcmk__is_daemon = false; + } + } + + if (pcmk__is_daemon) { + int user = getuid(); + struct passwd *pwent = getpwuid(user); + + if (pwent == NULL) { + crm_perror(LOG_ERR, "Cannot get name for uid: %d", user); + + } else if (!pcmk__strcase_any_of(pwent->pw_name, "root", CRM_DAEMON_USER, NULL)) { + crm_trace("Don't change active directory for regular user: %s", pwent->pw_name); + + } else if (chdir(CRM_CORE_DIR) < 0) { + crm_perror(LOG_INFO, "Cannot change active directory to " CRM_CORE_DIR); + + } else { + crm_info("Changed active directory to " CRM_CORE_DIR); + } + + /* Original meanings from signal(7) + * + * Signal Value Action Comment + * SIGTRAP 5 Core Trace/breakpoint trap + * SIGUSR1 30,10,16 Term User-defined signal 1 + * SIGUSR2 31,12,17 Term User-defined signal 2 + * + * Our usage is as similar as possible + */ + mainloop_add_signal(SIGUSR1, crm_enable_blackbox); + mainloop_add_signal(SIGUSR2, crm_disable_blackbox); + mainloop_add_signal(SIGTRAP, crm_trigger_blackbox); + + } else if (!quiet) { + crm_log_args(argc, argv); + } + + return TRUE; +} + +/* returns the old value */ +unsigned int +set_crm_log_level(unsigned int level) +{ + unsigned int old = crm_log_level; + + if (level > LOG_TRACE) { + level = LOG_TRACE; + } + crm_log_level = level; + crm_update_callsites(); + crm_trace("New log level: %d", level); + return old; +} + +void +crm_enable_stderr(int enable) +{ + if (enable && qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) != QB_LOG_STATE_ENABLED) { + qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_TRUE); + crm_update_callsites(); + + } else if (enable == FALSE) { + qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_ENABLED, QB_FALSE); + } +} + +/*! + * \brief Make logging more verbose + * + * If logging to stderr is not already enabled when this function is called, + * enable it. Otherwise, increase the log level by 1. + * + * \param[in] argc Ignored + * \param[in] argv Ignored + */ +void +crm_bump_log_level(int argc, char **argv) +{ + if (qb_log_ctl(QB_LOG_STDERR, QB_LOG_CONF_STATE_GET, 0) + != QB_LOG_STATE_ENABLED) { + crm_enable_stderr(TRUE); + } else { + set_crm_log_level(crm_log_level + 1); + } +} + +unsigned int +get_crm_log_level(void) +{ + return crm_log_level; +} + +/*! + * \brief Log the command line (once) + * + * \param[in] Number of values in \p argv + * \param[in] Command-line arguments (including command name) + * + * \note This function will only log once, even if called with different + * arguments. + */ +void +crm_log_args(int argc, char **argv) +{ + static bool logged = false; + gchar *arg_string = NULL; + + if ((argc == 0) || (argv == NULL) || logged) { + return; + } + logged = true; + arg_string = g_strjoinv(" ", argv); + crm_notice("Invoked: %s", arg_string); + g_free(arg_string); +} + +void +crm_log_output_fn(const char *file, const char *function, int line, int level, const char *prefix, + const char *output) +{ + const char *next = NULL; + const char *offset = NULL; + + if (level == LOG_NEVER) { + return; + } + + if (output == NULL) { + if (level != LOG_STDOUT) { + level = LOG_TRACE; + } + output = "-- empty --"; + } + + next = output; + do { + offset = next; + next = strchrnul(offset, '\n'); + do_crm_log_alias(level, file, function, line, "%s [ %.*s ]", prefix, + (int)(next - offset), offset); + if (next[0] != 0) { + next++; + } + + } while (next != NULL && next[0] != 0); +} + +void +pcmk__cli_init_logging(const char *name, unsigned int verbosity) +{ + crm_log_init(name, LOG_ERR, FALSE, FALSE, 0, NULL, TRUE); + + for (int i = 0; i < verbosity; i++) { + /* These arguments are ignored, so pass placeholders. */ + crm_bump_log_level(0, NULL); + } +} + +/*! + * \brief Log XML line-by-line in a formatted fashion + * + * \param[in] level Priority at which to log the messages + * \param[in] text Prefix for each line + * \param[in] xml XML to log + * + * \note This does nothing when \p level is \p LOG_STDOUT. + * \note Do not call this function directly. It should be called only from the + * \p do_crm_log_xml() macro. + */ +void +pcmk_log_xml_impl(uint8_t level, const char *text, const xmlNode *xml) +{ + if (xml == NULL) { + do_crm_log(level, "%s%sNo data to dump as XML", + pcmk__s(text, ""), pcmk__str_empty(text)? "" : " "); + + } else { + if (logger_out == NULL) { + CRM_CHECK(pcmk__log_output_new(&logger_out) == pcmk_rc_ok, return); + } + + pcmk__output_set_log_level(logger_out, level); + pcmk__xml_show(logger_out, text, xml, 1, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); + } +} + +/*! + * \internal + * \brief Free the logging library's internal log output object + */ +void +pcmk__free_common_logger(void) +{ + if (logger_out != NULL) { + logger_out->finish(logger_out, CRM_EX_OK, true, NULL); + pcmk__output_free(logger_out); + logger_out = NULL; + } +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +gboolean +crm_log_cli_init(const char *entity) +{ + pcmk__cli_init_logging(entity, 0); + return TRUE; +} + +gboolean +crm_add_logfile(const char *filename) +{ + return pcmk__add_logfile(filename) == pcmk_rc_ok; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/mainloop.c b/lib/common/mainloop.c new file mode 100644 index 0000000..3124e43 --- /dev/null +++ b/lib/common/mainloop.c @@ -0,0 +1,1480 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +struct mainloop_child_s { + pid_t pid; + char *desc; + unsigned timerid; + gboolean timeout; + void *privatedata; + + enum mainloop_child_flags flags; + + /* Called when a process dies */ + void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode); +}; + +struct trigger_s { + GSource source; + gboolean running; + gboolean trigger; + void *user_data; + guint id; + +}; + +struct mainloop_timer_s { + guint id; + guint period_ms; + bool repeat; + char *name; + GSourceFunc cb; + void *userdata; +}; + +static gboolean +crm_trigger_prepare(GSource * source, gint * timeout) +{ + crm_trigger_t *trig = (crm_trigger_t *) source; + + /* cluster-glue's FD and IPC related sources make use of + * g_source_add_poll() but do not set a timeout in their prepare + * functions + * + * This means mainloop's poll() will block until an event for one + * of these sources occurs - any /other/ type of source, such as + * this one or g_idle_*, that doesn't use g_source_add_poll() is + * S-O-L and won't be processed until there is something fd-based + * happens. + * + * Luckily the timeout we can set here affects all sources and + * puts an upper limit on how long poll() can take. + * + * So unconditionally set a small-ish timeout, not too small that + * we're in constant motion, which will act as an upper bound on + * how long the signal handling might be delayed for. + */ + *timeout = 500; /* Timeout in ms */ + + return trig->trigger; +} + +static gboolean +crm_trigger_check(GSource * source) +{ + crm_trigger_t *trig = (crm_trigger_t *) source; + + return trig->trigger; +} + +/*! + * \internal + * \brief GSource dispatch function for crm_trigger_t + * + * \param[in] source crm_trigger_t being dispatched + * \param[in] callback Callback passed at source creation + * \param[in,out] userdata User data passed at source creation + * + * \return G_SOURCE_REMOVE to remove source, G_SOURCE_CONTINUE to keep it + */ +static gboolean +crm_trigger_dispatch(GSource *source, GSourceFunc callback, gpointer userdata) +{ + gboolean rc = G_SOURCE_CONTINUE; + crm_trigger_t *trig = (crm_trigger_t *) source; + + if (trig->running) { + /* Wait until the existing job is complete before starting the next one */ + return G_SOURCE_CONTINUE; + } + trig->trigger = FALSE; + + if (callback) { + int callback_rc = callback(trig->user_data); + + if (callback_rc < 0) { + crm_trace("Trigger handler %p not yet complete", trig); + trig->running = TRUE; + } else if (callback_rc == 0) { + rc = G_SOURCE_REMOVE; + } + } + return rc; +} + +static void +crm_trigger_finalize(GSource * source) +{ + crm_trace("Trigger %p destroyed", source); +} + +static GSourceFuncs crm_trigger_funcs = { + crm_trigger_prepare, + crm_trigger_check, + crm_trigger_dispatch, + crm_trigger_finalize, +}; + +static crm_trigger_t * +mainloop_setup_trigger(GSource * source, int priority, int (*dispatch) (gpointer user_data), + gpointer userdata) +{ + crm_trigger_t *trigger = NULL; + + trigger = (crm_trigger_t *) source; + + trigger->id = 0; + trigger->trigger = FALSE; + trigger->user_data = userdata; + + if (dispatch) { + g_source_set_callback(source, dispatch, trigger, NULL); + } + + g_source_set_priority(source, priority); + g_source_set_can_recurse(source, FALSE); + + trigger->id = g_source_attach(source, NULL); + return trigger; +} + +void +mainloop_trigger_complete(crm_trigger_t * trig) +{ + crm_trace("Trigger handler %p complete", trig); + trig->running = FALSE; +} + +/*! + * \brief Create a trigger to be used as a mainloop source + * + * \param[in] priority Relative priority of source (lower number is higher priority) + * \param[in] dispatch Trigger dispatch function (should return 0 to remove the + * trigger from the mainloop, -1 if the trigger should be + * kept but the job is still running and not complete, and + * 1 if the trigger should be kept and the job is complete) + * \param[in] userdata Pointer to pass to \p dispatch + * + * \return Newly allocated mainloop source for trigger + */ +crm_trigger_t * +mainloop_add_trigger(int priority, int (*dispatch) (gpointer user_data), + gpointer userdata) +{ + GSource *source = NULL; + + CRM_ASSERT(sizeof(crm_trigger_t) > sizeof(GSource)); + source = g_source_new(&crm_trigger_funcs, sizeof(crm_trigger_t)); + CRM_ASSERT(source != NULL); + + return mainloop_setup_trigger(source, priority, dispatch, userdata); +} + +void +mainloop_set_trigger(crm_trigger_t * source) +{ + if(source) { + source->trigger = TRUE; + } +} + +gboolean +mainloop_destroy_trigger(crm_trigger_t * source) +{ + GSource *gs = NULL; + + if(source == NULL) { + return TRUE; + } + + gs = (GSource *)source; + + g_source_destroy(gs); /* Remove from mainloop, ref_count-- */ + g_source_unref(gs); /* The caller no longer carries a reference to source + * + * At this point the source should be free'd, + * unless we're currently processing said + * source, in which case mainloop holds an + * additional reference and it will be free'd + * once our processing completes + */ + return TRUE; +} + +// Define a custom glib source for signal handling + +// Data structure for custom glib source +typedef struct signal_s { + crm_trigger_t trigger; // trigger that invoked source (must be first) + void (*handler) (int sig); // signal handler + int signal; // signal that was received +} crm_signal_t; + +// Table to associate signal handlers with signal numbers +static crm_signal_t *crm_signals[NSIG]; + +/*! + * \internal + * \brief Dispatch an event from custom glib source for signals + * + * Given an signal event, clear the event trigger and call any registered + * signal handler. + * + * \param[in] source glib source that triggered this dispatch + * \param[in] callback (ignored) + * \param[in] userdata (ignored) + */ +static gboolean +crm_signal_dispatch(GSource *source, GSourceFunc callback, gpointer userdata) +{ + crm_signal_t *sig = (crm_signal_t *) source; + + if(sig->signal != SIGCHLD) { + crm_notice("Caught '%s' signal "CRM_XS" %d (%s handler)", + strsignal(sig->signal), sig->signal, + (sig->handler? "invoking" : "no")); + } + + sig->trigger.trigger = FALSE; + if (sig->handler) { + sig->handler(sig->signal); + } + return TRUE; +} + +/*! + * \internal + * \brief Handle a signal by setting a trigger for signal source + * + * \param[in] sig Signal number that was received + * + * \note This is the true signal handler for the mainloop signal source, and + * must be async-safe. + */ +static void +mainloop_signal_handler(int sig) +{ + if (sig > 0 && sig < NSIG && crm_signals[sig] != NULL) { + mainloop_set_trigger((crm_trigger_t *) crm_signals[sig]); + } +} + +// Functions implementing our custom glib source for signal handling +static GSourceFuncs crm_signal_funcs = { + crm_trigger_prepare, + crm_trigger_check, + crm_signal_dispatch, + crm_trigger_finalize, +}; + +/*! + * \internal + * \brief Set a true signal handler + * + * signal()-like interface to sigaction() + * + * \param[in] sig Signal number to register handler for + * \param[in] dispatch Signal handler + * + * \return The previous value of the signal handler, or SIG_ERR on error + * \note The dispatch function must be async-safe. + */ +sighandler_t +crm_signal_handler(int sig, sighandler_t dispatch) +{ + sigset_t mask; + struct sigaction sa; + struct sigaction old; + + if (sigemptyset(&mask) < 0) { + crm_err("Could not set handler for signal %d: %s", + sig, pcmk_rc_str(errno)); + return SIG_ERR; + } + + memset(&sa, 0, sizeof(struct sigaction)); + sa.sa_handler = dispatch; + sa.sa_flags = SA_RESTART; + sa.sa_mask = mask; + + if (sigaction(sig, &sa, &old) < 0) { + crm_err("Could not set handler for signal %d: %s", + sig, pcmk_rc_str(errno)); + return SIG_ERR; + } + return old.sa_handler; +} + +static void +mainloop_destroy_signal_entry(int sig) +{ + crm_signal_t *tmp = crm_signals[sig]; + + crm_signals[sig] = NULL; + + crm_trace("Destroying signal %d", sig); + mainloop_destroy_trigger((crm_trigger_t *) tmp); +} + +/*! + * \internal + * \brief Add a signal handler to a mainloop + * + * \param[in] sig Signal number to handle + * \param[in] dispatch Signal handler function + * + * \note The true signal handler merely sets a mainloop trigger to call this + * dispatch function via the mainloop. Therefore, the dispatch function + * does not need to be async-safe. + */ +gboolean +mainloop_add_signal(int sig, void (*dispatch) (int sig)) +{ + GSource *source = NULL; + int priority = G_PRIORITY_HIGH - 1; + + if (sig == SIGTERM) { + /* TERM is higher priority than other signals, + * signals are higher priority than other ipc. + * Yes, minus: smaller is "higher" + */ + priority--; + } + + if (sig >= NSIG || sig < 0) { + crm_err("Signal %d is out of range", sig); + return FALSE; + + } else if (crm_signals[sig] != NULL && crm_signals[sig]->handler == dispatch) { + crm_trace("Signal handler for %d is already installed", sig); + return TRUE; + + } else if (crm_signals[sig] != NULL) { + crm_err("Different signal handler for %d is already installed", sig); + return FALSE; + } + + CRM_ASSERT(sizeof(crm_signal_t) > sizeof(GSource)); + source = g_source_new(&crm_signal_funcs, sizeof(crm_signal_t)); + + crm_signals[sig] = (crm_signal_t *) mainloop_setup_trigger(source, priority, NULL, NULL); + CRM_ASSERT(crm_signals[sig] != NULL); + + crm_signals[sig]->handler = dispatch; + crm_signals[sig]->signal = sig; + + if (crm_signal_handler(sig, mainloop_signal_handler) == SIG_ERR) { + mainloop_destroy_signal_entry(sig); + return FALSE; + } +#if 0 + /* If we want signals to interrupt mainloop's poll(), instead of waiting for + * the timeout, then we should call siginterrupt() below + * + * For now, just enforce a low timeout + */ + if (siginterrupt(sig, 1) < 0) { + crm_perror(LOG_INFO, "Could not enable system call interruptions for signal %d", sig); + } +#endif + + return TRUE; +} + +gboolean +mainloop_destroy_signal(int sig) +{ + if (sig >= NSIG || sig < 0) { + crm_err("Signal %d is out of range", sig); + return FALSE; + + } else if (crm_signal_handler(sig, NULL) == SIG_ERR) { + crm_perror(LOG_ERR, "Could not uninstall signal handler for signal %d", sig); + return FALSE; + + } else if (crm_signals[sig] == NULL) { + return TRUE; + } + mainloop_destroy_signal_entry(sig); + return TRUE; +} + +static qb_array_t *gio_map = NULL; + +void +mainloop_cleanup(void) +{ + if (gio_map) { + qb_array_free(gio_map); + } + + for (int sig = 0; sig < NSIG; ++sig) { + mainloop_destroy_signal_entry(sig); + } +} + +/* + * libqb... + */ +struct gio_to_qb_poll { + int32_t is_used; + guint source; + int32_t events; + void *data; + qb_ipcs_dispatch_fn_t fn; + enum qb_loop_priority p; +}; + +static gboolean +gio_read_socket(GIOChannel * gio, GIOCondition condition, gpointer data) +{ + struct gio_to_qb_poll *adaptor = (struct gio_to_qb_poll *)data; + gint fd = g_io_channel_unix_get_fd(gio); + + crm_trace("%p.%d %d", data, fd, condition); + + /* if this assert get's hit, then there is a race condition between + * when we destroy a fd and when mainloop actually gives it up */ + CRM_ASSERT(adaptor->is_used > 0); + + return (adaptor->fn(fd, condition, adaptor->data) == 0); +} + +static void +gio_poll_destroy(gpointer data) +{ + struct gio_to_qb_poll *adaptor = (struct gio_to_qb_poll *)data; + + adaptor->is_used--; + CRM_ASSERT(adaptor->is_used >= 0); + + if (adaptor->is_used == 0) { + crm_trace("Marking adaptor %p unused", adaptor); + adaptor->source = 0; + } +} + +/*! + * \internal + * \brief Convert libqb's poll priority into GLib's one + * + * \param[in] prio libqb's poll priority (#QB_LOOP_MED assumed as fallback) + * + * \return best matching GLib's priority + */ +static gint +conv_prio_libqb2glib(enum qb_loop_priority prio) +{ + switch (prio) { + case QB_LOOP_LOW: return G_PRIORITY_LOW; + case QB_LOOP_HIGH: return G_PRIORITY_HIGH; + default: return G_PRIORITY_DEFAULT; // QB_LOOP_MED + } +} + +/*! + * \internal + * \brief Convert libqb's poll priority to rate limiting spec + * + * \param[in] prio libqb's poll priority (#QB_LOOP_MED assumed as fallback) + * + * \return best matching rate limiting spec + * \note This is the inverse of libqb's qb_ipcs_request_rate_limit(). + */ +static enum qb_ipcs_rate_limit +conv_libqb_prio2ratelimit(enum qb_loop_priority prio) +{ + switch (prio) { + case QB_LOOP_LOW: return QB_IPCS_RATE_SLOW; + case QB_LOOP_HIGH: return QB_IPCS_RATE_FAST; + default: return QB_IPCS_RATE_NORMAL; // QB_LOOP_MED + } +} + +static int32_t +gio_poll_dispatch_update(enum qb_loop_priority p, int32_t fd, int32_t evts, + void *data, qb_ipcs_dispatch_fn_t fn, int32_t add) +{ + struct gio_to_qb_poll *adaptor; + GIOChannel *channel; + int32_t res = 0; + + res = qb_array_index(gio_map, fd, (void **)&adaptor); + if (res < 0) { + crm_err("Array lookup failed for fd=%d: %d", fd, res); + return res; + } + + crm_trace("Adding fd=%d to mainloop as adaptor %p", fd, adaptor); + + if (add && adaptor->source) { + crm_err("Adaptor for descriptor %d is still in-use", fd); + return -EEXIST; + } + if (!add && !adaptor->is_used) { + crm_err("Adaptor for descriptor %d is not in-use", fd); + return -ENOENT; + } + + /* channel is created with ref_count = 1 */ + channel = g_io_channel_unix_new(fd); + if (!channel) { + crm_err("No memory left to add fd=%d", fd); + return -ENOMEM; + } + + if (adaptor->source) { + g_source_remove(adaptor->source); + adaptor->source = 0; + } + + /* Because unlike the poll() API, glib doesn't tell us about HUPs by default */ + evts |= (G_IO_HUP | G_IO_NVAL | G_IO_ERR); + + adaptor->fn = fn; + adaptor->events = evts; + adaptor->data = data; + adaptor->p = p; + adaptor->is_used++; + adaptor->source = + g_io_add_watch_full(channel, conv_prio_libqb2glib(p), evts, + gio_read_socket, adaptor, gio_poll_destroy); + + /* Now that mainloop now holds a reference to channel, + * thanks to g_io_add_watch_full(), drop ours from g_io_channel_unix_new(). + * + * This means that channel will be free'd by: + * g_main_context_dispatch() + * -> g_source_destroy_internal() + * -> g_source_callback_unref() + * shortly after gio_poll_destroy() completes + */ + g_io_channel_unref(channel); + + crm_trace("Added to mainloop with gsource id=%d", adaptor->source); + if (adaptor->source > 0) { + return 0; + } + + return -EINVAL; +} + +static int32_t +gio_poll_dispatch_add(enum qb_loop_priority p, int32_t fd, int32_t evts, + void *data, qb_ipcs_dispatch_fn_t fn) +{ + return gio_poll_dispatch_update(p, fd, evts, data, fn, QB_TRUE); +} + +static int32_t +gio_poll_dispatch_mod(enum qb_loop_priority p, int32_t fd, int32_t evts, + void *data, qb_ipcs_dispatch_fn_t fn) +{ + return gio_poll_dispatch_update(p, fd, evts, data, fn, QB_FALSE); +} + +static int32_t +gio_poll_dispatch_del(int32_t fd) +{ + struct gio_to_qb_poll *adaptor; + + crm_trace("Looking for fd=%d", fd); + if (qb_array_index(gio_map, fd, (void **)&adaptor) == 0) { + if (adaptor->source) { + g_source_remove(adaptor->source); + adaptor->source = 0; + } + } + return 0; +} + +struct qb_ipcs_poll_handlers gio_poll_funcs = { + .job_add = NULL, + .dispatch_add = gio_poll_dispatch_add, + .dispatch_mod = gio_poll_dispatch_mod, + .dispatch_del = gio_poll_dispatch_del, +}; + +static enum qb_ipc_type +pick_ipc_type(enum qb_ipc_type requested) +{ + const char *env = getenv("PCMK_ipc_type"); + + if (env && strcmp("shared-mem", env) == 0) { + return QB_IPC_SHM; + } else if (env && strcmp("socket", env) == 0) { + return QB_IPC_SOCKET; + } else if (env && strcmp("posix", env) == 0) { + return QB_IPC_POSIX_MQ; + } else if (env && strcmp("sysv", env) == 0) { + return QB_IPC_SYSV_MQ; + } else if (requested == QB_IPC_NATIVE) { + /* We prefer shared memory because the server never blocks on + * send. If part of a message fits into the socket, libqb + * needs to block until the remainder can be sent also. + * Otherwise the client will wait forever for the remaining + * bytes. + */ + return QB_IPC_SHM; + } + return requested; +} + +qb_ipcs_service_t * +mainloop_add_ipc_server(const char *name, enum qb_ipc_type type, + struct qb_ipcs_service_handlers *callbacks) +{ + return mainloop_add_ipc_server_with_prio(name, type, callbacks, QB_LOOP_MED); +} + +qb_ipcs_service_t * +mainloop_add_ipc_server_with_prio(const char *name, enum qb_ipc_type type, + struct qb_ipcs_service_handlers *callbacks, + enum qb_loop_priority prio) +{ + int rc = 0; + qb_ipcs_service_t *server = NULL; + + if (gio_map == NULL) { + gio_map = qb_array_create_2(64, sizeof(struct gio_to_qb_poll), 1); + } + + server = qb_ipcs_create(name, 0, pick_ipc_type(type), callbacks); + + if (server == NULL) { + crm_err("Could not create %s IPC server: %s (%d)", name, pcmk_strerror(rc), rc); + return NULL; + } + + if (prio != QB_LOOP_MED) { + qb_ipcs_request_rate_limit(server, conv_libqb_prio2ratelimit(prio)); + } + + /* All clients should use at least ipc_buffer_max as their buffer size */ + qb_ipcs_enforce_buffer_size(server, crm_ipc_default_buffer_size()); + qb_ipcs_poll_handlers_set(server, &gio_poll_funcs); + + rc = qb_ipcs_run(server); + if (rc < 0) { + crm_err("Could not start %s IPC server: %s (%d)", name, pcmk_strerror(rc), rc); + return NULL; // qb_ipcs_run() destroys server on failure + } + + return server; +} + +void +mainloop_del_ipc_server(qb_ipcs_service_t * server) +{ + if (server) { + qb_ipcs_destroy(server); + } +} + +struct mainloop_io_s { + char *name; + void *userdata; + + int fd; + guint source; + crm_ipc_t *ipc; + GIOChannel *channel; + + int (*dispatch_fn_ipc) (const char *buffer, ssize_t length, gpointer userdata); + int (*dispatch_fn_io) (gpointer userdata); + void (*destroy_fn) (gpointer userdata); + +}; + +/*! + * \internal + * \brief I/O watch callback function (GIOFunc) + * + * \param[in] gio I/O channel being watched + * \param[in] condition I/O condition satisfied + * \param[in] data User data passed when source was created + * + * \return G_SOURCE_REMOVE to remove source, G_SOURCE_CONTINUE to keep it + */ +static gboolean +mainloop_gio_callback(GIOChannel *gio, GIOCondition condition, gpointer data) +{ + gboolean rc = G_SOURCE_CONTINUE; + mainloop_io_t *client = data; + + CRM_ASSERT(client->fd == g_io_channel_unix_get_fd(gio)); + + if (condition & G_IO_IN) { + if (client->ipc) { + long read_rc = 0L; + int max = 10; + + do { + read_rc = crm_ipc_read(client->ipc); + if (read_rc <= 0) { + crm_trace("Could not read IPC message from %s: %s (%ld)", + client->name, pcmk_strerror(read_rc), read_rc); + + } else if (client->dispatch_fn_ipc) { + const char *buffer = crm_ipc_buffer(client->ipc); + + crm_trace("New %ld-byte IPC message from %s " + "after I/O condition %d", + read_rc, client->name, (int) condition); + if (client->dispatch_fn_ipc(buffer, read_rc, client->userdata) < 0) { + crm_trace("Connection to %s no longer required", client->name); + rc = G_SOURCE_REMOVE; + } + } + + } while ((rc == G_SOURCE_CONTINUE) && (read_rc > 0) && --max > 0); + + } else { + crm_trace("New I/O event for %s after I/O condition %d", + client->name, (int) condition); + if (client->dispatch_fn_io) { + if (client->dispatch_fn_io(client->userdata) < 0) { + crm_trace("Connection to %s no longer required", client->name); + rc = G_SOURCE_REMOVE; + } + } + } + } + + if (client->ipc && !crm_ipc_connected(client->ipc)) { + crm_err("Connection to %s closed " CRM_XS "client=%p condition=%d", + client->name, client, condition); + rc = G_SOURCE_REMOVE; + + } else if (condition & (G_IO_HUP | G_IO_NVAL | G_IO_ERR)) { + crm_trace("The connection %s[%p] has been closed (I/O condition=%d)", + client->name, client, condition); + rc = G_SOURCE_REMOVE; + + } else if ((condition & G_IO_IN) == 0) { + /* + #define GLIB_SYSDEF_POLLIN =1 + #define GLIB_SYSDEF_POLLPRI =2 + #define GLIB_SYSDEF_POLLOUT =4 + #define GLIB_SYSDEF_POLLERR =8 + #define GLIB_SYSDEF_POLLHUP =16 + #define GLIB_SYSDEF_POLLNVAL =32 + + typedef enum + { + G_IO_IN GLIB_SYSDEF_POLLIN, + G_IO_OUT GLIB_SYSDEF_POLLOUT, + G_IO_PRI GLIB_SYSDEF_POLLPRI, + G_IO_ERR GLIB_SYSDEF_POLLERR, + G_IO_HUP GLIB_SYSDEF_POLLHUP, + G_IO_NVAL GLIB_SYSDEF_POLLNVAL + } GIOCondition; + + A bitwise combination representing a condition to watch for on an event source. + + G_IO_IN There is data to read. + G_IO_OUT Data can be written (without blocking). + G_IO_PRI There is urgent data to read. + G_IO_ERR Error condition. + G_IO_HUP Hung up (the connection has been broken, usually for pipes and sockets). + G_IO_NVAL Invalid request. The file descriptor is not open. + */ + crm_err("Strange condition: %d", condition); + } + + /* G_SOURCE_REMOVE results in mainloop_gio_destroy() being called + * just before the source is removed from mainloop + */ + return rc; +} + +static void +mainloop_gio_destroy(gpointer c) +{ + mainloop_io_t *client = c; + char *c_name = strdup(client->name); + + /* client->source is valid but about to be destroyed (ref_count == 0) in gmain.c + * client->channel will still have ref_count > 0... should be == 1 + */ + crm_trace("Destroying client %s[%p]", c_name, c); + + if (client->ipc) { + crm_ipc_close(client->ipc); + } + + if (client->destroy_fn) { + void (*destroy_fn) (gpointer userdata) = client->destroy_fn; + + client->destroy_fn = NULL; + destroy_fn(client->userdata); + } + + if (client->ipc) { + crm_ipc_t *ipc = client->ipc; + + client->ipc = NULL; + crm_ipc_destroy(ipc); + } + + crm_trace("Destroyed client %s[%p]", c_name, c); + + free(client->name); client->name = NULL; + free(client); + + free(c_name); +} + +/*! + * \brief Connect to IPC and add it as a main loop source + * + * \param[in,out] ipc IPC connection to add + * \param[in] priority Event source priority to use for connection + * \param[in] userdata Data to register with callbacks + * \param[in] callbacks Dispatch and destroy callbacks for connection + * \param[out] source Newly allocated event source + * + * \return Standard Pacemaker return code + * + * \note On failure, the caller is still responsible for ipc. On success, the + * caller should call mainloop_del_ipc_client() when source is no longer + * needed, which will lead to the disconnection of the IPC later in the + * main loop if it is connected. However the IPC disconnects, + * mainloop_gio_destroy() will free ipc and source after calling the + * destroy callback. + */ +int +pcmk__add_mainloop_ipc(crm_ipc_t *ipc, int priority, void *userdata, + const struct ipc_client_callbacks *callbacks, + mainloop_io_t **source) +{ + CRM_CHECK((ipc != NULL) && (callbacks != NULL), return EINVAL); + + if (!crm_ipc_connect(ipc)) { + int rc = errno; + crm_debug("Connection to %s failed: %d", crm_ipc_name(ipc), errno); + return rc; + } + *source = mainloop_add_fd(crm_ipc_name(ipc), priority, crm_ipc_get_fd(ipc), + userdata, NULL); + if (*source == NULL) { + int rc = errno; + + crm_ipc_close(ipc); + return rc; + } + (*source)->ipc = ipc; + (*source)->destroy_fn = callbacks->destroy; + (*source)->dispatch_fn_ipc = callbacks->dispatch; + return pcmk_rc_ok; +} + +/*! + * \brief Get period for mainloop timer + * + * \param[in] timer Timer + * + * \return Period in ms + */ +guint +pcmk__mainloop_timer_get_period(const mainloop_timer_t *timer) +{ + if (timer) { + return timer->period_ms; + } + return 0; +} + +mainloop_io_t * +mainloop_add_ipc_client(const char *name, int priority, size_t max_size, + void *userdata, struct ipc_client_callbacks *callbacks) +{ + crm_ipc_t *ipc = crm_ipc_new(name, max_size); + mainloop_io_t *source = NULL; + int rc = pcmk__add_mainloop_ipc(ipc, priority, userdata, callbacks, + &source); + + if (rc != pcmk_rc_ok) { + if (crm_log_level == LOG_STDOUT) { + fprintf(stderr, "Connection to %s failed: %s", + name, pcmk_rc_str(rc)); + } + crm_ipc_destroy(ipc); + if (rc > 0) { + errno = rc; + } else { + errno = ENOTCONN; + } + return NULL; + } + return source; +} + +void +mainloop_del_ipc_client(mainloop_io_t * client) +{ + mainloop_del_fd(client); +} + +crm_ipc_t * +mainloop_get_ipc_client(mainloop_io_t * client) +{ + if (client) { + return client->ipc; + } + return NULL; +} + +mainloop_io_t * +mainloop_add_fd(const char *name, int priority, int fd, void *userdata, + struct mainloop_fd_callbacks * callbacks) +{ + mainloop_io_t *client = NULL; + + if (fd >= 0) { + client = calloc(1, sizeof(mainloop_io_t)); + if (client == NULL) { + return NULL; + } + client->name = strdup(name); + client->userdata = userdata; + + if (callbacks) { + client->destroy_fn = callbacks->destroy; + client->dispatch_fn_io = callbacks->dispatch; + } + + client->fd = fd; + client->channel = g_io_channel_unix_new(fd); + client->source = + g_io_add_watch_full(client->channel, priority, + (G_IO_IN | G_IO_HUP | G_IO_NVAL | G_IO_ERR), mainloop_gio_callback, + client, mainloop_gio_destroy); + + /* Now that mainloop now holds a reference to channel, + * thanks to g_io_add_watch_full(), drop ours from g_io_channel_unix_new(). + * + * This means that channel will be free'd by: + * g_main_context_dispatch() or g_source_remove() + * -> g_source_destroy_internal() + * -> g_source_callback_unref() + * shortly after mainloop_gio_destroy() completes + */ + g_io_channel_unref(client->channel); + crm_trace("Added connection %d for %s[%p].%d", client->source, client->name, client, fd); + } else { + errno = EINVAL; + } + + return client; +} + +void +mainloop_del_fd(mainloop_io_t * client) +{ + if (client != NULL) { + crm_trace("Removing client %s[%p]", client->name, client); + if (client->source) { + /* Results in mainloop_gio_destroy() being called just + * before the source is removed from mainloop + */ + g_source_remove(client->source); + } + } +} + +static GList *child_list = NULL; + +pid_t +mainloop_child_pid(mainloop_child_t * child) +{ + return child->pid; +} + +const char * +mainloop_child_name(mainloop_child_t * child) +{ + return child->desc; +} + +int +mainloop_child_timeout(mainloop_child_t * child) +{ + return child->timeout; +} + +void * +mainloop_child_userdata(mainloop_child_t * child) +{ + return child->privatedata; +} + +void +mainloop_clear_child_userdata(mainloop_child_t * child) +{ + child->privatedata = NULL; +} + +/* good function name */ +static void +child_free(mainloop_child_t *child) +{ + if (child->timerid != 0) { + crm_trace("Removing timer %d", child->timerid); + g_source_remove(child->timerid); + child->timerid = 0; + } + free(child->desc); + free(child); +} + +/* terrible function name */ +static int +child_kill_helper(mainloop_child_t *child) +{ + int rc; + if (child->flags & mainloop_leave_pid_group) { + crm_debug("Kill pid %d only. leave group intact.", child->pid); + rc = kill(child->pid, SIGKILL); + } else { + crm_debug("Kill pid %d's group", child->pid); + rc = kill(-child->pid, SIGKILL); + } + + if (rc < 0) { + if (errno != ESRCH) { + crm_perror(LOG_ERR, "kill(%d, KILL) failed", child->pid); + } + return -errno; + } + return 0; +} + +static gboolean +child_timeout_callback(gpointer p) +{ + mainloop_child_t *child = p; + int rc = 0; + + child->timerid = 0; + if (child->timeout) { + crm_warn("%s process (PID %d) will not die!", child->desc, (int)child->pid); + return FALSE; + } + + rc = child_kill_helper(child); + if (rc == -ESRCH) { + /* Nothing left to do. pid doesn't exist */ + return FALSE; + } + + child->timeout = TRUE; + crm_debug("%s process (PID %d) timed out", child->desc, (int)child->pid); + + child->timerid = g_timeout_add(5000, child_timeout_callback, child); + return FALSE; +} + +static bool +child_waitpid(mainloop_child_t *child, int flags) +{ + int rc = 0; + int core = 0; + int signo = 0; + int status = 0; + int exitcode = 0; + bool callback_needed = true; + + rc = waitpid(child->pid, &status, flags); + if (rc == 0) { // WNOHANG in flags, and child status is not available + crm_trace("Child process %d (%s) still active", + child->pid, child->desc); + callback_needed = false; + + } else if (rc != child->pid) { + /* According to POSIX, possible conditions: + * - child->pid was non-positive (process group or any child), + * and rc is specific child + * - errno ECHILD (pid does not exist or is not child) + * - errno EINVAL (invalid flags) + * - errno EINTR (caller interrupted by signal) + * + * @TODO Handle these cases more specifically. + */ + signo = SIGCHLD; + exitcode = 1; + crm_notice("Wait for child process %d (%s) interrupted: %s", + child->pid, child->desc, pcmk_rc_str(errno)); + + } else if (WIFEXITED(status)) { + exitcode = WEXITSTATUS(status); + crm_trace("Child process %d (%s) exited with status %d", + child->pid, child->desc, exitcode); + + } else if (WIFSIGNALED(status)) { + signo = WTERMSIG(status); + crm_trace("Child process %d (%s) exited with signal %d (%s)", + child->pid, child->desc, signo, strsignal(signo)); + +#ifdef WCOREDUMP // AIX, SunOS, maybe others + } else if (WCOREDUMP(status)) { + core = 1; + crm_err("Child process %d (%s) dumped core", + child->pid, child->desc); +#endif + + } else { // flags must contain WUNTRACED and/or WCONTINUED to reach this + crm_trace("Child process %d (%s) stopped or continued", + child->pid, child->desc); + callback_needed = false; + } + + if (callback_needed && child->callback) { + child->callback(child, child->pid, core, signo, exitcode); + } + return callback_needed; +} + +static void +child_death_dispatch(int signal) +{ + for (GList *iter = child_list; iter; ) { + GList *saved = iter; + mainloop_child_t *child = iter->data; + + iter = iter->next; + if (child_waitpid(child, WNOHANG)) { + crm_trace("Removing completed process %d from child list", + child->pid); + child_list = g_list_remove_link(child_list, saved); + g_list_free(saved); + child_free(child); + } + } +} + +static gboolean +child_signal_init(gpointer p) +{ + crm_trace("Installed SIGCHLD handler"); + /* Do NOT use g_child_watch_add() and friends, they rely on pthreads */ + mainloop_add_signal(SIGCHLD, child_death_dispatch); + + /* In case they terminated before the signal handler was installed */ + child_death_dispatch(SIGCHLD); + return FALSE; +} + +gboolean +mainloop_child_kill(pid_t pid) +{ + GList *iter; + mainloop_child_t *child = NULL; + mainloop_child_t *match = NULL; + /* It is impossible to block SIGKILL, this allows us to + * call waitpid without WNOHANG flag.*/ + int waitflags = 0, rc = 0; + + for (iter = child_list; iter != NULL && match == NULL; iter = iter->next) { + child = iter->data; + if (pid == child->pid) { + match = child; + } + } + + if (match == NULL) { + return FALSE; + } + + rc = child_kill_helper(match); + if(rc == -ESRCH) { + /* It's gone, but hasn't shown up in waitpid() yet. Wait until we get + * SIGCHLD and let handler clean it up as normal (so we get the correct + * return code/status). The blocking alternative would be to call + * child_waitpid(match, 0). + */ + crm_trace("Waiting for signal that child process %d completed", + match->pid); + return TRUE; + + } else if(rc != 0) { + /* If KILL for some other reason set the WNOHANG flag since we + * can't be certain what happened. + */ + waitflags = WNOHANG; + } + + if (!child_waitpid(match, waitflags)) { + /* not much we can do if this occurs */ + return FALSE; + } + + child_list = g_list_remove(child_list, match); + child_free(match); + return TRUE; +} + +/* Create/Log a new tracked process + * To track a process group, use -pid + * + * @TODO Using a non-positive pid (i.e. any child, or process group) would + * likely not be useful since we will free the child after the first + * completed process. + */ +void +mainloop_child_add_with_flags(pid_t pid, int timeout, const char *desc, void *privatedata, enum mainloop_child_flags flags, + void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode)) +{ + static bool need_init = TRUE; + mainloop_child_t *child = calloc(1, sizeof(mainloop_child_t)); + + child->pid = pid; + child->timerid = 0; + child->timeout = FALSE; + child->privatedata = privatedata; + child->callback = callback; + child->flags = flags; + pcmk__str_update(&child->desc, desc); + + if (timeout) { + child->timerid = g_timeout_add(timeout, child_timeout_callback, child); + } + + child_list = g_list_append(child_list, child); + + if(need_init) { + need_init = FALSE; + /* SIGCHLD processing has to be invoked from mainloop. + * We do not want it to be possible to both add a child pid + * to mainloop, and have the pid's exit callback invoked within + * the same callstack. */ + g_timeout_add(1, child_signal_init, NULL); + } +} + +void +mainloop_child_add(pid_t pid, int timeout, const char *desc, void *privatedata, + void (*callback) (mainloop_child_t * p, pid_t pid, int core, int signo, int exitcode)) +{ + mainloop_child_add_with_flags(pid, timeout, desc, privatedata, 0, callback); +} + +static gboolean +mainloop_timer_cb(gpointer user_data) +{ + int id = 0; + bool repeat = FALSE; + struct mainloop_timer_s *t = user_data; + + CRM_ASSERT(t != NULL); + + id = t->id; + t->id = 0; /* Ensure it's unset during callbacks so that + * mainloop_timer_running() works as expected + */ + + if(t->cb) { + crm_trace("Invoking callbacks for timer %s", t->name); + repeat = t->repeat; + if(t->cb(t->userdata) == FALSE) { + crm_trace("Timer %s complete", t->name); + repeat = FALSE; + } + } + + if(repeat) { + /* Restore if repeating */ + t->id = id; + } + + return repeat; +} + +bool +mainloop_timer_running(mainloop_timer_t *t) +{ + if(t && t->id != 0) { + return TRUE; + } + return FALSE; +} + +void +mainloop_timer_start(mainloop_timer_t *t) +{ + mainloop_timer_stop(t); + if(t && t->period_ms > 0) { + crm_trace("Starting timer %s", t->name); + t->id = g_timeout_add(t->period_ms, mainloop_timer_cb, t); + } +} + +void +mainloop_timer_stop(mainloop_timer_t *t) +{ + if(t && t->id != 0) { + crm_trace("Stopping timer %s", t->name); + g_source_remove(t->id); + t->id = 0; + } +} + +guint +mainloop_timer_set_period(mainloop_timer_t *t, guint period_ms) +{ + guint last = 0; + + if(t) { + last = t->period_ms; + t->period_ms = period_ms; + } + + if(t && t->id != 0 && last != t->period_ms) { + mainloop_timer_start(t); + } + return last; +} + +mainloop_timer_t * +mainloop_timer_add(const char *name, guint period_ms, bool repeat, GSourceFunc cb, void *userdata) +{ + mainloop_timer_t *t = calloc(1, sizeof(mainloop_timer_t)); + + if(t) { + if(name) { + t->name = crm_strdup_printf("%s-%u-%d", name, period_ms, repeat); + } else { + t->name = crm_strdup_printf("%p-%u-%d", t, period_ms, repeat); + } + t->id = 0; + t->period_ms = period_ms; + t->repeat = repeat; + t->cb = cb; + t->userdata = userdata; + crm_trace("Created timer %s with %p %p", t->name, userdata, t->userdata); + } + return t; +} + +void +mainloop_timer_del(mainloop_timer_t *t) +{ + if(t) { + crm_trace("Destroying timer %s", t->name); + mainloop_timer_stop(t); + free(t->name); + free(t); + } +} + +/* + * Helpers to make sure certain events aren't lost at shutdown + */ + +static gboolean +drain_timeout_cb(gpointer user_data) +{ + bool *timeout_popped = (bool*) user_data; + + *timeout_popped = TRUE; + return FALSE; +} + +/*! + * \brief Drain some remaining main loop events then quit it + * + * \param[in,out] mloop Main loop to drain and quit + * \param[in] n Drain up to this many pending events + */ +void +pcmk_quit_main_loop(GMainLoop *mloop, unsigned int n) +{ + if ((mloop != NULL) && g_main_loop_is_running(mloop)) { + GMainContext *ctx = g_main_loop_get_context(mloop); + + /* Drain up to n events in case some memory clean-up is pending + * (helpful to reduce noise in valgrind output). + */ + for (int i = 0; (i < n) && g_main_context_pending(ctx); ++i) { + g_main_context_dispatch(ctx); + } + g_main_loop_quit(mloop); + } +} + +/*! + * \brief Process main loop events while a certain condition is met + * + * \param[in,out] mloop Main loop to process + * \param[in] timer_ms Don't process longer than this amount of time + * \param[in] check Function that returns true if events should be + * processed + * + * \note This function is intended to be called at shutdown if certain important + * events should not be missed. The caller would likely quit the main loop + * or exit after calling this function. The check() function will be + * passed the remaining timeout in milliseconds. + */ +void +pcmk_drain_main_loop(GMainLoop *mloop, guint timer_ms, bool (*check)(guint)) +{ + bool timeout_popped = FALSE; + guint timer = 0; + GMainContext *ctx = NULL; + + CRM_CHECK(mloop && check, return); + + ctx = g_main_loop_get_context(mloop); + if (ctx) { + time_t start_time = time(NULL); + + timer = g_timeout_add(timer_ms, drain_timeout_cb, &timeout_popped); + while (!timeout_popped + && check(timer_ms - (time(NULL) - start_time) * 1000)) { + g_main_context_iteration(ctx, TRUE); + } + } + if (!timeout_popped && (timer > 0)) { + g_source_remove(timer); + } +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +gboolean +crm_signal(int sig, void (*dispatch) (int sig)) +{ + return crm_signal_handler(sig, dispatch) != SIG_ERR; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/messages.c b/lib/common/messages.c new file mode 100644 index 0000000..2c01eed --- /dev/null +++ b/lib/common/messages.c @@ -0,0 +1,291 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include + +#include +#include + +/*! + * \brief Create a Pacemaker request (for IPC or cluster layer) + * + * \param[in] task What to set as the request's task + * \param[in] msg_data What to add as the request's data contents + * \param[in] host_to What to set as the request's destination host + * \param[in] sys_to What to set as the request's destination system + * \param[in] sys_from If not NULL, set as request's origin system + * \param[in] uuid_from If not NULL, use in request's origin system + * \param[in] origin Name of function that called this one + * + * \return XML of new request + * + * \note One of sys_from or uuid_from must be non-NULL + * \note This function should not be called directly, but via the + * create_request() wrapper. + * \note The caller is responsible for freeing the result using free_xml(). + */ +xmlNode * +create_request_adv(const char *task, xmlNode *msg_data, + const char *host_to, const char *sys_to, + const char *sys_from, const char *uuid_from, + const char *origin) +{ + static uint ref_counter = 0; + + char *true_from = NULL; + xmlNode *request = NULL; + char *reference = crm_strdup_printf("%s-%s-%lld-%u", + (task? task : "_empty_"), + (sys_from? sys_from : "_empty_"), + (long long) time(NULL), ref_counter++); + + if (uuid_from != NULL) { + true_from = crm_strdup_printf("%s_%s", uuid_from, + (sys_from? sys_from : "none")); + } else if (sys_from != NULL) { + true_from = strdup(sys_from); + } else { + crm_err("Cannot create IPC request: No originating system specified"); + } + + // host_from will get set for us if necessary by the controller when routed + request = create_xml_node(NULL, __func__); + crm_xml_add(request, F_CRM_ORIGIN, origin); + crm_xml_add(request, F_TYPE, T_CRM); + crm_xml_add(request, F_CRM_VERSION, CRM_FEATURE_SET); + crm_xml_add(request, F_CRM_MSG_TYPE, XML_ATTR_REQUEST); + crm_xml_add(request, F_CRM_REFERENCE, reference); + crm_xml_add(request, F_CRM_TASK, task); + crm_xml_add(request, F_CRM_SYS_TO, sys_to); + crm_xml_add(request, F_CRM_SYS_FROM, true_from); + + /* HOSTTO will be ignored if it is to the DC anyway. */ + if (host_to != NULL && strlen(host_to) > 0) { + crm_xml_add(request, F_CRM_HOST_TO, host_to); + } + + if (msg_data != NULL) { + add_message_xml(request, F_CRM_DATA, msg_data); + } + free(reference); + free(true_from); + + return request; +} + +/*! + * \brief Create a Pacemaker reply (for IPC or cluster layer) + * + * \param[in] original_request XML of request this is a reply to + * \param[in] xml_response_data XML to copy as data section of reply + * \param[in] origin Name of function that called this one + * + * \return XML of new reply + * + * \note This function should not be called directly, but via the + * create_reply() wrapper. + * \note The caller is responsible for freeing the result using free_xml(). + */ +xmlNode * +create_reply_adv(const xmlNode *original_request, xmlNode *xml_response_data, + const char *origin) +{ + xmlNode *reply = NULL; + + const char *host_from = crm_element_value(original_request, F_CRM_HOST_FROM); + const char *sys_from = crm_element_value(original_request, F_CRM_SYS_FROM); + const char *sys_to = crm_element_value(original_request, F_CRM_SYS_TO); + const char *type = crm_element_value(original_request, F_CRM_MSG_TYPE); + const char *operation = crm_element_value(original_request, F_CRM_TASK); + const char *crm_msg_reference = crm_element_value(original_request, F_CRM_REFERENCE); + + if (type == NULL) { + crm_err("Cannot create new_message, no message type in original message"); + CRM_ASSERT(type != NULL); + return NULL; +#if 0 + } else if (strcasecmp(XML_ATTR_REQUEST, type) != 0) { + crm_err("Cannot create new_message, original message was not a request"); + return NULL; +#endif + } + reply = create_xml_node(NULL, __func__); + if (reply == NULL) { + crm_err("Cannot create new_message, malloc failed"); + return NULL; + } + + crm_xml_add(reply, F_CRM_ORIGIN, origin); + crm_xml_add(reply, F_TYPE, T_CRM); + crm_xml_add(reply, F_CRM_VERSION, CRM_FEATURE_SET); + crm_xml_add(reply, F_CRM_MSG_TYPE, XML_ATTR_RESPONSE); + crm_xml_add(reply, F_CRM_REFERENCE, crm_msg_reference); + crm_xml_add(reply, F_CRM_TASK, operation); + + /* since this is a reply, we reverse the from and to */ + crm_xml_add(reply, F_CRM_SYS_TO, sys_from); + crm_xml_add(reply, F_CRM_SYS_FROM, sys_to); + + /* HOSTTO will be ignored if it is to the DC anyway. */ + if (host_from != NULL && strlen(host_from) > 0) { + crm_xml_add(reply, F_CRM_HOST_TO, host_from); + } + + if (xml_response_data != NULL) { + add_message_xml(reply, F_CRM_DATA, xml_response_data); + } + + return reply; +} + +xmlNode * +get_message_xml(const xmlNode *msg, const char *field) +{ + return pcmk__xml_first_child(first_named_child(msg, field)); +} + +gboolean +add_message_xml(xmlNode *msg, const char *field, xmlNode *xml) +{ + xmlNode *holder = create_xml_node(msg, field); + + add_node_copy(holder, xml); + return TRUE; +} + +/*! + * \brief Get name to be used as identifier for cluster messages + * + * \param[in] name Actual system name to check + * + * \return Non-NULL cluster message identifier corresponding to name + * + * \note The Pacemaker daemons were renamed in version 2.0.0, but the old names + * must continue to be used as the identifier for cluster messages, so + * that mixed-version clusters are possible during a rolling upgrade. + */ +const char * +pcmk__message_name(const char *name) +{ + if (name == NULL) { + return "unknown"; + + } else if (!strcmp(name, "pacemaker-attrd")) { + return "attrd"; + + } else if (!strcmp(name, "pacemaker-based")) { + return CRM_SYSTEM_CIB; + + } else if (!strcmp(name, "pacemaker-controld")) { + return CRM_SYSTEM_CRMD; + + } else if (!strcmp(name, "pacemaker-execd")) { + return CRM_SYSTEM_LRMD; + + } else if (!strcmp(name, "pacemaker-fenced")) { + return "stonith-ng"; + + } else if (!strcmp(name, "pacemaker-schedulerd")) { + return CRM_SYSTEM_PENGINE; + + } else { + return name; + } +} + +/*! + * \internal + * \brief Register handlers for server commands + * + * \param[in] handlers Array of handler functions for supported server commands + * (the final entry must have a NULL command name, and if + * it has a handler it will be used as the default handler + * for unrecognized commands) + * + * \return Newly created hash table with commands and handlers + * \note The caller is responsible for freeing the return value with + * g_hash_table_destroy(). + */ +GHashTable * +pcmk__register_handlers(const pcmk__server_command_t handlers[]) +{ + GHashTable *commands = g_hash_table_new(g_str_hash, g_str_equal); + + if (handlers != NULL) { + int i; + + for (i = 0; handlers[i].command != NULL; ++i) { + g_hash_table_insert(commands, (gpointer) handlers[i].command, + handlers[i].handler); + } + if (handlers[i].handler != NULL) { + // g_str_hash() can't handle NULL, so use empty string for default + g_hash_table_insert(commands, (gpointer) "", handlers[i].handler); + } + } + return commands; +} + +/*! + * \internal + * \brief Process an incoming request + * + * \param[in,out] request Request to process + * \param[in] handlers Command table created by pcmk__register_handlers() + * + * \return XML to send as reply (or NULL if no reply is needed) + */ +xmlNode * +pcmk__process_request(pcmk__request_t *request, GHashTable *handlers) +{ + xmlNode *(*handler)(pcmk__request_t *request) = NULL; + + CRM_CHECK((request != NULL) && (request->op != NULL) && (handlers != NULL), + return NULL); + + if (pcmk_is_set(request->flags, pcmk__request_sync) + && (request->ipc_client != NULL)) { + CRM_CHECK(request->ipc_client->request_id == request->ipc_id, + return NULL); + } + + handler = g_hash_table_lookup(handlers, request->op); + if (handler == NULL) { + handler = g_hash_table_lookup(handlers, ""); // Default handler + if (handler == NULL) { + crm_info("Ignoring %s request from %s %s with no handler", + request->op, pcmk__request_origin_type(request), + pcmk__request_origin(request)); + return NULL; + } + } + + return (*handler)(request); +} + +/*! + * \internal + * \brief Free memory used within a request (but not the request itself) + * + * \param[in,out] request Request to reset + */ +void +pcmk__reset_request(pcmk__request_t *request) +{ + free(request->op); + request->op = NULL; + + pcmk__reset_result(&(request->result)); +} diff --git a/lib/common/mock.c b/lib/common/mock.c new file mode 100644 index 0000000..2bd8334 --- /dev/null +++ b/lib/common/mock.c @@ -0,0 +1,427 @@ +/* + * Copyright 2021-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "mock_private.h" + +/* This file is only used when running "make check". It is built into + * libcrmcommon_test.a, not into libcrmcommon.so. It is used to support + * constructing mock versions of library functions for unit testing. + * + * HOW TO ADD A MOCKED FUNCTION: + * + * - In this file, declare a bool pcmk__mock_X variable, and define a __wrap_X + * function with the same prototype as the actual function that performs the + * desired behavior if pcmk__mock_X is true and calls __real_X otherwise. + * You can use cmocka's mock_type() and mock_ptr_type() to pass extra + * information to the mocked function (see existing examples for details). + * + * - In mock_private.h, add declarations for extern bool pcmk__mock_X and the + * __real_X and __wrap_X function prototypes. + * + * - In mk/tap.mk, add the function name to the WRAPPED variable. + * + * HOW TO USE A MOCKED FUNCTION: + * + * - #include "mock_private.h" in your test file. + * + * - Write your test cases using pcmk__mock_X and cmocka's will_return() as + * needed per the comments for the mocked function below. See existing test + * cases for examples. + */ + +// LCOV_EXCL_START +/* calloc() + * + * If pcmk__mock_calloc is set to true, later calls to calloc() will return + * NULL and must be preceded by: + * + * expect_*(__wrap_calloc, nmemb[, ...]); + * expect_*(__wrap_calloc, size[, ...]); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_calloc = false; + +void * +__wrap_calloc(size_t nmemb, size_t size) +{ + if (!pcmk__mock_calloc) { + return __real_calloc(nmemb, size); + } + check_expected(nmemb); + check_expected(size); + return NULL; +} + + +/* getenv() + * + * If pcmk__mock_getenv is set to true, later calls to getenv() must be preceded + * by: + * + * expect_*(__wrap_getenv, name[, ...]); + * will_return(__wrap_getenv, return_value); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_getenv = false; + +char * +__wrap_getenv(const char *name) +{ + if (!pcmk__mock_getenv) { + return __real_getenv(name); + } + check_expected_ptr(name); + return mock_ptr_type(char *); +} + + +/* setenv() + * + * If pcmk__mock_setenv is set to true, later calls to setenv() must be preceded + * by: + * + * expect_*(__wrap_setenv, name[, ...]); + * expect_*(__wrap_setenv, value[, ...]); + * expect_*(__wrap_setenv, overwrite[, ...]); + * will_return(__wrap_setenv, errno_to_set); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + * + * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. + */ +bool pcmk__mock_setenv = false; + +int +__wrap_setenv(const char *name, const char *value, int overwrite) +{ + if (!pcmk__mock_setenv) { + return __real_setenv(name, value, overwrite); + } + check_expected_ptr(name); + check_expected_ptr(value); + check_expected(overwrite); + errno = mock_type(int); + return (errno == 0)? 0 : -1; +} + + +/* unsetenv() + * + * If pcmk__mock_unsetenv is set to true, later calls to unsetenv() must be + * preceded by: + * + * expect_*(__wrap_unsetenv, name[, ...]); + * will_return(__wrap_setenv, errno_to_set); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + * + * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. + */ +bool pcmk__mock_unsetenv = false; + +int +__wrap_unsetenv(const char *name) +{ + if (!pcmk__mock_unsetenv) { + return __real_unsetenv(name); + } + check_expected_ptr(name); + errno = mock_type(int); + return (errno == 0)? 0 : -1; +} + + +/* getpid() + * + * If pcmk__mock_getpid is set to true, later calls to getpid() must be preceded + * by: + * + * will_return(__wrap_getpid, return_value); + */ + +bool pcmk__mock_getpid = false; + +pid_t +__wrap_getpid(void) +{ + return pcmk__mock_getpid? mock_type(pid_t) : __real_getpid(); +} + + +/* setgrent(), getgrent() and endgrent() + * + * If pcmk__mock_grent is set to true, getgrent() will behave as if the only + * groups on the system are: + * + * - grp0 (user0, user1) + * - grp1 (user1) + * - grp2 (user2, user1) + */ + +bool pcmk__mock_grent = false; + +// Index of group that will be returned next from getgrent() +static int group_idx = 0; + +// Data used for testing +static const char* grp0_members[] = { + "user0", "user1", NULL +}; + +static const char* grp1_members[] = { + "user1", NULL +}; + +static const char* grp2_members[] = { + "user2", "user1", NULL +}; + +/* An array of "groups" (a struct from grp.h) + * + * The members of the groups are initalized here to some testing data, casting + * away the consts to make the compiler happy and simplify initialization. We + * never actually change these variables during the test! + * + * string literal = const char* (cannot be changed b/c ? ) + * vs. char* (it's getting casted to this) + */ +static const int NUM_GROUPS = 3; +static struct group groups[] = { + {(char*)"grp0", (char*)"", 0, (char**)grp0_members}, + {(char*)"grp1", (char*)"", 1, (char**)grp1_members}, + {(char*)"grp2", (char*)"", 2, (char**)grp2_members}, +}; + +// This function resets the group_idx to 0. +void +__wrap_setgrent(void) { + if (pcmk__mock_grent) { + group_idx = 0; + } else { + __real_setgrent(); + } +} + +/* This function returns the next group entry in the list of groups, or + * NULL if there aren't any left. + * group_idx is a global variable which keeps track of where you are in the list + */ +struct group * +__wrap_getgrent(void) { + if (pcmk__mock_grent) { + if (group_idx >= NUM_GROUPS) { + return NULL; + } + return &groups[group_idx++]; + } else { + return __real_getgrent(); + } +} + +void +__wrap_endgrent(void) { + if (!pcmk__mock_grent) { + __real_endgrent(); + } +} + + +/* fopen() + * + * If pcmk__mock_fopen is set to true, later calls to fopen() must be + * preceded by: + * + * expect_*(__wrap_fopen, pathname[, ...]); + * expect_*(__wrap_fopen, mode[, ...]); + * will_return(__wrap_fopen, errno_to_set); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_fopen = false; + +FILE * +__wrap_fopen(const char *pathname, const char *mode) +{ + if (pcmk__mock_fopen) { + check_expected_ptr(pathname); + check_expected_ptr(mode); + errno = mock_type(int); + + if (errno != 0) { + return NULL; + } else { + return __real_fopen(pathname, mode); + } + + } else { + return __real_fopen(pathname, mode); + } +} + + +/* getpwnam_r() + * + * If pcmk__mock_getpwnam_r is set to true, later calls to getpwnam_r() must be + * preceded by: + * + * expect_*(__wrap_getpwnam_r, name[, ...]); + * expect_*(__wrap_getpwnam_r, pwd[, ...]); + * expect_*(__wrap_getpwnam_r, buf[, ...]); + * expect_*(__wrap_getpwnam_r, buflen[, ...]); + * expect_*(__wrap_getpwnam_r, result[, ...]); + * will_return(__wrap_getpwnam_r, return_value); + * will_return(__wrap_getpwnam_r, ptr_to_result_struct); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_getpwnam_r = false; + +int +__wrap_getpwnam_r(const char *name, struct passwd *pwd, char *buf, + size_t buflen, struct passwd **result) +{ + if (pcmk__mock_getpwnam_r) { + int retval = mock_type(int); + + check_expected_ptr(name); + check_expected_ptr(pwd); + check_expected_ptr(buf); + check_expected(buflen); + check_expected_ptr(result); + *result = mock_ptr_type(struct passwd *); + return retval; + + } else { + return __real_getpwnam_r(name, pwd, buf, buflen, result); + } +} + +/* + * If pcmk__mock_readlink is set to true, later calls to readlink() must be + * preceded by: + * + * expect_*(__wrap_readlink, path[, ...]); + * expect_*(__wrap_readlink, buf[, ...]); + * expect_*(__wrap_readlink, bufsize[, ...]); + * will_return(__wrap_readlink, errno_to_set); + * will_return(__wrap_readlink, link_contents); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + * + * The mocked function will return 0 if errno_to_set is 0, and -1 otherwise. + */ + +bool pcmk__mock_readlink = false; + +ssize_t +__wrap_readlink(const char *restrict path, char *restrict buf, + size_t bufsize) +{ + if (pcmk__mock_readlink) { + const char *contents = NULL; + + check_expected_ptr(path); + check_expected_ptr(buf); + check_expected(bufsize); + errno = mock_type(int); + contents = mock_ptr_type(const char *); + + if (errno == 0) { + strncpy(buf, contents, bufsize - 1); + return strlen(contents); + } + return -1; + + } else { + return __real_readlink(path, buf, bufsize); + } +} + + +/* strdup() + * + * If pcmk__mock_strdup is set to true, later calls to strdup() will return + * NULL and must be preceded by: + * + * expect_*(__wrap_strdup, s[, ...]); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_strdup = false; + +char * +__wrap_strdup(const char *s) +{ + if (!pcmk__mock_strdup) { + return __real_strdup(s); + } + check_expected_ptr(s); + return NULL; +} + + +/* uname() + * + * If pcmk__mock_uname is set to true, later calls to uname() must be preceded + * by: + * + * expect_*(__wrap_uname, buf[, ...]); + * will_return(__wrap_uname, return_value); + * will_return(__wrap_uname, node_name_for_buf_parameter_to_uname); + * + * expect_* functions: https://api.cmocka.org/group__cmocka__param.html + */ + +bool pcmk__mock_uname = false; + +int +__wrap_uname(struct utsname *buf) +{ + if (pcmk__mock_uname) { + int retval = 0; + char *result = NULL; + + check_expected_ptr(buf); + retval = mock_type(int); + result = mock_ptr_type(char *); + + if (result != NULL) { + strcpy(buf->nodename, result); + } + return retval; + + } else { + return __real_uname(buf); + } +} + +// LCOV_EXCL_STOP diff --git a/lib/common/mock_private.h b/lib/common/mock_private.h new file mode 100644 index 0000000..45207c4 --- /dev/null +++ b/lib/common/mock_private.h @@ -0,0 +1,77 @@ +/* + * Copyright 2021-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef MOCK_PRIVATE__H +# define MOCK_PRIVATE__H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* This header is for the sole use of libcrmcommon_test and unit tests */ + +extern bool pcmk__mock_calloc; +void *__real_calloc(size_t nmemb, size_t size); +void *__wrap_calloc(size_t nmemb, size_t size); + +extern bool pcmk__mock_fopen; +FILE *__real_fopen(const char *pathname, const char *mode); +FILE *__wrap_fopen(const char *pathname, const char *mode); + +extern bool pcmk__mock_getenv; +char *__real_getenv(const char *name); +char *__wrap_getenv(const char *name); + +extern bool pcmk__mock_setenv; +int __real_setenv(const char *name, const char *value, int overwrite); +int __wrap_setenv(const char *name, const char *value, int overwrite); + +extern bool pcmk__mock_unsetenv; +int __real_unsetenv(const char *name); +int __wrap_unsetenv(const char *name); + +extern bool pcmk__mock_getpid; +pid_t __real_getpid(void); +pid_t __wrap_getpid(void); + +extern bool pcmk__mock_grent; +void __real_setgrent(void); +void __wrap_setgrent(void); +struct group * __wrap_getgrent(void); +struct group * __real_getgrent(void); +void __wrap_endgrent(void); +void __real_endgrent(void); + +extern bool pcmk__mock_getpwnam_r; +int __real_getpwnam_r(const char *name, struct passwd *pwd, + char *buf, size_t buflen, struct passwd **result); +int __wrap_getpwnam_r(const char *name, struct passwd *pwd, + char *buf, size_t buflen, struct passwd **result); + +extern bool pcmk__mock_readlink; +ssize_t __real_readlink(const char *restrict path, char *restrict buf, + size_t bufsize); +ssize_t __wrap_readlink(const char *restrict path, char *restrict buf, + size_t bufsize); + +extern bool pcmk__mock_strdup; +char *__real_strdup(const char *s); +char *__wrap_strdup(const char *s); + +extern bool pcmk__mock_uname; +int __real_uname(struct utsname *buf); +int __wrap_uname(struct utsname *buf); + +#endif // MOCK_PRIVATE__H diff --git a/lib/common/nodes.c b/lib/common/nodes.c new file mode 100644 index 0000000..a17d587 --- /dev/null +++ b/lib/common/nodes.c @@ -0,0 +1,24 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +void +pcmk__xe_add_node(xmlNode *xml, const char *node, int nodeid) +{ + if (node != NULL) { + crm_xml_add(xml, PCMK__XA_ATTR_NODE_NAME, node); + } + + if (nodeid > 0) { + crm_xml_add_int(xml, PCMK__XA_ATTR_NODE_ID, nodeid); + } +} diff --git a/lib/common/nvpair.c b/lib/common/nvpair.c new file mode 100644 index 0000000..3766c45 --- /dev/null +++ b/lib/common/nvpair.c @@ -0,0 +1,992 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "crmcommon_private.h" + +/* + * This file isolates handling of three types of name/value pairs: + * + * - pcmk_nvpair_t data type + * - XML attributes () + * - XML nvpair elements () + */ + +// pcmk_nvpair_t handling + +/*! + * \internal + * \brief Allocate a new name/value pair + * + * \param[in] name New name (required) + * \param[in] value New value + * + * \return Newly allocated name/value pair + * \note The caller is responsible for freeing the result with + * \c pcmk__free_nvpair(). + */ +static pcmk_nvpair_t * +pcmk__new_nvpair(const char *name, const char *value) +{ + pcmk_nvpair_t *nvpair = NULL; + + CRM_ASSERT(name); + + nvpair = calloc(1, sizeof(pcmk_nvpair_t)); + CRM_ASSERT(nvpair); + + pcmk__str_update(&nvpair->name, name); + pcmk__str_update(&nvpair->value, value); + return nvpair; +} + +/*! + * \internal + * \brief Free a name/value pair + * + * \param[in,out] nvpair Name/value pair to free + */ +static void +pcmk__free_nvpair(gpointer data) +{ + if (data) { + pcmk_nvpair_t *nvpair = data; + + free(nvpair->name); + free(nvpair->value); + free(nvpair); + } +} + +/*! + * \brief Prepend a name/value pair to a list + * + * \param[in,out] nvpairs List to modify + * \param[in] name New entry's name + * \param[in] value New entry's value + * + * \return New head of list + * \note The caller is responsible for freeing the list with + * \c pcmk_free_nvpairs(). + */ +GSList * +pcmk_prepend_nvpair(GSList *nvpairs, const char *name, const char *value) +{ + return g_slist_prepend(nvpairs, pcmk__new_nvpair(name, value)); +} + +/*! + * \brief Free a list of name/value pairs + * + * \param[in,out] list List to free + */ +void +pcmk_free_nvpairs(GSList *nvpairs) +{ + g_slist_free_full(nvpairs, pcmk__free_nvpair); +} + +/*! + * \internal + * \brief Compare two name/value pairs + * + * \param[in] a First name/value pair to compare + * \param[in] b Second name/value pair to compare + * + * \return 0 if a == b, 1 if a > b, -1 if a < b + */ +static gint +pcmk__compare_nvpair(gconstpointer a, gconstpointer b) +{ + int rc = 0; + const pcmk_nvpair_t *pair_a = a; + const pcmk_nvpair_t *pair_b = b; + + CRM_ASSERT(a != NULL); + CRM_ASSERT(pair_a->name != NULL); + + CRM_ASSERT(b != NULL); + CRM_ASSERT(pair_b->name != NULL); + + rc = strcmp(pair_a->name, pair_b->name); + if (rc < 0) { + return -1; + } else if (rc > 0) { + return 1; + } + return 0; +} + +/*! + * \brief Sort a list of name/value pairs + * + * \param[in,out] list List to sort + * + * \return New head of list + */ +GSList * +pcmk_sort_nvpairs(GSList *list) +{ + return g_slist_sort(list, pcmk__compare_nvpair); +} + +/*! + * \brief Create a list of name/value pairs from an XML node's attributes + * + * \param[in] XML to parse + * + * \return New list of name/value pairs + * \note It is the caller's responsibility to free the list with + * \c pcmk_free_nvpairs(). + */ +GSList * +pcmk_xml_attrs2nvpairs(const xmlNode *xml) +{ + GSList *result = NULL; + + for (xmlAttrPtr iter = pcmk__xe_first_attr(xml); iter != NULL; + iter = iter->next) { + + result = pcmk_prepend_nvpair(result, + (const char *) iter->name, + (const char *) pcmk__xml_attr_value(iter)); + } + return result; +} + +/*! + * \internal + * \brief Add an XML attribute corresponding to a name/value pair + * + * Suitable for glib list iterators, this function adds a NAME=VALUE + * XML attribute based on a given name/value pair. + * + * \param[in] data Name/value pair + * \param[out] user_data XML node to add attributes to + */ +static void +pcmk__nvpair_add_xml_attr(gpointer data, gpointer user_data) +{ + pcmk_nvpair_t *pair = data; + xmlNode *parent = user_data; + + crm_xml_add(parent, pair->name, pair->value); +} + +/*! + * \brief Add XML attributes based on a list of name/value pairs + * + * \param[in,out] list List of name/value pairs + * \param[in,out] xml XML node to add attributes to + */ +void +pcmk_nvpairs2xml_attrs(GSList *list, xmlNode *xml) +{ + g_slist_foreach(list, pcmk__nvpair_add_xml_attr, xml); +} + +// convenience function for name=value strings + +/*! + * \internal + * \brief Extract the name and value from an input string formatted as "name=value". + * If unable to extract them, they are returned as NULL. + * + * \param[in] input The input string, likely from the command line + * \param[out] name Everything before the first '=' in the input string + * \param[out] value Everything after the first '=' in the input string + * + * \return 2 if both name and value could be extracted, 1 if only one could, and + * and error code otherwise + */ +int +pcmk__scan_nvpair(const char *input, char **name, char **value) +{ +#ifdef HAVE_SSCANF_M + *name = NULL; + *value = NULL; + if (sscanf(input, "%m[^=]=%m[^\n]", name, value) <= 0) { + return -pcmk_err_bad_nvpair; + } +#else + char *sep = NULL; + *name = NULL; + *value = NULL; + + sep = strstr(optarg, "="); + if (sep == NULL) { + return -pcmk_err_bad_nvpair; + } + + *name = strndup(input, sep-input); + + if (*name == NULL) { + return -ENOMEM; + } + + /* If the last char in optarg is =, the user gave no + * value for the option. Leave it as NULL. + */ + if (*(sep+1) != '\0') { + *value = strdup(sep+1); + + if (*value == NULL) { + return -ENOMEM; + } + } +#endif + + if (*name != NULL && *value != NULL) { + return 2; + } else if (*name != NULL || *value != NULL) { + return 1; + } else { + return -pcmk_err_bad_nvpair; + } +} + +/*! + * \internal + * \brief Format a name/value pair. + * + * Units can optionally be provided for the value. Note that unlike most + * formatting functions, this one returns the formatted string. It is + * assumed that the most common use of this function will be to build up + * a string to be output as part of other functions. + * + * \note The caller is responsible for freeing the return value after use. + * + * \param[in] name The name of the nvpair. + * \param[in] value The value of the nvpair. + * \param[in] units Optional units for the value, or NULL. + * + * \return Newly allocated string with name/value pair + */ +char * +pcmk__format_nvpair(const char *name, const char *value, const char *units) +{ + return crm_strdup_printf("%s=\"%s%s\"", name, value, units ? units : ""); +} + +// XML attribute handling + +/*! + * \brief Create an XML attribute with specified name and value + * + * \param[in,out] node XML node to modify + * \param[in] name Attribute name to set + * \param[in] value Attribute value to set + * + * \return New value on success, \c NULL otherwise + * \note This does nothing if node, name, or value are \c NULL or empty. + */ +const char * +crm_xml_add(xmlNode *node, const char *name, const char *value) +{ + bool dirty = FALSE; + xmlAttr *attr = NULL; + + CRM_CHECK(node != NULL, return NULL); + CRM_CHECK(name != NULL, return NULL); + + if (value == NULL) { + return NULL; + } + + if (pcmk__tracking_xml_changes(node, FALSE)) { + const char *old = crm_element_value(node, name); + + if (old == NULL || value == NULL || strcmp(old, value) != 0) { + dirty = TRUE; + } + } + + if (dirty && (pcmk__check_acl(node, name, pcmk__xf_acl_create) == FALSE)) { + crm_trace("Cannot add %s=%s to %s", name, value, node->name); + return NULL; + } + + attr = xmlSetProp(node, (pcmkXmlStr) name, (pcmkXmlStr) value); + if (dirty) { + pcmk__mark_xml_attr_dirty(attr); + } + + CRM_CHECK(attr && attr->children && attr->children->content, return NULL); + return (char *)attr->children->content; +} + +/*! + * \brief Replace an XML attribute with specified name and (possibly NULL) value + * + * \param[in,out] node XML node to modify + * \param[in] name Attribute name to set + * \param[in] value Attribute value to set + * + * \return New value on success, \c NULL otherwise + * \note This does nothing if node or name is \c NULL or empty. + */ +const char * +crm_xml_replace(xmlNode *node, const char *name, const char *value) +{ + bool dirty = FALSE; + xmlAttr *attr = NULL; + const char *old_value = NULL; + + CRM_CHECK(node != NULL, return NULL); + CRM_CHECK(name != NULL && name[0] != 0, return NULL); + + old_value = crm_element_value(node, name); + + /* Could be re-setting the same value */ + CRM_CHECK(old_value != value, return value); + + if (pcmk__check_acl(node, name, pcmk__xf_acl_write) == FALSE) { + /* Create a fake object linked to doc->_private instead? */ + crm_trace("Cannot replace %s=%s to %s", name, value, node->name); + return NULL; + + } else if (old_value && !value) { + xml_remove_prop(node, name); + return NULL; + } + + if (pcmk__tracking_xml_changes(node, FALSE)) { + if (!old_value || !value || !strcmp(old_value, value)) { + dirty = TRUE; + } + } + + attr = xmlSetProp(node, (pcmkXmlStr) name, (pcmkXmlStr) value); + if (dirty) { + pcmk__mark_xml_attr_dirty(attr); + } + CRM_CHECK(attr && attr->children && attr->children->content, return NULL); + return (char *) attr->children->content; +} + +/*! + * \brief Create an XML attribute with specified name and integer value + * + * This is like \c crm_xml_add() but taking an integer value. + * + * \param[in,out] node XML node to modify + * \param[in] name Attribute name to set + * \param[in] value Attribute value to set + * + * \return New value as string on success, \c NULL otherwise + * \note This does nothing if node or name are \c NULL or empty. + */ +const char * +crm_xml_add_int(xmlNode *node, const char *name, int value) +{ + char *number = pcmk__itoa(value); + const char *added = crm_xml_add(node, name, number); + + free(number); + return added; +} + +/*! + * \brief Create an XML attribute with specified name and unsigned value + * + * This is like \c crm_xml_add() but taking a guint value. + * + * \param[in,out] node XML node to modify + * \param[in] name Attribute name to set + * \param[in] ms Attribute value to set + * + * \return New value as string on success, \c NULL otherwise + * \note This does nothing if node or name are \c NULL or empty. + */ +const char * +crm_xml_add_ms(xmlNode *node, const char *name, guint ms) +{ + char *number = crm_strdup_printf("%u", ms); + const char *added = crm_xml_add(node, name, number); + + free(number); + return added; +} + +// Maximum size of null-terminated string representation of 64-bit integer +// -9223372036854775808 +#define LLSTRSIZE 21 + +/*! + * \brief Create an XML attribute with specified name and long long int value + * + * This is like \c crm_xml_add() but taking a long long int value. It is a + * useful equivalent for defined types like time_t, etc. + * + * \param[in,out] xml XML node to modify + * \param[in] name Attribute name to set + * \param[in] value Attribute value to set + * + * \return New value as string on success, \c NULL otherwise + * \note This does nothing if xml or name are \c NULL or empty. + * This does not support greater than 64-bit values. + */ +const char * +crm_xml_add_ll(xmlNode *xml, const char *name, long long value) +{ + char s[LLSTRSIZE] = { '\0', }; + + if (snprintf(s, LLSTRSIZE, "%lld", (long long) value) == LLSTRSIZE) { + return NULL; + } + return crm_xml_add(xml, name, s); +} + +/*! + * \brief Create XML attributes for seconds and microseconds + * + * This is like \c crm_xml_add() but taking a struct timeval. + * + * \param[in,out] xml XML node to modify + * \param[in] name_sec Name of XML attribute for seconds + * \param[in] name_usec Name of XML attribute for microseconds (or NULL) + * \param[in] value Time value to set + * + * \return New seconds value as string on success, \c NULL otherwise + * \note This does nothing if xml, name_sec, or value is \c NULL. + */ +const char * +crm_xml_add_timeval(xmlNode *xml, const char *name_sec, const char *name_usec, + const struct timeval *value) +{ + const char *added = NULL; + + if (xml && name_sec && value) { + added = crm_xml_add_ll(xml, name_sec, (long long) value->tv_sec); + if (added && name_usec) { + // Any error is ignored (we successfully added seconds) + crm_xml_add_ll(xml, name_usec, (long long) value->tv_usec); + } + } + return added; +} + +/*! + * \brief Retrieve the value of an XML attribute + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * + * \return Value of specified attribute (may be \c NULL) + */ +const char * +crm_element_value(const xmlNode *data, const char *name) +{ + xmlAttr *attr = NULL; + + if (data == NULL) { + crm_err("Couldn't find %s in NULL", name ? name : ""); + CRM_LOG_ASSERT(data != NULL); + return NULL; + + } else if (name == NULL) { + crm_err("Couldn't find NULL in %s", crm_element_name(data)); + return NULL; + } + + /* The first argument to xmlHasProp() has always been const, + * but libxml2 <2.9.2 didn't declare that, so cast it + */ + attr = xmlHasProp((xmlNode *) data, (pcmkXmlStr) name); + if (!attr || !attr->children) { + return NULL; + } + return (const char *) attr->children->content; +} + +/*! + * \brief Retrieve the integer value of an XML attribute + * + * This is like \c crm_element_value() but getting the value as an integer. + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * \param[out] dest Where to store element value + * + * \return 0 on success, -1 otherwise + */ +int +crm_element_value_int(const xmlNode *data, const char *name, int *dest) +{ + const char *value = NULL; + + CRM_CHECK(dest != NULL, return -1); + value = crm_element_value(data, name); + if (value) { + long long value_ll; + + if ((pcmk__scan_ll(value, &value_ll, 0LL) != pcmk_rc_ok) + || (value_ll < INT_MIN) || (value_ll > INT_MAX)) { + *dest = PCMK__PARSE_INT_DEFAULT; + } else { + *dest = (int) value_ll; + return 0; + } + } + return -1; +} + +/*! + * \brief Retrieve the long long integer value of an XML attribute + * + * This is like \c crm_element_value() but getting the value as a long long int. + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * \param[out] dest Where to store element value + * + * \return 0 on success, -1 otherwise + */ +int +crm_element_value_ll(const xmlNode *data, const char *name, long long *dest) +{ + const char *value = NULL; + + CRM_CHECK(dest != NULL, return -1); + value = crm_element_value(data, name); + if ((value != NULL) + && (pcmk__scan_ll(value, dest, PCMK__PARSE_INT_DEFAULT) == pcmk_rc_ok)) { + return 0; + } + return -1; +} + +/*! + * \brief Retrieve the millisecond value of an XML attribute + * + * This is like \c crm_element_value() but returning the value as a guint. + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * \param[out] dest Where to store attribute value + * + * \return \c pcmk_ok on success, -1 otherwise + */ +int +crm_element_value_ms(const xmlNode *data, const char *name, guint *dest) +{ + const char *value = NULL; + long long value_ll; + + CRM_CHECK(dest != NULL, return -1); + *dest = 0; + value = crm_element_value(data, name); + if ((pcmk__scan_ll(value, &value_ll, 0LL) != pcmk_rc_ok) + || (value_ll < 0) || (value_ll > G_MAXUINT)) { + return -1; + } + *dest = (guint) value_ll; + return pcmk_ok; +} + +/*! + * \brief Retrieve the seconds-since-epoch value of an XML attribute + * + * This is like \c crm_element_value() but returning the value as a time_t. + * + * \param[in] xml XML node to check + * \param[in] name Attribute name to check + * \param[out] dest Where to store attribute value + * + * \return \c pcmk_ok on success, -1 otherwise + */ +int +crm_element_value_epoch(const xmlNode *xml, const char *name, time_t *dest) +{ + long long value_ll = 0; + + if (crm_element_value_ll(xml, name, &value_ll) < 0) { + return -1; + } + + /* Unfortunately, we can't do any bounds checking, since time_t has neither + * standardized bounds nor constants defined for them. + */ + *dest = (time_t) value_ll; + return pcmk_ok; +} + +/*! + * \brief Retrieve the value of XML second/microsecond attributes as time + * + * This is like \c crm_element_value() but returning value as a struct timeval. + * + * \param[in] xml XML to parse + * \param[in] name_sec Name of XML attribute for seconds + * \param[in] name_usec Name of XML attribute for microseconds + * \param[out] dest Where to store result + * + * \return \c pcmk_ok on success, -errno on error + * \note Values default to 0 if XML or XML attribute does not exist + */ +int +crm_element_value_timeval(const xmlNode *xml, const char *name_sec, + const char *name_usec, struct timeval *dest) +{ + long long value_i = 0; + + CRM_CHECK(dest != NULL, return -EINVAL); + dest->tv_sec = 0; + dest->tv_usec = 0; + + if (xml == NULL) { + return pcmk_ok; + } + + /* Unfortunately, we can't do any bounds checking, since there are no + * constants provided for the bounds of time_t and suseconds_t, and + * calculating them isn't worth the effort. If there are XML values + * beyond the native sizes, there will probably be worse problems anyway. + */ + + // Parse seconds + errno = 0; + if (crm_element_value_ll(xml, name_sec, &value_i) < 0) { + return -errno; + } + dest->tv_sec = (time_t) value_i; + + // Parse microseconds + if (crm_element_value_ll(xml, name_usec, &value_i) < 0) { + return -errno; + } + dest->tv_usec = (suseconds_t) value_i; + + return pcmk_ok; +} + +/*! + * \brief Retrieve a copy of the value of an XML attribute + * + * This is like \c crm_element_value() but allocating new memory for the result. + * + * \param[in] data XML node to check + * \param[in] name Attribute name to check + * + * \return Value of specified attribute (may be \c NULL) + * \note The caller is responsible for freeing the result. + */ +char * +crm_element_value_copy(const xmlNode *data, const char *name) +{ + char *value_copy = NULL; + + pcmk__str_update(&value_copy, crm_element_value(data, name)); + return value_copy; +} + +/*! + * \brief Add hash table entry to XML as (possibly legacy) name/value + * + * Suitable for \c g_hash_table_foreach(), this function takes a hash table key + * and value, with an XML node passed as user data, and adds an XML attribute + * with the specified name and value if it does not already exist. If the key + * name starts with a digit, this will instead add a \ child to the XML (for legacy compatibility with heartbeat). + * + * \param[in] key Key of hash table entry + * \param[in] value Value of hash table entry + * \param[in,out] user_data XML node + */ +void +hash2smartfield(gpointer key, gpointer value, gpointer user_data) +{ + const char *name = key; + const char *s_value = value; + + xmlNode *xml_node = user_data; + + if (isdigit(name[0])) { + xmlNode *tmp = create_xml_node(xml_node, XML_TAG_PARAM); + + crm_xml_add(tmp, XML_NVPAIR_ATTR_NAME, name); + crm_xml_add(tmp, XML_NVPAIR_ATTR_VALUE, s_value); + + } else if (crm_element_value(xml_node, name) == NULL) { + crm_xml_add(xml_node, name, s_value); + crm_trace("dumped: %s=%s", name, s_value); + + } else { + crm_trace("duplicate: %s=%s", name, s_value); + } +} + +/*! + * \brief Set XML attribute based on hash table entry + * + * Suitable for \c g_hash_table_foreach(), this function takes a hash table key + * and value, with an XML node passed as user data, and adds an XML attribute + * with the specified name and value if it does not already exist. + * + * \param[in] key Key of hash table entry + * \param[in] value Value of hash table entry + * \param[in,out] user_data XML node + */ +void +hash2field(gpointer key, gpointer value, gpointer user_data) +{ + const char *name = key; + const char *s_value = value; + + xmlNode *xml_node = user_data; + + if (crm_element_value(xml_node, name) == NULL) { + crm_xml_add(xml_node, name, s_value); + + } else { + crm_trace("duplicate: %s=%s", name, s_value); + } +} + +/*! + * \brief Set XML attribute based on hash table entry, as meta-attribute name + * + * Suitable for \c g_hash_table_foreach(), this function takes a hash table key + * and value, with an XML node passed as user data, and adds an XML attribute + * with the meta-attribute version of the specified name and value if it does + * not already exist and if the name does not appear to be cluster-internal. + * + * \param[in] key Key of hash table entry + * \param[in] value Value of hash table entry + * \param[in,out] user_data XML node + */ +void +hash2metafield(gpointer key, gpointer value, gpointer user_data) +{ + char *crm_name = NULL; + + if (key == NULL || value == NULL) { + return; + } + + /* Filter out cluster-generated attributes that contain a '#' or ':' + * (like fail-count and last-failure). + */ + for (crm_name = key; *crm_name; ++crm_name) { + if ((*crm_name == '#') || (*crm_name == ':')) { + return; + } + } + + crm_name = crm_meta_name(key); + hash2field(crm_name, value, user_data); + free(crm_name); +} + +// nvpair handling + +/*! + * \brief Create an XML name/value pair + * + * \param[in,out] parent If not \c NULL, make new XML node a child of this one + * \param[in] id Set this as XML ID (or NULL to auto-generate) + * \param[in] name Name to use + * \param[in] value Value to use + * + * \return New XML object on success, \c NULL otherwise + */ +xmlNode * +crm_create_nvpair_xml(xmlNode *parent, const char *id, const char *name, + const char *value) +{ + xmlNode *nvp; + + /* id can be NULL so we auto-generate one, and name can be NULL if this + * will be used to delete a name/value pair by ID, but both can't be NULL + */ + CRM_CHECK(id || name, return NULL); + + nvp = create_xml_node(parent, XML_CIB_TAG_NVPAIR); + CRM_CHECK(nvp, return NULL); + + if (id) { + crm_xml_add(nvp, XML_ATTR_ID, id); + } else { + const char *parent_id = ID(parent); + + crm_xml_set_id(nvp, "%s-%s", + (parent_id? parent_id : XML_CIB_TAG_NVPAIR), name); + } + crm_xml_add(nvp, XML_NVPAIR_ATTR_NAME, name); + crm_xml_add(nvp, XML_NVPAIR_ATTR_VALUE, value); + return nvp; +} + +/*! + * \brief Add XML nvpair element based on hash table entry + * + * Suitable for \c g_hash_table_foreach(), this function takes a hash table key + * and value, with an XML node passed as the user data, and adds an \c nvpair + * XML element with the specified name and value. + * + * \param[in] key Key of hash table entry + * \param[in] value Value of hash table entry + * \param[in,out] user_data XML node + */ +void +hash2nvpair(gpointer key, gpointer value, gpointer user_data) +{ + const char *name = key; + const char *s_value = value; + xmlNode *xml_node = user_data; + + crm_create_nvpair_xml(xml_node, name, name, s_value); + crm_trace("dumped: name=%s value=%s", name, s_value); +} + +/*! + * \brief Retrieve XML attributes as a hash table + * + * Given an XML element, this will look for any \ element child, + * creating a hash table of (newly allocated string) name/value pairs taken + * first from the attributes element's NAME=VALUE XML attributes, and then + * from any \ children of attributes. + * + * \param[in] XML node to parse + * + * \return Hash table with name/value pairs + * \note It is the caller's responsibility to free the result using + * \c g_hash_table_destroy(). + */ +GHashTable * +xml2list(const xmlNode *parent) +{ + xmlNode *child = NULL; + xmlAttrPtr pIter = NULL; + xmlNode *nvpair_list = NULL; + GHashTable *nvpair_hash = pcmk__strkey_table(free, free); + + CRM_CHECK(parent != NULL, return nvpair_hash); + + nvpair_list = find_xml_node(parent, XML_TAG_ATTRS, FALSE); + if (nvpair_list == NULL) { + crm_trace("No attributes in %s", crm_element_name(parent)); + crm_log_xml_trace(parent, "No attributes for resource op"); + } + + crm_log_xml_trace(nvpair_list, "Unpacking"); + + for (pIter = pcmk__xe_first_attr(nvpair_list); pIter != NULL; + pIter = pIter->next) { + + const char *p_name = (const char *)pIter->name; + const char *p_value = pcmk__xml_attr_value(pIter); + + crm_trace("Added %s=%s", p_name, p_value); + + g_hash_table_insert(nvpair_hash, strdup(p_name), strdup(p_value)); + } + + for (child = pcmk__xml_first_child(nvpair_list); child != NULL; + child = pcmk__xml_next(child)) { + + if (strcmp((const char *)child->name, XML_TAG_PARAM) == 0) { + const char *key = crm_element_value(child, XML_NVPAIR_ATTR_NAME); + const char *value = crm_element_value(child, XML_NVPAIR_ATTR_VALUE); + + crm_trace("Added %s=%s", key, value); + if (key != NULL && value != NULL) { + g_hash_table_insert(nvpair_hash, strdup(key), strdup(value)); + } + } + } + + return nvpair_hash; +} + +void +pcmk__xe_set_bool_attr(xmlNodePtr node, const char *name, bool value) +{ + crm_xml_add(node, name, value ? XML_BOOLEAN_TRUE : XML_BOOLEAN_FALSE); +} + +int +pcmk__xe_get_bool_attr(const xmlNode *node, const char *name, bool *value) +{ + const char *xml_value = NULL; + int ret, rc; + + if (node == NULL) { + return ENODATA; + } else if (name == NULL || value == NULL) { + return EINVAL; + } + + xml_value = crm_element_value(node, name); + + if (xml_value == NULL) { + return ENODATA; + } + + rc = crm_str_to_boolean(xml_value, &ret); + if (rc == 1) { + *value = ret; + return pcmk_rc_ok; + } else { + return pcmk_rc_bad_input; + } +} + +bool +pcmk__xe_attr_is_true(const xmlNode *node, const char *name) +{ + bool value = false; + int rc; + + rc = pcmk__xe_get_bool_attr(node, name, &value); + return rc == pcmk_rc_ok && value == true; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +int +pcmk_scan_nvpair(const char *input, char **name, char **value) +{ + return pcmk__scan_nvpair(input, name, value); +} + +char * +pcmk_format_nvpair(const char *name, const char *value, + const char *units) +{ + return pcmk__format_nvpair(name, value, units); +} + +char * +pcmk_format_named_time(const char *name, time_t epoch_time) +{ + char *now_s = pcmk__epoch2str(&epoch_time, 0); + char *result = crm_strdup_printf("%s=\"%s\"", name, pcmk__s(now_s, "")); + + free(now_s); + return result; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/operations.c b/lib/common/operations.c new file mode 100644 index 0000000..3db96cd --- /dev/null +++ b/lib/common/operations.c @@ -0,0 +1,530 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +/*! + * \brief Generate an operation key (RESOURCE_ACTION_INTERVAL) + * + * \param[in] rsc_id ID of resource being operated on + * \param[in] op_type Operation name + * \param[in] interval_ms Operation interval + * + * \return Newly allocated memory containing operation key as string + * + * \note This function asserts on errors, so it will never return NULL. + * The caller is responsible for freeing the result with free(). + */ +char * +pcmk__op_key(const char *rsc_id, const char *op_type, guint interval_ms) +{ + CRM_ASSERT(rsc_id != NULL); + CRM_ASSERT(op_type != NULL); + return crm_strdup_printf(PCMK__OP_FMT, rsc_id, op_type, interval_ms); +} + +static inline gboolean +convert_interval(const char *s, guint *interval_ms) +{ + unsigned long l; + + errno = 0; + l = strtoul(s, NULL, 10); + + if (errno != 0) { + return FALSE; + } + + *interval_ms = (guint) l; + return TRUE; +} + +/*! + * \internal + * \brief Check for underbar-separated substring match + * + * \param[in] key Overall string being checked + * \param[in] position Match before underbar at this \p key index + * \param[in] matches Substrings to match (may contain underbars) + * + * \return \p key index of underbar before any matching substring, + * or 0 if none + */ +static size_t +match_before(const char *key, size_t position, const char **matches) +{ + for (int i = 0; matches[i] != NULL; ++i) { + const size_t match_len = strlen(matches[i]); + + // Must have at least X_MATCH before position + if (position > (match_len + 1)) { + const size_t possible = position - match_len - 1; + + if ((key[possible] == '_') + && (strncmp(key + possible + 1, matches[i], match_len) == 0)) { + return possible; + } + } + } + return 0; +} + +gboolean +parse_op_key(const char *key, char **rsc_id, char **op_type, guint *interval_ms) +{ + guint local_interval_ms = 0; + const size_t key_len = (key == NULL)? 0 : strlen(key); + + // Operation keys must be formatted as RSC_ACTION_INTERVAL + size_t action_underbar = 0; // Index in key of underbar before ACTION + size_t interval_underbar = 0; // Index in key of underbar before INTERVAL + size_t possible = 0; + + /* Underbar was a poor choice of separator since both RSC and ACTION can + * contain underbars. Here, list action names and name prefixes that can. + */ + const char *actions_with_underbars[] = { + CRMD_ACTION_MIGRATED, + CRMD_ACTION_MIGRATE, + NULL + }; + const char *action_prefixes_with_underbars[] = { + "pre_" CRMD_ACTION_NOTIFY, + "post_" CRMD_ACTION_NOTIFY, + "confirmed-pre_" CRMD_ACTION_NOTIFY, + "confirmed-post_" CRMD_ACTION_NOTIFY, + NULL, + }; + + // Initialize output variables in case of early return + if (rsc_id) { + *rsc_id = NULL; + } + if (op_type) { + *op_type = NULL; + } + if (interval_ms) { + *interval_ms = 0; + } + + // RSC_ACTION_INTERVAL implies a minimum of 5 characters + if (key_len < 5) { + return FALSE; + } + + // Find, parse, and validate interval + interval_underbar = key_len - 2; + while ((interval_underbar > 2) && (key[interval_underbar] != '_')) { + --interval_underbar; + } + if ((interval_underbar == 2) + || !convert_interval(key + interval_underbar + 1, &local_interval_ms)) { + return FALSE; + } + + // Find the base (OCF) action name, disregarding prefixes + action_underbar = match_before(key, interval_underbar, + actions_with_underbars); + if (action_underbar == 0) { + action_underbar = interval_underbar - 2; + while ((action_underbar > 0) && (key[action_underbar] != '_')) { + --action_underbar; + } + if (action_underbar == 0) { + return FALSE; + } + } + possible = match_before(key, action_underbar, + action_prefixes_with_underbars); + if (possible != 0) { + action_underbar = possible; + } + + // Set output variables + if (rsc_id != NULL) { + *rsc_id = strndup(key, action_underbar); + CRM_ASSERT(*rsc_id != NULL); + } + if (op_type != NULL) { + *op_type = strndup(key + action_underbar + 1, + interval_underbar - action_underbar - 1); + CRM_ASSERT(*op_type != NULL); + } + if (interval_ms != NULL) { + *interval_ms = local_interval_ms; + } + return TRUE; +} + +char * +pcmk__notify_key(const char *rsc_id, const char *notify_type, + const char *op_type) +{ + CRM_CHECK(rsc_id != NULL, return NULL); + CRM_CHECK(op_type != NULL, return NULL); + CRM_CHECK(notify_type != NULL, return NULL); + return crm_strdup_printf("%s_%s_notify_%s_0", + rsc_id, notify_type, op_type); +} + +/*! + * \brief Parse a transition magic string into its constituent parts + * + * \param[in] magic Magic string to parse (must be non-NULL) + * \param[out] uuid If non-NULL, where to store copy of parsed UUID + * \param[out] transition_id If non-NULL, where to store parsed transition ID + * \param[out] action_id If non-NULL, where to store parsed action ID + * \param[out] op_status If non-NULL, where to store parsed result status + * \param[out] op_rc If non-NULL, where to store parsed actual rc + * \param[out] target_rc If non-NULL, where to stored parsed target rc + * + * \return TRUE if key was valid, FALSE otherwise + * \note If uuid is supplied and this returns TRUE, the caller is responsible + * for freeing the memory for *uuid using free(). + */ +gboolean +decode_transition_magic(const char *magic, char **uuid, int *transition_id, int *action_id, + int *op_status, int *op_rc, int *target_rc) +{ + int res = 0; + char *key = NULL; + gboolean result = TRUE; + int local_op_status = -1; + int local_op_rc = -1; + + CRM_CHECK(magic != NULL, return FALSE); + +#ifdef HAVE_SSCANF_M + res = sscanf(magic, "%d:%d;%ms", &local_op_status, &local_op_rc, &key); +#else + key = calloc(1, strlen(magic) - 3); // magic must have >=4 other characters + CRM_ASSERT(key); + res = sscanf(magic, "%d:%d;%s", &local_op_status, &local_op_rc, key); +#endif + if (res == EOF) { + crm_err("Could not decode transition information '%s': %s", + magic, pcmk_rc_str(errno)); + result = FALSE; + } else if (res < 3) { + crm_warn("Transition information '%s' incomplete (%d of 3 expected items)", + magic, res); + result = FALSE; + } else { + if (op_status) { + *op_status = local_op_status; + } + if (op_rc) { + *op_rc = local_op_rc; + } + result = decode_transition_key(key, uuid, transition_id, action_id, + target_rc); + } + free(key); + return result; +} + +char * +pcmk__transition_key(int transition_id, int action_id, int target_rc, + const char *node) +{ + CRM_CHECK(node != NULL, return NULL); + return crm_strdup_printf("%d:%d:%d:%-*s", + action_id, transition_id, target_rc, 36, node); +} + +/*! + * \brief Parse a transition key into its constituent parts + * + * \param[in] key Transition key to parse (must be non-NULL) + * \param[out] uuid If non-NULL, where to store copy of parsed UUID + * \param[out] transition_id If non-NULL, where to store parsed transition ID + * \param[out] action_id If non-NULL, where to store parsed action ID + * \param[out] target_rc If non-NULL, where to stored parsed target rc + * + * \return TRUE if key was valid, FALSE otherwise + * \note If uuid is supplied and this returns TRUE, the caller is responsible + * for freeing the memory for *uuid using free(). + */ +gboolean +decode_transition_key(const char *key, char **uuid, int *transition_id, int *action_id, + int *target_rc) +{ + int local_transition_id = -1; + int local_action_id = -1; + int local_target_rc = -1; + char local_uuid[37] = { '\0' }; + + // Initialize any supplied output arguments + if (uuid) { + *uuid = NULL; + } + if (transition_id) { + *transition_id = -1; + } + if (action_id) { + *action_id = -1; + } + if (target_rc) { + *target_rc = -1; + } + + CRM_CHECK(key != NULL, return FALSE); + if (sscanf(key, "%d:%d:%d:%36s", &local_action_id, &local_transition_id, + &local_target_rc, local_uuid) != 4) { + crm_err("Invalid transition key '%s'", key); + return FALSE; + } + if (strlen(local_uuid) != 36) { + crm_warn("Invalid UUID '%s' in transition key '%s'", local_uuid, key); + } + if (uuid) { + *uuid = strdup(local_uuid); + CRM_ASSERT(*uuid); + } + if (transition_id) { + *transition_id = local_transition_id; + } + if (action_id) { + *action_id = local_action_id; + } + if (target_rc) { + *target_rc = local_target_rc; + } + return TRUE; +} + +// Return true if a is an attribute that should be filtered +static bool +should_filter_for_digest(xmlAttrPtr a, void *user_data) +{ + if (strncmp((const char *) a->name, CRM_META "_", + sizeof(CRM_META " ") - 1) == 0) { + return true; + } + return pcmk__str_any_of((const char *) a->name, + XML_ATTR_ID, + XML_ATTR_CRM_VERSION, + XML_LRM_ATTR_OP_DIGEST, + XML_LRM_ATTR_TARGET, + XML_LRM_ATTR_TARGET_UUID, + "pcmk_external_ip", + NULL); +} + +/*! + * \internal + * \brief Remove XML attributes not needed for operation digest + * + * \param[in,out] param_set XML with operation parameters + */ +void +pcmk__filter_op_for_digest(xmlNode *param_set) +{ + char *key = NULL; + char *timeout = NULL; + guint interval_ms = 0; + + if (param_set == NULL) { + return; + } + + /* Timeout is useful for recurring operation digests, so grab it before + * removing meta-attributes + */ + key = crm_meta_name(XML_LRM_ATTR_INTERVAL_MS); + if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) { + interval_ms = 0; + } + free(key); + key = NULL; + if (interval_ms != 0) { + key = crm_meta_name(XML_ATTR_TIMEOUT); + timeout = crm_element_value_copy(param_set, key); + } + + // Remove all CRM_meta_* attributes and certain other attributes + pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL); + + // Add timeout back for recurring operation digests + if (timeout != NULL) { + crm_xml_add(param_set, key, timeout); + } + free(timeout); + free(key); +} + +int +rsc_op_expected_rc(const lrmd_event_data_t *op) +{ + int rc = 0; + + if (op && op->user_data) { + decode_transition_key(op->user_data, NULL, NULL, NULL, &rc); + } + return rc; +} + +gboolean +did_rsc_op_fail(lrmd_event_data_t * op, int target_rc) +{ + switch (op->op_status) { + case PCMK_EXEC_CANCELLED: + case PCMK_EXEC_PENDING: + return FALSE; + + case PCMK_EXEC_NOT_SUPPORTED: + case PCMK_EXEC_TIMEOUT: + case PCMK_EXEC_ERROR: + case PCMK_EXEC_NOT_CONNECTED: + case PCMK_EXEC_NO_FENCE_DEVICE: + case PCMK_EXEC_NO_SECRETS: + case PCMK_EXEC_INVALID: + return TRUE; + + default: + if (target_rc != op->rc) { + return TRUE; + } + } + + return FALSE; +} + +/*! + * \brief Create a CIB XML element for an operation + * + * \param[in,out] parent If not NULL, make new XML node a child of this + * \param[in] prefix Generate an ID using this prefix + * \param[in] task Operation task to set + * \param[in] interval_spec Operation interval to set + * \param[in] timeout If not NULL, operation timeout to set + * + * \return New XML object on success, NULL otherwise + */ +xmlNode * +crm_create_op_xml(xmlNode *parent, const char *prefix, const char *task, + const char *interval_spec, const char *timeout) +{ + xmlNode *xml_op; + + CRM_CHECK(prefix && task && interval_spec, return NULL); + + xml_op = create_xml_node(parent, XML_ATTR_OP); + crm_xml_set_id(xml_op, "%s-%s-%s", prefix, task, interval_spec); + crm_xml_add(xml_op, XML_LRM_ATTR_INTERVAL, interval_spec); + crm_xml_add(xml_op, "name", task); + if (timeout) { + crm_xml_add(xml_op, XML_ATTR_TIMEOUT, timeout); + } + return xml_op; +} + +/*! + * \brief Check whether an operation requires resource agent meta-data + * + * \param[in] rsc_class Resource agent class (or NULL to skip class check) + * \param[in] op Operation action (or NULL to skip op check) + * + * \return true if operation needs meta-data, false otherwise + * \note At least one of rsc_class and op must be specified. + */ +bool +crm_op_needs_metadata(const char *rsc_class, const char *op) +{ + /* Agent metadata is used to determine whether an agent reload is possible, + * so if this op is not relevant to that feature, we don't need metadata. + */ + + CRM_CHECK((rsc_class != NULL) || (op != NULL), return false); + + if ((rsc_class != NULL) + && !pcmk_is_set(pcmk_get_ra_caps(rsc_class), pcmk_ra_cap_params)) { + // Metadata is needed only for resource classes that use parameters + return false; + } + if (op == NULL) { + return true; + } + + // Metadata is needed only for these actions + return pcmk__str_any_of(op, CRMD_ACTION_START, CRMD_ACTION_STATUS, + CRMD_ACTION_PROMOTE, CRMD_ACTION_DEMOTE, + CRMD_ACTION_RELOAD, CRMD_ACTION_RELOAD_AGENT, + CRMD_ACTION_MIGRATE, CRMD_ACTION_MIGRATED, + CRMD_ACTION_NOTIFY, NULL); +} + +/*! + * \internal + * \brief Check whether an action name is for a fencing action + * + * \param[in] action Action name to check + * + * \return true if \p action is "off", "reboot", or "poweroff", otherwise false + */ +bool +pcmk__is_fencing_action(const char *action) +{ + return pcmk__str_any_of(action, "off", "reboot", "poweroff", NULL); +} + +bool +pcmk_is_probe(const char *task, guint interval) +{ + if (task == NULL) { + return false; + } + + return (interval == 0) && pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_none); +} + +bool +pcmk_xe_is_probe(const xmlNode *xml_op) +{ + const char *task = crm_element_value(xml_op, XML_LRM_ATTR_TASK); + const char *interval_ms_s = crm_element_value(xml_op, XML_LRM_ATTR_INTERVAL_MS); + int interval_ms; + + pcmk__scan_min_int(interval_ms_s, &interval_ms, 0); + return pcmk_is_probe(task, interval_ms); +} + +bool +pcmk_xe_mask_probe_failure(const xmlNode *xml_op) +{ + int status = PCMK_EXEC_UNKNOWN; + int rc = PCMK_OCF_OK; + + if (!pcmk_xe_is_probe(xml_op)) { + return false; + } + + crm_element_value_int(xml_op, XML_LRM_ATTR_OPSTATUS, &status); + crm_element_value_int(xml_op, XML_LRM_ATTR_RC, &rc); + + return rc == PCMK_OCF_NOT_INSTALLED || rc == PCMK_OCF_INVALID_PARAM || + status == PCMK_EXEC_NOT_INSTALLED; +} diff --git a/lib/common/options.c b/lib/common/options.c new file mode 100644 index 0000000..cb32b3f --- /dev/null +++ b/lib/common/options.c @@ -0,0 +1,497 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include + +#include +#include +#include +#include +#include + +#include + +void +pcmk__cli_help(char cmd) +{ + if (cmd == 'v' || cmd == '$') { + printf("Pacemaker %s\n", PACEMAKER_VERSION); + printf("Written by Andrew Beekhof and " + "the Pacemaker project contributors\n"); + + } else if (cmd == '!') { + printf("Pacemaker %s (Build: %s): %s\n", PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES); + } + + crm_exit(CRM_EX_OK); + while(1); // above does not return +} + + +/* + * Environment variable option handling + */ + +/*! + * \internal + * \brief Get the value of a Pacemaker environment variable option + * + * If an environment variable option is set, with either a PCMK_ or (for + * backward compatibility) HA_ prefix, log and return the value. + * + * \param[in] option Environment variable name (without prefix) + * + * \return Value of environment variable option, or NULL in case of + * option name too long or value not found + */ +const char * +pcmk__env_option(const char *option) +{ + const char *const prefixes[] = {"PCMK_", "HA_"}; + char env_name[NAME_MAX]; + const char *value = NULL; + + CRM_CHECK(!pcmk__str_empty(option), return NULL); + + for (int i = 0; i < PCMK__NELEM(prefixes); i++) { + int rv = snprintf(env_name, NAME_MAX, "%s%s", prefixes[i], option); + + if (rv < 0) { + crm_err("Failed to write %s%s to buffer: %s", prefixes[i], option, + strerror(errno)); + return NULL; + } + + if (rv >= sizeof(env_name)) { + crm_trace("\"%s%s\" is too long", prefixes[i], option); + continue; + } + + value = getenv(env_name); + if (value != NULL) { + crm_trace("Found %s = %s", env_name, value); + return value; + } + } + + crm_trace("Nothing found for %s", option); + return NULL; +} + +/*! + * \brief Set or unset a Pacemaker environment variable option + * + * Set an environment variable option with both a PCMK_ and (for + * backward compatibility) HA_ prefix. + * + * \param[in] option Environment variable name (without prefix) + * \param[in] value New value (or NULL to unset) + */ +void +pcmk__set_env_option(const char *option, const char *value) +{ + const char *const prefixes[] = {"PCMK_", "HA_"}; + char env_name[NAME_MAX]; + + CRM_CHECK(!pcmk__str_empty(option) && (strchr(option, '=') == NULL), + return); + + for (int i = 0; i < PCMK__NELEM(prefixes); i++) { + int rv = snprintf(env_name, NAME_MAX, "%s%s", prefixes[i], option); + + if (rv < 0) { + crm_err("Failed to write %s%s to buffer: %s", prefixes[i], option, + strerror(errno)); + return; + } + + if (rv >= sizeof(env_name)) { + crm_trace("\"%s%s\" is too long", prefixes[i], option); + continue; + } + + if (value != NULL) { + crm_trace("Setting %s to %s", env_name, value); + rv = setenv(env_name, value, 1); + } else { + crm_trace("Unsetting %s", env_name); + rv = unsetenv(env_name); + } + + if (rv < 0) { + crm_err("Failed to %sset %s: %s", (value != NULL)? "" : "un", + env_name, strerror(errno)); + } + } +} + +/*! + * \internal + * \brief Check whether Pacemaker environment variable option is enabled + * + * Given a Pacemaker environment variable option that can either be boolean + * or a list of daemon names, return true if the option is enabled for a given + * daemon. + * + * \param[in] daemon Daemon name (can be NULL) + * \param[in] option Pacemaker environment variable name + * + * \return true if variable is enabled for daemon, otherwise false + */ +bool +pcmk__env_option_enabled(const char *daemon, const char *option) +{ + const char *value = pcmk__env_option(option); + + return (value != NULL) + && (crm_is_true(value) + || ((daemon != NULL) && (strstr(value, daemon) != NULL))); +} + + +/* + * Cluster option handling + */ + +bool +pcmk__valid_interval_spec(const char *value) +{ + (void) crm_parse_interval_spec(value); + return errno == 0; +} + +bool +pcmk__valid_boolean(const char *value) +{ + int tmp; + + return crm_str_to_boolean(value, &tmp) == 1; +} + +bool +pcmk__valid_number(const char *value) +{ + if (value == NULL) { + return false; + + } else if (pcmk_str_is_minus_infinity(value) || + pcmk_str_is_infinity(value)) { + return true; + } + + return pcmk__scan_ll(value, NULL, 0LL) == pcmk_rc_ok; +} + +bool +pcmk__valid_positive_number(const char *value) +{ + long long num = 0LL; + + return pcmk_str_is_infinity(value) + || ((pcmk__scan_ll(value, &num, 0LL) == pcmk_rc_ok) && (num > 0)); +} + +bool +pcmk__valid_quorum(const char *value) +{ + return pcmk__strcase_any_of(value, "stop", "freeze", "ignore", "demote", "suicide", NULL); +} + +bool +pcmk__valid_script(const char *value) +{ + struct stat st; + + if (pcmk__str_eq(value, "/dev/null", pcmk__str_casei)) { + return true; + } + + if (stat(value, &st) != 0) { + crm_err("Script %s does not exist", value); + return false; + } + + if (S_ISREG(st.st_mode) == 0) { + crm_err("Script %s is not a regular file", value); + return false; + } + + if ((st.st_mode & (S_IXUSR | S_IXGRP)) == 0) { + crm_err("Script %s is not executable", value); + return false; + } + + return true; +} + +bool +pcmk__valid_percentage(const char *value) +{ + char *end = NULL; + long number = strtol(value, &end, 10); + + if (end && (end[0] != '%')) { + return false; + } + return number >= 0; +} + +/*! + * \internal + * \brief Check a table of configured options for a particular option + * + * \param[in,out] options Name/value pairs for configured options + * \param[in] validate If not NULL, validator function for option value + * \param[in] name Option name to look for + * \param[in] old_name Alternative option name to look for + * \param[in] def_value Default to use if option not configured + * + * \return Option value (from supplied options table or default value) + */ +static const char * +cluster_option_value(GHashTable *options, bool (*validate)(const char *), + const char *name, const char *old_name, + const char *def_value) +{ + const char *value = NULL; + char *new_value = NULL; + + CRM_ASSERT(name != NULL); + + if (options) { + value = g_hash_table_lookup(options, name); + + if ((value == NULL) && old_name) { + value = g_hash_table_lookup(options, old_name); + if (value != NULL) { + pcmk__config_warn("Support for legacy name '%s' for cluster " + "option '%s' is deprecated and will be " + "removed in a future release", + old_name, name); + + // Inserting copy with current name ensures we only warn once + new_value = strdup(value); + g_hash_table_insert(options, strdup(name), new_value); + value = new_value; + } + } + + if (value && validate && (validate(value) == FALSE)) { + pcmk__config_err("Using default value for cluster option '%s' " + "because '%s' is invalid", name, value); + value = NULL; + } + + if (value) { + return value; + } + } + + // No value found, use default + value = def_value; + + if (value == NULL) { + crm_trace("No value or default provided for cluster option '%s'", + name); + return NULL; + } + + if (validate) { + CRM_CHECK(validate(value) != FALSE, + crm_err("Bug: default value for cluster option '%s' is invalid", name); + return NULL); + } + + crm_trace("Using default value '%s' for cluster option '%s'", + value, name); + if (options) { + new_value = strdup(value); + g_hash_table_insert(options, strdup(name), new_value); + value = new_value; + } + return value; +} + +/*! + * \internal + * \brief Get the value of a cluster option + * + * \param[in,out] options Name/value pairs for configured options + * \param[in] option_list Possible cluster options + * \param[in] len Length of \p option_list + * \param[in] name (Primary) option name to look for + * + * \return Option value + */ +const char * +pcmk__cluster_option(GHashTable *options, + const pcmk__cluster_option_t *option_list, + int len, const char *name) +{ + const char *value = NULL; + + for (int lpc = 0; lpc < len; lpc++) { + if (pcmk__str_eq(name, option_list[lpc].name, pcmk__str_casei)) { + value = cluster_option_value(options, option_list[lpc].is_valid, + option_list[lpc].name, + option_list[lpc].alt_name, + option_list[lpc].default_value); + return value; + } + } + CRM_CHECK(FALSE, crm_err("Bug: looking for unknown option '%s'", name)); + return NULL; +} + +/*! + * \internal + * \brief Add a description element to a meta-data string + * + * \param[in,out] s Meta-data string to add to + * \param[in] tag Name of element to add ("longdesc" or "shortdesc") + * \param[in] desc Textual description to add + * \param[in] values If not \p NULL, the allowed values for the parameter + * \param[in] spaces If not \p NULL, spaces to insert at the beginning of + * each line + */ +static void +add_desc(GString *s, const char *tag, const char *desc, const char *values, + const char *spaces) +{ + char *escaped_en = crm_xml_escape(desc); + + if (spaces != NULL) { + g_string_append(s, spaces); + } + pcmk__g_strcat(s, "<", tag, " lang=\"en\">", escaped_en, NULL); + + if (values != NULL) { + pcmk__g_strcat(s, " Allowed values: ", values, NULL); + } + pcmk__g_strcat(s, "\n", NULL); + +#ifdef ENABLE_NLS + { + static const char *locale = NULL; + + char *localized = crm_xml_escape(_(desc)); + + if (strcmp(escaped_en, localized) != 0) { + if (locale == NULL) { + locale = strtok(setlocale(LC_ALL, NULL), "_"); + } + + if (spaces != NULL) { + g_string_append(s, spaces); + } + pcmk__g_strcat(s, "<", tag, " lang=\"", locale, "\">", localized, + NULL); + + if (values != NULL) { + pcmk__g_strcat(s, _(" Allowed values: "), _(values), NULL); + } + pcmk__g_strcat(s, "\n", NULL); + } + free(localized); + } +#endif + + free(escaped_en); +} + +gchar * +pcmk__format_option_metadata(const char *name, const char *desc_short, + const char *desc_long, + pcmk__cluster_option_t *option_list, int len) +{ + /* big enough to hold "pacemaker-schedulerd metadata" output */ + GString *s = g_string_sized_new(13000); + + pcmk__g_strcat(s, + "\n" + "\n" + " " PCMK_OCF_VERSION "\n", NULL); + + add_desc(s, "longdesc", desc_long, NULL, " "); + add_desc(s, "shortdesc", desc_short, NULL, " "); + + g_string_append(s, " \n"); + + for (int lpc = 0; lpc < len; lpc++) { + const char *opt_name = option_list[lpc].name; + const char *opt_type = option_list[lpc].type; + const char *opt_values = option_list[lpc].values; + const char *opt_default = option_list[lpc].default_value; + const char *opt_desc_short = option_list[lpc].description_short; + const char *opt_desc_long = option_list[lpc].description_long; + + // The standard requires long and short parameter descriptions + CRM_ASSERT((opt_desc_short != NULL) || (opt_desc_long != NULL)); + + if (opt_desc_short == NULL) { + opt_desc_short = opt_desc_long; + } else if (opt_desc_long == NULL) { + opt_desc_long = opt_desc_short; + } + + // The standard requires a parameter type + CRM_ASSERT(opt_type != NULL); + + pcmk__g_strcat(s, " \n", NULL); + + add_desc(s, "longdesc", opt_desc_long, opt_values, " "); + add_desc(s, "shortdesc", opt_desc_short, NULL, " "); + + pcmk__g_strcat(s, " \n"); + + while (ptr != NULL) { + pcmk__g_strcat(s, " \n"); + free(str); + + } else { + g_string_append(s, "/>\n"); + } + + g_string_append(s, " \n"); + } + g_string_append(s, " \n\n"); + + return g_string_free(s, FALSE); +} + +void +pcmk__validate_cluster_options(GHashTable *options, + pcmk__cluster_option_t *option_list, int len) +{ + for (int lpc = 0; lpc < len; lpc++) { + cluster_option_value(options, option_list[lpc].is_valid, + option_list[lpc].name, + option_list[lpc].alt_name, + option_list[lpc].default_value); + } +} diff --git a/lib/common/output.c b/lib/common/output.c new file mode 100644 index 0000000..2ea9b0b --- /dev/null +++ b/lib/common/output.c @@ -0,0 +1,318 @@ +/* + * Copyright 2019-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include + +#include "crmcommon_private.h" + +static GHashTable *formatters = NULL; + +#if defined(PCMK__UNIT_TESTING) +GHashTable * +pcmk__output_formatters(void) { + return formatters; +} +#endif + +void +pcmk__output_free(pcmk__output_t *out) { + if (out == NULL) { + return; + } + + out->free_priv(out); + + if (out->messages != NULL) { + g_hash_table_destroy(out->messages); + } + + g_free(out->request); + free(out); +} + +/*! + * \internal + * \brief Create a new \p pcmk__output_t structure + * + * This function does not register any message functions with the newly created + * object. + * + * \param[in,out] out Where to store the new output object + * \param[in] fmt_name How to format output + * \param[in] filename Where to write formatted output. This can be a + * filename (the file will be overwritten if it already + * exists), or \p NULL or \p "-" for stdout. For no + * output, pass a filename of \p "/dev/null". + * \param[in] argv List of command line arguments + * + * \return Standard Pacemaker return code + */ +int +pcmk__bare_output_new(pcmk__output_t **out, const char *fmt_name, + const char *filename, char **argv) +{ + pcmk__output_factory_t create = NULL; + + CRM_ASSERT(formatters != NULL && out != NULL); + + /* If no name was given, just try "text". It's up to each tool to register + * what it supports so this also may not be valid. + */ + if (fmt_name == NULL) { + create = g_hash_table_lookup(formatters, "text"); + } else { + create = g_hash_table_lookup(formatters, fmt_name); + } + + if (create == NULL) { + return pcmk_rc_unknown_format; + } + + *out = create(argv); + if (*out == NULL) { + return ENOMEM; + } + + if (pcmk__str_eq(filename, "-", pcmk__str_null_matches)) { + (*out)->dest = stdout; + } else { + (*out)->dest = fopen(filename, "w"); + if ((*out)->dest == NULL) { + pcmk__output_free(*out); + *out = NULL; + return errno; + } + } + + (*out)->quiet = false; + (*out)->messages = pcmk__strkey_table(free, NULL); + + if ((*out)->init(*out) == false) { + pcmk__output_free(*out); + return ENOMEM; + } + + setenv("OCF_OUTPUT_FORMAT", (*out)->fmt_name, 1); + + return pcmk_rc_ok; +} + +int +pcmk__output_new(pcmk__output_t **out, const char *fmt_name, + const char *filename, char **argv) +{ + int rc = pcmk__bare_output_new(out, fmt_name, filename, argv); + + if (rc == pcmk_rc_ok) { + /* Register libcrmcommon messages (currently they exist only for + * patchset) + */ + pcmk__register_patchset_messages(*out); + } + return rc; +} + +int +pcmk__register_format(GOptionGroup *group, const char *name, + pcmk__output_factory_t create, + const GOptionEntry *options) +{ + CRM_ASSERT(create != NULL && !pcmk__str_empty(name)); + + if (formatters == NULL) { + formatters = pcmk__strkey_table(free, NULL); + } + + if (options != NULL && group != NULL) { + g_option_group_add_entries(group, options); + } + + g_hash_table_insert(formatters, strdup(name), create); + return pcmk_rc_ok; +} + +void +pcmk__register_formats(GOptionGroup *group, + const pcmk__supported_format_t *formats) +{ + if (formats == NULL) { + return; + } + for (const pcmk__supported_format_t *entry = formats; entry->name != NULL; + entry++) { + pcmk__register_format(group, entry->name, entry->create, entry->options); + } +} + +void +pcmk__unregister_formats(void) { + if (formatters != NULL) { + g_hash_table_destroy(formatters); + formatters = NULL; + } +} + +int +pcmk__call_message(pcmk__output_t *out, const char *message_id, ...) { + va_list args; + int rc = pcmk_rc_ok; + pcmk__message_fn_t fn; + + CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id)); + + fn = g_hash_table_lookup(out->messages, message_id); + if (fn == NULL) { + crm_debug("Called unknown output message '%s' for format '%s'", + message_id, out->fmt_name); + return EINVAL; + } + + va_start(args, message_id); + rc = fn(out, args); + va_end(args); + + return rc; +} + +void +pcmk__register_message(pcmk__output_t *out, const char *message_id, + pcmk__message_fn_t fn) { + CRM_ASSERT(out != NULL && !pcmk__str_empty(message_id) && fn != NULL); + + g_hash_table_replace(out->messages, strdup(message_id), fn); +} + +void +pcmk__register_messages(pcmk__output_t *out, const pcmk__message_entry_t *table) +{ + for (const pcmk__message_entry_t *entry = table; entry->message_id != NULL; + entry++) { + if (pcmk__strcase_any_of(entry->fmt_name, "default", out->fmt_name, NULL)) { + pcmk__register_message(out, entry->message_id, entry->fn); + } + } +} + +void +pcmk__output_and_clear_error(GError **error, pcmk__output_t *out) +{ + if (error == NULL || *error == NULL) { + return; + } + + if (out != NULL) { + out->err(out, "%s: %s", g_get_prgname(), (*error)->message); + } else { + fprintf(stderr, "%s: %s\n", g_get_prgname(), (*error)->message); + } + + g_clear_error(error); +} + +/*! + * \internal + * \brief Create an XML-only output object + * + * Create an output object that supports only the XML format, and free + * existing XML if supplied (particularly useful for libpacemaker public API + * functions that want to free any previous result supplied by the caller). + * + * \param[out] out Where to put newly created output object + * \param[in,out] xml If non-NULL, this will be freed + * + * \return Standard Pacemaker return code + */ +int +pcmk__xml_output_new(pcmk__output_t **out, xmlNodePtr *xml) { + pcmk__supported_format_t xml_format[] = { + PCMK__SUPPORTED_FORMAT_XML, + { NULL, NULL, NULL } + }; + + if (*xml != NULL) { + xmlFreeNode(*xml); + *xml = NULL; + } + pcmk__register_formats(NULL, xml_format); + return pcmk__output_new(out, "xml", NULL, NULL); +} + +/*! + * \internal + * \brief Finish and free an XML-only output object + * + * \param[in,out] out Output object to free + * \param[out] xml If not NULL, where to store XML output + */ +void +pcmk__xml_output_finish(pcmk__output_t *out, xmlNodePtr *xml) { + out->finish(out, 0, FALSE, (void **) xml); + pcmk__output_free(out); +} + +/*! + * \internal + * \brief Create a new output object using the "log" format + * + * \param[out] out Where to store newly allocated output object + * + * \return Standard Pacemaker return code + */ +int +pcmk__log_output_new(pcmk__output_t **out) +{ + int rc = pcmk_rc_ok; + const char* argv[] = { "", NULL }; + pcmk__supported_format_t formats[] = { + PCMK__SUPPORTED_FORMAT_LOG, + { NULL, NULL, NULL } + }; + + pcmk__register_formats(NULL, formats); + rc = pcmk__output_new(out, "log", NULL, (char **) argv); + if ((rc != pcmk_rc_ok) || (*out == NULL)) { + crm_err("Can't log certain messages due to internal error: %s", + pcmk_rc_str(rc)); + return rc; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Create a new output object using the "text" format + * + * \param[out] out Where to store newly allocated output object + * \param[in] filename Name of output destination file + * + * \return Standard Pacemaker return code + */ +int +pcmk__text_output_new(pcmk__output_t **out, const char *filename) +{ + int rc = pcmk_rc_ok; + const char* argv[] = { "", NULL }; + pcmk__supported_format_t formats[] = { + PCMK__SUPPORTED_FORMAT_TEXT, + { NULL, NULL, NULL } + }; + + pcmk__register_formats(NULL, formats); + rc = pcmk__output_new(out, "text", filename, (char **) argv); + if ((rc != pcmk_rc_ok) || (*out == NULL)) { + crm_err("Can't create text output object to internal error: %s", + pcmk_rc_str(rc)); + return rc; + } + return pcmk_rc_ok; +} diff --git a/lib/common/output_html.c b/lib/common/output_html.c new file mode 100644 index 0000000..47b14c1 --- /dev/null +++ b/lib/common/output_html.c @@ -0,0 +1,477 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +static const char *stylesheet_default = + ".bold { font-weight: bold }\n" + + ".online { color: green }\n" + ".offline { color: red }\n" + ".maint { color: blue }\n" + ".standby { color: blue }\n" + ".health_red { color: red }\n" + ".health_yellow { color: GoldenRod }\n" + + ".rsc-failed { color: red }\n" + ".rsc-failure-ignored { color: DarkGreen }\n" + ".rsc-managed { color: blue }\n" + ".rsc-multiple { color: orange }\n" + ".rsc-ok { color: green }\n" + + ".warning { color: red; font-weight: bold }"; + +static gboolean cgi_output = FALSE; +static char *stylesheet_link = NULL; +static char *title = NULL; +static GSList *extra_headers = NULL; + +GOptionEntry pcmk__html_output_entries[] = { + { "html-cgi", 0, 0, G_OPTION_ARG_NONE, &cgi_output, + "Add CGI headers (requires --output-as=html)", + NULL }, + + { "html-stylesheet", 0, 0, G_OPTION_ARG_STRING, &stylesheet_link, + "Link to an external stylesheet (requires --output-as=html)", + "URI" }, + + { "html-title", 0, 0, G_OPTION_ARG_STRING, &title, + "Specify a page title (requires --output-as=html)", + "TITLE" }, + + { NULL } +}; + +/* The first several elements of this struct must be the same as the first + * several elements of private_data_s in lib/common/output_xml.c. This + * struct gets passed to a bunch of the pcmk__output_xml_* functions which + * assume an XML private_data_s. Keeping them laid out the same means this + * still works. + */ +typedef struct private_data_s { + /* Begin members that must match the XML version */ + xmlNode *root; + GQueue *parent_q; + GSList *errors; + /* End members that must match the XML version */ +} private_data_t; + +static void +html_free_priv(pcmk__output_t *out) { + private_data_t *priv = NULL; + + if (out == NULL || out->priv == NULL) { + return; + } + + priv = out->priv; + + xmlFreeNode(priv->root); + g_queue_free(priv->parent_q); + g_slist_free(priv->errors); + free(priv); + out->priv = NULL; +} + +static bool +html_init(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + /* If html_init was previously called on this output struct, just return. */ + if (out->priv != NULL) { + return true; + } else { + out->priv = calloc(1, sizeof(private_data_t)); + if (out->priv == NULL) { + return false; + } + + priv = out->priv; + } + + priv->parent_q = g_queue_new(); + + priv->root = create_xml_node(NULL, "html"); + xmlCreateIntSubset(priv->root->doc, (pcmkXmlStr) "html", NULL, NULL); + + crm_xml_add(priv->root, "lang", "en"); + g_queue_push_tail(priv->parent_q, priv->root); + priv->errors = NULL; + + pcmk__output_xml_create_parent(out, "body", NULL); + + return true; +} + +static void +add_error_node(gpointer data, gpointer user_data) { + char *str = (char *) data; + pcmk__output_t *out = (pcmk__output_t *) user_data; + out->list_item(out, NULL, "%s", str); +} + +static void +html_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + private_data_t *priv = NULL; + htmlNodePtr head_node = NULL; + htmlNodePtr charset_node = NULL; + + CRM_ASSERT(out != NULL); + + priv = out->priv; + + /* If root is NULL, html_init failed and we are being called from pcmk__output_free + * in the pcmk__output_new path. + */ + if (priv == NULL || priv->root == NULL) { + return; + } + + if (cgi_output && print) { + fprintf(out->dest, "Content-Type: text/html\n\n"); + } + + /* Add the head node last - it's not needed earlier because it doesn't contain + * anything else that the user could add, and we want it done last to pick up + * any options that may have been given. + */ + head_node = xmlNewNode(NULL, (pcmkXmlStr) "head"); + + if (title != NULL ) { + pcmk_create_xml_text_node(head_node, "title", title); + } else if (out->request != NULL) { + pcmk_create_xml_text_node(head_node, "title", out->request); + } + + charset_node = create_xml_node(head_node, "meta"); + crm_xml_add(charset_node, "charset", "utf-8"); + + /* Add any extra header nodes the caller might have created. */ + for (int i = 0; i < g_slist_length(extra_headers); i++) { + xmlAddChild(head_node, xmlCopyNode(g_slist_nth_data(extra_headers, i), 1)); + } + + /* Stylesheets are included two different ways. The first is via a built-in + * default (see the stylesheet_default const above). The second is via the + * html-stylesheet option, and this should obviously be a link to a + * stylesheet. The second can override the first. At least one should be + * given. + */ + pcmk_create_xml_text_node(head_node, "style", stylesheet_default); + + if (stylesheet_link != NULL) { + htmlNodePtr link_node = create_xml_node(head_node, "link"); + pcmk__xe_set_props(link_node, "rel", "stylesheet", + "href", stylesheet_link, + NULL); + } + + xmlAddPrevSibling(priv->root->children, head_node); + + if (g_slist_length(priv->errors) > 0) { + out->begin_list(out, "Errors", NULL, NULL); + g_slist_foreach(priv->errors, add_error_node, (gpointer) out); + out->end_list(out); + } + + if (print) { + htmlDocDump(out->dest, priv->root->doc); + } + + if (copy_dest != NULL) { + *copy_dest = copy_xml(priv->root); + } + + g_slist_free_full(extra_headers, (GDestroyNotify) xmlFreeNode); + extra_headers = NULL; +} + +static void +html_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + + out->dest = freopen(NULL, "w", out->dest); + CRM_ASSERT(out->dest != NULL); + + html_free_priv(out); + html_init(out); +} + +static void +html_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + char *rc_buf = NULL; + + CRM_ASSERT(out != NULL); + + rc_buf = crm_strdup_printf("Return code: %d", exit_status); + + pcmk__output_create_xml_text_node(out, "h2", "Command Output"); + pcmk__output_create_html_node(out, "div", NULL, NULL, rc_buf); + + if (proc_stdout != NULL) { + pcmk__output_create_html_node(out, "div", NULL, NULL, "Stdout"); + pcmk__output_create_html_node(out, "div", NULL, "output", proc_stdout); + } + if (proc_stderr != NULL) { + pcmk__output_create_html_node(out, "div", NULL, NULL, "Stderr"); + pcmk__output_create_html_node(out, "div", NULL, "output", proc_stderr); + } + + free(rc_buf); +} + +static void +html_version(pcmk__output_t *out, bool extended) { + CRM_ASSERT(out != NULL); + + pcmk__output_create_xml_text_node(out, "h2", "Version Information"); + pcmk__output_create_html_node(out, "div", NULL, NULL, "Program: Pacemaker"); + pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Version: %s", PACEMAKER_VERSION)); + pcmk__output_create_html_node(out, "div", NULL, NULL, + "Author: Andrew Beekhof and " + "the Pacemaker project contributors"); + pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Build: %s", BUILD_VERSION)); + pcmk__output_create_html_node(out, "div", NULL, NULL, crm_strdup_printf("Features: %s", CRM_FEATURES)); +} + +G_GNUC_PRINTF(2, 3) +static void +html_err(pcmk__output_t *out, const char *format, ...) { + private_data_t *priv = NULL; + int len = 0; + char *buf = NULL; + va_list ap; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + priv->errors = g_slist_append(priv->errors, buf); +} + +G_GNUC_PRINTF(2, 3) +static int +html_info(pcmk__output_t *out, const char *format, ...) { + return pcmk_rc_no_output; +} + +static void +html_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + htmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL); + + node = pcmk__output_create_html_node(out, "pre", NULL, NULL, buf); + crm_xml_add(node, "lang", "xml"); +} + +G_GNUC_PRINTF(4, 5) +static void +html_begin_list(pcmk__output_t *out, const char *singular_noun, + const char *plural_noun, const char *format, ...) { + int q_len = 0; + private_data_t *priv = NULL; + xmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + /* If we are already in a list (the queue depth is always at least + * one because of the element), first create a
  • element + * to hold the

    and the new list. + */ + q_len = g_queue_get_length(priv->parent_q); + if (q_len > 2) { + pcmk__output_xml_create_parent(out, "li", NULL); + } + + if (format != NULL) { + va_list ap; + char *buf = NULL; + int len; + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + va_end(ap); + CRM_ASSERT(len >= 0); + + if (q_len > 2) { + pcmk__output_create_xml_text_node(out, "h3", buf); + } else { + pcmk__output_create_xml_text_node(out, "h2", buf); + } + + free(buf); + } + + node = pcmk__output_xml_create_parent(out, "ul", NULL); + g_queue_push_tail(priv->parent_q, node); +} + +G_GNUC_PRINTF(3, 4) +static void +html_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { + htmlNodePtr item_node = NULL; + va_list ap; + char *buf = NULL; + int len; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + item_node = pcmk__output_create_xml_text_node(out, "li", buf); + free(buf); + + if (name != NULL) { + crm_xml_add(item_node, "class", name); + } +} + +static void +html_increment_list(pcmk__output_t *out) { + /* This function intentially left blank */ +} + +static void +html_end_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + /* Remove the
      tag. */ + g_queue_pop_tail(priv->parent_q); + pcmk__output_xml_pop_parent(out); + + /* Remove the
    • created for nested lists. */ + if (g_queue_get_length(priv->parent_q) > 2) { + pcmk__output_xml_pop_parent(out); + } +} + +static bool +html_is_quiet(pcmk__output_t *out) { + return false; +} + +static void +html_spacer(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + pcmk__output_create_xml_node(out, "br", NULL); +} + +static void +html_progress(pcmk__output_t *out, bool end) { + /* This function intentially left blank */ +} + +pcmk__output_t * +pcmk__mk_html_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "html"; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = html_init; + retval->free_priv = html_free_priv; + retval->finish = html_finish; + retval->reset = html_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = html_subprocess_output; + retval->version = html_version; + retval->info = html_info; + retval->transient = html_info; + retval->err = html_err; + retval->output_xml = html_output_xml; + + retval->begin_list = html_begin_list; + retval->list_item = html_list_item; + retval->increment_list = html_increment_list; + retval->end_list = html_end_list; + + retval->is_quiet = html_is_quiet; + retval->spacer = html_spacer; + retval->progress = html_progress; + retval->prompt = pcmk__text_prompt; + + return retval; +} + +xmlNodePtr +pcmk__output_create_html_node(pcmk__output_t *out, const char *element_name, const char *id, + const char *class_name, const char *text) { + htmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "html", pcmk__str_none), return NULL); + + node = pcmk__output_create_xml_text_node(out, element_name, text); + + if (class_name != NULL) { + crm_xml_add(node, "class", class_name); + } + + if (id != NULL) { + crm_xml_add(node, "id", id); + } + + return node; +} + +void +pcmk__html_add_header(const char *name, ...) { + htmlNodePtr header_node; + va_list ap; + + va_start(ap, name); + + header_node = xmlNewNode(NULL, (pcmkXmlStr) name); + while (1) { + char *key = va_arg(ap, char *); + char *value; + + if (key == NULL) { + break; + } + + value = va_arg(ap, char *); + crm_xml_add(header_node, key, value); + } + + extra_headers = g_slist_append(extra_headers, header_node); + + va_end(ap); +} diff --git a/lib/common/output_log.c b/lib/common/output_log.c new file mode 100644 index 0000000..aca168d --- /dev/null +++ b/lib/common/output_log.c @@ -0,0 +1,353 @@ +/* + * Copyright 2019-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include + +#include +#include +#include +#include + +GOptionEntry pcmk__log_output_entries[] = { + { NULL } +}; + +typedef struct private_data_s { + /* gathered in log_begin_list */ + GQueue/**/ *prefixes; + uint8_t log_level; +} private_data_t; + +static void +log_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + /* This function intentionally left blank */ +} + +static void +log_free_priv(pcmk__output_t *out) { + private_data_t *priv = NULL; + + if (out == NULL || out->priv == NULL) { + return; + } + + priv = out->priv; + + g_queue_free(priv->prefixes); + free(priv); + out->priv = NULL; +} + +static bool +log_init(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + /* If log_init was previously called on this output struct, just return. */ + if (out->priv != NULL) { + return true; + } + + out->priv = calloc(1, sizeof(private_data_t)); + if (out->priv == NULL) { + return false; + } + + priv = out->priv; + + priv->prefixes = g_queue_new(); + priv->log_level = LOG_INFO; + + return true; +} + +static void +log_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + /* This function intentionally left blank */ +} + +static void +log_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + + out->dest = freopen(NULL, "w", out->dest); + CRM_ASSERT(out->dest != NULL); + + log_free_priv(out); + log_init(out); +} + +static void +log_version(pcmk__output_t *out, bool extended) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + if (extended) { + do_crm_log(priv->log_level, "Pacemaker %s (Build: %s): %s", + PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES); + } else { + do_crm_log(priv->log_level, "Pacemaker %s", PACEMAKER_VERSION); + do_crm_log(priv->log_level, "Written by Andrew Beekhof and" + "the Pacemaker project contributors"); + } +} + +G_GNUC_PRINTF(2, 3) +static void +log_err(pcmk__output_t *out, const char *format, ...) { + va_list ap; + char* buffer = NULL; + int len = 0; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + /* Informational output does not get indented, to separate it from other + * potentially indented list output. + */ + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + crm_err("%s", buffer); + + free(buffer); +} + +static void +log_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + xmlNodePtr node = NULL; + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + node = create_xml_node(NULL, name); + xmlNodeSetContent(node, (pcmkXmlStr) buf); + do_crm_log_xml(priv->log_level, name, node); + free(node); +} + +G_GNUC_PRINTF(4, 5) +static void +log_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, + const char *format, ...) { + int len = 0; + va_list ap; + char* buffer = NULL; + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + /* Don't skip empty prefixes, + * otherwise there will be mismatch + * in the log_end_list */ + if(strcmp(buffer, "") == 0) { + /* nothing */ + } + + g_queue_push_tail(priv->prefixes, buffer); +} + +G_GNUC_PRINTF(3, 4) +static void +log_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { + int len = 0; + va_list ap; + private_data_t *priv = NULL; + char prefix[LINE_MAX] = { 0 }; + int offset = 0; + char* buffer = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + for (GList* gIter = priv->prefixes->head; gIter; gIter = gIter->next) { + if (strcmp(prefix, "") != 0) { + offset += snprintf(prefix + offset, LINE_MAX - offset, ": %s", (char *)gIter->data); + } else { + offset = snprintf(prefix, LINE_MAX, "%s", (char *)gIter->data); + } + } + + va_start(ap, format); + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + if (strcmp(buffer, "") != 0) { /* We don't want empty messages */ + if ((name != NULL) && (strcmp(name, "") != 0)) { + if (strcmp(prefix, "") != 0) { + do_crm_log(priv->log_level, "%s: %s: %s", prefix, name, buffer); + } else { + do_crm_log(priv->log_level, "%s: %s", name, buffer); + } + } else { + if (strcmp(prefix, "") != 0) { + do_crm_log(priv->log_level, "%s: %s", prefix, buffer); + } else { + do_crm_log(priv->log_level, "%s", buffer); + } + } + } + free(buffer); +} + +static void +log_end_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + if (priv->prefixes == NULL) { + return; + } + CRM_ASSERT(priv->prefixes->tail != NULL); + + free((char *)priv->prefixes->tail->data); + g_queue_pop_tail(priv->prefixes); +} + +G_GNUC_PRINTF(2, 3) +static int +log_info(pcmk__output_t *out, const char *format, ...) { + private_data_t *priv = NULL; + int len = 0; + va_list ap; + char* buffer = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + do_crm_log(priv->log_level, "%s", buffer); + + free(buffer); + return pcmk_rc_ok; +} + +G_GNUC_PRINTF(2, 3) +static int +log_transient(pcmk__output_t *out, const char *format, ...) +{ + private_data_t *priv = NULL; + int len = 0; + va_list ap; + char *buffer = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + len = vasprintf(&buffer, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + do_crm_log(QB_MAX(priv->log_level, LOG_DEBUG), "%s", buffer); + + free(buffer); + return pcmk_rc_ok; +} + +static bool +log_is_quiet(pcmk__output_t *out) { + return false; +} + +static void +log_spacer(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static void +log_progress(pcmk__output_t *out, bool end) { + /* This function intentionally left blank */ +} + +static void +log_prompt(const char *prompt, bool echo, char **dest) { + /* This function intentionally left blank */ +} + +pcmk__output_t * +pcmk__mk_log_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "log"; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = log_init; + retval->free_priv = log_free_priv; + retval->finish = log_finish; + retval->reset = log_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = log_subprocess_output; + retval->version = log_version; + retval->info = log_info; + retval->transient = log_transient; + retval->err = log_err; + retval->output_xml = log_output_xml; + + retval->begin_list = log_begin_list; + retval->list_item = log_list_item; + retval->end_list = log_end_list; + + retval->is_quiet = log_is_quiet; + retval->spacer = log_spacer; + retval->progress = log_progress; + retval->prompt = log_prompt; + + return retval; +} + +uint8_t +pcmk__output_get_log_level(const pcmk__output_t *out) +{ + private_data_t *priv = NULL; + + CRM_ASSERT((out != NULL) && (out->priv != NULL)); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return 0); + + priv = out->priv; + return priv->log_level; +} + +void +pcmk__output_set_log_level(pcmk__output_t *out, uint8_t log_level) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "log", pcmk__str_none), return); + + priv = out->priv; + priv->log_level = log_level; +} diff --git a/lib/common/output_none.c b/lib/common/output_none.c new file mode 100644 index 0000000..581a8b4 --- /dev/null +++ b/lib/common/output_none.c @@ -0,0 +1,152 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include + +GOptionEntry pcmk__none_output_entries[] = { + { NULL } +}; + +static void +none_free_priv(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static bool +none_init(pcmk__output_t *out) { + return true; +} + +static void +none_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + /* This function intentionally left blank */ +} + +static void +none_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + none_free_priv(out); + none_init(out); +} + +static void +none_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + /* This function intentionally left blank */ +} + +static void +none_version(pcmk__output_t *out, bool extended) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(2, 3) +static void +none_err(pcmk__output_t *out, const char *format, ...) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(2, 3) +static int +none_info(pcmk__output_t *out, const char *format, ...) { + return pcmk_rc_no_output; +} + +static void +none_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(4, 5) +static void +none_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, + const char *format, ...) { + /* This function intentionally left blank */ +} + +G_GNUC_PRINTF(3, 4) +static void +none_list_item(pcmk__output_t *out, const char *id, const char *format, ...) { + /* This function intentionally left blank */ +} + +static void +none_increment_list(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static void +none_end_list(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static bool +none_is_quiet(pcmk__output_t *out) { + return out->quiet; +} + +static void +none_spacer(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static void +none_progress(pcmk__output_t *out, bool end) { + /* This function intentionally left blank */ +} + +static void +none_prompt(const char *prompt, bool echo, char **dest) { + /* This function intentionally left blank */ +} + +pcmk__output_t * +pcmk__mk_none_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = PCMK__VALUE_NONE; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = none_init; + retval->free_priv = none_free_priv; + retval->finish = none_finish; + retval->reset = none_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = none_subprocess_output; + retval->version = none_version; + retval->info = none_info; + retval->transient = none_info; + retval->err = none_err; + retval->output_xml = none_output_xml; + + retval->begin_list = none_begin_list; + retval->list_item = none_list_item; + retval->increment_list = none_increment_list; + retval->end_list = none_end_list; + + retval->is_quiet = none_is_quiet; + retval->spacer = none_spacer; + retval->progress = none_progress; + retval->prompt = none_prompt; + + return retval; +} diff --git a/lib/common/output_text.c b/lib/common/output_text.c new file mode 100644 index 0000000..6bd362d --- /dev/null +++ b/lib/common/output_text.c @@ -0,0 +1,446 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include + +#include +#include +#include +#include + +static gboolean fancy = FALSE; + +GOptionEntry pcmk__text_output_entries[] = { + { "text-fancy", 0, 0, G_OPTION_ARG_NONE, &fancy, + "Use more highly formatted output (requires --output-as=text)", + NULL }, + + { NULL } +}; + +typedef struct text_list_data_s { + unsigned int len; + char *singular_noun; + char *plural_noun; +} text_list_data_t; + +typedef struct private_data_s { + GQueue *parent_q; +} private_data_t; + +static void +text_free_priv(pcmk__output_t *out) { + private_data_t *priv = NULL; + + if (out == NULL || out->priv == NULL) { + return; + } + + priv = out->priv; + + g_queue_free(priv->parent_q); + free(priv); + out->priv = NULL; +} + +static bool +text_init(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + /* If text_init was previously called on this output struct, just return. */ + if (out->priv != NULL) { + return true; + } else { + out->priv = calloc(1, sizeof(private_data_t)); + if (out->priv == NULL) { + return false; + } + + priv = out->priv; + } + + priv->parent_q = g_queue_new(); + return true; +} + +static void +text_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + CRM_ASSERT(out != NULL && out->dest != NULL); + fflush(out->dest); +} + +static void +text_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + + if (out->dest != stdout) { + out->dest = freopen(NULL, "w", out->dest); + } + + CRM_ASSERT(out->dest != NULL); + + text_free_priv(out); + text_init(out); +} + +static void +text_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + CRM_ASSERT(out != NULL); + + if (proc_stdout != NULL) { + fprintf(out->dest, "%s\n", proc_stdout); + } + + if (proc_stderr != NULL) { + fprintf(out->dest, "%s\n", proc_stderr); + } +} + +static void +text_version(pcmk__output_t *out, bool extended) { + CRM_ASSERT(out != NULL && out->dest != NULL); + + if (extended) { + fprintf(out->dest, "Pacemaker %s (Build: %s): %s\n", PACEMAKER_VERSION, BUILD_VERSION, CRM_FEATURES); + } else { + fprintf(out->dest, "Pacemaker %s\n", PACEMAKER_VERSION); + fprintf(out->dest, "Written by Andrew Beekhof and " + "the Pacemaker project contributors\n"); + } +} + +G_GNUC_PRINTF(2, 3) +static void +text_err(pcmk__output_t *out, const char *format, ...) { + va_list ap; + int len = 0; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + + /* Informational output does not get indented, to separate it from other + * potentially indented list output. + */ + len = vfprintf(stderr, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + /* Add a newline. */ + fprintf(stderr, "\n"); +} + +G_GNUC_PRINTF(2, 3) +static int +text_info(pcmk__output_t *out, const char *format, ...) { + va_list ap; + int len = 0; + + CRM_ASSERT(out != NULL); + + if (out->is_quiet(out)) { + return pcmk_rc_no_output; + } + + va_start(ap, format); + + /* Informational output does not get indented, to separate it from other + * potentially indented list output. + */ + len = vfprintf(out->dest, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + /* Add a newline. */ + fprintf(out->dest, "\n"); + return pcmk_rc_ok; +} + +G_GNUC_PRINTF(2, 3) +static int +text_transient(pcmk__output_t *out, const char *format, ...) +{ + return pcmk_rc_no_output; +} + +static void +text_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + CRM_ASSERT(out != NULL); + pcmk__indented_printf(out, "%s", buf); +} + +G_GNUC_PRINTF(4, 5) +static void +text_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, + const char *format, ...) { + private_data_t *priv = NULL; + text_list_data_t *new_list = NULL; + va_list ap; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + + if (fancy && format) { + pcmk__indented_vprintf(out, format, ap); + fprintf(out->dest, ":\n"); + } + + va_end(ap); + + new_list = calloc(1, sizeof(text_list_data_t)); + new_list->len = 0; + pcmk__str_update(&new_list->singular_noun, singular_noun); + pcmk__str_update(&new_list->plural_noun, plural_noun); + + g_queue_push_tail(priv->parent_q, new_list); +} + +G_GNUC_PRINTF(3, 4) +static void +text_list_item(pcmk__output_t *out, const char *id, const char *format, ...) { + va_list ap; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + + if (fancy) { + if (id != NULL) { + /* Not really a good way to do this all in one call, so make it two. + * The first handles the indentation and list styling. The second + * just prints right after that one. + */ + pcmk__indented_printf(out, "%s: ", id); + vfprintf(out->dest, format, ap); + } else { + pcmk__indented_vprintf(out, format, ap); + } + } else { + pcmk__indented_vprintf(out, format, ap); + } + + fputc('\n', out->dest); + fflush(out->dest); + va_end(ap); + + out->increment_list(out); +} + +static void +text_increment_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + gpointer tail; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + tail = g_queue_peek_tail(priv->parent_q); + CRM_ASSERT(tail != NULL); + ((text_list_data_t *) tail)->len++; +} + +static void +text_end_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + text_list_data_t *node = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + node = g_queue_pop_tail(priv->parent_q); + + if (node->singular_noun != NULL && node->plural_noun != NULL) { + if (node->len == 1) { + pcmk__indented_printf(out, "%d %s found\n", node->len, node->singular_noun); + } else { + pcmk__indented_printf(out, "%d %s found\n", node->len, node->plural_noun); + } + } + + free(node); +} + +static bool +text_is_quiet(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + return out->quiet; +} + +static void +text_spacer(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + fprintf(out->dest, "\n"); +} + +static void +text_progress(pcmk__output_t *out, bool end) { + CRM_ASSERT(out != NULL); + + if (out->dest == stdout) { + fprintf(out->dest, "."); + + if (end) { + fprintf(out->dest, "\n"); + } + } +} + +pcmk__output_t * +pcmk__mk_text_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "text"; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = text_init; + retval->free_priv = text_free_priv; + retval->finish = text_finish; + retval->reset = text_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = text_subprocess_output; + retval->version = text_version; + retval->info = text_info; + retval->transient = text_transient; + retval->err = text_err; + retval->output_xml = text_output_xml; + + retval->begin_list = text_begin_list; + retval->list_item = text_list_item; + retval->increment_list = text_increment_list; + retval->end_list = text_end_list; + + retval->is_quiet = text_is_quiet; + retval->spacer = text_spacer; + retval->progress = text_progress; + retval->prompt = pcmk__text_prompt; + + return retval; +} + +G_GNUC_PRINTF(2, 0) +void +pcmk__formatted_vprintf(pcmk__output_t *out, const char *format, va_list args) { + int len = 0; + + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "text", pcmk__str_none), return); + + len = vfprintf(out->dest, format, args); + CRM_ASSERT(len >= 0); +} + +G_GNUC_PRINTF(2, 3) +void +pcmk__formatted_printf(pcmk__output_t *out, const char *format, ...) { + va_list ap; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + pcmk__formatted_vprintf(out, format, ap); + va_end(ap); +} + +G_GNUC_PRINTF(2, 0) +void +pcmk__indented_vprintf(pcmk__output_t *out, const char *format, va_list args) { + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_eq(out->fmt_name, "text", pcmk__str_none), return); + + if (fancy) { + int level = 0; + private_data_t *priv = out->priv; + + CRM_ASSERT(priv != NULL); + + level = g_queue_get_length(priv->parent_q); + + for (int i = 0; i < level; i++) { + fprintf(out->dest, " "); + } + + if (level > 0) { + fprintf(out->dest, "* "); + } + } + + pcmk__formatted_vprintf(out, format, args); +} + +G_GNUC_PRINTF(2, 3) +void +pcmk__indented_printf(pcmk__output_t *out, const char *format, ...) { + va_list ap; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + pcmk__indented_vprintf(out, format, ap); + va_end(ap); +} + +void +pcmk__text_prompt(const char *prompt, bool echo, char **dest) +{ + int rc = 0; + struct termios settings; + tcflag_t orig_c_lflag = 0; + + CRM_ASSERT(prompt != NULL); + CRM_ASSERT(dest != NULL); + + if (!echo) { + rc = tcgetattr(0, &settings); + if (rc == 0) { + orig_c_lflag = settings.c_lflag; + settings.c_lflag &= ~ECHO; + rc = tcsetattr(0, TCSANOW, &settings); + } + } + + if (rc == 0) { + fprintf(stderr, "%s: ", prompt); + + if (*dest != NULL) { + free(*dest); + *dest = NULL; + } + +#if HAVE_SSCANF_M + rc = scanf("%ms", dest); +#else + *dest = calloc(1, 1024); + rc = scanf("%1023s", *dest); +#endif + fprintf(stderr, "\n"); + } + + if (rc < 1) { + free(*dest); + *dest = NULL; + } + + if (orig_c_lflag != 0) { + settings.c_lflag = orig_c_lflag; + /* rc = */ tcsetattr(0, TCSANOW, &settings); + } +} diff --git a/lib/common/output_xml.c b/lib/common/output_xml.c new file mode 100644 index 0000000..0972638 --- /dev/null +++ b/lib/common/output_xml.c @@ -0,0 +1,541 @@ +/* + * Copyright 2019-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +static gboolean legacy_xml = FALSE; +static gboolean simple_list = FALSE; +static gboolean substitute = FALSE; + +GOptionEntry pcmk__xml_output_entries[] = { + { "xml-legacy", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &legacy_xml, + NULL, + NULL }, + { "xml-simple-list", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &simple_list, + NULL, + NULL }, + { "xml-substitute", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &substitute, + NULL, + NULL }, + + { NULL } +}; + +typedef struct subst_s { + const char *from; + const char *to; +} subst_t; + +static subst_t substitutions[] = { + { "Active Resources", "resources" }, + { "Allocation Scores", "allocations" }, + { "Allocation Scores and Utilization Information", "allocations_utilizations" }, + { "Cluster Summary", "summary" }, + { "Current cluster status", "cluster_status" }, + { "Executing Cluster Transition", "transition" }, + { "Failed Resource Actions", "failures" }, + { "Fencing History", "fence_history" }, + { "Full List of Resources", "resources" }, + { "Inactive Resources", "resources" }, + { "Migration Summary", "node_history" }, + { "Negative Location Constraints", "bans" }, + { "Node Attributes", "node_attributes" }, + { "Operations", "node_history" }, + { "Resource Config", "resource_config" }, + { "Resource Operations", "operations" }, + { "Revised Cluster Status", "revised_cluster_status" }, + { "Transition Summary", "actions" }, + { "Utilization Information", "utilizations" }, + + { NULL, NULL } +}; + +/* The first several elements of this struct must be the same as the first + * several elements of private_data_s in lib/common/output_html.c. That + * struct gets passed to a bunch of the pcmk__output_xml_* functions which + * assume an XML private_data_s. Keeping them laid out the same means this + * still works. + */ +typedef struct private_data_s { + /* Begin members that must match the HTML version */ + xmlNode *root; + GQueue *parent_q; + GSList *errors; + /* End members that must match the HTML version */ + bool legacy_xml; +} private_data_t; + +static void +xml_free_priv(pcmk__output_t *out) { + private_data_t *priv = NULL; + + if (out == NULL || out->priv == NULL) { + return; + } + + priv = out->priv; + + free_xml(priv->root); + g_queue_free(priv->parent_q); + g_slist_free(priv->errors); + free(priv); + out->priv = NULL; +} + +static bool +xml_init(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL); + + /* If xml_init was previously called on this output struct, just return. */ + if (out->priv != NULL) { + return true; + } else { + out->priv = calloc(1, sizeof(private_data_t)); + if (out->priv == NULL) { + return false; + } + + priv = out->priv; + } + + if (legacy_xml) { + priv->root = create_xml_node(NULL, "crm_mon"); + crm_xml_add(priv->root, "version", PACEMAKER_VERSION); + } else { + priv->root = create_xml_node(NULL, "pacemaker-result"); + crm_xml_add(priv->root, "api-version", PCMK__API_VERSION); + + if (out->request != NULL) { + crm_xml_add(priv->root, "request", out->request); + } + } + + priv->parent_q = g_queue_new(); + priv->errors = NULL; + g_queue_push_tail(priv->parent_q, priv->root); + + /* Copy this from the file-level variable. This means that it is only settable + * as a command line option, and that pcmk__output_new must be called after all + * command line processing is completed. + */ + priv->legacy_xml = legacy_xml; + + return true; +} + +static void +add_error_node(gpointer data, gpointer user_data) { + char *str = (char *) data; + xmlNodePtr node = (xmlNodePtr) user_data; + pcmk_create_xml_text_node(node, "error", str); +} + +static void +xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { + private_data_t *priv = NULL; + xmlNodePtr node; + + CRM_ASSERT(out != NULL); + priv = out->priv; + + /* If root is NULL, xml_init failed and we are being called from pcmk__output_free + * in the pcmk__output_new path. + */ + if (priv == NULL || priv->root == NULL) { + return; + } + + if (legacy_xml) { + GSList *node = priv->errors; + + if (exit_status != CRM_EX_OK) { + fprintf(stderr, "%s\n", crm_exit_str(exit_status)); + } + + while (node != NULL) { + fprintf(stderr, "%s\n", (char *) node->data); + node = node->next; + } + } else { + char *rc_as_str = pcmk__itoa(exit_status); + + node = create_xml_node(priv->root, "status"); + pcmk__xe_set_props(node, "code", rc_as_str, + "message", crm_exit_str(exit_status), + NULL); + + if (g_slist_length(priv->errors) > 0) { + xmlNodePtr errors_node = create_xml_node(node, "errors"); + g_slist_foreach(priv->errors, add_error_node, (gpointer) errors_node); + } + + free(rc_as_str); + } + + if (print) { + char *buf = dump_xml_formatted_with_text(priv->root); + fprintf(out->dest, "%s", buf); + fflush(out->dest); + free(buf); + } + + if (copy_dest != NULL) { + *copy_dest = copy_xml(priv->root); + } +} + +static void +xml_reset(pcmk__output_t *out) { + CRM_ASSERT(out != NULL); + + out->dest = freopen(NULL, "w", out->dest); + CRM_ASSERT(out->dest != NULL); + + xml_free_priv(out); + xml_init(out); +} + +static void +xml_subprocess_output(pcmk__output_t *out, int exit_status, + const char *proc_stdout, const char *proc_stderr) { + xmlNodePtr node, child_node; + char *rc_as_str = NULL; + + CRM_ASSERT(out != NULL); + + rc_as_str = pcmk__itoa(exit_status); + + node = pcmk__output_xml_create_parent(out, "command", + "code", rc_as_str, + NULL); + + if (proc_stdout != NULL) { + child_node = pcmk_create_xml_text_node(node, "output", proc_stdout); + crm_xml_add(child_node, "source", "stdout"); + } + + if (proc_stderr != NULL) { + child_node = pcmk_create_xml_text_node(node, "output", proc_stderr); + crm_xml_add(child_node, "source", "stderr"); + } + + free(rc_as_str); +} + +static void +xml_version(pcmk__output_t *out, bool extended) { + CRM_ASSERT(out != NULL); + + pcmk__output_create_xml_node(out, "version", + "program", "Pacemaker", + "version", PACEMAKER_VERSION, + "author", "Andrew Beekhof and the " + "Pacemaker project contributors", + "build", BUILD_VERSION, + "features", CRM_FEATURES, + NULL); +} + +G_GNUC_PRINTF(2, 3) +static void +xml_err(pcmk__output_t *out, const char *format, ...) { + private_data_t *priv = NULL; + int len = 0; + char *buf = NULL; + va_list ap; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + + priv->errors = g_slist_append(priv->errors, buf); +} + +G_GNUC_PRINTF(2, 3) +static int +xml_info(pcmk__output_t *out, const char *format, ...) { + return pcmk_rc_no_output; +} + +static void +xml_output_xml(pcmk__output_t *out, const char *name, const char *buf) { + xmlNodePtr parent = NULL; + xmlNodePtr cdata_node = NULL; + + CRM_ASSERT(out != NULL); + + parent = pcmk__output_create_xml_node(out, name, NULL); + cdata_node = xmlNewCDataBlock(getDocPtr(parent), (pcmkXmlStr) buf, strlen(buf)); + xmlAddChild(parent, cdata_node); +} + +G_GNUC_PRINTF(4, 5) +static void +xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, + const char *format, ...) { + va_list ap; + char *name = NULL; + char *buf = NULL; + int len; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + if (substitute) { + for (subst_t *s = substitutions; s->from != NULL; s++) { + if (!strcmp(s->from, buf)) { + name = g_strdup(s->to); + break; + } + } + } + + if (name == NULL) { + name = g_ascii_strdown(buf, -1); + } + + if (legacy_xml || simple_list) { + pcmk__output_xml_create_parent(out, name, NULL); + } else { + pcmk__output_xml_create_parent(out, "list", + "name", name, + NULL); + } + + g_free(name); + free(buf); +} + +G_GNUC_PRINTF(3, 4) +static void +xml_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { + xmlNodePtr item_node = NULL; + va_list ap; + char *buf = NULL; + int len; + + CRM_ASSERT(out != NULL); + + va_start(ap, format); + len = vasprintf(&buf, format, ap); + CRM_ASSERT(len >= 0); + va_end(ap); + + item_node = pcmk__output_create_xml_text_node(out, "item", buf); + + if (name != NULL) { + crm_xml_add(item_node, "name", name); + } + + free(buf); +} + +static void +xml_increment_list(pcmk__output_t *out) { + /* This function intentially left blank */ +} + +static void +xml_end_list(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + priv = out->priv; + + if (priv->legacy_xml || simple_list) { + g_queue_pop_tail(priv->parent_q); + } else { + char *buf = NULL; + xmlNodePtr node; + + node = g_queue_pop_tail(priv->parent_q); + buf = crm_strdup_printf("%lu", xmlChildElementCount(node)); + crm_xml_add(node, "count", buf); + free(buf); + } +} + +static bool +xml_is_quiet(pcmk__output_t *out) { + return false; +} + +static void +xml_spacer(pcmk__output_t *out) { + /* This function intentionally left blank */ +} + +static void +xml_progress(pcmk__output_t *out, bool end) { + /* This function intentionally left blank */ +} + +pcmk__output_t * +pcmk__mk_xml_output(char **argv) { + pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); + + if (retval == NULL) { + return NULL; + } + + retval->fmt_name = "xml"; + retval->request = pcmk__quote_cmdline(argv); + + retval->init = xml_init; + retval->free_priv = xml_free_priv; + retval->finish = xml_finish; + retval->reset = xml_reset; + + retval->register_message = pcmk__register_message; + retval->message = pcmk__call_message; + + retval->subprocess_output = xml_subprocess_output; + retval->version = xml_version; + retval->info = xml_info; + retval->transient = xml_info; + retval->err = xml_err; + retval->output_xml = xml_output_xml; + + retval->begin_list = xml_begin_list; + retval->list_item = xml_list_item; + retval->increment_list = xml_increment_list; + retval->end_list = xml_end_list; + + retval->is_quiet = xml_is_quiet; + retval->spacer = xml_spacer; + retval->progress = xml_progress; + retval->prompt = pcmk__text_prompt; + + return retval; +} + +xmlNodePtr +pcmk__output_xml_create_parent(pcmk__output_t *out, const char *name, ...) { + va_list args; + xmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + + node = pcmk__output_create_xml_node(out, name, NULL); + + va_start(args, name); + pcmk__xe_set_propv(node, args); + va_end(args); + + pcmk__output_xml_push_parent(out, node); + return node; +} + +void +pcmk__output_xml_add_node_copy(pcmk__output_t *out, xmlNodePtr node) { + private_data_t *priv = NULL; + xmlNodePtr parent = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_ASSERT(node != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + + priv = out->priv; + parent = g_queue_peek_tail(priv->parent_q); + + // Shouldn't happen unless the caller popped priv->root + CRM_CHECK(parent != NULL, return); + + add_node_copy(parent, node); +} + +xmlNodePtr +pcmk__output_create_xml_node(pcmk__output_t *out, const char *name, ...) { + xmlNodePtr node = NULL; + private_data_t *priv = NULL; + va_list args; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + + priv = out->priv; + + node = create_xml_node(g_queue_peek_tail(priv->parent_q), name); + va_start(args, name); + pcmk__xe_set_propv(node, args); + va_end(args); + + return node; +} + +xmlNodePtr +pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const char *content) { + xmlNodePtr node = NULL; + + CRM_ASSERT(out != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + + node = pcmk__output_create_xml_node(out, name, NULL); + xmlNodeSetContent(node, (pcmkXmlStr) content); + return node; +} + +void +pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr parent) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_ASSERT(parent != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + + priv = out->priv; + + g_queue_push_tail(priv->parent_q, parent); +} + +void +pcmk__output_xml_pop_parent(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); + + priv = out->priv; + + CRM_ASSERT(g_queue_get_length(priv->parent_q) > 0); + g_queue_pop_tail(priv->parent_q); +} + +xmlNodePtr +pcmk__output_xml_peek_parent(pcmk__output_t *out) { + private_data_t *priv = NULL; + + CRM_ASSERT(out != NULL && out->priv != NULL); + CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); + + priv = out->priv; + + /* If queue is empty NULL will be returned */ + return g_queue_peek_tail(priv->parent_q); +} diff --git a/lib/common/patchset.c b/lib/common/patchset.c new file mode 100644 index 0000000..8c1362d --- /dev/null +++ b/lib/common/patchset.c @@ -0,0 +1,1516 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include // CRM_XML_LOG_BASE, etc. +#include "crmcommon_private.h" + +static xmlNode *subtract_xml_comment(xmlNode *parent, xmlNode *left, + xmlNode *right, gboolean *changed); + +/* Add changes for specified XML to patchset. + * For patchset format, refer to diff schema. + */ +static void +add_xml_changes_to_patchset(xmlNode *xml, xmlNode *patchset) +{ + xmlNode *cIter = NULL; + xmlAttr *pIter = NULL; + xmlNode *change = NULL; + xml_node_private_t *nodepriv = xml->_private; + const char *value = NULL; + + // If this XML node is new, just report that + if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_created)) { + GString *xpath = pcmk__element_xpath(xml->parent); + + if (xpath != NULL) { + int position = pcmk__xml_position(xml, pcmk__xf_deleted); + + change = create_xml_node(patchset, XML_DIFF_CHANGE); + + crm_xml_add(change, XML_DIFF_OP, "create"); + crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); + crm_xml_add_int(change, XML_DIFF_POSITION, position); + add_node_copy(change, xml); + g_string_free(xpath, TRUE); + } + + return; + } + + // Check each of the XML node's attributes for changes + for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; + pIter = pIter->next) { + xmlNode *attr = NULL; + + nodepriv = pIter->_private; + if (!pcmk_any_flags_set(nodepriv->flags, pcmk__xf_deleted|pcmk__xf_dirty)) { + continue; + } + + if (change == NULL) { + GString *xpath = pcmk__element_xpath(xml); + + if (xpath != NULL) { + change = create_xml_node(patchset, XML_DIFF_CHANGE); + + crm_xml_add(change, XML_DIFF_OP, "modify"); + crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); + + change = create_xml_node(change, XML_DIFF_LIST); + g_string_free(xpath, TRUE); + } + } + + attr = create_xml_node(change, XML_DIFF_ATTR); + + crm_xml_add(attr, XML_NVPAIR_ATTR_NAME, (const char *)pIter->name); + if (nodepriv->flags & pcmk__xf_deleted) { + crm_xml_add(attr, XML_DIFF_OP, "unset"); + + } else { + crm_xml_add(attr, XML_DIFF_OP, "set"); + + value = crm_element_value(xml, (const char *) pIter->name); + crm_xml_add(attr, XML_NVPAIR_ATTR_VALUE, value); + } + } + + if (change) { + xmlNode *result = NULL; + + change = create_xml_node(change->parent, XML_DIFF_RESULT); + result = create_xml_node(change, (const char *)xml->name); + + for (pIter = pcmk__xe_first_attr(xml); pIter != NULL; + pIter = pIter->next) { + nodepriv = pIter->_private; + if (!pcmk_is_set(nodepriv->flags, pcmk__xf_deleted)) { + value = crm_element_value(xml, (const char *) pIter->name); + crm_xml_add(result, (const char *)pIter->name, value); + } + } + } + + // Now recursively do the same for each child node of this node + for (cIter = pcmk__xml_first_child(xml); cIter != NULL; + cIter = pcmk__xml_next(cIter)) { + add_xml_changes_to_patchset(cIter, patchset); + } + + nodepriv = xml->_private; + if (patchset && pcmk_is_set(nodepriv->flags, pcmk__xf_moved)) { + GString *xpath = pcmk__element_xpath(xml); + + crm_trace("%s.%s moved to position %d", + xml->name, ID(xml), pcmk__xml_position(xml, pcmk__xf_skip)); + + if (xpath != NULL) { + change = create_xml_node(patchset, XML_DIFF_CHANGE); + + crm_xml_add(change, XML_DIFF_OP, "move"); + crm_xml_add(change, XML_DIFF_PATH, (const char *) xpath->str); + crm_xml_add_int(change, XML_DIFF_POSITION, + pcmk__xml_position(xml, pcmk__xf_deleted)); + g_string_free(xpath, TRUE); + } + } +} + +static bool +is_config_change(xmlNode *xml) +{ + GList *gIter = NULL; + xml_node_private_t *nodepriv = NULL; + xml_doc_private_t *docpriv; + xmlNode *config = first_named_child(xml, XML_CIB_TAG_CONFIGURATION); + + if (config) { + nodepriv = config->_private; + } + if ((nodepriv != NULL) && pcmk_is_set(nodepriv->flags, pcmk__xf_dirty)) { + return TRUE; + } + + if ((xml->doc != NULL) && (xml->doc->_private != NULL)) { + docpriv = xml->doc->_private; + for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { + pcmk__deleted_xml_t *deleted_obj = gIter->data; + + if (strstr(deleted_obj->path, + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION) != NULL) { + return TRUE; + } + } + } + return FALSE; +} + +static void +xml_repair_v1_diff(xmlNode *last, xmlNode *next, xmlNode *local_diff, + gboolean changed) +{ + int lpc = 0; + xmlNode *cib = NULL; + xmlNode *diff_child = NULL; + + const char *tag = NULL; + + const char *vfields[] = { + XML_ATTR_GENERATION_ADMIN, + XML_ATTR_GENERATION, + XML_ATTR_NUMUPDATES, + }; + + if (local_diff == NULL) { + crm_trace("Nothing to do"); + return; + } + + tag = "diff-removed"; + diff_child = find_xml_node(local_diff, tag, FALSE); + if (diff_child == NULL) { + diff_child = create_xml_node(local_diff, tag); + } + + tag = XML_TAG_CIB; + cib = find_xml_node(diff_child, tag, FALSE); + if (cib == NULL) { + cib = create_xml_node(diff_child, tag); + } + + for (lpc = 0; (last != NULL) && (lpc < PCMK__NELEM(vfields)); lpc++) { + const char *value = crm_element_value(last, vfields[lpc]); + + crm_xml_add(diff_child, vfields[lpc], value); + if (changed || lpc == 2) { + crm_xml_add(cib, vfields[lpc], value); + } + } + + tag = "diff-added"; + diff_child = find_xml_node(local_diff, tag, FALSE); + if (diff_child == NULL) { + diff_child = create_xml_node(local_diff, tag); + } + + tag = XML_TAG_CIB; + cib = find_xml_node(diff_child, tag, FALSE); + if (cib == NULL) { + cib = create_xml_node(diff_child, tag); + } + + for (lpc = 0; next && lpc < PCMK__NELEM(vfields); lpc++) { + const char *value = crm_element_value(next, vfields[lpc]); + + crm_xml_add(diff_child, vfields[lpc], value); + } + + for (xmlAttrPtr a = pcmk__xe_first_attr(next); a != NULL; a = a->next) { + const char *p_value = crm_element_value(next, (const char *) a->name); + + xmlSetProp(cib, a->name, (pcmkXmlStr) p_value); + } + + crm_log_xml_explicit(local_diff, "Repaired-diff"); +} + +static xmlNode * +xml_create_patchset_v1(xmlNode *source, xmlNode *target, bool config, + bool suppress) +{ + xmlNode *patchset = diff_xml_object(source, target, suppress); + + if (patchset) { + CRM_LOG_ASSERT(xml_document_dirty(target)); + xml_repair_v1_diff(source, target, patchset, config); + crm_xml_add(patchset, "format", "1"); + } + return patchset; +} + +static xmlNode * +xml_create_patchset_v2(xmlNode *source, xmlNode *target) +{ + int lpc = 0; + GList *gIter = NULL; + xml_doc_private_t *docpriv; + + xmlNode *v = NULL; + xmlNode *version = NULL; + xmlNode *patchset = NULL; + const char *vfields[] = { + XML_ATTR_GENERATION_ADMIN, + XML_ATTR_GENERATION, + XML_ATTR_NUMUPDATES, + }; + + CRM_ASSERT(target); + if (!xml_document_dirty(target)) { + return NULL; + } + + CRM_ASSERT(target->doc); + docpriv = target->doc->_private; + + patchset = create_xml_node(NULL, XML_TAG_DIFF); + crm_xml_add_int(patchset, "format", 2); + + version = create_xml_node(patchset, XML_DIFF_VERSION); + + v = create_xml_node(version, XML_DIFF_VSOURCE); + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + const char *value = crm_element_value(source, vfields[lpc]); + + if (value == NULL) { + value = "1"; + } + crm_xml_add(v, vfields[lpc], value); + } + + v = create_xml_node(version, XML_DIFF_VTARGET); + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + const char *value = crm_element_value(target, vfields[lpc]); + + if (value == NULL) { + value = "1"; + } + crm_xml_add(v, vfields[lpc], value); + } + + for (gIter = docpriv->deleted_objs; gIter; gIter = gIter->next) { + pcmk__deleted_xml_t *deleted_obj = gIter->data; + xmlNode *change = create_xml_node(patchset, XML_DIFF_CHANGE); + + crm_xml_add(change, XML_DIFF_OP, "delete"); + crm_xml_add(change, XML_DIFF_PATH, deleted_obj->path); + if (deleted_obj->position >= 0) { + crm_xml_add_int(change, XML_DIFF_POSITION, deleted_obj->position); + } + } + + add_xml_changes_to_patchset(target, patchset); + return patchset; +} + +xmlNode * +xml_create_patchset(int format, xmlNode *source, xmlNode *target, + bool *config_changed, bool manage_version) +{ + int counter = 0; + bool config = FALSE; + xmlNode *patch = NULL; + const char *version = crm_element_value(source, XML_ATTR_CRM_VERSION); + + xml_acl_disable(target); + if (!xml_document_dirty(target)) { + crm_trace("No change %d", format); + return NULL; /* No change */ + } + + config = is_config_change(target); + if (config_changed) { + *config_changed = config; + } + + if (manage_version && config) { + crm_trace("Config changed %d", format); + crm_xml_add(target, XML_ATTR_NUMUPDATES, "0"); + + crm_element_value_int(target, XML_ATTR_GENERATION, &counter); + crm_xml_add_int(target, XML_ATTR_GENERATION, counter+1); + + } else if (manage_version) { + crm_element_value_int(target, XML_ATTR_NUMUPDATES, &counter); + crm_trace("Status changed %d - %d %s", format, counter, + crm_element_value(source, XML_ATTR_NUMUPDATES)); + crm_xml_add_int(target, XML_ATTR_NUMUPDATES, (counter + 1)); + } + + if (format == 0) { + if (compare_version("3.0.8", version) < 0) { + format = 2; + } else { + format = 1; + } + crm_trace("Using patch format %d for version: %s", format, version); + } + + switch (format) { + case 1: + patch = xml_create_patchset_v1(source, target, config, FALSE); + break; + case 2: + patch = xml_create_patchset_v2(source, target); + break; + default: + crm_err("Unknown patch format: %d", format); + return NULL; + } + return patch; +} + +void +patchset_process_digest(xmlNode *patch, xmlNode *source, xmlNode *target, + bool with_digest) +{ + int format = 1; + const char *version = NULL; + char *digest = NULL; + + if ((patch == NULL) || (source == NULL) || (target == NULL)) { + return; + } + + /* We should always call xml_accept_changes() before calculating a digest. + * Otherwise, with an on-tracking dirty target, we could get a wrong digest. + */ + CRM_LOG_ASSERT(!xml_document_dirty(target)); + + crm_element_value_int(patch, "format", &format); + if ((format > 1) && !with_digest) { + return; + } + + version = crm_element_value(source, XML_ATTR_CRM_VERSION); + digest = calculate_xml_versioned_digest(target, FALSE, TRUE, version); + + crm_xml_add(patch, XML_ATTR_DIGEST, digest); + free(digest); + + return; +} + +// Return true if attribute name is not "id" +static bool +not_id(xmlAttrPtr attr, void *user_data) +{ + return strcmp((const char *) attr->name, XML_ATTR_ID) != 0; +} + +// Apply the removals section of an v1 patchset to an XML node +static void +process_v1_removals(xmlNode *target, xmlNode *patch) +{ + xmlNode *patch_child = NULL; + xmlNode *cIter = NULL; + + char *id = NULL; + const char *name = NULL; + const char *value = NULL; + + if ((target == NULL) || (patch == NULL)) { + return; + } + + if (target->type == XML_COMMENT_NODE) { + gboolean dummy; + + subtract_xml_comment(target->parent, target, patch, &dummy); + } + + name = crm_element_name(target); + CRM_CHECK(name != NULL, return); + CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(patch), + pcmk__str_casei), + return); + CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return); + + // Check for XML_DIFF_MARKER in a child + id = crm_element_value_copy(target, XML_ATTR_ID); + value = crm_element_value(patch, XML_DIFF_MARKER); + if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { + crm_trace("We are the root of the deletion: %s.id=%s", name, id); + free_xml(target); + free(id); + return; + } + + // Removing then restoring id would change ordering of properties + pcmk__xe_remove_matching_attrs(patch, not_id, NULL); + + // Changes to child objects + cIter = pcmk__xml_first_child(target); + while (cIter) { + xmlNode *target_child = cIter; + + cIter = pcmk__xml_next(cIter); + patch_child = pcmk__xml_match(patch, target_child, false); + process_v1_removals(target_child, patch_child); + } + free(id); +} + +// Apply the additions section of an v1 patchset to an XML node +static void +process_v1_additions(xmlNode *parent, xmlNode *target, xmlNode *patch) +{ + xmlNode *patch_child = NULL; + xmlNode *target_child = NULL; + xmlAttrPtr xIter = NULL; + + const char *id = NULL; + const char *name = NULL; + const char *value = NULL; + + if (patch == NULL) { + return; + } else if ((parent == NULL) && (target == NULL)) { + return; + } + + // Check for XML_DIFF_MARKER in a child + value = crm_element_value(patch, XML_DIFF_MARKER); + if ((target == NULL) && (value != NULL) + && (strcmp(value, "added:top") == 0)) { + id = ID(patch); + name = crm_element_name(patch); + crm_trace("We are the root of the addition: %s.id=%s", name, id); + add_node_copy(parent, patch); + return; + + } else if (target == NULL) { + id = ID(patch); + name = crm_element_name(patch); + crm_err("Could not locate: %s.id=%s", name, id); + return; + } + + if (target->type == XML_COMMENT_NODE) { + pcmk__xc_update(parent, target, patch); + } + + name = crm_element_name(target); + CRM_CHECK(name != NULL, return); + CRM_CHECK(pcmk__str_eq(crm_element_name(target), crm_element_name(patch), + pcmk__str_casei), + return); + CRM_CHECK(pcmk__str_eq(ID(target), ID(patch), pcmk__str_casei), return); + + for (xIter = pcmk__xe_first_attr(patch); xIter != NULL; + xIter = xIter->next) { + const char *p_name = (const char *) xIter->name; + const char *p_value = crm_element_value(patch, p_name); + + xml_remove_prop(target, p_name); // Preserve patch order + crm_xml_add(target, p_name, p_value); + } + + // Changes to child objects + for (patch_child = pcmk__xml_first_child(patch); patch_child != NULL; + patch_child = pcmk__xml_next(patch_child)) { + + target_child = pcmk__xml_match(target, patch_child, false); + process_v1_additions(target, target_child, patch_child); + } +} + +/*! + * \internal + * \brief Find additions or removals in a patch set + * + * \param[in] patchset XML of patch + * \param[in] format Patch version + * \param[in] added TRUE if looking for additions, FALSE if removals + * \param[in,out] patch_node Will be set to node if found + * + * \return TRUE if format is valid, FALSE if invalid + */ +static bool +find_patch_xml_node(const xmlNode *patchset, int format, bool added, + xmlNode **patch_node) +{ + xmlNode *cib_node; + const char *label; + + switch (format) { + case 1: + label = added? "diff-added" : "diff-removed"; + *patch_node = find_xml_node(patchset, label, FALSE); + cib_node = find_xml_node(*patch_node, "cib", FALSE); + if (cib_node != NULL) { + *patch_node = cib_node; + } + break; + case 2: + label = added? "target" : "source"; + *patch_node = find_xml_node(patchset, "version", FALSE); + *patch_node = find_xml_node(*patch_node, label, FALSE); + break; + default: + crm_warn("Unknown patch format: %d", format); + *patch_node = NULL; + return FALSE; + } + return TRUE; +} + +// Get CIB versions used for additions and deletions in a patchset +bool +xml_patch_versions(const xmlNode *patchset, int add[3], int del[3]) +{ + int lpc = 0; + int format = 1; + xmlNode *tmp = NULL; + + const char *vfields[] = { + XML_ATTR_GENERATION_ADMIN, + XML_ATTR_GENERATION, + XML_ATTR_NUMUPDATES, + }; + + + crm_element_value_int(patchset, "format", &format); + + /* Process removals */ + if (!find_patch_xml_node(patchset, format, FALSE, &tmp)) { + return -EINVAL; + } + if (tmp != NULL) { + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + crm_element_value_int(tmp, vfields[lpc], &(del[lpc])); + crm_trace("Got %d for del[%s]", del[lpc], vfields[lpc]); + } + } + + /* Process additions */ + if (!find_patch_xml_node(patchset, format, TRUE, &tmp)) { + return -EINVAL; + } + if (tmp != NULL) { + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + crm_element_value_int(tmp, vfields[lpc], &(add[lpc])); + crm_trace("Got %d for add[%s]", add[lpc], vfields[lpc]); + } + } + return pcmk_ok; +} + +/*! + * \internal + * \brief Check whether patchset can be applied to current CIB + * + * \param[in] xml Root of current CIB + * \param[in] patchset Patchset to check + * \param[in] format Patchset version + * + * \return Standard Pacemaker return code + */ +static int +xml_patch_version_check(const xmlNode *xml, const xmlNode *patchset, int format) +{ + int lpc = 0; + bool changed = FALSE; + + int this[] = { 0, 0, 0 }; + int add[] = { 0, 0, 0 }; + int del[] = { 0, 0, 0 }; + + const char *vfields[] = { + XML_ATTR_GENERATION_ADMIN, + XML_ATTR_GENERATION, + XML_ATTR_NUMUPDATES, + }; + + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + crm_element_value_int(xml, vfields[lpc], &(this[lpc])); + crm_trace("Got %d for this[%s]", this[lpc], vfields[lpc]); + if (this[lpc] < 0) { + this[lpc] = 0; + } + } + + /* Set some defaults in case nothing is present */ + add[0] = this[0]; + add[1] = this[1]; + add[2] = this[2] + 1; + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + del[lpc] = this[lpc]; + } + + xml_patch_versions(patchset, add, del); + + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + if (this[lpc] < del[lpc]) { + crm_debug("Current %s is too low (%d.%d.%d < %d.%d.%d --> %d.%d.%d)", + vfields[lpc], this[0], this[1], this[2], + del[0], del[1], del[2], add[0], add[1], add[2]); + return pcmk_rc_diff_resync; + + } else if (this[lpc] > del[lpc]) { + crm_info("Current %s is too high (%d.%d.%d > %d.%d.%d --> %d.%d.%d) %p", + vfields[lpc], this[0], this[1], this[2], + del[0], del[1], del[2], add[0], add[1], add[2], patchset); + crm_log_xml_info(patchset, "OldPatch"); + return pcmk_rc_old_data; + } + } + + for (lpc = 0; lpc < PCMK__NELEM(vfields); lpc++) { + if (add[lpc] > del[lpc]) { + changed = TRUE; + } + } + + if (!changed) { + crm_notice("Versions did not change in patch %d.%d.%d", + add[0], add[1], add[2]); + return pcmk_rc_old_data; + } + + crm_debug("Can apply patch %d.%d.%d to %d.%d.%d", + add[0], add[1], add[2], this[0], this[1], this[2]); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Apply a version 1 patchset to an XML node + * + * \param[in,out] xml XML to apply patchset to + * \param[in] patchset Patchset to apply + * + * \return Standard Pacemaker return code + */ +static int +apply_v1_patchset(xmlNode *xml, const xmlNode *patchset) +{ + int rc = pcmk_rc_ok; + int root_nodes_seen = 0; + + xmlNode *child_diff = NULL; + xmlNode *added = find_xml_node(patchset, "diff-added", FALSE); + xmlNode *removed = find_xml_node(patchset, "diff-removed", FALSE); + xmlNode *old = copy_xml(xml); + + crm_trace("Subtraction Phase"); + for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL; + child_diff = pcmk__xml_next(child_diff)) { + CRM_CHECK(root_nodes_seen == 0, rc = FALSE); + if (root_nodes_seen == 0) { + process_v1_removals(xml, child_diff); + } + root_nodes_seen++; + } + + if (root_nodes_seen > 1) { + crm_err("(-) Diffs cannot contain more than one change set... saw %d", + root_nodes_seen); + rc = ENOTUNIQ; + } + + root_nodes_seen = 0; + crm_trace("Addition Phase"); + if (rc == pcmk_rc_ok) { + xmlNode *child_diff = NULL; + + for (child_diff = pcmk__xml_first_child(added); child_diff != NULL; + child_diff = pcmk__xml_next(child_diff)) { + CRM_CHECK(root_nodes_seen == 0, rc = FALSE); + if (root_nodes_seen == 0) { + process_v1_additions(NULL, xml, child_diff); + } + root_nodes_seen++; + } + } + + if (root_nodes_seen > 1) { + crm_err("(+) Diffs cannot contain more than one change set... saw %d", + root_nodes_seen); + rc = ENOTUNIQ; + } + + purge_diff_markers(xml); // Purge prior to checking digest + + free_xml(old); + return rc; +} + +// Return first child matching element name and optionally id or position +static xmlNode * +first_matching_xml_child(const xmlNode *parent, const char *name, + const char *id, int position) +{ + xmlNode *cIter = NULL; + + for (cIter = pcmk__xml_first_child(parent); cIter != NULL; + cIter = pcmk__xml_next(cIter)) { + if (strcmp((const char *) cIter->name, name) != 0) { + continue; + } else if (id) { + const char *cid = ID(cIter); + + if ((cid == NULL) || (strcmp(cid, id) != 0)) { + continue; + } + } + + // "position" makes sense only for XML comments for now + if ((cIter->type == XML_COMMENT_NODE) + && (position >= 0) + && (pcmk__xml_position(cIter, pcmk__xf_skip) != position)) { + continue; + } + + return cIter; + } + return NULL; +} + +/*! + * \internal + * \brief Simplified, more efficient alternative to get_xpath_object() + * + * \param[in] top Root of XML to search + * \param[in] key Search xpath + * \param[in] target_position If deleting, where to delete + * + * \return XML child matching xpath if found, NULL otherwise + * + * \note This only works on simplified xpaths found in v2 patchset diffs, + * i.e. the only allowed search predicate is [@id='XXX']. + */ +static xmlNode * +search_v2_xpath(const xmlNode *top, const char *key, int target_position) +{ + xmlNode *target = (xmlNode *) top->doc; + const char *current = key; + char *section; + char *remainder; + char *id; + char *tag; + char *path = NULL; + int rc; + size_t key_len; + + CRM_CHECK(key != NULL, return NULL); + key_len = strlen(key); + + /* These are scanned from key after a slash, so they can't be bigger + * than key_len - 1 characters plus a null terminator. + */ + + remainder = calloc(key_len, sizeof(char)); + CRM_ASSERT(remainder != NULL); + + section = calloc(key_len, sizeof(char)); + CRM_ASSERT(section != NULL); + + id = calloc(key_len, sizeof(char)); + CRM_ASSERT(id != NULL); + + tag = calloc(key_len, sizeof(char)); + CRM_ASSERT(tag != NULL); + + do { + // Look for /NEXT_COMPONENT/REMAINING_COMPONENTS + rc = sscanf(current, "/%[^/]%s", section, remainder); + if (rc > 0) { + // Separate FIRST_COMPONENT into TAG[@id='ID'] + int f = sscanf(section, "%[^[][@" XML_ATTR_ID "='%[^']", tag, id); + int current_position = -1; + + /* The target position is for the final component tag, so only use + * it if there is nothing left to search after this component. + */ + if ((rc == 1) && (target_position >= 0)) { + current_position = target_position; + } + + switch (f) { + case 1: + target = first_matching_xml_child(target, tag, NULL, + current_position); + break; + case 2: + target = first_matching_xml_child(target, tag, id, + current_position); + break; + default: + // This should not be possible + target = NULL; + break; + } + current = remainder; + } + + // Continue if something remains to search, and we've matched so far + } while ((rc == 2) && target); + + if (target) { + crm_trace("Found %s for %s", + (path = (char *) xmlGetNodePath(target)), key); + free(path); + } else { + crm_debug("No match for %s", key); + } + + free(remainder); + free(section); + free(tag); + free(id); + return target; +} + +typedef struct xml_change_obj_s { + const xmlNode *change; + xmlNode *match; +} xml_change_obj_t; + +static gint +sort_change_obj_by_position(gconstpointer a, gconstpointer b) +{ + const xml_change_obj_t *change_obj_a = a; + const xml_change_obj_t *change_obj_b = b; + int position_a = -1; + int position_b = -1; + + crm_element_value_int(change_obj_a->change, XML_DIFF_POSITION, &position_a); + crm_element_value_int(change_obj_b->change, XML_DIFF_POSITION, &position_b); + + if (position_a < position_b) { + return -1; + + } else if (position_a > position_b) { + return 1; + } + + return 0; +} + +/*! + * \internal + * \brief Apply a version 2 patchset to an XML node + * + * \param[in,out] xml XML to apply patchset to + * \param[in] patchset Patchset to apply + * + * \return Standard Pacemaker return code + */ +static int +apply_v2_patchset(xmlNode *xml, const xmlNode *patchset) +{ + int rc = pcmk_rc_ok; + const xmlNode *change = NULL; + GList *change_objs = NULL; + GList *gIter = NULL; + + for (change = pcmk__xml_first_child(patchset); change != NULL; + change = pcmk__xml_next(change)) { + xmlNode *match = NULL; + const char *op = crm_element_value(change, XML_DIFF_OP); + const char *xpath = crm_element_value(change, XML_DIFF_PATH); + int position = -1; + + if (op == NULL) { + continue; + } + + crm_trace("Processing %s %s", change->name, op); + + // "delete" changes for XML comments are generated with "position" + if (strcmp(op, "delete") == 0) { + crm_element_value_int(change, XML_DIFF_POSITION, &position); + } + match = search_v2_xpath(xml, xpath, position); + crm_trace("Performing %s on %s with %p", op, xpath, match); + + if ((match == NULL) && (strcmp(op, "delete") == 0)) { + crm_debug("No %s match for %s in %p", op, xpath, xml->doc); + continue; + + } else if (match == NULL) { + crm_err("No %s match for %s in %p", op, xpath, xml->doc); + rc = pcmk_rc_diff_failed; + continue; + + } else if ((strcmp(op, "create") == 0) || (strcmp(op, "move") == 0)) { + // Delay the adding of a "create" object + xml_change_obj_t *change_obj = calloc(1, sizeof(xml_change_obj_t)); + + CRM_ASSERT(change_obj != NULL); + + change_obj->change = change; + change_obj->match = match; + + change_objs = g_list_append(change_objs, change_obj); + + if (strcmp(op, "move") == 0) { + // Temporarily put the "move" object after the last sibling + if ((match->parent != NULL) && (match->parent->last != NULL)) { + xmlAddNextSibling(match->parent->last, match); + } + } + + } else if (strcmp(op, "delete") == 0) { + free_xml(match); + + } else if (strcmp(op, "modify") == 0) { + xmlNode *attrs = NULL; + + attrs = pcmk__xml_first_child(first_named_child(change, + XML_DIFF_RESULT)); + if (attrs == NULL) { + rc = ENOMSG; + continue; + } + pcmk__xe_remove_matching_attrs(match, NULL, NULL); // Remove all + + for (xmlAttrPtr pIter = pcmk__xe_first_attr(attrs); pIter != NULL; + pIter = pIter->next) { + const char *name = (const char *) pIter->name; + const char *value = crm_element_value(attrs, name); + + crm_xml_add(match, name, value); + } + + } else { + crm_err("Unknown operation: %s", op); + rc = pcmk_rc_diff_failed; + } + } + + // Changes should be generated in the right order. Double checking. + change_objs = g_list_sort(change_objs, sort_change_obj_by_position); + + for (gIter = change_objs; gIter; gIter = gIter->next) { + xml_change_obj_t *change_obj = gIter->data; + xmlNode *match = change_obj->match; + const char *op = NULL; + const char *xpath = NULL; + + change = change_obj->change; + + op = crm_element_value(change, XML_DIFF_OP); + xpath = crm_element_value(change, XML_DIFF_PATH); + + crm_trace("Continue performing %s on %s with %p", op, xpath, match); + + if (strcmp(op, "create") == 0) { + int position = 0; + xmlNode *child = NULL; + xmlNode *match_child = NULL; + + match_child = match->children; + crm_element_value_int(change, XML_DIFF_POSITION, &position); + + while ((match_child != NULL) + && (position != pcmk__xml_position(match_child, pcmk__xf_skip))) { + match_child = match_child->next; + } + + child = xmlDocCopyNode(change->children, match->doc, 1); + if (match_child) { + crm_trace("Adding %s at position %d", child->name, position); + xmlAddPrevSibling(match_child, child); + + } else if (match->last) { + crm_trace("Adding %s at position %d (end)", + child->name, position); + xmlAddNextSibling(match->last, child); + + } else { + crm_trace("Adding %s at position %d (first)", + child->name, position); + CRM_LOG_ASSERT(position == 0); + xmlAddChild(match, child); + } + pcmk__mark_xml_created(child); + + } else if (strcmp(op, "move") == 0) { + int position = 0; + + crm_element_value_int(change, XML_DIFF_POSITION, &position); + if (position != pcmk__xml_position(match, pcmk__xf_skip)) { + xmlNode *match_child = NULL; + int p = position; + + if (p > pcmk__xml_position(match, pcmk__xf_skip)) { + p++; // Skip ourselves + } + + CRM_ASSERT(match->parent != NULL); + match_child = match->parent->children; + + while ((match_child != NULL) + && (p != pcmk__xml_position(match_child, pcmk__xf_skip))) { + match_child = match_child->next; + } + + crm_trace("Moving %s to position %d (was %d, prev %p, %s %p)", + match->name, position, + pcmk__xml_position(match, pcmk__xf_skip), + match->prev, (match_child? "next":"last"), + (match_child? match_child : match->parent->last)); + + if (match_child) { + xmlAddPrevSibling(match_child, match); + + } else { + CRM_ASSERT(match->parent->last != NULL); + xmlAddNextSibling(match->parent->last, match); + } + + } else { + crm_trace("%s is already in position %d", + match->name, position); + } + + if (position != pcmk__xml_position(match, pcmk__xf_skip)) { + crm_err("Moved %s.%s to position %d instead of %d (%p)", + match->name, ID(match), + pcmk__xml_position(match, pcmk__xf_skip), + position, match->prev); + rc = pcmk_rc_diff_failed; + } + } + } + + g_list_free_full(change_objs, free); + return rc; +} + +int +xml_apply_patchset(xmlNode *xml, xmlNode *patchset, bool check_version) +{ + int format = 1; + int rc = pcmk_ok; + xmlNode *old = NULL; + const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST); + + if (patchset == NULL) { + return rc; + } + + pcmk__if_tracing( + { + pcmk__output_t *logger_out = NULL; + + rc = pcmk_rc2legacy(pcmk__log_output_new(&logger_out)); + CRM_CHECK(rc == pcmk_ok, return rc); + + pcmk__output_set_log_level(logger_out, LOG_TRACE); + rc = logger_out->message(logger_out, "xml-patchset", patchset); + logger_out->finish(logger_out, pcmk_rc2exitc(rc), true, + NULL); + pcmk__output_free(logger_out); + rc = pcmk_ok; + }, + {} + ); + + crm_element_value_int(patchset, "format", &format); + if (check_version) { + rc = pcmk_rc2legacy(xml_patch_version_check(xml, patchset, format)); + if (rc != pcmk_ok) { + return rc; + } + } + + if (digest) { + // Make it available for logging if result doesn't have expected digest + old = copy_xml(xml); + } + + if (rc == pcmk_ok) { + switch (format) { + case 1: + rc = pcmk_rc2legacy(apply_v1_patchset(xml, patchset)); + break; + case 2: + rc = pcmk_rc2legacy(apply_v2_patchset(xml, patchset)); + break; + default: + crm_err("Unknown patch format: %d", format); + rc = -EINVAL; + } + } + + if ((rc == pcmk_ok) && (digest != NULL)) { + char *new_digest = NULL; + char *version = crm_element_value_copy(xml, XML_ATTR_CRM_VERSION); + + new_digest = calculate_xml_versioned_digest(xml, FALSE, TRUE, version); + if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { + crm_info("v%d digest mis-match: expected %s, calculated %s", + format, digest, new_digest); + rc = -pcmk_err_diff_failed; + pcmk__if_tracing( + { + save_xml_to_file(old, "PatchDigest:input", NULL); + save_xml_to_file(xml, "PatchDigest:result", NULL); + save_xml_to_file(patchset, "PatchDigest:diff", NULL); + }, + {} + ); + + } else { + crm_trace("v%d digest matched: expected %s, calculated %s", + format, digest, new_digest); + } + free(new_digest); + free(version); + } + free_xml(old); + return rc; +} + +void +purge_diff_markers(xmlNode *a_node) +{ + xmlNode *child = NULL; + + CRM_CHECK(a_node != NULL, return); + + xml_remove_prop(a_node, XML_DIFF_MARKER); + for (child = pcmk__xml_first_child(a_node); child != NULL; + child = pcmk__xml_next(child)) { + purge_diff_markers(child); + } +} + +xmlNode * +diff_xml_object(xmlNode *old, xmlNode *new, gboolean suppress) +{ + xmlNode *tmp1 = NULL; + xmlNode *diff = create_xml_node(NULL, "diff"); + xmlNode *removed = create_xml_node(diff, "diff-removed"); + xmlNode *added = create_xml_node(diff, "diff-added"); + + crm_xml_add(diff, XML_ATTR_CRM_VERSION, CRM_FEATURE_SET); + + tmp1 = subtract_xml_object(removed, old, new, FALSE, NULL, "removed:top"); + if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) { + free_xml(tmp1); + } + + tmp1 = subtract_xml_object(added, new, old, TRUE, NULL, "added:top"); + if (suppress && (tmp1 != NULL) && can_prune_leaf(tmp1)) { + free_xml(tmp1); + } + + if ((added->children == NULL) && (removed->children == NULL)) { + free_xml(diff); + diff = NULL; + } + + return diff; +} + +static xmlNode * +subtract_xml_comment(xmlNode *parent, xmlNode *left, xmlNode *right, + gboolean *changed) +{ + CRM_CHECK(left != NULL, return NULL); + CRM_CHECK(left->type == XML_COMMENT_NODE, return NULL); + + if ((right == NULL) || !pcmk__str_eq((const char *)left->content, + (const char *)right->content, + pcmk__str_casei)) { + xmlNode *deleted = NULL; + + deleted = add_node_copy(parent, left); + *changed = TRUE; + + return deleted; + } + + return NULL; +} + +xmlNode * +subtract_xml_object(xmlNode *parent, xmlNode *left, xmlNode *right, + gboolean full, gboolean *changed, const char *marker) +{ + gboolean dummy = FALSE; + xmlNode *diff = NULL; + xmlNode *right_child = NULL; + xmlNode *left_child = NULL; + xmlAttrPtr xIter = NULL; + + const char *id = NULL; + const char *name = NULL; + const char *value = NULL; + const char *right_val = NULL; + + if (changed == NULL) { + changed = &dummy; + } + + if (left == NULL) { + return NULL; + } + + if (left->type == XML_COMMENT_NODE) { + return subtract_xml_comment(parent, left, right, changed); + } + + id = ID(left); + if (right == NULL) { + xmlNode *deleted = NULL; + + crm_trace("Processing <%s " XML_ATTR_ID "=%s> (complete copy)", + crm_element_name(left), id); + deleted = add_node_copy(parent, left); + crm_xml_add(deleted, XML_DIFF_MARKER, marker); + + *changed = TRUE; + return deleted; + } + + name = crm_element_name(left); + CRM_CHECK(name != NULL, return NULL); + CRM_CHECK(pcmk__str_eq(crm_element_name(left), crm_element_name(right), + pcmk__str_casei), + return NULL); + + // Check for XML_DIFF_MARKER in a child + value = crm_element_value(right, XML_DIFF_MARKER); + if ((value != NULL) && (strcmp(value, "removed:top") == 0)) { + crm_trace("We are the root of the deletion: %s.id=%s", name, id); + *changed = TRUE; + return NULL; + } + + // @TODO Avoiding creating the full hierarchy would save work here + diff = create_xml_node(parent, name); + + // Changes to child objects + for (left_child = pcmk__xml_first_child(left); left_child != NULL; + left_child = pcmk__xml_next(left_child)) { + gboolean child_changed = FALSE; + + right_child = pcmk__xml_match(right, left_child, false); + subtract_xml_object(diff, left_child, right_child, full, &child_changed, + marker); + if (child_changed) { + *changed = TRUE; + } + } + + if (!*changed) { + /* Nothing to do */ + + } else if (full) { + xmlAttrPtr pIter = NULL; + + for (pIter = pcmk__xe_first_attr(left); pIter != NULL; + pIter = pIter->next) { + const char *p_name = (const char *)pIter->name; + const char *p_value = pcmk__xml_attr_value(pIter); + + xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); + } + + // We have everything we need + goto done; + } + + // Changes to name/value pairs + for (xIter = pcmk__xe_first_attr(left); xIter != NULL; + xIter = xIter->next) { + const char *prop_name = (const char *) xIter->name; + xmlAttrPtr right_attr = NULL; + xml_node_private_t *nodepriv = NULL; + + if (strcmp(prop_name, XML_ATTR_ID) == 0) { + // id already obtained when present ~ this case, so just reuse + xmlSetProp(diff, (pcmkXmlStr) XML_ATTR_ID, (pcmkXmlStr) id); + continue; + } + + if (pcmk__xa_filterable(prop_name)) { + continue; + } + + right_attr = xmlHasProp(right, (pcmkXmlStr) prop_name); + if (right_attr) { + nodepriv = right_attr->_private; + } + + right_val = crm_element_value(right, prop_name); + if ((right_val == NULL) || (nodepriv && pcmk_is_set(nodepriv->flags, pcmk__xf_deleted))) { + /* new */ + *changed = TRUE; + if (full) { + xmlAttrPtr pIter = NULL; + + for (pIter = pcmk__xe_first_attr(left); pIter != NULL; + pIter = pIter->next) { + const char *p_name = (const char *) pIter->name; + const char *p_value = pcmk__xml_attr_value(pIter); + + xmlSetProp(diff, (pcmkXmlStr) p_name, (pcmkXmlStr) p_value); + } + break; + + } else { + const char *left_value = crm_element_value(left, prop_name); + + xmlSetProp(diff, (pcmkXmlStr) prop_name, (pcmkXmlStr) value); + crm_xml_add(diff, prop_name, left_value); + } + + } else { + /* Only now do we need the left value */ + const char *left_value = crm_element_value(left, prop_name); + + if (strcmp(left_value, right_val) == 0) { + /* unchanged */ + + } else { + *changed = TRUE; + if (full) { + xmlAttrPtr pIter = NULL; + + crm_trace("Changes detected to %s in " + "<%s " XML_ATTR_ID "=%s>", + prop_name, crm_element_name(left), id); + for (pIter = pcmk__xe_first_attr(left); pIter != NULL; + pIter = pIter->next) { + const char *p_name = (const char *) pIter->name; + const char *p_value = pcmk__xml_attr_value(pIter); + + xmlSetProp(diff, (pcmkXmlStr) p_name, + (pcmkXmlStr) p_value); + } + break; + + } else { + crm_trace("Changes detected to %s (%s -> %s) in " + "<%s " XML_ATTR_ID "=%s>", + prop_name, left_value, right_val, + crm_element_name(left), id); + crm_xml_add(diff, prop_name, left_value); + } + } + } + } + + if (!*changed) { + free_xml(diff); + return NULL; + + } else if (!full && (id != NULL)) { + crm_xml_add(diff, XML_ATTR_ID, id); + } + done: + return diff; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +gboolean +apply_xml_diff(xmlNode *old_xml, xmlNode *diff, xmlNode **new_xml) +{ + gboolean result = TRUE; + int root_nodes_seen = 0; + const char *digest = crm_element_value(diff, XML_ATTR_DIGEST); + const char *version = crm_element_value(diff, XML_ATTR_CRM_VERSION); + + xmlNode *child_diff = NULL; + xmlNode *added = find_xml_node(diff, "diff-added", FALSE); + xmlNode *removed = find_xml_node(diff, "diff-removed", FALSE); + + CRM_CHECK(new_xml != NULL, return FALSE); + + crm_trace("Subtraction Phase"); + for (child_diff = pcmk__xml_first_child(removed); child_diff != NULL; + child_diff = pcmk__xml_next(child_diff)) { + CRM_CHECK(root_nodes_seen == 0, result = FALSE); + if (root_nodes_seen == 0) { + *new_xml = subtract_xml_object(NULL, old_xml, child_diff, FALSE, + NULL, NULL); + } + root_nodes_seen++; + } + + if (root_nodes_seen == 0) { + *new_xml = copy_xml(old_xml); + + } else if (root_nodes_seen > 1) { + crm_err("(-) Diffs cannot contain more than one change set... saw %d", + root_nodes_seen); + result = FALSE; + } + + root_nodes_seen = 0; + crm_trace("Addition Phase"); + if (result) { + xmlNode *child_diff = NULL; + + for (child_diff = pcmk__xml_first_child(added); child_diff != NULL; + child_diff = pcmk__xml_next(child_diff)) { + CRM_CHECK(root_nodes_seen == 0, result = FALSE); + if (root_nodes_seen == 0) { + pcmk__xml_update(NULL, *new_xml, child_diff, true); + } + root_nodes_seen++; + } + } + + if (root_nodes_seen > 1) { + crm_err("(+) Diffs cannot contain more than one change set... saw %d", + root_nodes_seen); + result = FALSE; + + } else if (result && (digest != NULL)) { + char *new_digest = NULL; + + purge_diff_markers(*new_xml); // Purge now so diff is ok + new_digest = calculate_xml_versioned_digest(*new_xml, FALSE, TRUE, + version); + if (!pcmk__str_eq(new_digest, digest, pcmk__str_casei)) { + crm_info("Digest mis-match: expected %s, calculated %s", + digest, new_digest); + result = FALSE; + + pcmk__if_tracing( + { + save_xml_to_file(old_xml, "diff:original", NULL); + save_xml_to_file(diff, "diff:input", NULL); + save_xml_to_file(*new_xml, "diff:new", NULL); + }, + {} + ); + + } else { + crm_trace("Digest matched: expected %s, calculated %s", + digest, new_digest); + } + free(new_digest); + + } else if (result) { + purge_diff_markers(*new_xml); // Purge now so diff is ok + } + + return result; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/patchset_display.c b/lib/common/patchset_display.c new file mode 100644 index 0000000..731d437 --- /dev/null +++ b/lib/common/patchset_display.c @@ -0,0 +1,519 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include "crmcommon_private.h" + +/*! + * \internal + * \brief Output an XML patchset header + * + * This function parses a header from an XML patchset (an \p XML_ATTR_DIFF + * element and its children). + * + * All header lines contain three integers separated by dots, of the form + * {0}.{1}.{2}: + * * \p {0}: \p XML_ATTR_GENERATION_ADMIN + * * \p {1}: \p XML_ATTR_GENERATION + * * \p {2}: \p XML_ATTR_NUMUPDATES + * + * Lines containing \p "---" describe removals and end with the patch format + * number. Lines containing \p "+++" describe additions and end with the patch + * digest. + * + * \param[in,out] out Output object + * \param[in] patchset XML patchset to output + * + * \return Standard Pacemaker return code + * + * \note This function produces output only for text-like formats. + */ +static int +xml_show_patchset_header(pcmk__output_t *out, const xmlNode *patchset) +{ + int rc = pcmk_rc_no_output; + int add[] = { 0, 0, 0 }; + int del[] = { 0, 0, 0 }; + + xml_patch_versions(patchset, add, del); + + if ((add[0] != del[0]) || (add[1] != del[1]) || (add[2] != del[2])) { + const char *fmt = crm_element_value(patchset, "format"); + const char *digest = crm_element_value(patchset, XML_ATTR_DIGEST); + + out->info(out, "Diff: --- %d.%d.%d %s", del[0], del[1], del[2], fmt); + rc = out->info(out, "Diff: +++ %d.%d.%d %s", + add[0], add[1], add[2], digest); + + } else if ((add[0] != 0) || (add[1] != 0) || (add[2] != 0)) { + rc = out->info(out, "Local-only Change: %d.%d.%d", + add[0], add[1], add[2]); + } + + return rc; +} + +/*! + * \internal + * \brief Output a user-friendly form of XML additions or removals + * + * \param[in,out] out Output object + * \param[in] prefix String to prepend to every line of output + * \param[in] data XML node to output + * \param[in] depth Current indentation level + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This function produces output only for text-like formats. + */ +static int +xml_show_patchset_v1_recursive(pcmk__output_t *out, const char *prefix, + const xmlNode *data, int depth, uint32_t options) +{ + if (!xml_has_children(data) + || (crm_element_value(data, XML_DIFF_MARKER) != NULL)) { + + // Found a change; clear the pcmk__xml_fmt_diff_short option if set + options &= ~pcmk__xml_fmt_diff_short; + + if (pcmk_is_set(options, pcmk__xml_fmt_diff_plus)) { + prefix = PCMK__XML_PREFIX_CREATED; + } else { // pcmk_is_set(options, pcmk__xml_fmt_diff_minus) + prefix = PCMK__XML_PREFIX_DELETED; + } + } + + if (pcmk_is_set(options, pcmk__xml_fmt_diff_short)) { + int rc = pcmk_rc_no_output; + + // Keep looking for the actual change + for (const xmlNode *child = pcmk__xml_first_child(data); child != NULL; + child = pcmk__xml_next(child)) { + int temp_rc = xml_show_patchset_v1_recursive(out, prefix, child, + depth + 1, options); + rc = pcmk__output_select_rc(rc, temp_rc); + } + return rc; + } + + return pcmk__xml_show(out, prefix, data, depth, + options + |pcmk__xml_fmt_open + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); +} + +/*! + * \internal + * \brief Output a user-friendly form of an XML patchset (format 1) + * + * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its + * children) into a user-friendly combined diff output. + * + * \param[in,out] out Output object + * \param[in] patchset XML patchset to output + * \param[in] options Group of \p pcmk__xml_fmt_options flags + * + * \return Standard Pacemaker return code + * + * \note This function produces output only for text-like formats. + */ +static int +xml_show_patchset_v1(pcmk__output_t *out, const xmlNode *patchset, + uint32_t options) +{ + const xmlNode *removed = NULL; + const xmlNode *added = NULL; + const xmlNode *child = NULL; + bool is_first = true; + int rc = xml_show_patchset_header(out, patchset); + + /* It's not clear whether "- " or "+ " ever does *not* get overridden by + * PCMK__XML_PREFIX_DELETED or PCMK__XML_PREFIX_CREATED in practice. + * However, v1 patchsets can only exist during rolling upgrades from + * Pacemaker 1.1.11, so not worth worrying about. + */ + removed = find_xml_node(patchset, "diff-removed", FALSE); + for (child = pcmk__xml_first_child(removed); child != NULL; + child = pcmk__xml_next(child)) { + int temp_rc = xml_show_patchset_v1_recursive(out, "- ", child, 0, + options + |pcmk__xml_fmt_diff_minus); + rc = pcmk__output_select_rc(rc, temp_rc); + + if (is_first) { + is_first = false; + } else { + rc = pcmk__output_select_rc(rc, out->info(out, " --- ")); + } + } + + is_first = true; + added = find_xml_node(patchset, "diff-added", FALSE); + for (child = pcmk__xml_first_child(added); child != NULL; + child = pcmk__xml_next(child)) { + int temp_rc = xml_show_patchset_v1_recursive(out, "+ ", child, 0, + options + |pcmk__xml_fmt_diff_plus); + rc = pcmk__output_select_rc(rc, temp_rc); + + if (is_first) { + is_first = false; + } else { + rc = pcmk__output_select_rc(rc, out->info(out, " +++ ")); + } + } + + return rc; +} + +/*! + * \internal + * \brief Output a user-friendly form of an XML patchset (format 2) + * + * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its + * children) into a user-friendly combined diff output. + * + * \param[in,out] out Output object + * \param[in] patchset XML patchset to output + * + * \return Standard Pacemaker return code + * + * \note This function produces output only for text-like formats. + */ +static int +xml_show_patchset_v2(pcmk__output_t *out, const xmlNode *patchset) +{ + int rc = xml_show_patchset_header(out, patchset); + int temp_rc = pcmk_rc_no_output; + + for (const xmlNode *change = pcmk__xml_first_child(patchset); + change != NULL; change = pcmk__xml_next(change)) { + const char *op = crm_element_value(change, XML_DIFF_OP); + const char *xpath = crm_element_value(change, XML_DIFF_PATH); + + if (op == NULL) { + continue; + } + + if (strcmp(op, "create") == 0) { + char *prefix = crm_strdup_printf(PCMK__XML_PREFIX_CREATED " %s: ", + xpath); + + temp_rc = pcmk__xml_show(out, prefix, change->children, 0, + pcmk__xml_fmt_pretty|pcmk__xml_fmt_open); + rc = pcmk__output_select_rc(rc, temp_rc); + + // Overwrite all except the first two characters with spaces + for (char *ch = prefix + 2; *ch != '\0'; ch++) { + *ch = ' '; + } + + temp_rc = pcmk__xml_show(out, prefix, change->children, 0, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_children + |pcmk__xml_fmt_close); + rc = pcmk__output_select_rc(rc, temp_rc); + free(prefix); + + } else if (strcmp(op, "move") == 0) { + const char *position = crm_element_value(change, XML_DIFF_POSITION); + + temp_rc = out->info(out, + PCMK__XML_PREFIX_MOVED " %s moved to offset %s", + xpath, position); + rc = pcmk__output_select_rc(rc, temp_rc); + + } else if (strcmp(op, "modify") == 0) { + xmlNode *clist = first_named_child(change, XML_DIFF_LIST); + GString *buffer_set = NULL; + GString *buffer_unset = NULL; + + for (const xmlNode *child = pcmk__xml_first_child(clist); + child != NULL; child = pcmk__xml_next(child)) { + const char *name = crm_element_value(child, "name"); + + op = crm_element_value(child, XML_DIFF_OP); + if (op == NULL) { + continue; + } + + if (strcmp(op, "set") == 0) { + const char *value = crm_element_value(child, "value"); + + pcmk__add_separated_word(&buffer_set, 256, "@", ", "); + pcmk__g_strcat(buffer_set, name, "=", value, NULL); + + } else if (strcmp(op, "unset") == 0) { + pcmk__add_separated_word(&buffer_unset, 256, "@", ", "); + g_string_append(buffer_unset, name); + } + } + + if (buffer_set != NULL) { + temp_rc = out->info(out, "+ %s: %s", xpath, buffer_set->str); + rc = pcmk__output_select_rc(rc, temp_rc); + g_string_free(buffer_set, TRUE); + } + + if (buffer_unset != NULL) { + temp_rc = out->info(out, "-- %s: %s", + xpath, buffer_unset->str); + rc = pcmk__output_select_rc(rc, temp_rc); + g_string_free(buffer_unset, TRUE); + } + + } else if (strcmp(op, "delete") == 0) { + int position = -1; + + crm_element_value_int(change, XML_DIFF_POSITION, &position); + if (position >= 0) { + temp_rc = out->info(out, "-- %s (%d)", xpath, position); + } else { + temp_rc = out->info(out, "-- %s", xpath); + } + rc = pcmk__output_select_rc(rc, temp_rc); + } + } + + return rc; +} + +/*! + * \internal + * \brief Output a user-friendly form of an XML patchset + * + * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its + * children) into a user-friendly combined diff output. + * + * \param[in,out] out Output object + * \param[in] args Message-specific arguments + * + * \return Standard Pacemaker return code + * + * \note \p args should contain only the XML patchset + */ +PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr") +static int +xml_patchset_default(pcmk__output_t *out, va_list args) +{ + xmlNodePtr patchset = va_arg(args, xmlNodePtr); + + int format = 1; + + if (patchset == NULL) { + crm_trace("Empty patch"); + return pcmk_rc_no_output; + } + + crm_element_value_int(patchset, "format", &format); + switch (format) { + case 1: + return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); + case 2: + return xml_show_patchset_v2(out, patchset); + default: + crm_err("Unknown patch format: %d", format); + return pcmk_rc_bad_xml_patch; + } +} + +/*! + * \internal + * \brief Output a user-friendly form of an XML patchset + * + * This function parses an XML patchset (an \p XML_ATTR_DIFF element and its + * children) into a user-friendly combined diff output. + * + * \param[in,out] out Output object + * \param[in] args Message-specific arguments + * + * \return Standard Pacemaker return code + * + * \note \p args should contain only the XML patchset + */ +PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr") +static int +xml_patchset_log(pcmk__output_t *out, va_list args) +{ + static struct qb_log_callsite *patchset_cs = NULL; + + xmlNodePtr patchset = va_arg(args, xmlNodePtr); + + uint8_t log_level = pcmk__output_get_log_level(out); + int format = 1; + + if (log_level == LOG_NEVER) { + return pcmk_rc_no_output; + } + + if (patchset == NULL) { + crm_trace("Empty patch"); + return pcmk_rc_no_output; + } + + if (patchset_cs == NULL) { + patchset_cs = qb_log_callsite_get(__func__, __FILE__, "xml-patchset", + log_level, __LINE__, + crm_trace_nonlog); + } + + if (!crm_is_callsite_active(patchset_cs, log_level, crm_trace_nonlog)) { + // Nothing would be logged, so skip all the work + return pcmk_rc_no_output; + } + + crm_element_value_int(patchset, "format", &format); + switch (format) { + case 1: + if (log_level < LOG_DEBUG) { + return xml_show_patchset_v1(out, patchset, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_diff_short); + } + return xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); + case 2: + return xml_show_patchset_v2(out, patchset); + default: + crm_err("Unknown patch format: %d", format); + return pcmk_rc_bad_xml_patch; + } +} + +/*! + * \internal + * \brief Output an XML patchset + * + * This function outputs an XML patchset (an \p XML_ATTR_DIFF element and its + * children) without modification, as a CDATA block. + * + * \param[in,out] out Output object + * \param[in] args Message-specific arguments + * + * \return Standard Pacemaker return code + * + * \note \p args should contain only the XML patchset + */ +PCMK__OUTPUT_ARGS("xml-patchset", "xmlNodePtr") +static int +xml_patchset_xml(pcmk__output_t *out, va_list args) +{ + xmlNodePtr patchset = va_arg(args, xmlNodePtr); + + if (patchset != NULL) { + char *buf = dump_xml_formatted_with_text(patchset); + + out->output_xml(out, "xml-patchset", buf); + free(buf); + return pcmk_rc_ok; + } + crm_trace("Empty patch"); + return pcmk_rc_no_output; +} + +static pcmk__message_entry_t fmt_functions[] = { + { "xml-patchset", "default", xml_patchset_default }, + { "xml-patchset", "log", xml_patchset_log }, + { "xml-patchset", "xml", xml_patchset_xml }, + + { NULL, NULL, NULL } +}; + +/*! + * \internal + * \brief Register the formatting functions for XML patchsets + * + * \param[in,out] out Output object + */ +void +pcmk__register_patchset_messages(pcmk__output_t *out) { + pcmk__register_messages(out, fmt_functions); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +void +xml_log_patchset(uint8_t log_level, const char *function, + const xmlNode *patchset) +{ + /* This function has some duplication relative to the message functions. + * This way, we can maintain the const xmlNode * in the signature. The + * message functions must be non-const. They have to support XML output + * objects, which must make a copy of a the patchset, requiring a non-const + * function call. + * + * In contrast, this legacy function doesn't need to support XML output. + */ + static struct qb_log_callsite *patchset_cs = NULL; + + pcmk__output_t *out = NULL; + int format = 1; + int rc = pcmk_rc_no_output; + + switch (log_level) { + case LOG_NEVER: + return; + case LOG_STDOUT: + CRM_CHECK(pcmk__text_output_new(&out, NULL) == pcmk_rc_ok, return); + break; + default: + if (patchset_cs == NULL) { + patchset_cs = qb_log_callsite_get(__func__, __FILE__, + "xml-patchset", log_level, + __LINE__, crm_trace_nonlog); + } + if (!crm_is_callsite_active(patchset_cs, log_level, + crm_trace_nonlog)) { + return; + } + CRM_CHECK(pcmk__log_output_new(&out) == pcmk_rc_ok, return); + pcmk__output_set_log_level(out, log_level); + break; + } + + if (patchset == NULL) { + // Should come after the LOG_NEVER check + crm_trace("Empty patch"); + goto done; + } + + crm_element_value_int(patchset, "format", &format); + switch (format) { + case 1: + if (log_level < LOG_DEBUG) { + rc = xml_show_patchset_v1(out, patchset, + pcmk__xml_fmt_pretty + |pcmk__xml_fmt_diff_short); + } else { // Note: LOG_STDOUT > LOG_DEBUG + rc = xml_show_patchset_v1(out, patchset, pcmk__xml_fmt_pretty); + } + break; + case 2: + rc = xml_show_patchset_v2(out, patchset); + break; + default: + crm_err("Unknown patch format: %d", format); + rc = pcmk_rc_bad_xml_patch; + break; + } + +done: + out->finish(out, pcmk_rc2exitc(rc), true, NULL); + pcmk__output_free(out); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/pid.c b/lib/common/pid.c new file mode 100644 index 0000000..bb53153 --- /dev/null +++ b/lib/common/pid.c @@ -0,0 +1,247 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include + +#include + +int +pcmk__pid_active(pid_t pid, const char *daemon) +{ + static pid_t last_asked_pid = 0; /* log spam prevention */ + int rc = 0; + + if (pid <= 0) { + return EINVAL; + } + + rc = kill(pid, 0); + if ((rc < 0) && (errno == ESRCH)) { + return ESRCH; /* no such PID detected */ + + } else if ((daemon == NULL) || !pcmk__procfs_has_pids()) { + // The kill result is all we have, we can't check the name + + if (rc == 0) { + return pcmk_rc_ok; + } + rc = errno; + if (last_asked_pid != pid) { + crm_info("Cannot examine PID %lld: %s", + (long long) pid, pcmk_rc_str(rc)); + last_asked_pid = pid; + } + return rc; /* errno != ESRCH */ + + } else { + /* make sure PID hasn't been reused by another process + XXX: might still be just a zombie, which could confuse decisions */ + bool checked_through_kill = (rc == 0); + char exe_path[PATH_MAX], myexe_path[PATH_MAX]; + + rc = pcmk__procfs_pid2path(pid, exe_path, sizeof(exe_path)); + if (rc != pcmk_rc_ok) { + if (rc != EACCES) { + // Check again to filter out races + if ((kill(pid, 0) < 0) && (errno == ESRCH)) { + return ESRCH; + } + } + if (last_asked_pid != pid) { + if (rc == EACCES) { + crm_info("Could not get executable for PID %lld: %s " + CRM_XS " rc=%d", + (long long) pid, pcmk_rc_str(rc), rc); + } else { + crm_err("Could not get executable for PID %lld: %s " + CRM_XS " rc=%d", + (long long) pid, pcmk_rc_str(rc), rc); + } + last_asked_pid = pid; + } + if (rc == EACCES) { + // Trust kill if it was OK (we can't double-check via path) + return checked_through_kill? pcmk_rc_ok : EACCES; + } else { + return ESRCH; /* most likely errno == ENOENT */ + } + } + + if (daemon[0] != '/') { + rc = snprintf(myexe_path, sizeof(myexe_path), CRM_DAEMON_DIR"/%s", + daemon); + } else { + rc = snprintf(myexe_path, sizeof(myexe_path), "%s", daemon); + } + + if (rc > 0 && rc < sizeof(myexe_path) && !strcmp(exe_path, myexe_path)) { + return pcmk_rc_ok; + } + } + + return ESRCH; +} + +#define LOCKSTRLEN 11 + +/*! + * \internal + * \brief Read a process ID from a file + * + * \param[in] filename Process ID file to read + * \param[out] pid Where to put PID that was read + * + * \return Standard Pacemaker return code + */ +int +pcmk__read_pidfile(const char *filename, pid_t *pid) +{ + int fd; + struct stat sbuf; + int rc = pcmk_rc_ok; + long long pid_read = 0; + char buf[LOCKSTRLEN + 1]; + + CRM_CHECK((filename != NULL) && (pid != NULL), return EINVAL); + + fd = open(filename, O_RDONLY); + if (fd < 0) { + return errno; + } + + if ((fstat(fd, &sbuf) >= 0) && (sbuf.st_size < LOCKSTRLEN)) { + sleep(2); /* if someone was about to create one, + * give'm a sec to do so + */ + } + + if (read(fd, buf, sizeof(buf)) < 1) { + rc = errno; + goto bail; + } + + errno = 0; + rc = sscanf(buf, "%lld", &pid_read); + + if (rc > 0) { + if (pid_read <= 0) { + rc = ESRCH; + } else { + rc = pcmk_rc_ok; + *pid = (pid_t) pid_read; + crm_trace("Read pid %lld from %s", pid_read, filename); + } + } else if (rc == 0) { + rc = ENODATA; + } else { + rc = errno; + } + + bail: + close(fd); + return rc; +} + +/*! + * \internal + * \brief Check whether a process from a PID file matches expected values + * + * \param[in] filename Path of PID file + * \param[in] expected_pid If positive, compare to this PID + * \param[in] expected_name If not NULL, the PID from the PID file is valid + * only if it is active as a process with this name + * \param[out] pid If not NULL, store PID found in PID file here + * + * \return Standard Pacemaker return code + */ +int +pcmk__pidfile_matches(const char *filename, pid_t expected_pid, + const char *expected_name, pid_t *pid) +{ + pid_t pidfile_pid = 0; + int rc = pcmk__read_pidfile(filename, &pidfile_pid); + + if (pid) { + *pid = pidfile_pid; + } + + if (rc != pcmk_rc_ok) { + // Error reading PID file or invalid contents + unlink(filename); + rc = ENOENT; + + } else if ((expected_pid > 0) && (pidfile_pid == expected_pid)) { + // PID in file matches what was expected + rc = pcmk_rc_ok; + + } else if (pcmk__pid_active(pidfile_pid, expected_name) == ESRCH) { + // Contains a stale value + unlink(filename); + rc = ENOENT; + + } else if ((expected_pid > 0) && (pidfile_pid != expected_pid)) { + // Locked by existing process + rc = EEXIST; + } + + return rc; +} + +/*! + * \internal + * \brief Create a PID file for the current process (if not already existent) + * + * \param[in] filename Name of PID file to create + * \param[in] name Name of current process + * + * \return Standard Pacemaker return code + */ +int +pcmk__lock_pidfile(const char *filename, const char *name) +{ + pid_t mypid = getpid(); + int fd = 0; + int rc = 0; + char buf[LOCKSTRLEN + 2]; + + rc = pcmk__pidfile_matches(filename, 0, name, NULL); + if ((rc != pcmk_rc_ok) && (rc != ENOENT)) { + // Locked by existing process + return rc; + } + + fd = open(filename, O_CREAT | O_WRONLY | O_EXCL, 0644); + if (fd < 0) { + return errno; + } + + snprintf(buf, sizeof(buf), "%*lld\n", LOCKSTRLEN - 1, (long long) mypid); + rc = write(fd, buf, LOCKSTRLEN); + close(fd); + + if (rc != LOCKSTRLEN) { + crm_perror(LOG_ERR, "Incomplete write to %s", filename); + return errno; + } + + rc = pcmk__pidfile_matches(filename, mypid, name, NULL); + if (rc != pcmk_rc_ok) { + // Something is really wrong -- maybe I/O error on read back? + unlink(filename); + } + return rc; +} diff --git a/lib/common/procfs.c b/lib/common/procfs.c new file mode 100644 index 0000000..8179cc9 --- /dev/null +++ b/lib/common/procfs.c @@ -0,0 +1,227 @@ +/* + * Copyright 2015-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include + +/*! + * \internal + * \brief Get process ID and name associated with a /proc directory entry + * + * \param[in] entry Directory entry (must be result of readdir() on /proc) + * \param[out] name If not NULL, a char[16] to hold the process name + * \param[out] pid If not NULL, will be set to process ID of entry + * + * \return Standard Pacemaker return code + * \note This should be called only on Linux systems, as not all systems that + * support /proc store process names and IDs in the same way. The kernel + * limits the process name to the first 15 characters (plus terminator). + * It would be nice if there were a public kernel API constant for that + * limit, but there isn't. + */ +static int +pcmk__procfs_process_info(const struct dirent *entry, char *name, pid_t *pid) +{ + int fd, local_pid; + FILE *file; + struct stat statbuf; + char procpath[128] = { 0 }; + + /* We're only interested in entries whose name is a PID, + * so skip anything non-numeric or that is too long. + * + * 114 = 128 - strlen("/proc/") - strlen("/status") - 1 + */ + local_pid = atoi(entry->d_name); + if ((local_pid <= 0) || (strlen(entry->d_name) > 114)) { + return -1; + } + if (pid) { + *pid = (pid_t) local_pid; + } + + /* Get this entry's file information */ + strcpy(procpath, "/proc/"); + strcat(procpath, entry->d_name); + fd = open(procpath, O_RDONLY); + if (fd < 0 ) { + return -1; + } + if (fstat(fd, &statbuf) < 0) { + close(fd); + return -1; + } + close(fd); + + /* We're only interested in subdirectories */ + if (!S_ISDIR(statbuf.st_mode)) { + return -1; + } + + /* Read the first entry ("Name:") from the process's status file. + * We could handle the valgrind case if we parsed the cmdline file + * instead, but that's more of a pain than it's worth. + */ + if (name != NULL) { + strcat(procpath, "/status"); + file = fopen(procpath, "r"); + if (!file) { + return -1; + } + if (fscanf(file, "Name:\t%15[^\n]", name) != 1) { + fclose(file); + return -1; + } + name[15] = 0; + fclose(file); + } + + return 0; +} + +/*! + * \internal + * \brief Return process ID of a named process + * + * \param[in] name Process name (as used in /proc/.../status) + * + * \return Process ID of named process if running, 0 otherwise + * + * \note This will return 0 if the process is being run via valgrind. + * This should be called only on Linux systems. + */ +pid_t +pcmk__procfs_pid_of(const char *name) +{ + DIR *dp; + struct dirent *entry; + pid_t pid = 0; + char entry_name[64] = { 0 }; + + dp = opendir("/proc"); + if (dp == NULL) { + crm_notice("Can not read /proc directory to track existing components"); + return 0; + } + + while ((entry = readdir(dp)) != NULL) { + if ((pcmk__procfs_process_info(entry, entry_name, &pid) == pcmk_rc_ok) + && pcmk__str_eq(entry_name, name, pcmk__str_casei) + && (pcmk__pid_active(pid, NULL) == pcmk_rc_ok)) { + + crm_info("Found %s active as process %lld", name, (long long) pid); + break; + } + pid = 0; + } + closedir(dp); + return pid; +} + +/*! + * \internal + * \brief Calculate number of logical CPU cores from procfs + * + * \return Number of cores (or 1 if unable to determine) + */ +unsigned int +pcmk__procfs_num_cores(void) +{ + int cores = 0; + FILE *stream = NULL; + + /* Parse /proc/stat instead of /proc/cpuinfo because it's smaller */ + stream = fopen("/proc/stat", "r"); + if (stream == NULL) { + crm_perror(LOG_INFO, "Could not open /proc/stat"); + } else { + char buffer[2048]; + + while (fgets(buffer, sizeof(buffer), stream)) { + if (pcmk__starts_with(buffer, "cpu") && isdigit(buffer[3])) { + ++cores; + } + } + fclose(stream); + } + return cores? cores : 1; +} + +/*! + * \internal + * \brief Get the executable path corresponding to a process ID + * + * \param[in] pid Process ID to check + * \param[out] path Where to store executable path + * \param[in] path_size Size of \p path in characters (ideally PATH_MAX) + * + * \return Standard Pacemaker error code (as possible errno values from + * readlink()) + */ +int +pcmk__procfs_pid2path(pid_t pid, char path[], size_t path_size) +{ +#if HAVE_LINUX_PROCFS + char procfs_exe_path[PATH_MAX]; + ssize_t link_rc; + + if (snprintf(procfs_exe_path, PATH_MAX, "/proc/%lld/exe", + (long long) pid) >= PATH_MAX) { + return ENAMETOOLONG; // Truncated (shouldn't be possible in practice) + } + + link_rc = readlink(procfs_exe_path, path, path_size - 1); + if (link_rc < 0) { + return errno; + } else if (link_rc >= (path_size - 1)) { + return ENAMETOOLONG; + } + + path[link_rc] = '\0'; + return pcmk_rc_ok; +#else + return EOPNOTSUPP; +#endif // HAVE_LINUX_PROCFS +} + +/*! + * \internal + * \brief Check whether process ID information is available from procfs + * + * \return true if process ID information is available, otherwise false + */ +bool +pcmk__procfs_has_pids(void) +{ +#if HAVE_LINUX_PROCFS + static bool have_pids = false; + static bool checked = false; + + if (!checked) { + char path[PATH_MAX]; + + have_pids = pcmk__procfs_pid2path(getpid(), path, sizeof(path)) == pcmk_rc_ok; + checked = true; + } + return have_pids; +#else + return false; +#endif // HAVE_LINUX_PROCFS +} diff --git a/lib/common/remote.c b/lib/common/remote.c new file mode 100644 index 0000000..8c5969a --- /dev/null +++ b/lib/common/remote.c @@ -0,0 +1,1270 @@ +/* + * Copyright 2008-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // PRIx32 + +#include +#include + +#include +#include +#include +#include + +#ifdef HAVE_GNUTLS_GNUTLS_H +# include +#endif + +/* Swab macros from linux/swab.h */ +#ifdef HAVE_LINUX_SWAB_H +# include +#else +/* + * casts are necessary for constants, because we never know how for sure + * how U/UL/ULL map to __u16, __u32, __u64. At least not in a portable way. + */ +#define __swab16(x) ((uint16_t)( \ + (((uint16_t)(x) & (uint16_t)0x00ffU) << 8) | \ + (((uint16_t)(x) & (uint16_t)0xff00U) >> 8))) + +#define __swab32(x) ((uint32_t)( \ + (((uint32_t)(x) & (uint32_t)0x000000ffUL) << 24) | \ + (((uint32_t)(x) & (uint32_t)0x0000ff00UL) << 8) | \ + (((uint32_t)(x) & (uint32_t)0x00ff0000UL) >> 8) | \ + (((uint32_t)(x) & (uint32_t)0xff000000UL) >> 24))) + +#define __swab64(x) ((uint64_t)( \ + (((uint64_t)(x) & (uint64_t)0x00000000000000ffULL) << 56) | \ + (((uint64_t)(x) & (uint64_t)0x000000000000ff00ULL) << 40) | \ + (((uint64_t)(x) & (uint64_t)0x0000000000ff0000ULL) << 24) | \ + (((uint64_t)(x) & (uint64_t)0x00000000ff000000ULL) << 8) | \ + (((uint64_t)(x) & (uint64_t)0x000000ff00000000ULL) >> 8) | \ + (((uint64_t)(x) & (uint64_t)0x0000ff0000000000ULL) >> 24) | \ + (((uint64_t)(x) & (uint64_t)0x00ff000000000000ULL) >> 40) | \ + (((uint64_t)(x) & (uint64_t)0xff00000000000000ULL) >> 56))) +#endif + +#define REMOTE_MSG_VERSION 1 +#define ENDIAN_LOCAL 0xBADADBBD + +struct remote_header_v0 { + uint32_t endian; /* Detect messages from hosts with different endian-ness */ + uint32_t version; + uint64_t id; + uint64_t flags; + uint32_t size_total; + uint32_t payload_offset; + uint32_t payload_compressed; + uint32_t payload_uncompressed; + + /* New fields get added here */ + +} __attribute__ ((packed)); + +/*! + * \internal + * \brief Retrieve remote message header, in local endianness + * + * Return a pointer to the header portion of a remote connection's message + * buffer, converting the header to local endianness if needed. + * + * \param[in,out] remote Remote connection with new message + * + * \return Pointer to message header, localized if necessary + */ +static struct remote_header_v0 * +localized_remote_header(pcmk__remote_t *remote) +{ + struct remote_header_v0 *header = (struct remote_header_v0 *)remote->buffer; + if(remote->buffer_offset < sizeof(struct remote_header_v0)) { + return NULL; + + } else if(header->endian != ENDIAN_LOCAL) { + uint32_t endian = __swab32(header->endian); + + CRM_LOG_ASSERT(endian == ENDIAN_LOCAL); + if(endian != ENDIAN_LOCAL) { + crm_err("Invalid message detected, endian mismatch: %" PRIx32 + " is neither %" PRIx32 " nor the swab'd %" PRIx32, + ENDIAN_LOCAL, header->endian, endian); + return NULL; + } + + header->id = __swab64(header->id); + header->flags = __swab64(header->flags); + header->endian = __swab32(header->endian); + + header->version = __swab32(header->version); + header->size_total = __swab32(header->size_total); + header->payload_offset = __swab32(header->payload_offset); + header->payload_compressed = __swab32(header->payload_compressed); + header->payload_uncompressed = __swab32(header->payload_uncompressed); + } + + return header; +} + +#ifdef HAVE_GNUTLS_GNUTLS_H + +int +pcmk__tls_client_handshake(pcmk__remote_t *remote, int timeout_ms) +{ + int rc = 0; + int pollrc = 0; + time_t time_limit = time(NULL) + timeout_ms / 1000; + + do { + rc = gnutls_handshake(*remote->tls_session); + if ((rc == GNUTLS_E_INTERRUPTED) || (rc == GNUTLS_E_AGAIN)) { + pollrc = pcmk__remote_ready(remote, 1000); + if ((pollrc != pcmk_rc_ok) && (pollrc != ETIME)) { + /* poll returned error, there is no hope */ + crm_trace("TLS handshake poll failed: %s (%d)", + pcmk_strerror(pollrc), pollrc); + return pcmk_legacy2rc(pollrc); + } + } else if (rc < 0) { + crm_trace("TLS handshake failed: %s (%d)", + gnutls_strerror(rc), rc); + return EPROTO; + } else { + return pcmk_rc_ok; + } + } while (time(NULL) < time_limit); + return ETIME; +} + +/*! + * \internal + * \brief Set minimum prime size required by TLS client + * + * \param[in] session TLS session to affect + */ +static void +set_minimum_dh_bits(const gnutls_session_t *session) +{ + int dh_min_bits; + + pcmk__scan_min_int(getenv("PCMK_dh_min_bits"), &dh_min_bits, 0); + + /* This function is deprecated since GnuTLS 3.1.7, in favor of letting + * the priority string imply the DH requirements, but this is the only + * way to give the user control over compatibility with older servers. + */ + if (dh_min_bits > 0) { + crm_info("Requiring server use a Diffie-Hellman prime of at least %d bits", + dh_min_bits); + gnutls_dh_set_prime_bits(*session, dh_min_bits); + } +} + +static unsigned int +get_bound_dh_bits(unsigned int dh_bits) +{ + int dh_min_bits; + int dh_max_bits; + + pcmk__scan_min_int(getenv("PCMK_dh_min_bits"), &dh_min_bits, 0); + pcmk__scan_min_int(getenv("PCMK_dh_max_bits"), &dh_max_bits, 0); + if ((dh_max_bits > 0) && (dh_max_bits < dh_min_bits)) { + crm_warn("Ignoring PCMK_dh_max_bits less than PCMK_dh_min_bits"); + dh_max_bits = 0; + } + if ((dh_min_bits > 0) && (dh_bits < dh_min_bits)) { + return dh_min_bits; + } + if ((dh_max_bits > 0) && (dh_bits > dh_max_bits)) { + return dh_max_bits; + } + return dh_bits; +} + +/*! + * \internal + * \brief Initialize a new TLS session + * + * \param[in] csock Connected socket for TLS session + * \param[in] conn_type GNUTLS_SERVER or GNUTLS_CLIENT + * \param[in] cred_type GNUTLS_CRD_ANON or GNUTLS_CRD_PSK + * \param[in] credentials TLS session credentials + * + * \return Pointer to newly created session object, or NULL on error + */ +gnutls_session_t * +pcmk__new_tls_session(int csock, unsigned int conn_type, + gnutls_credentials_type_t cred_type, void *credentials) +{ + int rc = GNUTLS_E_SUCCESS; + const char *prio_base = NULL; + char *prio = NULL; + gnutls_session_t *session = NULL; + + /* Determine list of acceptable ciphers, etc. Pacemaker always adds the + * values required for its functionality. + * + * For an example of anonymous authentication, see: + * http://www.manpagez.com/info/gnutls/gnutls-2.10.4/gnutls_81.php#Echo-Server-with-anonymous-authentication + */ + + prio_base = getenv("PCMK_tls_priorities"); + if (prio_base == NULL) { + prio_base = PCMK_GNUTLS_PRIORITIES; + } + prio = crm_strdup_printf("%s:%s", prio_base, + (cred_type == GNUTLS_CRD_ANON)? "+ANON-DH" : "+DHE-PSK:+PSK"); + + session = gnutls_malloc(sizeof(gnutls_session_t)); + if (session == NULL) { + rc = GNUTLS_E_MEMORY_ERROR; + goto error; + } + + rc = gnutls_init(session, conn_type); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + /* @TODO On the server side, it would be more efficient to cache the + * priority with gnutls_priority_init2() and set it with + * gnutls_priority_set() for all sessions. + */ + rc = gnutls_priority_set_direct(*session, prio, NULL); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + if (conn_type == GNUTLS_CLIENT) { + set_minimum_dh_bits(session); + } + + gnutls_transport_set_ptr(*session, + (gnutls_transport_ptr_t) GINT_TO_POINTER(csock)); + + rc = gnutls_credentials_set(*session, cred_type, credentials); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + free(prio); + return session; + +error: + crm_err("Could not initialize %s TLS %s session: %s " + CRM_XS " rc=%d priority='%s'", + (cred_type == GNUTLS_CRD_ANON)? "anonymous" : "PSK", + (conn_type == GNUTLS_SERVER)? "server" : "client", + gnutls_strerror(rc), rc, prio); + free(prio); + if (session != NULL) { + gnutls_free(session); + } + return NULL; +} + +/*! + * \internal + * \brief Initialize Diffie-Hellman parameters for a TLS server + * + * \param[out] dh_params Parameter object to initialize + * + * \return Standard Pacemaker return code + * \todo The current best practice is to allow the client and server to + * negotiate the Diffie-Hellman parameters via a TLS extension (RFC 7919). + * However, we have to support both older versions of GnuTLS (<3.6) that + * don't support the extension on our side, and older Pacemaker versions + * that don't support the extension on the other side. The next best + * practice would be to use a known good prime (see RFC 5114 section 2.2), + * possibly stored in a file distributed with Pacemaker. + */ +int +pcmk__init_tls_dh(gnutls_dh_params_t *dh_params) +{ + int rc = GNUTLS_E_SUCCESS; + unsigned int dh_bits = 0; + + rc = gnutls_dh_params_init(dh_params); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + dh_bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, + GNUTLS_SEC_PARAM_NORMAL); + if (dh_bits == 0) { + rc = GNUTLS_E_DH_PRIME_UNACCEPTABLE; + goto error; + } + dh_bits = get_bound_dh_bits(dh_bits); + + crm_info("Generating Diffie-Hellman parameters with %u-bit prime for TLS", + dh_bits); + rc = gnutls_dh_params_generate2(*dh_params, dh_bits); + if (rc != GNUTLS_E_SUCCESS) { + goto error; + } + + return pcmk_rc_ok; + +error: + crm_err("Could not initialize Diffie-Hellman parameters for TLS: %s " + CRM_XS " rc=%d", gnutls_strerror(rc), rc); + return EPROTO; +} + +/*! + * \internal + * \brief Process handshake data from TLS client + * + * Read as much TLS handshake data as is available. + * + * \param[in] client Client connection + * + * \return Standard Pacemaker return code (of particular interest, EAGAIN + * if some data was successfully read but more data is needed) + */ +int +pcmk__read_handshake_data(const pcmk__client_t *client) +{ + int rc = 0; + + CRM_ASSERT(client && client->remote && client->remote->tls_session); + + do { + rc = gnutls_handshake(*client->remote->tls_session); + } while (rc == GNUTLS_E_INTERRUPTED); + + if (rc == GNUTLS_E_AGAIN) { + /* No more data is available at the moment. This function should be + * invoked again once the client sends more. + */ + return EAGAIN; + } else if (rc != GNUTLS_E_SUCCESS) { + crm_err("TLS handshake with remote client failed: %s " + CRM_XS " rc=%d", gnutls_strerror(rc), rc); + return EPROTO; + } + return pcmk_rc_ok; +} + +// \return Standard Pacemaker return code +static int +send_tls(gnutls_session_t *session, struct iovec *iov) +{ + const char *unsent = iov->iov_base; + size_t unsent_len = iov->iov_len; + ssize_t gnutls_rc; + + if (unsent == NULL) { + return EINVAL; + } + + crm_trace("Sending TLS message of %llu bytes", + (unsigned long long) unsent_len); + while (true) { + gnutls_rc = gnutls_record_send(*session, unsent, unsent_len); + + if (gnutls_rc == GNUTLS_E_INTERRUPTED || gnutls_rc == GNUTLS_E_AGAIN) { + crm_trace("Retrying to send %llu bytes remaining", + (unsigned long long) unsent_len); + + } else if (gnutls_rc < 0) { + // Caller can log as error if necessary + crm_info("TLS connection terminated: %s " CRM_XS " rc=%lld", + gnutls_strerror((int) gnutls_rc), + (long long) gnutls_rc); + return ECONNABORTED; + + } else if (gnutls_rc < unsent_len) { + crm_trace("Sent %lld of %llu bytes remaining", + (long long) gnutls_rc, (unsigned long long) unsent_len); + unsent_len -= gnutls_rc; + unsent += gnutls_rc; + } else { + crm_trace("Sent all %lld bytes remaining", (long long) gnutls_rc); + break; + } + } + return pcmk_rc_ok; +} +#endif + +// \return Standard Pacemaker return code +static int +send_plaintext(int sock, struct iovec *iov) +{ + const char *unsent = iov->iov_base; + size_t unsent_len = iov->iov_len; + ssize_t write_rc; + + if (unsent == NULL) { + return EINVAL; + } + + crm_debug("Sending plaintext message of %llu bytes to socket %d", + (unsigned long long) unsent_len, sock); + while (true) { + write_rc = write(sock, unsent, unsent_len); + if (write_rc < 0) { + int rc = errno; + + if ((errno == EINTR) || (errno == EAGAIN)) { + crm_trace("Retrying to send %llu bytes remaining to socket %d", + (unsigned long long) unsent_len, sock); + continue; + } + + // Caller can log as error if necessary + crm_info("Could not send message: %s " CRM_XS " rc=%d socket=%d", + pcmk_rc_str(rc), rc, sock); + return rc; + + } else if (write_rc < unsent_len) { + crm_trace("Sent %lld of %llu bytes remaining", + (long long) write_rc, (unsigned long long) unsent_len); + unsent += write_rc; + unsent_len -= write_rc; + continue; + + } else { + crm_trace("Sent all %lld bytes remaining: %.100s", + (long long) write_rc, (char *) (iov->iov_base)); + break; + } + } + return pcmk_rc_ok; +} + +// \return Standard Pacemaker return code +static int +remote_send_iovs(pcmk__remote_t *remote, struct iovec *iov, int iovs) +{ + int rc = pcmk_rc_ok; + + for (int lpc = 0; (lpc < iovs) && (rc == pcmk_rc_ok); lpc++) { +#ifdef HAVE_GNUTLS_GNUTLS_H + if (remote->tls_session) { + rc = send_tls(remote->tls_session, &(iov[lpc])); + continue; + } +#endif + if (remote->tcp_socket) { + rc = send_plaintext(remote->tcp_socket, &(iov[lpc])); + } else { + rc = ESOCKTNOSUPPORT; + } + } + return rc; +} + +/*! + * \internal + * \brief Send an XML message over a Pacemaker Remote connection + * + * \param[in,out] remote Pacemaker Remote connection to use + * \param[in] msg XML to send + * + * \return Standard Pacemaker return code + */ +int +pcmk__remote_send_xml(pcmk__remote_t *remote, xmlNode *msg) +{ + int rc = pcmk_rc_ok; + static uint64_t id = 0; + char *xml_text = NULL; + + struct iovec iov[2]; + struct remote_header_v0 *header; + + CRM_CHECK((remote != NULL) && (msg != NULL), return EINVAL); + + xml_text = dump_xml_unformatted(msg); + CRM_CHECK(xml_text != NULL, return EINVAL); + + header = calloc(1, sizeof(struct remote_header_v0)); + CRM_ASSERT(header != NULL); + + iov[0].iov_base = header; + iov[0].iov_len = sizeof(struct remote_header_v0); + + iov[1].iov_base = xml_text; + iov[1].iov_len = 1 + strlen(xml_text); + + id++; + header->id = id; + header->endian = ENDIAN_LOCAL; + header->version = REMOTE_MSG_VERSION; + header->payload_offset = iov[0].iov_len; + header->payload_uncompressed = iov[1].iov_len; + header->size_total = iov[0].iov_len + iov[1].iov_len; + + rc = remote_send_iovs(remote, iov, 2); + if (rc != pcmk_rc_ok) { + crm_err("Could not send remote message: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + } + + free(iov[0].iov_base); + free(iov[1].iov_base); + return rc; +} + +/*! + * \internal + * \brief Obtain the XML from the currently buffered remote connection message + * + * \param[in,out] remote Remote connection possibly with message available + * + * \return Newly allocated XML object corresponding to message data, or NULL + * \note This effectively removes the message from the connection buffer. + */ +xmlNode * +pcmk__remote_message_xml(pcmk__remote_t *remote) +{ + xmlNode *xml = NULL; + struct remote_header_v0 *header = localized_remote_header(remote); + + if (header == NULL) { + return NULL; + } + + /* Support compression on the receiving end now, in case we ever want to add it later */ + if (header->payload_compressed) { + int rc = 0; + unsigned int size_u = 1 + header->payload_uncompressed; + char *uncompressed = calloc(1, header->payload_offset + size_u); + + crm_trace("Decompressing message data %d bytes into %d bytes", + header->payload_compressed, size_u); + + rc = BZ2_bzBuffToBuffDecompress(uncompressed + header->payload_offset, &size_u, + remote->buffer + header->payload_offset, + header->payload_compressed, 1, 0); + + if (rc != BZ_OK && header->version > REMOTE_MSG_VERSION) { + crm_warn("Couldn't decompress v%d message, we only understand v%d", + header->version, REMOTE_MSG_VERSION); + free(uncompressed); + return NULL; + + } else if (rc != BZ_OK) { + crm_err("Decompression failed: %s " CRM_XS " bzerror=%d", + bz2_strerror(rc), rc); + free(uncompressed); + return NULL; + } + + CRM_ASSERT(size_u == header->payload_uncompressed); + + memcpy(uncompressed, remote->buffer, header->payload_offset); /* Preserve the header */ + remote->buffer_size = header->payload_offset + size_u; + + free(remote->buffer); + remote->buffer = uncompressed; + header = localized_remote_header(remote); + } + + /* take ownership of the buffer */ + remote->buffer_offset = 0; + + CRM_LOG_ASSERT(remote->buffer[sizeof(struct remote_header_v0) + header->payload_uncompressed - 1] == 0); + + xml = string2xml(remote->buffer + header->payload_offset); + if (xml == NULL && header->version > REMOTE_MSG_VERSION) { + crm_warn("Couldn't parse v%d message, we only understand v%d", + header->version, REMOTE_MSG_VERSION); + + } else if (xml == NULL) { + crm_err("Couldn't parse: '%.120s'", remote->buffer + header->payload_offset); + } + + return xml; +} + +static int +get_remote_socket(const pcmk__remote_t *remote) +{ +#ifdef HAVE_GNUTLS_GNUTLS_H + if (remote->tls_session) { + void *sock_ptr = gnutls_transport_get_ptr(*remote->tls_session); + + return GPOINTER_TO_INT(sock_ptr); + } +#endif + + if (remote->tcp_socket) { + return remote->tcp_socket; + } + + crm_err("Remote connection type undetermined (bug?)"); + return -1; +} + +/*! + * \internal + * \brief Wait for a remote session to have data to read + * + * \param[in] remote Connection to check + * \param[in] timeout_ms Maximum time (in ms) to wait + * + * \return Standard Pacemaker return code (of particular interest, pcmk_rc_ok if + * there is data ready to be read, and ETIME if there is no data within + * the specified timeout) + */ +int +pcmk__remote_ready(const pcmk__remote_t *remote, int timeout_ms) +{ + struct pollfd fds = { 0, }; + int sock = 0; + int rc = 0; + time_t start; + int timeout = timeout_ms; + + sock = get_remote_socket(remote); + if (sock <= 0) { + crm_trace("No longer connected"); + return ENOTCONN; + } + + start = time(NULL); + errno = 0; + do { + fds.fd = sock; + fds.events = POLLIN; + + /* If we got an EINTR while polling, and we have a + * specific timeout we are trying to honor, attempt + * to adjust the timeout to the closest second. */ + if (errno == EINTR && (timeout > 0)) { + timeout = timeout_ms - ((time(NULL) - start) * 1000); + if (timeout < 1000) { + timeout = 1000; + } + } + + rc = poll(&fds, 1, timeout); + } while (rc < 0 && errno == EINTR); + + if (rc < 0) { + return errno; + } + return (rc == 0)? ETIME : pcmk_rc_ok; +} + +/*! + * \internal + * \brief Read bytes from non-blocking remote connection + * + * \param[in,out] remote Remote connection to read + * + * \return Standard Pacemaker return code (of particular interest, pcmk_rc_ok if + * a full message has been received, or EAGAIN for a partial message) + * \note Use only with non-blocking sockets after polling the socket. + * \note This function will return when the socket read buffer is empty or an + * error is encountered. + */ +static int +read_available_remote_data(pcmk__remote_t *remote) +{ + int rc = pcmk_rc_ok; + size_t read_len = sizeof(struct remote_header_v0); + struct remote_header_v0 *header = localized_remote_header(remote); + bool received = false; + ssize_t read_rc; + + if(header) { + /* Stop at the end of the current message */ + read_len = header->size_total; + } + + /* automatically grow the buffer when needed */ + if(remote->buffer_size < read_len) { + remote->buffer_size = 2 * read_len; + crm_trace("Expanding buffer to %llu bytes", + (unsigned long long) remote->buffer_size); + remote->buffer = pcmk__realloc(remote->buffer, remote->buffer_size + 1); + } + +#ifdef HAVE_GNUTLS_GNUTLS_H + if (!received && remote->tls_session) { + read_rc = gnutls_record_recv(*(remote->tls_session), + remote->buffer + remote->buffer_offset, + remote->buffer_size - remote->buffer_offset); + if (read_rc == GNUTLS_E_INTERRUPTED) { + rc = EINTR; + } else if (read_rc == GNUTLS_E_AGAIN) { + rc = EAGAIN; + } else if (read_rc < 0) { + crm_debug("TLS receive failed: %s (%lld)", + gnutls_strerror(read_rc), (long long) read_rc); + rc = EIO; + } + received = true; + } +#endif + + if (!received && remote->tcp_socket) { + read_rc = read(remote->tcp_socket, + remote->buffer + remote->buffer_offset, + remote->buffer_size - remote->buffer_offset); + if (read_rc < 0) { + rc = errno; + } + received = true; + } + + if (!received) { + crm_err("Remote connection type undetermined (bug?)"); + return ESOCKTNOSUPPORT; + } + + /* process any errors. */ + if (read_rc > 0) { + remote->buffer_offset += read_rc; + /* always null terminate buffer, the +1 to alloc always allows for this. */ + remote->buffer[remote->buffer_offset] = '\0'; + crm_trace("Received %lld more bytes (%llu total)", + (long long) read_rc, + (unsigned long long) remote->buffer_offset); + + } else if ((rc == EINTR) || (rc == EAGAIN)) { + crm_trace("No data available for non-blocking remote read: %s (%d)", + pcmk_rc_str(rc), rc); + + } else if (read_rc == 0) { + crm_debug("End of remote data encountered after %llu bytes", + (unsigned long long) remote->buffer_offset); + return ENOTCONN; + + } else { + crm_debug("Error receiving remote data after %llu bytes: %s (%d)", + (unsigned long long) remote->buffer_offset, + pcmk_rc_str(rc), rc); + return ENOTCONN; + } + + header = localized_remote_header(remote); + if(header) { + if(remote->buffer_offset < header->size_total) { + crm_trace("Read partial remote message (%llu of %u bytes)", + (unsigned long long) remote->buffer_offset, + header->size_total); + } else { + crm_trace("Read full remote message of %llu bytes", + (unsigned long long) remote->buffer_offset); + return pcmk_rc_ok; + } + } + + return EAGAIN; +} + +/*! + * \internal + * \brief Read one message from a remote connection + * + * \param[in,out] remote Remote connection to read + * \param[in] timeout_ms Fail if message not read in this many milliseconds + * (10s will be used if 0, and 60s if negative) + * + * \return Standard Pacemaker return code + */ +int +pcmk__read_remote_message(pcmk__remote_t *remote, int timeout_ms) +{ + int rc = pcmk_rc_ok; + time_t start = time(NULL); + int remaining_timeout = 0; + + if (timeout_ms == 0) { + timeout_ms = 10000; + } else if (timeout_ms < 0) { + timeout_ms = 60000; + } + + remaining_timeout = timeout_ms; + while (remaining_timeout > 0) { + + crm_trace("Waiting for remote data (%d ms of %d ms timeout remaining)", + remaining_timeout, timeout_ms); + rc = pcmk__remote_ready(remote, remaining_timeout); + + if (rc == ETIME) { + crm_err("Timed out (%d ms) while waiting for remote data", + remaining_timeout); + return rc; + + } else if (rc != pcmk_rc_ok) { + crm_debug("Wait for remote data aborted (will retry): %s " + CRM_XS " rc=%d", pcmk_rc_str(rc), rc); + + } else { + rc = read_available_remote_data(remote); + if (rc == pcmk_rc_ok) { + return rc; + } else if (rc == EAGAIN) { + crm_trace("Waiting for more remote data"); + } else { + crm_debug("Could not receive remote data: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + } + } + + // Don't waste time retrying after fatal errors + if ((rc == ENOTCONN) || (rc == ESOCKTNOSUPPORT)) { + return rc; + } + + remaining_timeout = timeout_ms - ((time(NULL) - start) * 1000); + } + return ETIME; +} + +struct tcp_async_cb_data { + int sock; + int timeout_ms; + time_t start; + void *userdata; + void (*callback) (void *userdata, int rc, int sock); +}; + +// \return TRUE if timer should be rescheduled, FALSE otherwise +static gboolean +check_connect_finished(gpointer userdata) +{ + struct tcp_async_cb_data *cb_data = userdata; + int rc; + + fd_set rset, wset; + struct timeval ts = { 0, }; + + if (cb_data->start == 0) { + // Last connect() returned success immediately + rc = pcmk_rc_ok; + goto dispatch_done; + } + + // If the socket is ready for reading or writing, the connect succeeded + FD_ZERO(&rset); + FD_SET(cb_data->sock, &rset); + wset = rset; + rc = select(cb_data->sock + 1, &rset, &wset, NULL, &ts); + + if (rc < 0) { // select() error + rc = errno; + if ((rc == EINPROGRESS) || (rc == EAGAIN)) { + if ((time(NULL) - cb_data->start) < (cb_data->timeout_ms / 1000)) { + return TRUE; // There is time left, so reschedule timer + } else { + rc = ETIMEDOUT; + } + } + crm_trace("Could not check socket %d for connection success: %s (%d)", + cb_data->sock, pcmk_rc_str(rc), rc); + + } else if (rc == 0) { // select() timeout + if ((time(NULL) - cb_data->start) < (cb_data->timeout_ms / 1000)) { + return TRUE; // There is time left, so reschedule timer + } + crm_debug("Timed out while waiting for socket %d connection success", + cb_data->sock); + rc = ETIMEDOUT; + + // select() returned number of file descriptors that are ready + + } else if (FD_ISSET(cb_data->sock, &rset) + || FD_ISSET(cb_data->sock, &wset)) { + + // The socket is ready; check it for connection errors + int error = 0; + socklen_t len = sizeof(error); + + if (getsockopt(cb_data->sock, SOL_SOCKET, SO_ERROR, &error, &len) < 0) { + rc = errno; + crm_trace("Couldn't check socket %d for connection errors: %s (%d)", + cb_data->sock, pcmk_rc_str(rc), rc); + } else if (error != 0) { + rc = error; + crm_trace("Socket %d connected with error: %s (%d)", + cb_data->sock, pcmk_rc_str(rc), rc); + } else { + rc = pcmk_rc_ok; + } + + } else { // Should not be possible + crm_trace("select() succeeded, but socket %d not in resulting " + "read/write sets", cb_data->sock); + rc = EAGAIN; + } + + dispatch_done: + if (rc == pcmk_rc_ok) { + crm_trace("Socket %d is connected", cb_data->sock); + } else { + close(cb_data->sock); + cb_data->sock = -1; + } + + if (cb_data->callback) { + cb_data->callback(cb_data->userdata, rc, cb_data->sock); + } + free(cb_data); + return FALSE; // Do not reschedule timer +} + +/*! + * \internal + * \brief Attempt to connect socket, calling callback when done + * + * Set a given socket non-blocking, then attempt to connect to it, + * retrying periodically until success or a timeout is reached. + * Call a caller-supplied callback function when completed. + * + * \param[in] sock Newly created socket + * \param[in] addr Socket address information for connect + * \param[in] addrlen Size of socket address information in bytes + * \param[in] timeout_ms Fail if not connected within this much time + * \param[out] timer_id If not NULL, store retry timer ID here + * \param[in] userdata User data to pass to callback + * \param[in] callback Function to call when connection attempt completes + * + * \return Standard Pacemaker return code + */ +static int +connect_socket_retry(int sock, const struct sockaddr *addr, socklen_t addrlen, + int timeout_ms, int *timer_id, void *userdata, + void (*callback) (void *userdata, int rc, int sock)) +{ + int rc = 0; + int interval = 500; + int timer; + struct tcp_async_cb_data *cb_data = NULL; + + rc = pcmk__set_nonblocking(sock); + if (rc != pcmk_rc_ok) { + crm_warn("Could not set socket non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + return rc; + } + + rc = connect(sock, addr, addrlen); + if (rc < 0 && (errno != EINPROGRESS) && (errno != EAGAIN)) { + rc = errno; + crm_warn("Could not connect socket: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + return rc; + } + + cb_data = calloc(1, sizeof(struct tcp_async_cb_data)); + cb_data->userdata = userdata; + cb_data->callback = callback; + cb_data->sock = sock; + cb_data->timeout_ms = timeout_ms; + + if (rc == 0) { + /* The connect was successful immediately, we still return to mainloop + * and let this callback get called later. This avoids the user of this api + * to have to account for the fact the callback could be invoked within this + * function before returning. */ + cb_data->start = 0; + interval = 1; + } else { + cb_data->start = time(NULL); + } + + /* This timer function does a non-blocking poll on the socket to see if we + * can use it. Once we can, the connect has completed. This method allows us + * to connect without blocking the mainloop. + * + * @TODO Use a mainloop fd callback for this instead of polling. Something + * about the way mainloop is currently polling prevents this from + * working at the moment though. (See connect(2) regarding EINPROGRESS + * for possible new handling needed.) + */ + crm_trace("Scheduling check in %dms for whether connect to fd %d finished", + interval, sock); + timer = g_timeout_add(interval, check_connect_finished, cb_data); + if (timer_id) { + *timer_id = timer; + } + + // timer callback should be taking care of cb_data + // cppcheck-suppress memleak + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Attempt once to connect socket and set it non-blocking + * + * \param[in] sock Newly created socket + * \param[in] addr Socket address information for connect + * \param[in] addrlen Size of socket address information in bytes + * + * \return Standard Pacemaker return code + */ +static int +connect_socket_once(int sock, const struct sockaddr *addr, socklen_t addrlen) +{ + int rc = connect(sock, addr, addrlen); + + if (rc < 0) { + rc = errno; + crm_warn("Could not connect socket: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + return rc; + } + + rc = pcmk__set_nonblocking(sock); + if (rc != pcmk_rc_ok) { + crm_warn("Could not set socket non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + return rc; + } + + return pcmk_ok; +} + +/*! + * \internal + * \brief Connect to server at specified TCP port + * + * \param[in] host Name of server to connect to + * \param[in] port Server port to connect to + * \param[in] timeout_ms If asynchronous, fail if not connected in this time + * \param[out] timer_id If asynchronous and this is non-NULL, retry timer ID + * will be put here (for ease of cancelling by caller) + * \param[out] sock_fd Where to store socket file descriptor + * \param[in] userdata If asynchronous, data to pass to callback + * \param[in] callback If NULL, attempt a single synchronous connection, + * otherwise retry asynchronously then call this + * + * \return Standard Pacemaker return code + */ +int +pcmk__connect_remote(const char *host, int port, int timeout, int *timer_id, + int *sock_fd, void *userdata, + void (*callback) (void *userdata, int rc, int sock)) +{ + char buffer[INET6_ADDRSTRLEN]; + struct addrinfo *res = NULL; + struct addrinfo *rp = NULL; + struct addrinfo hints; + const char *server = host; + int rc; + int sock = -1; + + CRM_CHECK((host != NULL) && (sock_fd != NULL), return EINVAL); + + // Get host's IP address(es) + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */ + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_CANONNAME; + rc = getaddrinfo(server, NULL, &hints, &res); + if (rc != 0) { + crm_err("Unable to get IP address info for %s: %s", + server, gai_strerror(rc)); + rc = ENOTCONN; + goto async_cleanup; + } + if (!res || !res->ai_addr) { + crm_err("Unable to get IP address info for %s: no result", server); + rc = ENOTCONN; + goto async_cleanup; + } + + // getaddrinfo() returns a list of host's addresses, try them in order + for (rp = res; rp != NULL; rp = rp->ai_next) { + struct sockaddr *addr = rp->ai_addr; + + if (!addr) { + continue; + } + + if (rp->ai_canonname) { + server = res->ai_canonname; + } + crm_debug("Got canonical name %s for %s", server, host); + + sock = socket(rp->ai_family, SOCK_STREAM, IPPROTO_TCP); + if (sock == -1) { + rc = errno; + crm_warn("Could not create socket for remote connection to %s:%d: " + "%s " CRM_XS " rc=%d", server, port, pcmk_rc_str(rc), rc); + continue; + } + + /* Set port appropriately for address family */ + /* (void*) casts avoid false-positive compiler alignment warnings */ + if (addr->sa_family == AF_INET6) { + ((struct sockaddr_in6 *)(void*)addr)->sin6_port = htons(port); + } else { + ((struct sockaddr_in *)(void*)addr)->sin_port = htons(port); + } + + memset(buffer, 0, PCMK__NELEM(buffer)); + pcmk__sockaddr2str(addr, buffer); + crm_info("Attempting remote connection to %s:%d", buffer, port); + + if (callback) { + if (connect_socket_retry(sock, rp->ai_addr, rp->ai_addrlen, timeout, + timer_id, userdata, callback) == pcmk_rc_ok) { + goto async_cleanup; /* Success for now, we'll hear back later in the callback */ + } + + } else if (connect_socket_once(sock, rp->ai_addr, + rp->ai_addrlen) == pcmk_rc_ok) { + break; /* Success */ + } + + // Connect failed + close(sock); + sock = -1; + rc = ENOTCONN; + } + +async_cleanup: + + if (res) { + freeaddrinfo(res); + } + *sock_fd = sock; + return rc; +} + +/*! + * \internal + * \brief Convert an IP address (IPv4 or IPv6) to a string for logging + * + * \param[in] sa Socket address for IP + * \param[out] s Storage for at least INET6_ADDRSTRLEN bytes + * + * \note sa The socket address can be a pointer to struct sockaddr_in (IPv4), + * struct sockaddr_in6 (IPv6) or struct sockaddr_storage (either), + * as long as its sa_family member is set correctly. + */ +void +pcmk__sockaddr2str(const void *sa, char *s) +{ + switch (((const struct sockaddr *) sa)->sa_family) { + case AF_INET: + inet_ntop(AF_INET, &(((const struct sockaddr_in *) sa)->sin_addr), + s, INET6_ADDRSTRLEN); + break; + + case AF_INET6: + inet_ntop(AF_INET6, + &(((const struct sockaddr_in6 *) sa)->sin6_addr), + s, INET6_ADDRSTRLEN); + break; + + default: + strcpy(s, ""); + } +} + +/*! + * \internal + * \brief Accept a client connection on a remote server socket + * + * \param[in] ssock Server socket file descriptor being listened on + * \param[out] csock Where to put new client socket's file descriptor + * + * \return Standard Pacemaker return code + */ +int +pcmk__accept_remote_connection(int ssock, int *csock) +{ + int rc; + struct sockaddr_storage addr; + socklen_t laddr = sizeof(addr); + char addr_str[INET6_ADDRSTRLEN]; + + /* accept the connection */ + memset(&addr, 0, sizeof(addr)); + *csock = accept(ssock, (struct sockaddr *)&addr, &laddr); + if (*csock == -1) { + rc = errno; + crm_err("Could not accept remote client connection: %s " + CRM_XS " rc=%d", pcmk_rc_str(rc), rc); + return rc; + } + pcmk__sockaddr2str(&addr, addr_str); + crm_info("Accepted new remote client connection from %s", addr_str); + + rc = pcmk__set_nonblocking(*csock); + if (rc != pcmk_rc_ok) { + crm_err("Could not set socket non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + close(*csock); + *csock = -1; + return rc; + } + +#ifdef TCP_USER_TIMEOUT + if (pcmk__get_sbd_timeout() > 0) { + // Time to fail and retry before watchdog + unsigned int optval = (unsigned int) pcmk__get_sbd_timeout() / 2; + + rc = setsockopt(*csock, SOL_TCP, TCP_USER_TIMEOUT, + &optval, sizeof(optval)); + if (rc < 0) { + rc = errno; + crm_err("Could not set TCP timeout to %d ms on remote connection: " + "%s " CRM_XS " rc=%d", optval, pcmk_rc_str(rc), rc); + close(*csock); + *csock = -1; + return rc; + } + } +#endif + + return rc; +} + +/*! + * \brief Get the default remote connection TCP port on this host + * + * \return Remote connection TCP port number + */ +int +crm_default_remote_port(void) +{ + static int port = 0; + + if (port == 0) { + const char *env = getenv("PCMK_remote_port"); + + if (env) { + errno = 0; + port = strtol(env, NULL, 10); + if (errno || (port < 1) || (port > 65535)) { + crm_warn("Environment variable PCMK_remote_port has invalid value '%s', using %d instead", + env, DEFAULT_REMOTE_PORT); + port = DEFAULT_REMOTE_PORT; + } + } else { + port = DEFAULT_REMOTE_PORT; + } + } + return port; +} diff --git a/lib/common/results.c b/lib/common/results.c new file mode 100644 index 0000000..93d79eb --- /dev/null +++ b/lib/common/results.c @@ -0,0 +1,1049 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include + +#include +#include + +G_DEFINE_QUARK(pcmk-rc-error-quark, pcmk__rc_error) +G_DEFINE_QUARK(pcmk-exitc-error-quark, pcmk__exitc_error) + +// General (all result code types) + +/*! + * \brief Get the name and description of a given result code + * + * A result code can be interpreted as a member of any one of several families. + * + * \param[in] code The result code to look up + * \param[in] type How \p code should be interpreted + * \param[out] name Where to store the result code's name + * \param[out] desc Where to store the result code's description + * + * \return Standard Pacemaker return code + */ +int +pcmk_result_get_strings(int code, enum pcmk_result_type type, const char **name, + const char **desc) +{ + const char *code_name = NULL; + const char *code_desc = NULL; + + switch (type) { + case pcmk_result_legacy: + code_name = pcmk_errorname(code); + code_desc = pcmk_strerror(code); + break; + case pcmk_result_rc: + code_name = pcmk_rc_name(code); + code_desc = pcmk_rc_str(code); + break; + case pcmk_result_exitcode: + code_name = crm_exit_name(code); + code_desc = crm_exit_str((crm_exit_t) code); + break; + default: + return pcmk_rc_undetermined; + } + + if (name != NULL) { + *name = code_name; + } + + if (desc != NULL) { + *desc = code_desc; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Get the lower and upper bounds of a result code family + * + * \param[in] type Type of result code + * \param[out] lower Where to store the lower bound + * \param[out] upper Where to store the upper bound + * + * \return Standard Pacemaker return code + * + * \note There is no true upper bound on standard Pacemaker return codes or + * legacy return codes. All system \p errno values are valid members of + * these result code families, and there is no global upper limit nor a + * constant by which to refer to the highest \p errno value on a given + * system. + */ +int +pcmk__result_bounds(enum pcmk_result_type type, int *lower, int *upper) +{ + CRM_ASSERT((lower != NULL) && (upper != NULL)); + + switch (type) { + case pcmk_result_legacy: + *lower = pcmk_ok; + *upper = 256; // should be enough for almost any system error code + break; + case pcmk_result_rc: + *lower = pcmk_rc_error - pcmk__n_rc + 1; + *upper = 256; + break; + case pcmk_result_exitcode: + *lower = CRM_EX_OK; + *upper = CRM_EX_MAX; + break; + default: + *lower = 0; + *upper = -1; + return pcmk_rc_undetermined; + } + return pcmk_rc_ok; +} + +// @COMPAT Legacy function return codes + +//! \deprecated Use standard return codes and pcmk_rc_name() instead +const char * +pcmk_errorname(int rc) +{ + rc = abs(rc); + switch (rc) { + case pcmk_err_generic: return "pcmk_err_generic"; + case pcmk_err_no_quorum: return "pcmk_err_no_quorum"; + case pcmk_err_schema_validation: return "pcmk_err_schema_validation"; + case pcmk_err_transform_failed: return "pcmk_err_transform_failed"; + case pcmk_err_old_data: return "pcmk_err_old_data"; + case pcmk_err_diff_failed: return "pcmk_err_diff_failed"; + case pcmk_err_diff_resync: return "pcmk_err_diff_resync"; + case pcmk_err_cib_modified: return "pcmk_err_cib_modified"; + case pcmk_err_cib_backup: return "pcmk_err_cib_backup"; + case pcmk_err_cib_save: return "pcmk_err_cib_save"; + case pcmk_err_cib_corrupt: return "pcmk_err_cib_corrupt"; + case pcmk_err_multiple: return "pcmk_err_multiple"; + case pcmk_err_node_unknown: return "pcmk_err_node_unknown"; + case pcmk_err_already: return "pcmk_err_already"; + case pcmk_err_bad_nvpair: return "pcmk_err_bad_nvpair"; + case pcmk_err_unknown_format: return "pcmk_err_unknown_format"; + default: return pcmk_rc_name(rc); // system errno + } +} + +//! \deprecated Use standard return codes and pcmk_rc_str() instead +const char * +pcmk_strerror(int rc) +{ + return pcmk_rc_str(pcmk_legacy2rc(rc)); +} + +// Standard Pacemaker API return codes + +/* This array is used only for nonzero values of pcmk_rc_e. Its values must be + * kept in the exact reverse order of the enum value numbering (i.e. add new + * values to the end of the array). + */ +static const struct pcmk__rc_info { + const char *name; + const char *desc; + int legacy_rc; +} pcmk__rcs[] = { + { "pcmk_rc_error", + "Error", + -pcmk_err_generic, + }, + { "pcmk_rc_unknown_format", + "Unknown output format", + -pcmk_err_unknown_format, + }, + { "pcmk_rc_bad_nvpair", + "Bad name/value pair given", + -pcmk_err_bad_nvpair, + }, + { "pcmk_rc_already", + "Already in requested state", + -pcmk_err_already, + }, + { "pcmk_rc_node_unknown", + "Node not found", + -pcmk_err_node_unknown, + }, + { "pcmk_rc_multiple", + "Resource active on multiple nodes", + -pcmk_err_multiple, + }, + { "pcmk_rc_cib_corrupt", + "Could not parse on-disk configuration", + -pcmk_err_cib_corrupt, + }, + { "pcmk_rc_cib_save", + "Could not save new configuration to disk", + -pcmk_err_cib_save, + }, + { "pcmk_rc_cib_backup", + "Could not archive previous configuration", + -pcmk_err_cib_backup, + }, + { "pcmk_rc_cib_modified", + "On-disk configuration was manually modified", + -pcmk_err_cib_modified, + }, + { "pcmk_rc_diff_resync", + "Application of update diff failed, requesting full refresh", + -pcmk_err_diff_resync, + }, + { "pcmk_rc_diff_failed", + "Application of update diff failed", + -pcmk_err_diff_failed, + }, + { "pcmk_rc_old_data", + "Update was older than existing configuration", + -pcmk_err_old_data, + }, + { "pcmk_rc_transform_failed", + "Schema transform failed", + -pcmk_err_transform_failed, + }, + { "pcmk_rc_schema_unchanged", + "Schema is already the latest available", + -pcmk_err_schema_unchanged, + }, + { "pcmk_rc_schema_validation", + "Update does not conform to the configured schema", + -pcmk_err_schema_validation, + }, + { "pcmk_rc_no_quorum", + "Operation requires quorum", + -pcmk_err_no_quorum, + }, + { "pcmk_rc_ipc_unauthorized", + "IPC server is blocked by unauthorized process", + -pcmk_err_generic, + }, + { "pcmk_rc_ipc_unresponsive", + "IPC server is unresponsive", + -pcmk_err_generic, + }, + { "pcmk_rc_ipc_pid_only", + "IPC server process is active but not accepting connections", + -pcmk_err_generic, + }, + { "pcmk_rc_op_unsatisfied", + "Not applicable under current conditions", + -pcmk_err_generic, + }, + { "pcmk_rc_undetermined", + "Result undetermined", + -pcmk_err_generic, + }, + { "pcmk_rc_before_range", + "Result occurs before given range", + -pcmk_err_generic, + }, + { "pcmk_rc_within_range", + "Result occurs within given range", + -pcmk_err_generic, + }, + { "pcmk_rc_after_range", + "Result occurs after given range", + -pcmk_err_generic, + }, + { "pcmk_rc_no_output", + "Output message produced no output", + -pcmk_err_generic, + }, + { "pcmk_rc_no_input", + "Input file not available", + -pcmk_err_generic, + }, + { "pcmk_rc_underflow", + "Value too small to be stored in data type", + -pcmk_err_generic, + }, + { "pcmk_rc_dot_error", + "Error writing dot(1) file", + -pcmk_err_generic, + }, + { "pcmk_rc_graph_error", + "Error writing graph file", + -pcmk_err_generic, + }, + { "pcmk_rc_invalid_transition", + "Cluster simulation produced invalid transition", + -pcmk_err_generic, + }, + { "pcmk_rc_unpack_error", + "Unable to parse CIB XML", + -pcmk_err_generic, + }, + { "pcmk_rc_duplicate_id", + "Two or more XML elements have the same ID", + -pcmk_err_generic, + }, + { "pcmk_rc_disabled", + "Disabled", + -pcmk_err_generic, + }, + { "pcmk_rc_bad_input", + "Bad input value provided", + -pcmk_err_generic, + }, + { "pcmk_rc_bad_xml_patch", + "Bad XML patch format", + -pcmk_err_generic, + }, +}; + +/*! + * \internal + * \brief The number of enum pcmk_rc_e values, excluding \c pcmk_rc_ok + * + * This constant stores the number of negative standard Pacemaker return codes. + * These represent Pacemaker-custom error codes. The count does not include + * positive system error numbers, nor does it include \c pcmk_rc_ok (success). + */ +const size_t pcmk__n_rc = PCMK__NELEM(pcmk__rcs); + +/*! + * \brief Get a return code constant name as a string + * + * \param[in] rc Integer return code to convert + * + * \return String of constant name corresponding to rc + */ +const char * +pcmk_rc_name(int rc) +{ + if ((rc <= pcmk_rc_error) && ((pcmk_rc_error - rc) < pcmk__n_rc)) { + return pcmk__rcs[pcmk_rc_error - rc].name; + } + switch (rc) { + case pcmk_rc_ok: return "pcmk_rc_ok"; + case E2BIG: return "E2BIG"; + case EACCES: return "EACCES"; + case EADDRINUSE: return "EADDRINUSE"; + case EADDRNOTAVAIL: return "EADDRNOTAVAIL"; + case EAFNOSUPPORT: return "EAFNOSUPPORT"; + case EAGAIN: return "EAGAIN"; + case EALREADY: return "EALREADY"; + case EBADF: return "EBADF"; + case EBADMSG: return "EBADMSG"; + case EBUSY: return "EBUSY"; + case ECANCELED: return "ECANCELED"; + case ECHILD: return "ECHILD"; + case ECOMM: return "ECOMM"; + case ECONNABORTED: return "ECONNABORTED"; + case ECONNREFUSED: return "ECONNREFUSED"; + case ECONNRESET: return "ECONNRESET"; + /* case EDEADLK: return "EDEADLK"; */ + case EDESTADDRREQ: return "EDESTADDRREQ"; + case EDOM: return "EDOM"; + case EDQUOT: return "EDQUOT"; + case EEXIST: return "EEXIST"; + case EFAULT: return "EFAULT"; + case EFBIG: return "EFBIG"; + case EHOSTDOWN: return "EHOSTDOWN"; + case EHOSTUNREACH: return "EHOSTUNREACH"; + case EIDRM: return "EIDRM"; + case EILSEQ: return "EILSEQ"; + case EINPROGRESS: return "EINPROGRESS"; + case EINTR: return "EINTR"; + case EINVAL: return "EINVAL"; + case EIO: return "EIO"; + case EISCONN: return "EISCONN"; + case EISDIR: return "EISDIR"; + case ELIBACC: return "ELIBACC"; + case ELOOP: return "ELOOP"; + case EMFILE: return "EMFILE"; + case EMLINK: return "EMLINK"; + case EMSGSIZE: return "EMSGSIZE"; +#ifdef EMULTIHOP // Not available on OpenBSD + case EMULTIHOP: return "EMULTIHOP"; +#endif + case ENAMETOOLONG: return "ENAMETOOLONG"; + case ENETDOWN: return "ENETDOWN"; + case ENETRESET: return "ENETRESET"; + case ENETUNREACH: return "ENETUNREACH"; + case ENFILE: return "ENFILE"; + case ENOBUFS: return "ENOBUFS"; + case ENODATA: return "ENODATA"; + case ENODEV: return "ENODEV"; + case ENOENT: return "ENOENT"; + case ENOEXEC: return "ENOEXEC"; + case ENOKEY: return "ENOKEY"; + case ENOLCK: return "ENOLCK"; +#ifdef ENOLINK // Not available on OpenBSD + case ENOLINK: return "ENOLINK"; +#endif + case ENOMEM: return "ENOMEM"; + case ENOMSG: return "ENOMSG"; + case ENOPROTOOPT: return "ENOPROTOOPT"; + case ENOSPC: return "ENOSPC"; +#ifdef ENOSR + case ENOSR: return "ENOSR"; +#endif +#ifdef ENOSTR + case ENOSTR: return "ENOSTR"; +#endif + case ENOSYS: return "ENOSYS"; + case ENOTBLK: return "ENOTBLK"; + case ENOTCONN: return "ENOTCONN"; + case ENOTDIR: return "ENOTDIR"; + case ENOTEMPTY: return "ENOTEMPTY"; + case ENOTSOCK: return "ENOTSOCK"; +#if ENOTSUP != EOPNOTSUPP + case ENOTSUP: return "ENOTSUP"; +#endif + case ENOTTY: return "ENOTTY"; + case ENOTUNIQ: return "ENOTUNIQ"; + case ENXIO: return "ENXIO"; + case EOPNOTSUPP: return "EOPNOTSUPP"; + case EOVERFLOW: return "EOVERFLOW"; + case EPERM: return "EPERM"; + case EPFNOSUPPORT: return "EPFNOSUPPORT"; + case EPIPE: return "EPIPE"; + case EPROTO: return "EPROTO"; + case EPROTONOSUPPORT: return "EPROTONOSUPPORT"; + case EPROTOTYPE: return "EPROTOTYPE"; + case ERANGE: return "ERANGE"; + case EREMOTE: return "EREMOTE"; + case EREMOTEIO: return "EREMOTEIO"; + case EROFS: return "EROFS"; + case ESHUTDOWN: return "ESHUTDOWN"; + case ESPIPE: return "ESPIPE"; + case ESOCKTNOSUPPORT: return "ESOCKTNOSUPPORT"; + case ESRCH: return "ESRCH"; + case ESTALE: return "ESTALE"; + case ETIME: return "ETIME"; + case ETIMEDOUT: return "ETIMEDOUT"; + case ETXTBSY: return "ETXTBSY"; +#ifdef EUNATCH + case EUNATCH: return "EUNATCH"; +#endif + case EUSERS: return "EUSERS"; + /* case EWOULDBLOCK: return "EWOULDBLOCK"; */ + case EXDEV: return "EXDEV"; + +#ifdef EBADE // Not available on OS X + case EBADE: return "EBADE"; + case EBADFD: return "EBADFD"; + case EBADSLT: return "EBADSLT"; + case EDEADLOCK: return "EDEADLOCK"; + case EBADR: return "EBADR"; + case EBADRQC: return "EBADRQC"; + case ECHRNG: return "ECHRNG"; +#ifdef EISNAM // Not available on OS X, Illumos, Solaris + case EISNAM: return "EISNAM"; + case EKEYEXPIRED: return "EKEYEXPIRED"; + case EKEYREVOKED: return "EKEYREVOKED"; +#endif + case EKEYREJECTED: return "EKEYREJECTED"; + case EL2HLT: return "EL2HLT"; + case EL2NSYNC: return "EL2NSYNC"; + case EL3HLT: return "EL3HLT"; + case EL3RST: return "EL3RST"; + case ELIBBAD: return "ELIBBAD"; + case ELIBMAX: return "ELIBMAX"; + case ELIBSCN: return "ELIBSCN"; + case ELIBEXEC: return "ELIBEXEC"; +#ifdef ENOMEDIUM // Not available on OS X, Illumos, Solaris + case ENOMEDIUM: return "ENOMEDIUM"; + case EMEDIUMTYPE: return "EMEDIUMTYPE"; +#endif + case ENONET: return "ENONET"; + case ENOPKG: return "ENOPKG"; + case EREMCHG: return "EREMCHG"; + case ERESTART: return "ERESTART"; + case ESTRPIPE: return "ESTRPIPE"; +#ifdef EUCLEAN // Not available on OS X, Illumos, Solaris + case EUCLEAN: return "EUCLEAN"; +#endif + case EXFULL: return "EXFULL"; +#endif // EBADE + default: return "Unknown"; + } +} + +/*! + * \brief Get a user-friendly description of a return code + * + * \param[in] rc Integer return code to convert + * + * \return String description of rc + */ +const char * +pcmk_rc_str(int rc) +{ + if (rc == pcmk_rc_ok) { + return "OK"; + } + if ((rc <= pcmk_rc_error) && ((pcmk_rc_error - rc) < pcmk__n_rc)) { + return pcmk__rcs[pcmk_rc_error - rc].desc; + } + if (rc < 0) { + return "Error"; + } + + // Handle values that could be defined by system or by portability.h + switch (rc) { +#ifdef PCMK__ENOTUNIQ + case ENOTUNIQ: return "Name not unique on network"; +#endif +#ifdef PCMK__ECOMM + case ECOMM: return "Communication error on send"; +#endif +#ifdef PCMK__ELIBACC + case ELIBACC: return "Can not access a needed shared library"; +#endif +#ifdef PCMK__EREMOTEIO + case EREMOTEIO: return "Remote I/O error"; +#endif +#ifdef PCMK__ENOKEY + case ENOKEY: return "Required key not available"; +#endif +#ifdef PCMK__ENODATA + case ENODATA: return "No data available"; +#endif +#ifdef PCMK__ETIME + case ETIME: return "Timer expired"; +#endif +#ifdef PCMK__EKEYREJECTED + case EKEYREJECTED: return "Key was rejected by service"; +#endif + default: return strerror(rc); + } +} + +// This returns negative values for errors +//! \deprecated Use standard return codes instead +int +pcmk_rc2legacy(int rc) +{ + if (rc >= 0) { + return -rc; // OK or system errno + } + if ((rc <= pcmk_rc_error) && ((pcmk_rc_error - rc) < pcmk__n_rc)) { + return pcmk__rcs[pcmk_rc_error - rc].legacy_rc; + } + return -pcmk_err_generic; +} + +//! \deprecated Use standard return codes instead +int +pcmk_legacy2rc(int legacy_rc) +{ + legacy_rc = abs(legacy_rc); + switch (legacy_rc) { + case pcmk_err_no_quorum: return pcmk_rc_no_quorum; + case pcmk_err_schema_validation: return pcmk_rc_schema_validation; + case pcmk_err_schema_unchanged: return pcmk_rc_schema_unchanged; + case pcmk_err_transform_failed: return pcmk_rc_transform_failed; + case pcmk_err_old_data: return pcmk_rc_old_data; + case pcmk_err_diff_failed: return pcmk_rc_diff_failed; + case pcmk_err_diff_resync: return pcmk_rc_diff_resync; + case pcmk_err_cib_modified: return pcmk_rc_cib_modified; + case pcmk_err_cib_backup: return pcmk_rc_cib_backup; + case pcmk_err_cib_save: return pcmk_rc_cib_save; + case pcmk_err_cib_corrupt: return pcmk_rc_cib_corrupt; + case pcmk_err_multiple: return pcmk_rc_multiple; + case pcmk_err_node_unknown: return pcmk_rc_node_unknown; + case pcmk_err_already: return pcmk_rc_already; + case pcmk_err_bad_nvpair: return pcmk_rc_bad_nvpair; + case pcmk_err_unknown_format: return pcmk_rc_unknown_format; + case pcmk_err_generic: return pcmk_rc_error; + case pcmk_ok: return pcmk_rc_ok; + default: return legacy_rc; // system errno + } +} + +// Exit status codes + +const char * +crm_exit_name(crm_exit_t exit_code) +{ + switch (exit_code) { + case CRM_EX_OK: return "CRM_EX_OK"; + case CRM_EX_ERROR: return "CRM_EX_ERROR"; + case CRM_EX_INVALID_PARAM: return "CRM_EX_INVALID_PARAM"; + case CRM_EX_UNIMPLEMENT_FEATURE: return "CRM_EX_UNIMPLEMENT_FEATURE"; + case CRM_EX_INSUFFICIENT_PRIV: return "CRM_EX_INSUFFICIENT_PRIV"; + case CRM_EX_NOT_INSTALLED: return "CRM_EX_NOT_INSTALLED"; + case CRM_EX_NOT_CONFIGURED: return "CRM_EX_NOT_CONFIGURED"; + case CRM_EX_NOT_RUNNING: return "CRM_EX_NOT_RUNNING"; + case CRM_EX_PROMOTED: return "CRM_EX_PROMOTED"; + case CRM_EX_FAILED_PROMOTED: return "CRM_EX_FAILED_PROMOTED"; + case CRM_EX_USAGE: return "CRM_EX_USAGE"; + case CRM_EX_DATAERR: return "CRM_EX_DATAERR"; + case CRM_EX_NOINPUT: return "CRM_EX_NOINPUT"; + case CRM_EX_NOUSER: return "CRM_EX_NOUSER"; + case CRM_EX_NOHOST: return "CRM_EX_NOHOST"; + case CRM_EX_UNAVAILABLE: return "CRM_EX_UNAVAILABLE"; + case CRM_EX_SOFTWARE: return "CRM_EX_SOFTWARE"; + case CRM_EX_OSERR: return "CRM_EX_OSERR"; + case CRM_EX_OSFILE: return "CRM_EX_OSFILE"; + case CRM_EX_CANTCREAT: return "CRM_EX_CANTCREAT"; + case CRM_EX_IOERR: return "CRM_EX_IOERR"; + case CRM_EX_TEMPFAIL: return "CRM_EX_TEMPFAIL"; + case CRM_EX_PROTOCOL: return "CRM_EX_PROTOCOL"; + case CRM_EX_NOPERM: return "CRM_EX_NOPERM"; + case CRM_EX_CONFIG: return "CRM_EX_CONFIG"; + case CRM_EX_FATAL: return "CRM_EX_FATAL"; + case CRM_EX_PANIC: return "CRM_EX_PANIC"; + case CRM_EX_DISCONNECT: return "CRM_EX_DISCONNECT"; + case CRM_EX_DIGEST: return "CRM_EX_DIGEST"; + case CRM_EX_NOSUCH: return "CRM_EX_NOSUCH"; + case CRM_EX_QUORUM: return "CRM_EX_QUORUM"; + case CRM_EX_UNSAFE: return "CRM_EX_UNSAFE"; + case CRM_EX_EXISTS: return "CRM_EX_EXISTS"; + case CRM_EX_MULTIPLE: return "CRM_EX_MULTIPLE"; + case CRM_EX_EXPIRED: return "CRM_EX_EXPIRED"; + case CRM_EX_NOT_YET_IN_EFFECT: return "CRM_EX_NOT_YET_IN_EFFECT"; + case CRM_EX_INDETERMINATE: return "CRM_EX_INDETERMINATE"; + case CRM_EX_UNSATISFIED: return "CRM_EX_UNSATISFIED"; + case CRM_EX_OLD: return "CRM_EX_OLD"; + case CRM_EX_TIMEOUT: return "CRM_EX_TIMEOUT"; + case CRM_EX_DEGRADED: return "CRM_EX_DEGRADED"; + case CRM_EX_DEGRADED_PROMOTED: return "CRM_EX_DEGRADED_PROMOTED"; + case CRM_EX_NONE: return "CRM_EX_NONE"; + case CRM_EX_MAX: return "CRM_EX_UNKNOWN"; + } + return "CRM_EX_UNKNOWN"; +} + +const char * +crm_exit_str(crm_exit_t exit_code) +{ + switch (exit_code) { + case CRM_EX_OK: return "OK"; + case CRM_EX_ERROR: return "Error occurred"; + case CRM_EX_INVALID_PARAM: return "Invalid parameter"; + case CRM_EX_UNIMPLEMENT_FEATURE: return "Unimplemented"; + case CRM_EX_INSUFFICIENT_PRIV: return "Insufficient privileges"; + case CRM_EX_NOT_INSTALLED: return "Not installed"; + case CRM_EX_NOT_CONFIGURED: return "Not configured"; + case CRM_EX_NOT_RUNNING: return "Not running"; + case CRM_EX_PROMOTED: return "Promoted"; + case CRM_EX_FAILED_PROMOTED: return "Failed in promoted role"; + case CRM_EX_USAGE: return "Incorrect usage"; + case CRM_EX_DATAERR: return "Invalid data given"; + case CRM_EX_NOINPUT: return "Input file not available"; + case CRM_EX_NOUSER: return "User does not exist"; + case CRM_EX_NOHOST: return "Host does not exist"; + case CRM_EX_UNAVAILABLE: return "Necessary service unavailable"; + case CRM_EX_SOFTWARE: return "Internal software bug"; + case CRM_EX_OSERR: return "Operating system error occurred"; + case CRM_EX_OSFILE: return "System file not available"; + case CRM_EX_CANTCREAT: return "Cannot create output file"; + case CRM_EX_IOERR: return "I/O error occurred"; + case CRM_EX_TEMPFAIL: return "Temporary failure, try again"; + case CRM_EX_PROTOCOL: return "Protocol violated"; + case CRM_EX_NOPERM: return "Insufficient privileges"; + case CRM_EX_CONFIG: return "Invalid configuration"; + case CRM_EX_FATAL: return "Fatal error occurred, will not respawn"; + case CRM_EX_PANIC: return "System panic required"; + case CRM_EX_DISCONNECT: return "Not connected"; + case CRM_EX_DIGEST: return "Digest mismatch"; + case CRM_EX_NOSUCH: return "No such object"; + case CRM_EX_QUORUM: return "Quorum required"; + case CRM_EX_UNSAFE: return "Operation not safe"; + case CRM_EX_EXISTS: return "Requested item already exists"; + case CRM_EX_MULTIPLE: return "Multiple items match request"; + case CRM_EX_EXPIRED: return "Requested item has expired"; + case CRM_EX_NOT_YET_IN_EFFECT: return "Requested item is not yet in effect"; + case CRM_EX_INDETERMINATE: return "Could not determine status"; + case CRM_EX_UNSATISFIED: return "Not applicable under current conditions"; + case CRM_EX_OLD: return "Update was older than existing configuration"; + case CRM_EX_TIMEOUT: return "Timeout occurred"; + case CRM_EX_DEGRADED: return "Service is active but might fail soon"; + case CRM_EX_DEGRADED_PROMOTED: return "Service is promoted but might fail soon"; + case CRM_EX_NONE: return "No exit status available"; + case CRM_EX_MAX: return "Error occurred"; + } + if ((exit_code > 128) && (exit_code < CRM_EX_MAX)) { + return "Interrupted by signal"; + } + return "Unknown exit status"; +} + +/*! + * \brief Map a function return code to the most similar exit code + * + * \param[in] rc Function return code + * + * \return Most similar exit code + */ +crm_exit_t +pcmk_rc2exitc(int rc) +{ + switch (rc) { + case pcmk_rc_ok: + case pcmk_rc_no_output: // quiet mode, or nothing to output + return CRM_EX_OK; + + case pcmk_rc_no_quorum: + return CRM_EX_QUORUM; + + case pcmk_rc_old_data: + return CRM_EX_OLD; + + case pcmk_rc_schema_validation: + case pcmk_rc_transform_failed: + case pcmk_rc_unpack_error: + return CRM_EX_CONFIG; + + case pcmk_rc_bad_nvpair: + return CRM_EX_INVALID_PARAM; + + case EACCES: + return CRM_EX_INSUFFICIENT_PRIV; + + case EBADF: + case EINVAL: + case EFAULT: + case ENOSYS: + case EOVERFLOW: + case pcmk_rc_underflow: + return CRM_EX_SOFTWARE; + + case EBADMSG: + case EMSGSIZE: + case ENOMSG: + case ENOPROTOOPT: + case EPROTO: + case EPROTONOSUPPORT: + case EPROTOTYPE: + return CRM_EX_PROTOCOL; + + case ECOMM: + case ENOMEM: + return CRM_EX_OSERR; + + case ECONNABORTED: + case ECONNREFUSED: + case ECONNRESET: + case ENOTCONN: + return CRM_EX_DISCONNECT; + + case EEXIST: + case pcmk_rc_already: + return CRM_EX_EXISTS; + + case EIO: + case pcmk_rc_dot_error: + case pcmk_rc_graph_error: + return CRM_EX_IOERR; + + case ENOTSUP: +#if EOPNOTSUPP != ENOTSUP + case EOPNOTSUPP: +#endif + return CRM_EX_UNIMPLEMENT_FEATURE; + + case ENOTUNIQ: + case pcmk_rc_multiple: + return CRM_EX_MULTIPLE; + + case ENODEV: + case ENOENT: + case ENXIO: + case pcmk_rc_unknown_format: + return CRM_EX_NOSUCH; + + case pcmk_rc_node_unknown: + return CRM_EX_NOHOST; + + case ETIME: + case ETIMEDOUT: + return CRM_EX_TIMEOUT; + + case EAGAIN: + case EBUSY: + return CRM_EX_UNSATISFIED; + + case pcmk_rc_before_range: + return CRM_EX_NOT_YET_IN_EFFECT; + + case pcmk_rc_after_range: + return CRM_EX_EXPIRED; + + case pcmk_rc_undetermined: + return CRM_EX_INDETERMINATE; + + case pcmk_rc_op_unsatisfied: + return CRM_EX_UNSATISFIED; + + case pcmk_rc_within_range: + return CRM_EX_OK; + + case pcmk_rc_no_input: + return CRM_EX_NOINPUT; + + case pcmk_rc_duplicate_id: + return CRM_EX_MULTIPLE; + + case pcmk_rc_bad_input: + case pcmk_rc_bad_xml_patch: + return CRM_EX_DATAERR; + + default: + return CRM_EX_ERROR; + } +} + +/*! + * \brief Map a function return code to the most similar OCF exit code + * + * \param[in] rc Function return code + * + * \return Most similar OCF exit code + */ +enum ocf_exitcode +pcmk_rc2ocf(int rc) +{ + switch (rc) { + case pcmk_rc_ok: + return PCMK_OCF_OK; + + case pcmk_rc_bad_nvpair: + return PCMK_OCF_INVALID_PARAM; + + case EACCES: + return PCMK_OCF_INSUFFICIENT_PRIV; + + case ENOTSUP: +#if EOPNOTSUPP != ENOTSUP + case EOPNOTSUPP: +#endif + return PCMK_OCF_UNIMPLEMENT_FEATURE; + + default: + return PCMK_OCF_UNKNOWN_ERROR; + } +} + + +// Other functions + +const char * +bz2_strerror(int rc) +{ + // See ftp://sources.redhat.com/pub/bzip2/docs/manual_3.html#SEC17 + switch (rc) { + case BZ_OK: + case BZ_RUN_OK: + case BZ_FLUSH_OK: + case BZ_FINISH_OK: + case BZ_STREAM_END: + return "Ok"; + case BZ_CONFIG_ERROR: + return "libbz2 has been improperly compiled on your platform"; + case BZ_SEQUENCE_ERROR: + return "library functions called in the wrong order"; + case BZ_PARAM_ERROR: + return "parameter is out of range or otherwise incorrect"; + case BZ_MEM_ERROR: + return "memory allocation failed"; + case BZ_DATA_ERROR: + return "data integrity error is detected during decompression"; + case BZ_DATA_ERROR_MAGIC: + return "the compressed stream does not start with the correct magic bytes"; + case BZ_IO_ERROR: + return "error reading or writing in the compressed file"; + case BZ_UNEXPECTED_EOF: + return "compressed file finishes before the logical end of stream is detected"; + case BZ_OUTBUFF_FULL: + return "output data will not fit into the buffer provided"; + } + return "Data compression error"; +} + +crm_exit_t +crm_exit(crm_exit_t rc) +{ + /* A compiler could theoretically use any type for crm_exit_t, but an int + * should always hold it, so cast to int to keep static analysis happy. + */ + if ((((int) rc) < 0) || (((int) rc) > CRM_EX_MAX)) { + rc = CRM_EX_ERROR; + } + + mainloop_cleanup(); + crm_xml_cleanup(); + + free(pcmk__our_nodename); + + if (crm_system_name) { + crm_info("Exiting %s " CRM_XS " with status %d", crm_system_name, rc); + free(crm_system_name); + } else { + crm_trace("Exiting with status %d", rc); + } + pcmk__free_common_logger(); + qb_log_fini(); // Don't log anything after this point + + exit(rc); +} + +/* + * External action results + */ + +/*! + * \internal + * \brief Set the result of an action + * + * \param[out] result Where to set action result + * \param[in] exit_status OCF exit status to set + * \param[in] exec_status Execution status to set + * \param[in] exit_reason Human-friendly description of event to set + */ +void +pcmk__set_result(pcmk__action_result_t *result, int exit_status, + enum pcmk_exec_status exec_status, const char *exit_reason) +{ + if (result == NULL) { + return; + } + + result->exit_status = exit_status; + result->execution_status = exec_status; + + if (!pcmk__str_eq(result->exit_reason, exit_reason, pcmk__str_none)) { + free(result->exit_reason); + result->exit_reason = (exit_reason == NULL)? NULL : strdup(exit_reason); + } +} + + +/*! + * \internal + * \brief Set the result of an action, with a formatted exit reason + * + * \param[out] result Where to set action result + * \param[in] exit_status OCF exit status to set + * \param[in] exec_status Execution status to set + * \param[in] format printf-style format for a human-friendly + * description of reason for result + * \param[in] ... arguments for \p format + */ +G_GNUC_PRINTF(4, 5) +void +pcmk__format_result(pcmk__action_result_t *result, int exit_status, + enum pcmk_exec_status exec_status, + const char *format, ...) +{ + va_list ap; + int len = 0; + char *reason = NULL; + + if (result == NULL) { + return; + } + + result->exit_status = exit_status; + result->execution_status = exec_status; + + if (format != NULL) { + va_start(ap, format); + len = vasprintf(&reason, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + } + free(result->exit_reason); + result->exit_reason = reason; +} + +/*! + * \internal + * \brief Set the output of an action + * + * \param[out] result Action result to set output for + * \param[in] out Action output to set (must be dynamically + * allocated) + * \param[in] err Action error output to set (must be dynamically + * allocated) + * + * \note \p result will take ownership of \p out and \p err, so the caller + * should not free them. + */ +void +pcmk__set_result_output(pcmk__action_result_t *result, char *out, char *err) +{ + if (result == NULL) { + return; + } + + free(result->action_stdout); + result->action_stdout = out; + + free(result->action_stderr); + result->action_stderr = err; +} + +/*! + * \internal + * \brief Clear a result's exit reason, output, and error output + * + * \param[in,out] result Result to reset + */ +void +pcmk__reset_result(pcmk__action_result_t *result) +{ + if (result == NULL) { + return; + } + + free(result->exit_reason); + result->exit_reason = NULL; + + free(result->action_stdout); + result->action_stdout = NULL; + + free(result->action_stderr); + result->action_stderr = NULL; +} + +/*! + * \internal + * \brief Copy the result of an action + * + * \param[in] src Result to copy + * \param[out] dst Where to copy \p src to + */ +void +pcmk__copy_result(const pcmk__action_result_t *src, pcmk__action_result_t *dst) +{ + CRM_CHECK((src != NULL) && (dst != NULL), return); + dst->exit_status = src->exit_status; + dst->execution_status = src->execution_status; + pcmk__str_update(&dst->exit_reason, src->exit_reason); + pcmk__str_update(&dst->action_stdout, src->action_stdout); + pcmk__str_update(&dst->action_stderr, src->action_stderr); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +crm_exit_t +crm_errno2exit(int rc) +{ + return pcmk_rc2exitc(pcmk_legacy2rc(rc)); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/schemas.c b/lib/common/schemas.c new file mode 100644 index 0000000..88a3051 --- /dev/null +++ b/lib/common/schemas.c @@ -0,0 +1,1303 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include /* PCMK__XML_LOG_BASE */ + +typedef struct { + unsigned char v[2]; +} schema_version_t; + +#define SCHEMA_ZERO { .v = { 0, 0 } } + +#define schema_scanf(s, prefix, version, suffix) \ + sscanf((s), prefix "%hhu.%hhu" suffix, &((version).v[0]), &((version).v[1])) + +#define schema_strdup_printf(prefix, version, suffix) \ + crm_strdup_printf(prefix "%u.%u" suffix, (version).v[0], (version).v[1]) + +typedef struct { + xmlRelaxNGPtr rng; + xmlRelaxNGValidCtxtPtr valid; + xmlRelaxNGParserCtxtPtr parser; +} relaxng_ctx_cache_t; + +enum schema_validator_e { + schema_validator_none, + schema_validator_rng +}; + +struct schema_s { + char *name; + char *transform; + void *cache; + enum schema_validator_e validator; + int after_transform; + schema_version_t version; + char *transform_enter; + bool transform_onleave; +}; + +static struct schema_s *known_schemas = NULL; +static int xml_schema_max = 0; +static bool silent_logging = FALSE; + +static void +xml_log(int priority, const char *fmt, ...) +G_GNUC_PRINTF(2, 3); + +static void +xml_log(int priority, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (silent_logging == FALSE) { + /* XXX should not this enable dechunking as well? */ + PCMK__XML_LOG_BASE(priority, FALSE, 0, NULL, fmt, ap); + } + va_end(ap); +} + +static int +xml_latest_schema_index(void) +{ + // @COMPAT: pacemaker-next is deprecated since 2.1.5 + return xml_schema_max - 3; // index from 0, ignore "pacemaker-next"/"none" +} + +static int +xml_minimum_schema_index(void) +{ + static int best = 0; + if (best == 0) { + int lpc = 0; + + best = xml_latest_schema_index(); + for (lpc = best; lpc > 0; lpc--) { + if (known_schemas[lpc].version.v[0] + < known_schemas[best].version.v[0]) { + return best; + } else { + best = lpc; + } + } + best = xml_latest_schema_index(); + } + return best; +} + +const char * +xml_latest_schema(void) +{ + return get_schema_name(xml_latest_schema_index()); +} + +static inline bool +version_from_filename(const char *filename, schema_version_t *version) +{ + int rc = schema_scanf(filename, "pacemaker-", *version, ".rng"); + + return (rc == 2); +} + +static int +schema_filter(const struct dirent *a) +{ + int rc = 0; + schema_version_t version = SCHEMA_ZERO; + + if (strstr(a->d_name, "pacemaker-") != a->d_name) { + /* crm_trace("%s - wrong prefix", a->d_name); */ + + } else if (!pcmk__ends_with_ext(a->d_name, ".rng")) { + /* crm_trace("%s - wrong suffix", a->d_name); */ + + } else if (!version_from_filename(a->d_name, &version)) { + /* crm_trace("%s - wrong format", a->d_name); */ + + } else { + /* crm_debug("%s - candidate", a->d_name); */ + rc = 1; + } + + return rc; +} + +static int +schema_sort(const struct dirent **a, const struct dirent **b) +{ + schema_version_t a_version = SCHEMA_ZERO; + schema_version_t b_version = SCHEMA_ZERO; + + if (!version_from_filename(a[0]->d_name, &a_version) + || !version_from_filename(b[0]->d_name, &b_version)) { + // Shouldn't be possible, but makes static analysis happy + return 0; + } + + for (int i = 0; i < 2; ++i) { + if (a_version.v[i] < b_version.v[i]) { + return -1; + } else if (a_version.v[i] > b_version.v[i]) { + return 1; + } + } + return 0; +} + +/*! + * \internal + * \brief Add given schema + auxiliary data to internal bookkeeping. + * + * \note When providing \p version, should not be called directly but + * through \c add_schema_by_version. + */ +static void +add_schema(enum schema_validator_e validator, const schema_version_t *version, + const char *name, const char *transform, + const char *transform_enter, bool transform_onleave, + int after_transform) +{ + int last = xml_schema_max; + bool have_version = FALSE; + + xml_schema_max++; + known_schemas = pcmk__realloc(known_schemas, + xml_schema_max * sizeof(struct schema_s)); + CRM_ASSERT(known_schemas != NULL); + memset(known_schemas+last, 0, sizeof(struct schema_s)); + known_schemas[last].validator = validator; + known_schemas[last].after_transform = after_transform; + + for (int i = 0; i < 2; ++i) { + known_schemas[last].version.v[i] = version->v[i]; + if (version->v[i]) { + have_version = TRUE; + } + } + if (have_version) { + known_schemas[last].name = schema_strdup_printf("pacemaker-", *version, ""); + } else { + CRM_ASSERT(name); + schema_scanf(name, "%*[^-]-", known_schemas[last].version, ""); + known_schemas[last].name = strdup(name); + } + + if (transform) { + known_schemas[last].transform = strdup(transform); + } + if (transform_enter) { + known_schemas[last].transform_enter = strdup(transform_enter); + } + known_schemas[last].transform_onleave = transform_onleave; + if (after_transform == 0) { + after_transform = xml_schema_max; /* upgrade is a one-way */ + } + known_schemas[last].after_transform = after_transform; + + if (known_schemas[last].after_transform < 0) { + crm_debug("Added supported schema %d: %s", + last, known_schemas[last].name); + + } else if (known_schemas[last].transform) { + crm_debug("Added supported schema %d: %s (upgrades to %d with %s.xsl)", + last, known_schemas[last].name, + known_schemas[last].after_transform, + known_schemas[last].transform); + + } else { + crm_debug("Added supported schema %d: %s (upgrades to %d)", + last, known_schemas[last].name, + known_schemas[last].after_transform); + } +} + +/*! + * \internal + * \brief Add version-specified schema + auxiliary data to internal bookkeeping. + * \return Standard Pacemaker return value (the only possible values are + * \c ENOENT when no upgrade schema is associated, or \c pcmk_rc_ok otherwise. + * + * \note There's no reliance on the particular order of schemas entering here. + * + * \par A bit of theory + * We track 3 XSLT stylesheets that differ per usage: + * - "upgrade": + * . sparsely spread over the sequence of all available schemas, + * as they are only relevant when major version of the schema + * is getting bumped -- in that case, it MUST be set + * . name convention: upgrade-X.Y.xsl + * - "upgrade-enter": + * . may only accompany "upgrade" occurrence, but doesn't need to + * be present anytime such one is, i.e., it MAY not be set when + * "upgrade" is + * . name convention: upgrade-X.Y-enter.xsl, + * when not present: upgrade-enter.xsl + * - "upgrade-leave": + * . like "upgrade-enter", but SHOULD be present whenever + * "upgrade-enter" is (and vice versa, but that's only + * to prevent confusion based on observing the files, + * it would get ignored regardless) + * . name convention: (see "upgrade-enter") + */ +static int +add_schema_by_version(const schema_version_t *version, int next, + bool transform_expected) +{ + bool transform_onleave = FALSE; + int rc = pcmk_rc_ok; + struct stat s; + char *xslt = NULL, + *transform_upgrade = NULL, + *transform_enter = NULL; + + /* prologue for further transform_expected handling */ + if (transform_expected) { + /* check if there's suitable "upgrade" stylesheet */ + transform_upgrade = schema_strdup_printf("upgrade-", *version, ); + xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, + transform_upgrade); + } + + if (!transform_expected) { + /* jump directly to the end */ + + } else if (stat(xslt, &s) == 0) { + /* perhaps there's also a targeted "upgrade-enter" stylesheet */ + transform_enter = schema_strdup_printf("upgrade-", *version, "-enter"); + free(xslt); + xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, + transform_enter); + if (stat(xslt, &s) != 0) { + /* or initially, at least a generic one */ + crm_debug("Upgrade-enter transform %s.xsl not found", xslt); + free(xslt); + free(transform_enter); + transform_enter = strdup("upgrade-enter"); + xslt = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, + transform_enter); + if (stat(xslt, &s) != 0) { + crm_debug("Upgrade-enter transform %s.xsl not found, either", xslt); + free(xslt); + xslt = NULL; + } + } + /* xslt contains full path to "upgrade-enter" stylesheet */ + if (xslt != NULL) { + /* then there should be "upgrade-leave" counterpart (enter->leave) */ + memcpy(strrchr(xslt, '-') + 1, "leave", sizeof("leave") - 1); + transform_onleave = (stat(xslt, &s) == 0); + free(xslt); + } else { + free(transform_enter); + transform_enter = NULL; + } + + } else { + crm_err("Upgrade transform %s not found", xslt); + free(xslt); + free(transform_upgrade); + transform_upgrade = NULL; + next = -1; + rc = ENOENT; + } + + add_schema(schema_validator_rng, version, NULL, + transform_upgrade, transform_enter, transform_onleave, next); + + free(transform_upgrade); + free(transform_enter); + + return rc; +} + +static void +wrap_libxslt(bool finalize) +{ + static xsltSecurityPrefsPtr secprefs; + int ret = 0; + + /* security framework preferences */ + if (!finalize) { + CRM_ASSERT(secprefs == NULL); + secprefs = xsltNewSecurityPrefs(); + ret = xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_FILE, + xsltSecurityForbid) + | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_CREATE_DIRECTORY, + xsltSecurityForbid) + | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_READ_NETWORK, + xsltSecurityForbid) + | xsltSetSecurityPrefs(secprefs, XSLT_SECPREF_WRITE_NETWORK, + xsltSecurityForbid); + if (ret != 0) { + return; + } + } else { + xsltFreeSecurityPrefs(secprefs); + secprefs = NULL; + } + + /* cleanup only */ + if (finalize) { + xsltCleanupGlobals(); + } +} + +/*! + * \internal + * \brief Load pacemaker schemas into cache + * + * \note This currently also serves as an entry point for the + * generic initialization of the libxslt library. + */ +void +crm_schema_init(void) +{ + int lpc, max; + char *base = pcmk__xml_artefact_root(pcmk__xml_artefact_ns_legacy_rng); + struct dirent **namelist = NULL; + const schema_version_t zero = SCHEMA_ZERO; + + wrap_libxslt(false); + + max = scandir(base, &namelist, schema_filter, schema_sort); + if (max < 0) { + crm_notice("scandir(%s) failed: %s (%d)", base, strerror(errno), errno); + free(base); + + } else { + free(base); + for (lpc = 0; lpc < max; lpc++) { + bool transform_expected = FALSE; + int next = 0; + schema_version_t version = SCHEMA_ZERO; + + if (!version_from_filename(namelist[lpc]->d_name, &version)) { + // Shouldn't be possible, but makes static analysis happy + crm_err("Skipping schema '%s': could not parse version", + namelist[lpc]->d_name); + continue; + } + if ((lpc + 1) < max) { + schema_version_t next_version = SCHEMA_ZERO; + + if (version_from_filename(namelist[lpc+1]->d_name, &next_version) + && (version.v[0] < next_version.v[0])) { + transform_expected = TRUE; + } + + } else { + next = -1; + } + if (add_schema_by_version(&version, next, transform_expected) + == ENOENT) { + break; + } + } + + for (lpc = 0; lpc < max; lpc++) { + free(namelist[lpc]); + } + free(namelist); + } + + // @COMPAT: Deprecated since 2.1.5 + add_schema(schema_validator_rng, &zero, "pacemaker-next", + NULL, NULL, FALSE, -1); + + add_schema(schema_validator_none, &zero, PCMK__VALUE_NONE, + NULL, NULL, FALSE, -1); +} + +#if 0 +static void +relaxng_invalid_stderr(void *userData, xmlErrorPtr error) +{ + /* + Structure xmlError + struct _xmlError { + int domain : What part of the library raised this er + int code : The error code, e.g. an xmlParserError + char * message : human-readable informative error messag + xmlErrorLevel level : how consequent is the error + char * file : the filename + int line : the line number if available + char * str1 : extra string information + char * str2 : extra string information + char * str3 : extra string information + int int1 : extra number information + int int2 : column number of the error or 0 if N/A + void * ctxt : the parser context if available + void * node : the node in the tree + } + */ + crm_err("Structured error: line=%d, level=%d %s", error->line, error->level, error->message); +} +#endif + +static gboolean +validate_with_relaxng(xmlDocPtr doc, gboolean to_logs, const char *relaxng_file, + relaxng_ctx_cache_t **cached_ctx) +{ + int rc = 0; + gboolean valid = TRUE; + relaxng_ctx_cache_t *ctx = NULL; + + CRM_CHECK(doc != NULL, return FALSE); + CRM_CHECK(relaxng_file != NULL, return FALSE); + + if (cached_ctx && *cached_ctx) { + ctx = *cached_ctx; + + } else { + crm_debug("Creating RNG parser context"); + ctx = calloc(1, sizeof(relaxng_ctx_cache_t)); + + xmlLoadExtDtdDefaultValue = 1; + ctx->parser = xmlRelaxNGNewParserCtxt(relaxng_file); + CRM_CHECK(ctx->parser != NULL, goto cleanup); + + if (to_logs) { + xmlRelaxNGSetParserErrors(ctx->parser, + (xmlRelaxNGValidityErrorFunc) xml_log, + (xmlRelaxNGValidityWarningFunc) xml_log, + GUINT_TO_POINTER(LOG_ERR)); + } else { + xmlRelaxNGSetParserErrors(ctx->parser, + (xmlRelaxNGValidityErrorFunc) fprintf, + (xmlRelaxNGValidityWarningFunc) fprintf, + stderr); + } + + ctx->rng = xmlRelaxNGParse(ctx->parser); + CRM_CHECK(ctx->rng != NULL, + crm_err("Could not find/parse %s", relaxng_file); + goto cleanup); + + ctx->valid = xmlRelaxNGNewValidCtxt(ctx->rng); + CRM_CHECK(ctx->valid != NULL, goto cleanup); + + if (to_logs) { + xmlRelaxNGSetValidErrors(ctx->valid, + (xmlRelaxNGValidityErrorFunc) xml_log, + (xmlRelaxNGValidityWarningFunc) xml_log, + GUINT_TO_POINTER(LOG_ERR)); + } else { + xmlRelaxNGSetValidErrors(ctx->valid, + (xmlRelaxNGValidityErrorFunc) fprintf, + (xmlRelaxNGValidityWarningFunc) fprintf, + stderr); + } + } + + /* xmlRelaxNGSetValidStructuredErrors( */ + /* valid, relaxng_invalid_stderr, valid); */ + + xmlLineNumbersDefault(1); + rc = xmlRelaxNGValidateDoc(ctx->valid, doc); + if (rc > 0) { + valid = FALSE; + + } else if (rc < 0) { + crm_err("Internal libxml error during validation"); + } + + cleanup: + + if (cached_ctx) { + *cached_ctx = ctx; + + } else { + if (ctx->parser != NULL) { + xmlRelaxNGFreeParserCtxt(ctx->parser); + } + if (ctx->valid != NULL) { + xmlRelaxNGFreeValidCtxt(ctx->valid); + } + if (ctx->rng != NULL) { + xmlRelaxNGFree(ctx->rng); + } + free(ctx); + } + + return valid; +} + +/*! + * \internal + * \brief Clean up global memory associated with XML schemas + */ +void +crm_schema_cleanup(void) +{ + int lpc; + relaxng_ctx_cache_t *ctx = NULL; + + for (lpc = 0; lpc < xml_schema_max; lpc++) { + + switch (known_schemas[lpc].validator) { + case schema_validator_none: // not cached + break; + case schema_validator_rng: // cached + ctx = (relaxng_ctx_cache_t *) known_schemas[lpc].cache; + if (ctx == NULL) { + break; + } + if (ctx->parser != NULL) { + xmlRelaxNGFreeParserCtxt(ctx->parser); + } + if (ctx->valid != NULL) { + xmlRelaxNGFreeValidCtxt(ctx->valid); + } + if (ctx->rng != NULL) { + xmlRelaxNGFree(ctx->rng); + } + free(ctx); + known_schemas[lpc].cache = NULL; + break; + } + free(known_schemas[lpc].name); + free(known_schemas[lpc].transform); + free(known_schemas[lpc].transform_enter); + } + free(known_schemas); + known_schemas = NULL; + + wrap_libxslt(true); +} + +static gboolean +validate_with(xmlNode *xml, int method, gboolean to_logs) +{ + xmlDocPtr doc = NULL; + gboolean valid = FALSE; + char *file = NULL; + + if (method < 0) { + return FALSE; + } + + if (known_schemas[method].validator == schema_validator_none) { + return TRUE; + } + + CRM_CHECK(xml != NULL, return FALSE); + + if (pcmk__str_eq(known_schemas[method].name, "pacemaker-next", + pcmk__str_none)) { + crm_warn("The pacemaker-next schema is deprecated and will be removed " + "in a future release."); + } + + doc = getDocPtr(xml); + file = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_rng, + known_schemas[method].name); + + crm_trace("Validating with %s (type=%d)", + pcmk__s(file, "missing schema"), known_schemas[method].validator); + switch (known_schemas[method].validator) { + case schema_validator_rng: + valid = + validate_with_relaxng(doc, to_logs, file, + (relaxng_ctx_cache_t **) & (known_schemas[method].cache)); + break; + default: + crm_err("Unknown validator type: %d", + known_schemas[method].validator); + break; + } + + free(file); + return valid; +} + +static bool +validate_with_silent(xmlNode *xml, int method) +{ + bool rc, sl_backup = silent_logging; + silent_logging = TRUE; + rc = validate_with(xml, method, TRUE); + silent_logging = sl_backup; + return rc; +} + +static void +dump_file(const char *filename) +{ + + FILE *fp = NULL; + int ch, line = 0; + + CRM_CHECK(filename != NULL, return); + + fp = fopen(filename, "r"); + if (fp == NULL) { + crm_perror(LOG_ERR, "Could not open %s for reading", filename); + return; + } + + fprintf(stderr, "%4d ", ++line); + do { + ch = getc(fp); + if (ch == EOF) { + putc('\n', stderr); + break; + } else if (ch == '\n') { + fprintf(stderr, "\n%4d ", ++line); + } else { + putc(ch, stderr); + } + } while (1); + + fclose(fp); +} + +gboolean +validate_xml_verbose(xmlNode *xml_blob) +{ + int fd = 0; + xmlDoc *doc = NULL; + xmlNode *xml = NULL; + gboolean rc = FALSE; + char *filename = NULL; + + filename = crm_strdup_printf("%s/cib-invalid.XXXXXX", pcmk__get_tmpdir()); + + umask(S_IWGRP | S_IWOTH | S_IROTH); + fd = mkstemp(filename); + write_xml_fd(xml_blob, filename, fd, FALSE); + + dump_file(filename); + + doc = xmlParseFile(filename); + xml = xmlDocGetRootElement(doc); + rc = validate_xml(xml, NULL, FALSE); + free_xml(xml); + + unlink(filename); + free(filename); + + return rc; +} + +gboolean +validate_xml(xmlNode *xml_blob, const char *validation, gboolean to_logs) +{ + int version = 0; + + if (validation == NULL) { + validation = crm_element_value(xml_blob, XML_ATTR_VALIDATION); + } + + if (validation == NULL) { + int lpc = 0; + bool valid = FALSE; + + for (lpc = 0; lpc < xml_schema_max; lpc++) { + if (validate_with(xml_blob, lpc, FALSE)) { + valid = TRUE; + crm_xml_add(xml_blob, XML_ATTR_VALIDATION, + known_schemas[lpc].name); + crm_info("XML validated against %s", known_schemas[lpc].name); + if(known_schemas[lpc].after_transform == 0) { + break; + } + } + } + + return valid; + } + + version = get_schema_version(validation); + if (strcmp(validation, PCMK__VALUE_NONE) == 0) { + return TRUE; + } else if (version < xml_schema_max) { + return validate_with(xml_blob, version, to_logs); + } + + crm_err("Unknown validator: %s", validation); + return FALSE; +} + +static void +cib_upgrade_err(void *ctx, const char *fmt, ...) +G_GNUC_PRINTF(2, 3); + +/* With this arrangement, an attempt to identify the message severity + as explicitly signalled directly from XSLT is performed in rather + a smart way (no reliance on formatting string + arguments being + always specified as ["%s", purposeful_string], as it can also be + ["%s: %s", some_prefix, purposeful_string] etc. so every argument + pertaining %s specifier is investigated), and if such a mark found, + the respective level is determined and, when the messages are to go + to the native logs, the mark itself gets dropped + (by the means of string shift). + + NOTE: whether the native logging is the right sink is decided per + the ctx parameter -- NULL denotes this case, otherwise it + carries a pointer to the numeric expression of the desired + target logging level (messages with higher level will be + suppressed) + + NOTE: on some architectures, this string shift may not have any + effect, but that's an acceptable tradeoff + + The logging level for not explicitly designated messages + (suspicious, likely internal errors or some runaways) is + LOG_WARNING. + */ +static void +cib_upgrade_err(void *ctx, const char *fmt, ...) +{ + va_list ap, aq; + char *arg_cur; + + bool found = FALSE; + const char *fmt_iter = fmt; + uint8_t msg_log_level = LOG_WARNING; /* default for runaway messages */ + const unsigned * log_level = (const unsigned *) ctx; + enum { + escan_seennothing, + escan_seenpercent, + } scan_state = escan_seennothing; + + va_start(ap, fmt); + va_copy(aq, ap); + + while (!found && *fmt_iter != '\0') { + /* while casing schema borrowed from libqb:qb_vsnprintf_serialize */ + switch (*fmt_iter++) { + case '%': + if (scan_state == escan_seennothing) { + scan_state = escan_seenpercent; + } else if (scan_state == escan_seenpercent) { + scan_state = escan_seennothing; + } + break; + case 's': + if (scan_state == escan_seenpercent) { + scan_state = escan_seennothing; + arg_cur = va_arg(aq, char *); + if (arg_cur != NULL) { + switch (arg_cur[0]) { + case 'W': + if (!strncmp(arg_cur, "WARNING: ", + sizeof("WARNING: ") - 1)) { + msg_log_level = LOG_WARNING; + } + if (ctx == NULL) { + memmove(arg_cur, arg_cur + sizeof("WARNING: ") - 1, + strlen(arg_cur + sizeof("WARNING: ") - 1) + 1); + } + found = TRUE; + break; + case 'I': + if (!strncmp(arg_cur, "INFO: ", + sizeof("INFO: ") - 1)) { + msg_log_level = LOG_INFO; + } + if (ctx == NULL) { + memmove(arg_cur, arg_cur + sizeof("INFO: ") - 1, + strlen(arg_cur + sizeof("INFO: ") - 1) + 1); + } + found = TRUE; + break; + case 'D': + if (!strncmp(arg_cur, "DEBUG: ", + sizeof("DEBUG: ") - 1)) { + msg_log_level = LOG_DEBUG; + } + if (ctx == NULL) { + memmove(arg_cur, arg_cur + sizeof("DEBUG: ") - 1, + strlen(arg_cur + sizeof("DEBUG: ") - 1) + 1); + } + found = TRUE; + break; + } + } + } + break; + case '#': case '-': case ' ': case '+': case '\'': case 'I': case '.': + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case '*': + break; + case 'l': + case 'z': + case 't': + case 'j': + case 'd': case 'i': + case 'o': + case 'u': + case 'x': case 'X': + case 'e': case 'E': + case 'f': case 'F': + case 'g': case 'G': + case 'a': case 'A': + case 'c': + case 'p': + if (scan_state == escan_seenpercent) { + (void) va_arg(aq, void *); /* skip forward */ + scan_state = escan_seennothing; + } + break; + default: + scan_state = escan_seennothing; + break; + } + } + + if (log_level != NULL) { + /* intention of the following offset is: + cibadmin -V -> start showing INFO labelled messages */ + if (*log_level + 4 >= msg_log_level) { + vfprintf(stderr, fmt, ap); + } + } else { + PCMK__XML_LOG_BASE(msg_log_level, TRUE, 0, "CIB upgrade: ", fmt, ap); + } + + va_end(aq); + va_end(ap); +} + + +/* Denotes temporary emergency fix for "xmldiff'ing not text-node-ready"; + proper fix is most likely to teach __xml_diff_object and friends to + deal with XML_TEXT_NODE (and more?), i.e., those nodes currently + missing "_private" field (implicitly as NULL) which clashes with + unchecked accesses (e.g. in __xml_offset) -- the outcome may be that + those unexpected XML nodes will simply be ignored for the purpose of + diff'ing, or it may be made more robust, or per the user's preference + (which then may be exposed as crm_diff switch). + + Said XML_TEXT_NODE may appear unexpectedly due to how upgrade-2.10.xsl + is arranged. + + The emergency fix is simple: reparse XSLT output with blank-ignoring + parser. */ +#ifndef PCMK_SCHEMAS_EMERGENCY_XSLT +#define PCMK_SCHEMAS_EMERGENCY_XSLT 1 +#endif + +static xmlNode * +apply_transformation(xmlNode *xml, const char *transform, gboolean to_logs) +{ + char *xform = NULL; + xmlNode *out = NULL; + xmlDocPtr res = NULL; + xmlDocPtr doc = NULL; + xsltStylesheet *xslt = NULL; +#if PCMK_SCHEMAS_EMERGENCY_XSLT != 0 + xmlChar *emergency_result; + int emergency_txt_len; + int emergency_res; +#endif + + CRM_CHECK(xml != NULL, return FALSE); + doc = getDocPtr(xml); + xform = pcmk__xml_artefact_path(pcmk__xml_artefact_ns_legacy_xslt, + transform); + + xmlLoadExtDtdDefaultValue = 1; + xmlSubstituteEntitiesDefault(1); + + /* for capturing, e.g., what's emitted via */ + if (to_logs) { + xsltSetGenericErrorFunc(NULL, cib_upgrade_err); + } else { + xsltSetGenericErrorFunc(&crm_log_level, cib_upgrade_err); + } + + xslt = xsltParseStylesheetFile((pcmkXmlStr) xform); + CRM_CHECK(xslt != NULL, goto cleanup); + + res = xsltApplyStylesheet(xslt, doc, NULL); + CRM_CHECK(res != NULL, goto cleanup); + + xsltSetGenericErrorFunc(NULL, NULL); /* restore default one */ + + +#if PCMK_SCHEMAS_EMERGENCY_XSLT != 0 + emergency_res = xsltSaveResultToString(&emergency_result, + &emergency_txt_len, res, xslt); + xmlFreeDoc(res); + CRM_CHECK(emergency_res == 0, goto cleanup); + out = string2xml((const char *) emergency_result); + free(emergency_result); +#else + out = xmlDocGetRootElement(res); +#endif + + cleanup: + if (xslt) { + xsltFreeStylesheet(xslt); + } + + free(xform); + + return out; +} + +/*! + * \internal + * \brief Possibly full enter->upgrade->leave trip per internal bookkeeping. + * + * \note Only emits warnings about enter/leave phases in case of issues. + */ +static xmlNode * +apply_upgrade(xmlNode *xml, const struct schema_s *schema, gboolean to_logs) +{ + bool transform_onleave = schema->transform_onleave; + char *transform_leave; + xmlNode *upgrade = NULL, + *final = NULL; + + if (schema->transform_enter) { + crm_debug("Upgrading %s-style configuration, pre-upgrade phase with %s.xsl", + schema->name, schema->transform_enter); + upgrade = apply_transformation(xml, schema->transform_enter, to_logs); + if (upgrade == NULL) { + crm_warn("Upgrade-enter transformation %s.xsl failed", + schema->transform_enter); + transform_onleave = FALSE; + } + } + if (upgrade == NULL) { + upgrade = xml; + } + + crm_debug("Upgrading %s-style configuration, main phase with %s.xsl", + schema->name, schema->transform); + final = apply_transformation(upgrade, schema->transform, to_logs); + if (upgrade != xml) { + free_xml(upgrade); + upgrade = NULL; + } + + if (final != NULL && transform_onleave) { + upgrade = final; + /* following condition ensured in add_schema_by_version */ + CRM_ASSERT(schema->transform_enter != NULL); + transform_leave = strdup(schema->transform_enter); + /* enter -> leave */ + memcpy(strrchr(transform_leave, '-') + 1, "leave", sizeof("leave") - 1); + crm_debug("Upgrading %s-style configuration, post-upgrade phase with %s.xsl", + schema->name, transform_leave); + final = apply_transformation(upgrade, transform_leave, to_logs); + if (final == NULL) { + crm_warn("Upgrade-leave transformation %s.xsl failed", transform_leave); + final = upgrade; + } else { + free_xml(upgrade); + } + free(transform_leave); + } + + return final; +} + +const char * +get_schema_name(int version) +{ + if (version < 0 || version >= xml_schema_max) { + return "unknown"; + } + return known_schemas[version].name; +} + +int +get_schema_version(const char *name) +{ + int lpc = 0; + + if (name == NULL) { + name = PCMK__VALUE_NONE; + } + for (; lpc < xml_schema_max; lpc++) { + if (pcmk__str_eq(name, known_schemas[lpc].name, pcmk__str_casei)) { + return lpc; + } + } + return -1; +} + +/* set which validation to use */ +int +update_validation(xmlNode **xml_blob, int *best, int max, gboolean transform, + gboolean to_logs) +{ + xmlNode *xml = NULL; + char *value = NULL; + int max_stable_schemas = xml_latest_schema_index(); + int lpc = 0, match = -1, rc = pcmk_ok; + int next = -1; /* -1 denotes "inactive" value */ + + CRM_CHECK(best != NULL, return -EINVAL); + *best = 0; + + CRM_CHECK(xml_blob != NULL, return -EINVAL); + CRM_CHECK(*xml_blob != NULL, return -EINVAL); + + xml = *xml_blob; + value = crm_element_value_copy(xml, XML_ATTR_VALIDATION); + + if (value != NULL) { + match = get_schema_version(value); + + lpc = match; + if (lpc >= 0 && transform == FALSE) { + *best = lpc++; + + } else if (lpc < 0) { + crm_debug("Unknown validation schema"); + lpc = 0; + } + } + + if (match >= max_stable_schemas) { + /* nothing to do */ + free(value); + *best = match; + return pcmk_ok; + } + + while (lpc <= max_stable_schemas) { + crm_debug("Testing '%s' validation (%d of %d)", + known_schemas[lpc].name ? known_schemas[lpc].name : "", + lpc, max_stable_schemas); + + if (validate_with(xml, lpc, to_logs) == FALSE) { + if (next != -1) { + crm_info("Configuration not valid for schema: %s", + known_schemas[lpc].name); + next = -1; + } else { + crm_trace("%s validation failed", + known_schemas[lpc].name ? known_schemas[lpc].name : ""); + } + if (*best) { + /* we've satisfied the validation, no need to check further */ + break; + } + rc = -pcmk_err_schema_validation; + + } else { + if (next != -1) { + crm_debug("Configuration valid for schema: %s", + known_schemas[next].name); + next = -1; + } + rc = pcmk_ok; + } + + if (rc == pcmk_ok) { + *best = lpc; + } + + if (rc == pcmk_ok && transform) { + xmlNode *upgrade = NULL; + next = known_schemas[lpc].after_transform; + + if (next <= lpc) { + /* There is no next version, or next would regress */ + crm_trace("Stopping at %s", known_schemas[lpc].name); + break; + + } else if (max > 0 && (lpc == max || next > max)) { + crm_trace("Upgrade limit reached at %s (lpc=%d, next=%d, max=%d)", + known_schemas[lpc].name, lpc, next, max); + break; + + } else if (known_schemas[lpc].transform == NULL + /* possibly avoid transforming when readily valid + (in general more restricted when crossing the major + version boundary, as X.0 "transitional" version is + expected to be more strict than it's successors that + may re-allow constructs from previous major line) */ + || validate_with_silent(xml, next)) { + crm_debug("%s-style configuration is also valid for %s", + known_schemas[lpc].name, known_schemas[next].name); + + lpc = next; + + } else { + crm_debug("Upgrading %s-style configuration to %s with %s.xsl", + known_schemas[lpc].name, known_schemas[next].name, + known_schemas[lpc].transform); + + upgrade = apply_upgrade(xml, &known_schemas[lpc], to_logs); + if (upgrade == NULL) { + crm_err("Transformation %s.xsl failed", + known_schemas[lpc].transform); + rc = -pcmk_err_transform_failed; + + } else if (validate_with(upgrade, next, to_logs)) { + crm_info("Transformation %s.xsl successful", + known_schemas[lpc].transform); + lpc = next; + *best = next; + free_xml(xml); + xml = upgrade; + rc = pcmk_ok; + + } else { + crm_err("Transformation %s.xsl did not produce a valid configuration", + known_schemas[lpc].transform); + crm_log_xml_info(upgrade, "transform:bad"); + free_xml(upgrade); + rc = -pcmk_err_schema_validation; + } + next = -1; + } + } + + if (transform == FALSE || rc != pcmk_ok) { + /* we need some progress! */ + lpc++; + } + } + + if (*best > match && *best) { + crm_info("%s the configuration from %s to %s", + transform?"Transformed":"Upgraded", + value ? value : "", known_schemas[*best].name); + crm_xml_add(xml, XML_ATTR_VALIDATION, known_schemas[*best].name); + } + + *xml_blob = xml; + free(value); + return rc; +} + +gboolean +cli_config_update(xmlNode **xml, int *best_version, gboolean to_logs) +{ + gboolean rc = TRUE; + const char *value = crm_element_value(*xml, XML_ATTR_VALIDATION); + char *const orig_value = strdup(value == NULL ? "(none)" : value); + + int version = get_schema_version(value); + int orig_version = version; + int min_version = xml_minimum_schema_index(); + + if (version < min_version) { + // Current configuration schema is not acceptable, try to update + xmlNode *converted = NULL; + + converted = copy_xml(*xml); + update_validation(&converted, &version, 0, TRUE, to_logs); + + value = crm_element_value(converted, XML_ATTR_VALIDATION); + if (version < min_version) { + // Updated configuration schema is still not acceptable + + if (version < orig_version || orig_version == -1) { + // We couldn't validate any schema at all + if (to_logs) { + pcmk__config_err("Cannot upgrade configuration (claiming " + "schema %s) to at least %s because it " + "does not validate with any schema from " + "%s to %s", + orig_value, + get_schema_name(min_version), + get_schema_name(orig_version), + xml_latest_schema()); + } else { + fprintf(stderr, "Cannot upgrade configuration (claiming " + "schema %s) to at least %s because it " + "does not validate with any schema from " + "%s to %s\n", + orig_value, + get_schema_name(min_version), + get_schema_name(orig_version), + xml_latest_schema()); + } + } else { + // We updated configuration successfully, but still too low + if (to_logs) { + pcmk__config_err("Cannot upgrade configuration (claiming " + "schema %s) to at least %s because it " + "would not upgrade past %s", + orig_value, + get_schema_name(min_version), + pcmk__s(value, "unspecified version")); + } else { + fprintf(stderr, "Cannot upgrade configuration (claiming " + "schema %s) to at least %s because it " + "would not upgrade past %s\n", + orig_value, + get_schema_name(min_version), + pcmk__s(value, "unspecified version")); + } + } + + free_xml(converted); + converted = NULL; + rc = FALSE; + + } else { + // Updated configuration schema is acceptable + free_xml(*xml); + *xml = converted; + + if (version < xml_latest_schema_index()) { + if (to_logs) { + pcmk__config_warn("Configuration with schema %s was " + "internally upgraded to acceptable (but " + "not most recent) %s", + orig_value, get_schema_name(version)); + } + } else { + if (to_logs) { + crm_info("Configuration with schema %s was internally " + "upgraded to latest version %s", + orig_value, get_schema_name(version)); + } + } + } + + } else if (version >= get_schema_version(PCMK__VALUE_NONE)) { + // Schema validation is disabled + if (to_logs) { + pcmk__config_warn("Schema validation of configuration is disabled " + "(enabling is encouraged and prevents common " + "misconfigurations)"); + + } else { + fprintf(stderr, "Schema validation of configuration is disabled " + "(enabling is encouraged and prevents common " + "misconfigurations)\n"); + } + } + + if (best_version) { + *best_version = version; + } + + free(orig_value); + return rc; +} diff --git a/lib/common/scores.c b/lib/common/scores.c new file mode 100644 index 0000000..63c314e --- /dev/null +++ b/lib/common/scores.c @@ -0,0 +1,166 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include // snprintf(), NULL +#include // strcpy(), strdup() +#include // size_t + +int pcmk__score_red = 0; +int pcmk__score_green = 0; +int pcmk__score_yellow = 0; + +/*! + * \brief Get the integer value of a score string + * + * Given a string representation of a score, return the integer equivalent. + * This accepts infinity strings as well as red, yellow, and green, and + * bounds the result to +/-INFINITY. + * + * \param[in] score Score as string + * + * \return Integer value corresponding to \p score + */ +int +char2score(const char *score) +{ + if (score == NULL) { + return 0; + + } else if (pcmk_str_is_minus_infinity(score)) { + return -CRM_SCORE_INFINITY; + + } else if (pcmk_str_is_infinity(score)) { + return CRM_SCORE_INFINITY; + + } else if (pcmk__str_eq(score, PCMK__VALUE_RED, pcmk__str_casei)) { + return pcmk__score_red; + + } else if (pcmk__str_eq(score, PCMK__VALUE_YELLOW, pcmk__str_casei)) { + return pcmk__score_yellow; + + } else if (pcmk__str_eq(score, PCMK__VALUE_GREEN, pcmk__str_casei)) { + return pcmk__score_green; + + } else { + long long score_ll; + + pcmk__scan_ll(score, &score_ll, 0LL); + if (score_ll > CRM_SCORE_INFINITY) { + return CRM_SCORE_INFINITY; + + } else if (score_ll < -CRM_SCORE_INFINITY) { + return -CRM_SCORE_INFINITY; + + } else { + return (int) score_ll; + } + } +} + +/*! + * \brief Return a displayable static string for a score value + * + * Given a score value, return a pointer to a static string representation of + * the score suitable for log messages, output, etc. + * + * \param[in] score Score to display + * + * \return Pointer to static memory containing string representation of \p score + * \note Subsequent calls to this function will overwrite the returned value, so + * it should be used only in a local context such as a printf()-style + * statement. + */ +const char * +pcmk_readable_score(int score) +{ + // The longest possible result is "-INFINITY" + static char score_s[sizeof(CRM_MINUS_INFINITY_S)]; + + if (score >= CRM_SCORE_INFINITY) { + strcpy(score_s, CRM_INFINITY_S); + + } else if (score <= -CRM_SCORE_INFINITY) { + strcpy(score_s, CRM_MINUS_INFINITY_S); + + } else { + // Range is limited to +/-1000000, so no chance of overflow + snprintf(score_s, sizeof(score_s), "%d", score); + } + + return score_s; +} + +/*! + * \internal + * \brief Add two scores, bounding to +/-INFINITY + * + * \param[in] score1 First score to add + * \param[in] score2 Second score to add + * + * \note This function does not have context about what the scores mean, so it + * does not log any messages. + */ +int +pcmk__add_scores(int score1, int score2) +{ + /* As long as CRM_SCORE_INFINITY is less than half of the maximum integer, + * we can ignore the possibility of integer overflow. + */ + int result = score1 + score2; + + // First handle the cases where one or both is infinite + if ((score1 <= -CRM_SCORE_INFINITY) || (score2 <= -CRM_SCORE_INFINITY)) { + return -CRM_SCORE_INFINITY; + } + if ((score1 >= CRM_SCORE_INFINITY) || (score2 >= CRM_SCORE_INFINITY)) { + return CRM_SCORE_INFINITY; + } + + // Bound result to infinity. + if (result >= CRM_SCORE_INFINITY) { + return CRM_SCORE_INFINITY; + } + if (result <= -CRM_SCORE_INFINITY) { + return -CRM_SCORE_INFINITY; + } + + return result; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +char * +score2char(int score) +{ + char *result = strdup(pcmk_readable_score(score)); + + CRM_ASSERT(result != NULL); + return result; +} + +char * +score2char_stack(int score, char *buf, size_t len) +{ + CRM_CHECK((buf != NULL) && (len >= sizeof(CRM_MINUS_INFINITY_S)), + return NULL); + strcpy(buf, pcmk_readable_score(score)); + return buf; +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/strings.c b/lib/common/strings.c new file mode 100644 index 0000000..b245102 --- /dev/null +++ b/lib/common/strings.c @@ -0,0 +1,1363 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include "crm/common/results.h" +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include // DBL_MIN +#include +#include +#include + +/*! + * \internal + * \brief Scan a long long integer from a string + * + * \param[in] text String to scan + * \param[out] result If not NULL, where to store scanned value + * \param[in] default_value Value to use if text is NULL or invalid + * \param[out] end_text If not NULL, where to store pointer to first + * non-integer character + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok on success, + * \c EINVAL on failed string conversion due to invalid input, + * or \c EOVERFLOW on arithmetic overflow) + * \note Sets \c errno on error + */ +static int +scan_ll(const char *text, long long *result, long long default_value, + char **end_text) +{ + long long local_result = default_value; + char *local_end_text = NULL; + int rc = pcmk_rc_ok; + + errno = 0; + if (text != NULL) { + local_result = strtoll(text, &local_end_text, 10); + if (errno == ERANGE) { + rc = EOVERFLOW; + crm_warn("Integer parsed from '%s' was clipped to %lld", + text, local_result); + + } else if (errno != 0) { + rc = errno; + local_result = default_value; + crm_warn("Could not parse integer from '%s' (using %lld instead): " + "%s", text, default_value, pcmk_rc_str(rc)); + + } else if (local_end_text == text) { + rc = EINVAL; + local_result = default_value; + crm_warn("Could not parse integer from '%s' (using %lld instead): " + "No digits found", text, default_value); + } + + if ((end_text == NULL) && !pcmk__str_empty(local_end_text)) { + crm_warn("Characters left over after parsing '%s': '%s'", + text, local_end_text); + } + errno = rc; + } + if (end_text != NULL) { + *end_text = local_end_text; + } + if (result != NULL) { + *result = local_result; + } + return rc; +} + +/*! + * \internal + * \brief Scan a long long integer value from a string + * + * \param[in] text The string to scan (may be NULL) + * \param[out] result Where to store result (or NULL to ignore) + * \param[in] default_value Value to use if text is NULL or invalid + * + * \return Standard Pacemaker return code + */ +int +pcmk__scan_ll(const char *text, long long *result, long long default_value) +{ + long long local_result = default_value; + int rc = pcmk_rc_ok; + + if (text != NULL) { + rc = scan_ll(text, &local_result, default_value, NULL); + if (rc != pcmk_rc_ok) { + local_result = default_value; + } + } + if (result != NULL) { + *result = local_result; + } + return rc; +} + +/*! + * \internal + * \brief Scan an integer value from a string, constrained to a minimum + * + * \param[in] text The string to scan (may be NULL) + * \param[out] result Where to store result (or NULL to ignore) + * \param[in] minimum Value to use as default and minimum + * + * \return Standard Pacemaker return code + * \note If the value is larger than the maximum integer, EOVERFLOW will be + * returned and \p result will be set to the maximum integer. + */ +int +pcmk__scan_min_int(const char *text, int *result, int minimum) +{ + int rc; + long long result_ll; + + rc = pcmk__scan_ll(text, &result_ll, (long long) minimum); + + if (result_ll < (long long) minimum) { + crm_warn("Clipped '%s' to minimum acceptable value %d", text, minimum); + result_ll = (long long) minimum; + + } else if (result_ll > INT_MAX) { + crm_warn("Clipped '%s' to maximum integer %d", text, INT_MAX); + result_ll = (long long) INT_MAX; + rc = EOVERFLOW; + } + + if (result != NULL) { + *result = (int) result_ll; + } + return rc; +} + +/*! + * \internal + * \brief Scan a TCP port number from a string + * + * \param[in] text The string to scan + * \param[out] port Where to store result (or NULL to ignore) + * + * \return Standard Pacemaker return code + * \note \p port will be -1 if \p text is NULL or invalid + */ +int +pcmk__scan_port(const char *text, int *port) +{ + long long port_ll; + int rc = pcmk__scan_ll(text, &port_ll, -1LL); + + if ((text != NULL) && (rc == pcmk_rc_ok) // wasn't default or invalid + && ((port_ll < 0LL) || (port_ll > 65535LL))) { + crm_warn("Ignoring port specification '%s' " + "not in valid range (0-65535)", text); + rc = (port_ll < 0LL)? pcmk_rc_before_range : pcmk_rc_after_range; + port_ll = -1LL; + } + if (port != NULL) { + *port = (int) port_ll; + } + return rc; +} + +/*! + * \internal + * \brief Scan a double-precision floating-point value from a string + * + * \param[in] text The string to parse + * \param[out] result Parsed value on success, or + * \c PCMK__PARSE_DBL_DEFAULT on error + * \param[in] default_text Default string to parse if \p text is + * \c NULL + * \param[out] end_text If not \c NULL, where to store a pointer + * to the position immediately after the + * value + * + * \return Standard Pacemaker return code (\c pcmk_rc_ok on success, + * \c EINVAL on failed string conversion due to invalid input, + * \c EOVERFLOW on arithmetic overflow, \c pcmk_rc_underflow + * on arithmetic underflow, or \c errno from \c strtod() on + * other parse errors) + */ +int +pcmk__scan_double(const char *text, double *result, const char *default_text, + char **end_text) +{ + int rc = pcmk_rc_ok; + char *local_end_text = NULL; + + CRM_ASSERT(result != NULL); + *result = PCMK__PARSE_DBL_DEFAULT; + + text = (text != NULL) ? text : default_text; + + if (text == NULL) { + rc = EINVAL; + crm_debug("No text and no default conversion value supplied"); + + } else { + errno = 0; + *result = strtod(text, &local_end_text); + + if (errno == ERANGE) { + /* + * Overflow: strtod() returns +/- HUGE_VAL and sets errno to + * ERANGE + * + * Underflow: strtod() returns "a value whose magnitude is + * no greater than the smallest normalized + * positive" double. Whether ERANGE is set is + * implementation-defined. + */ + const char *over_under; + + if (QB_ABS(*result) > DBL_MIN) { + rc = EOVERFLOW; + over_under = "over"; + } else { + rc = pcmk_rc_underflow; + over_under = "under"; + } + + crm_debug("Floating-point value parsed from '%s' would %sflow " + "(using %g instead)", text, over_under, *result); + + } else if (errno != 0) { + rc = errno; + // strtod() set *result = 0 on parse failure + *result = PCMK__PARSE_DBL_DEFAULT; + + crm_debug("Could not parse floating-point value from '%s' (using " + "%.1f instead): %s", text, PCMK__PARSE_DBL_DEFAULT, + pcmk_rc_str(rc)); + + } else if (local_end_text == text) { + // errno == 0, but nothing was parsed + rc = EINVAL; + *result = PCMK__PARSE_DBL_DEFAULT; + + crm_debug("Could not parse floating-point value from '%s' (using " + "%.1f instead): No digits found", text, + PCMK__PARSE_DBL_DEFAULT); + + } else if (QB_ABS(*result) <= DBL_MIN) { + /* + * errno == 0 and text was parsed, but value might have + * underflowed. + * + * ERANGE might not be set for underflow. Check magnitude + * of *result, but also make sure the input number is not + * actually zero (0 <= DBL_MIN is not underflow). + * + * This check must come last. A parse failure in strtod() + * also sets *result == 0, so a parse failure would match + * this test condition prematurely. + */ + for (const char *p = text; p != local_end_text; p++) { + if (strchr("0.eE", *p) == NULL) { + rc = pcmk_rc_underflow; + crm_debug("Floating-point value parsed from '%s' would " + "underflow (using %g instead)", text, *result); + break; + } + } + + } else { + crm_trace("Floating-point value parsed successfully from " + "'%s': %g", text, *result); + } + + if ((end_text == NULL) && !pcmk__str_empty(local_end_text)) { + crm_debug("Characters left over after parsing '%s': '%s'", + text, local_end_text); + } + } + + if (end_text != NULL) { + *end_text = local_end_text; + } + + return rc; +} + +/*! + * \internal + * \brief Parse a guint from a string stored in a hash table + * + * \param[in] table Hash table to search + * \param[in] key Hash table key to use to retrieve string + * \param[in] default_val What to use if key has no entry in table + * \param[out] result If not NULL, where to store parsed integer + * + * \return Standard Pacemaker return code + */ +int +pcmk__guint_from_hash(GHashTable *table, const char *key, guint default_val, + guint *result) +{ + const char *value; + long long value_ll; + int rc = pcmk_rc_ok; + + CRM_CHECK((table != NULL) && (key != NULL), return EINVAL); + + if (result != NULL) { + *result = default_val; + } + + value = g_hash_table_lookup(table, key); + if (value == NULL) { + return pcmk_rc_ok; + } + + rc = pcmk__scan_ll(value, &value_ll, 0LL); + if (rc != pcmk_rc_ok) { + return rc; + } + + if ((value_ll < 0) || (value_ll > G_MAXUINT)) { + crm_warn("Could not parse non-negative integer from %s", value); + return ERANGE; + } + + if (result != NULL) { + *result = (guint) value_ll; + } + return pcmk_rc_ok; +} + +#ifndef NUMCHARS +# define NUMCHARS "0123456789." +#endif + +#ifndef WHITESPACE +# define WHITESPACE " \t\n\r\f" +#endif + +/*! + * \brief Parse a time+units string and return milliseconds equivalent + * + * \param[in] input String with a number and optional unit (optionally + * with whitespace before and/or after the number). If + * missing, the unit defaults to seconds. + * + * \return Milliseconds corresponding to string expression, or + * PCMK__PARSE_INT_DEFAULT on error + */ +long long +crm_get_msec(const char *input) +{ + const char *num_start = NULL; + const char *units; + long long multiplier = 1000; + long long divisor = 1; + long long msec = PCMK__PARSE_INT_DEFAULT; + size_t num_len = 0; + char *end_text = NULL; + + if (input == NULL) { + return PCMK__PARSE_INT_DEFAULT; + } + + num_start = input + strspn(input, WHITESPACE); + num_len = strspn(num_start, NUMCHARS); + if (num_len < 1) { + return PCMK__PARSE_INT_DEFAULT; + } + units = num_start + num_len; + units += strspn(units, WHITESPACE); + + if (!strncasecmp(units, "ms", 2) || !strncasecmp(units, "msec", 4)) { + multiplier = 1; + divisor = 1; + } else if (!strncasecmp(units, "us", 2) || !strncasecmp(units, "usec", 4)) { + multiplier = 1; + divisor = 1000; + } else if (!strncasecmp(units, "s", 1) || !strncasecmp(units, "sec", 3)) { + multiplier = 1000; + divisor = 1; + } else if (!strncasecmp(units, "m", 1) || !strncasecmp(units, "min", 3)) { + multiplier = 60 * 1000; + divisor = 1; + } else if (!strncasecmp(units, "h", 1) || !strncasecmp(units, "hr", 2)) { + multiplier = 60 * 60 * 1000; + divisor = 1; + } else if ((*units != '\0') && (*units != '\n') && (*units != '\r')) { + return PCMK__PARSE_INT_DEFAULT; + } + + scan_ll(num_start, &msec, PCMK__PARSE_INT_DEFAULT, &end_text); + if (msec > (LLONG_MAX / multiplier)) { + // Arithmetics overflow while multiplier/divisor mutually exclusive + return LLONG_MAX; + } + msec *= multiplier; + msec /= divisor; + return msec; +} + +gboolean +crm_is_true(const char *s) +{ + gboolean ret = FALSE; + + if (s != NULL) { + crm_str_to_boolean(s, &ret); + } + return ret; +} + +int +crm_str_to_boolean(const char *s, int *ret) +{ + if (s == NULL) { + return -1; + + } else if (strcasecmp(s, "true") == 0 + || strcasecmp(s, "on") == 0 + || strcasecmp(s, "yes") == 0 || strcasecmp(s, "y") == 0 || strcasecmp(s, "1") == 0) { + *ret = TRUE; + return 1; + + } else if (strcasecmp(s, "false") == 0 + || strcasecmp(s, "off") == 0 + || strcasecmp(s, "no") == 0 || strcasecmp(s, "n") == 0 || strcasecmp(s, "0") == 0) { + *ret = FALSE; + return 1; + } + return -1; +} + +/*! + * \internal + * \brief Replace any trailing newlines in a string with \0's + * + * \param[in,out] str String to trim + * + * \return \p str + */ +char * +pcmk__trim(char *str) +{ + int len; + + if (str == NULL) { + return str; + } + + for (len = strlen(str) - 1; len >= 0 && str[len] == '\n'; len--) { + str[len] = '\0'; + } + + return str; +} + +/*! + * \brief Check whether a string starts with a certain sequence + * + * \param[in] str String to check + * \param[in] prefix Sequence to match against beginning of \p str + * + * \return \c true if \p str begins with match, \c false otherwise + * \note This is equivalent to !strncmp(s, prefix, strlen(prefix)) + * but is likely less efficient when prefix is a string literal + * if the compiler optimizes away the strlen() at compile time, + * and more efficient otherwise. + */ +bool +pcmk__starts_with(const char *str, const char *prefix) +{ + const char *s = str; + const char *p = prefix; + + if (!s || !p) { + return false; + } + while (*s && *p) { + if (*s++ != *p++) { + return false; + } + } + return (*p == 0); +} + +static inline bool +ends_with(const char *s, const char *match, bool as_extension) +{ + if (pcmk__str_empty(match)) { + return true; + } else if (s == NULL) { + return false; + } else { + size_t slen, mlen; + + /* Besides as_extension, we could also check + !strchr(&match[1], match[0]) but that would be inefficient. + */ + if (as_extension) { + s = strrchr(s, match[0]); + return (s == NULL)? false : !strcmp(s, match); + } + + mlen = strlen(match); + slen = strlen(s); + return ((slen >= mlen) && !strcmp(s + slen - mlen, match)); + } +} + +/*! + * \internal + * \brief Check whether a string ends with a certain sequence + * + * \param[in] s String to check + * \param[in] match Sequence to match against end of \p s + * + * \return \c true if \p s ends case-sensitively with match, \c false otherwise + * \note pcmk__ends_with_ext() can be used if the first character of match + * does not recur in match. + */ +bool +pcmk__ends_with(const char *s, const char *match) +{ + return ends_with(s, match, false); +} + +/*! + * \internal + * \brief Check whether a string ends with a certain "extension" + * + * \param[in] s String to check + * \param[in] match Extension to match against end of \p s, that is, + * its first character must not occur anywhere + * in the rest of that very sequence (example: file + * extension where the last dot is its delimiter, + * e.g., ".html"); incorrect results may be + * returned otherwise. + * + * \return \c true if \p s ends (verbatim, i.e., case sensitively) + * with "extension" designated as \p match (including empty + * string), \c false otherwise + * + * \note Main incentive to prefer this function over \c pcmk__ends_with() + * where possible is the efficiency (at the cost of added + * restriction on \p match as stated; the complexity class + * remains the same, though: BigO(M+N) vs. BigO(M+2N)). + */ +bool +pcmk__ends_with_ext(const char *s, const char *match) +{ + return ends_with(s, match, true); +} + +/*! + * \internal + * \brief Create a hash of a string suitable for use with GHashTable + * + * \param[in] v String to hash + * + * \return A hash of \p v compatible with g_str_hash() before glib 2.28 + * \note glib changed their hash implementation: + * + * https://gitlab.gnome.org/GNOME/glib/commit/354d655ba8a54b754cb5a3efb42767327775696c + * + * Note that the new g_str_hash is presumably a *better* hash (it's actually + * a correct implementation of DJB's hash), but we need to preserve existing + * behaviour, because the hash key ultimately determines the "sort" order + * when iterating through GHashTables, which affects allocation of scores to + * clone instances when iterating through rsc->allowed_nodes. It (somehow) + * also appears to have some minor impact on the ordering of a few + * pseudo_event IDs in the transition graph. + */ +static guint +pcmk__str_hash(gconstpointer v) +{ + const signed char *p; + guint32 h = 0; + + for (p = v; *p != '\0'; p++) + h = (h << 5) - h + *p; + + return h; +} + +/*! + * \internal + * \brief Create a hash table with case-sensitive strings as keys + * + * \param[in] key_destroy_func Function to free a key + * \param[in] value_destroy_func Function to free a value + * + * \return Newly allocated hash table + * \note It is the caller's responsibility to free the result, using + * g_hash_table_destroy(). + */ +GHashTable * +pcmk__strkey_table(GDestroyNotify key_destroy_func, + GDestroyNotify value_destroy_func) +{ + return g_hash_table_new_full(pcmk__str_hash, g_str_equal, + key_destroy_func, value_destroy_func); +} + +/* used with hash tables where case does not matter */ +static gboolean +pcmk__strcase_equal(gconstpointer a, gconstpointer b) +{ + return pcmk__str_eq((const char *)a, (const char *)b, pcmk__str_casei); +} + +static guint +pcmk__strcase_hash(gconstpointer v) +{ + const signed char *p; + guint32 h = 0; + + for (p = v; *p != '\0'; p++) + h = (h << 5) - h + g_ascii_tolower(*p); + + return h; +} + +/*! + * \internal + * \brief Create a hash table with case-insensitive strings as keys + * + * \param[in] key_destroy_func Function to free a key + * \param[in] value_destroy_func Function to free a value + * + * \return Newly allocated hash table + * \note It is the caller's responsibility to free the result, using + * g_hash_table_destroy(). + */ +GHashTable * +pcmk__strikey_table(GDestroyNotify key_destroy_func, + GDestroyNotify value_destroy_func) +{ + return g_hash_table_new_full(pcmk__strcase_hash, pcmk__strcase_equal, + key_destroy_func, value_destroy_func); +} + +static void +copy_str_table_entry(gpointer key, gpointer value, gpointer user_data) +{ + if (key && value && user_data) { + g_hash_table_insert((GHashTable*)user_data, strdup(key), strdup(value)); + } +} + +/*! + * \internal + * \brief Copy a hash table that uses dynamically allocated strings + * + * \param[in,out] old_table Hash table to duplicate + * + * \return New hash table with copies of everything in \p old_table + * \note This assumes the hash table uses dynamically allocated strings -- that + * is, both the key and value free functions are free(). + */ +GHashTable * +pcmk__str_table_dup(GHashTable *old_table) +{ + GHashTable *new_table = NULL; + + if (old_table) { + new_table = pcmk__strkey_table(free, free); + g_hash_table_foreach(old_table, copy_str_table_entry, new_table); + } + return new_table; +} + +/*! + * \internal + * \brief Add a word to a string list of words + * + * \param[in,out] list Pointer to current string list (may not be \p NULL) + * \param[in] init_size \p list will be initialized to at least this size, + * if it needs initialization (if 0, use GLib's default + * initial string size) + * \param[in] word String to add to \p list (\p list will be + * unchanged if this is \p NULL or the empty string) + * \param[in] separator String to separate words in \p list + * (a space will be used if this is NULL) + * + * \note \p word may contain \p separator, though that would be a bad idea if + * the string needs to be parsed later. + */ +void +pcmk__add_separated_word(GString **list, size_t init_size, const char *word, + const char *separator) +{ + CRM_ASSERT(list != NULL); + + if (pcmk__str_empty(word)) { + return; + } + + if (*list == NULL) { + if (init_size > 0) { + *list = g_string_sized_new(init_size); + } else { + *list = g_string_new(NULL); + } + } + + if ((*list)->len == 0) { + // Don't add a separator before the first word in the list + separator = ""; + + } else if (separator == NULL) { + // Default to space-separated + separator = " "; + } + + g_string_append(*list, separator); + g_string_append(*list, word); +} + +/*! + * \internal + * \brief Compress data + * + * \param[in] data Data to compress + * \param[in] length Number of characters of data to compress + * \param[in] max Maximum size of compressed data (or 0 to estimate) + * \param[out] result Where to store newly allocated compressed result + * \param[out] result_len Where to store actual compressed length of result + * + * \return Standard Pacemaker return code + */ +int +pcmk__compress(const char *data, unsigned int length, unsigned int max, + char **result, unsigned int *result_len) +{ + int rc; + char *compressed = NULL; + char *uncompressed = strdup(data); +#ifdef CLOCK_MONOTONIC + struct timespec after_t; + struct timespec before_t; +#endif + + if (max == 0) { + max = (length * 1.01) + 601; // Size guaranteed to hold result + } + +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &before_t); +#endif + + compressed = calloc((size_t) max, sizeof(char)); + CRM_ASSERT(compressed); + + *result_len = max; + rc = BZ2_bzBuffToBuffCompress(compressed, result_len, uncompressed, length, + CRM_BZ2_BLOCKS, 0, CRM_BZ2_WORK); + free(uncompressed); + if (rc != BZ_OK) { + crm_err("Compression of %d bytes failed: %s " CRM_XS " bzerror=%d", + length, bz2_strerror(rc), rc); + free(compressed); + return pcmk_rc_error; + } + +#ifdef CLOCK_MONOTONIC + clock_gettime(CLOCK_MONOTONIC, &after_t); + + crm_trace("Compressed %d bytes into %d (ratio %d:1) in %.0fms", + length, *result_len, length / (*result_len), + (after_t.tv_sec - before_t.tv_sec) * 1000 + + (after_t.tv_nsec - before_t.tv_nsec) / 1e6); +#else + crm_trace("Compressed %d bytes into %d (ratio %d:1)", + length, *result_len, length / (*result_len)); +#endif + + *result = compressed; + return pcmk_rc_ok; +} + +char * +crm_strdup_printf(char const *format, ...) +{ + va_list ap; + int len = 0; + char *string = NULL; + + va_start(ap, format); + len = vasprintf (&string, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + return string; +} + +int +pcmk__parse_ll_range(const char *srcstring, long long *start, long long *end) +{ + char *remainder = NULL; + int rc = pcmk_rc_ok; + + CRM_ASSERT(start != NULL && end != NULL); + + *start = PCMK__PARSE_INT_DEFAULT; + *end = PCMK__PARSE_INT_DEFAULT; + + crm_trace("Attempting to decode: [%s]", srcstring); + if (pcmk__str_eq(srcstring, "", pcmk__str_null_matches)) { + return ENODATA; + } else if (pcmk__str_eq(srcstring, "-", pcmk__str_none)) { + return pcmk_rc_bad_input; + } + + /* String starts with a dash, so this is either a range with + * no beginning or garbage. + * */ + if (*srcstring == '-') { + int rc = scan_ll(srcstring+1, end, PCMK__PARSE_INT_DEFAULT, &remainder); + + if (rc != pcmk_rc_ok || *remainder != '\0') { + return pcmk_rc_bad_input; + } else { + return pcmk_rc_ok; + } + } + + rc = scan_ll(srcstring, start, PCMK__PARSE_INT_DEFAULT, &remainder); + if (rc != pcmk_rc_ok) { + return rc; + } + + if (*remainder && *remainder == '-') { + if (*(remainder+1)) { + char *more_remainder = NULL; + int rc = scan_ll(remainder+1, end, PCMK__PARSE_INT_DEFAULT, + &more_remainder); + + if (rc != pcmk_rc_ok) { + return rc; + } else if (*more_remainder != '\0') { + return pcmk_rc_bad_input; + } + } + } else if (*remainder && *remainder != '-') { + *start = PCMK__PARSE_INT_DEFAULT; + return pcmk_rc_bad_input; + } else { + /* The input string contained only one number. Set start and end + * to the same value and return pcmk_rc_ok. This gives the caller + * a way to tell this condition apart from a range with no end. + */ + *end = *start; + } + + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Find a string in a list of strings + * + * \note This function takes the same flags and has the same behavior as + * pcmk__str_eq(). + * + * \note No matter what input string or flags are provided, an empty + * list will always return FALSE. + * + * \param[in] s String to search for + * \param[in] lst List to search + * \param[in] flags A bitfield of pcmk__str_flags to modify operation + * + * \return \c TRUE if \p s is in \p lst, or \c FALSE otherwise + */ +gboolean +pcmk__str_in_list(const gchar *s, const GList *lst, uint32_t flags) +{ + for (const GList *ele = lst; ele != NULL; ele = ele->next) { + if (pcmk__str_eq(s, ele->data, flags)) { + return TRUE; + } + } + + return FALSE; +} + +static bool +str_any_of(const char *s, va_list args, uint32_t flags) +{ + if (s == NULL) { + return pcmk_is_set(flags, pcmk__str_null_matches); + } + + while (1) { + const char *ele = va_arg(args, const char *); + + if (ele == NULL) { + break; + } else if (pcmk__str_eq(s, ele, flags)) { + return true; + } + } + + return false; +} + +/*! + * \internal + * \brief Is a string a member of a list of strings? + * + * \param[in] s String to search for in \p ... + * \param[in] ... Strings to compare \p s against. The final string + * must be NULL. + * + * \note The comparison is done case-insensitively. The function name is + * meant to be reminiscent of strcasecmp. + * + * \return \c true if \p s is in \p ..., or \c false otherwise + */ +bool +pcmk__strcase_any_of(const char *s, ...) +{ + va_list ap; + bool rc; + + va_start(ap, s); + rc = str_any_of(s, ap, pcmk__str_casei); + va_end(ap); + return rc; +} + +/*! + * \internal + * \brief Is a string a member of a list of strings? + * + * \param[in] s String to search for in \p ... + * \param[in] ... Strings to compare \p s against. The final string + * must be NULL. + * + * \note The comparison is done taking case into account. + * + * \return \c true if \p s is in \p ..., or \c false otherwise + */ +bool +pcmk__str_any_of(const char *s, ...) +{ + va_list ap; + bool rc; + + va_start(ap, s); + rc = str_any_of(s, ap, pcmk__str_none); + va_end(ap); + return rc; +} + +/*! + * \internal + * \brief Check whether a character is in any of a list of strings + * + * \param[in] ch Character (ASCII) to search for + * \param[in] ... Strings to search. Final argument must be + * \c NULL. + * + * \return \c true if any of \p ... contain \p ch, \c false otherwise + * \note \p ... must contain at least one argument (\c NULL). + */ +bool +pcmk__char_in_any_str(int ch, ...) +{ + bool rc = false; + va_list ap; + + /* + * Passing a char to va_start() can generate compiler warnings, + * so ch is declared as an int. + */ + va_start(ap, ch); + + while (1) { + const char *ele = va_arg(ap, const char *); + + if (ele == NULL) { + break; + } else if (strchr(ele, ch) != NULL) { + rc = true; + break; + } + } + + va_end(ap); + return rc; +} + +/*! + * \internal + * \brief Sort strings, with numeric portions sorted numerically + * + * Sort two strings case-insensitively like strcasecmp(), but with any numeric + * portions of the string sorted numerically. This is particularly useful for + * node names (for example, "node10" will sort higher than "node9" but lower + * than "remotenode9"). + * + * \param[in] s1 First string to compare (must not be NULL) + * \param[in] s2 Second string to compare (must not be NULL) + * + * \retval -1 \p s1 comes before \p s2 + * \retval 0 \p s1 and \p s2 are equal + * \retval 1 \p s1 comes after \p s2 + */ +int +pcmk__numeric_strcasecmp(const char *s1, const char *s2) +{ + CRM_ASSERT((s1 != NULL) && (s2 != NULL)); + + while (*s1 && *s2) { + if (isdigit(*s1) && isdigit(*s2)) { + // If node names contain a number, sort numerically + + char *end1 = NULL; + char *end2 = NULL; + long num1 = strtol(s1, &end1, 10); + long num2 = strtol(s2, &end2, 10); + + // allow ordering e.g. 007 > 7 + size_t len1 = end1 - s1; + size_t len2 = end2 - s2; + + if (num1 < num2) { + return -1; + } else if (num1 > num2) { + return 1; + } else if (len1 < len2) { + return -1; + } else if (len1 > len2) { + return 1; + } + s1 = end1; + s2 = end2; + } else { + // Compare non-digits case-insensitively + int lower1 = tolower(*s1); + int lower2 = tolower(*s2); + + if (lower1 < lower2) { + return -1; + } else if (lower1 > lower2) { + return 1; + } + ++s1; + ++s2; + } + } + if (!*s1 && *s2) { + return -1; + } else if (*s1 && !*s2) { + return 1; + } + return 0; +} + +/*! + * \internal + * \brief Sort strings. + * + * This is your one-stop function for string comparison. By default, this + * function works like \p g_strcmp0. That is, like \p strcmp but a \p NULL + * string sorts before a non-NULL string. + * + * The \p pcmk__str_none flag produces the default behavior. Behavior can be + * changed with various flags: + * + * - \p pcmk__str_regex - The second string is a regular expression that the + * first string will be matched against. + * - \p pcmk__str_casei - By default, comparisons are done taking case into + * account. This flag makes comparisons case- + * insensitive. This can be combined with + * \p pcmk__str_regex. + * - \p pcmk__str_null_matches - If one string is \p NULL and the other is not, + * still return \p 0. + * - \p pcmk__str_star_matches - If one string is \p "*" and the other is not, + * still return \p 0. + * + * \param[in] s1 First string to compare + * \param[in] s2 Second string to compare, or a regular expression to + * match if \p pcmk__str_regex is set + * \param[in] flags A bitfield of \p pcmk__str_flags to modify operation + * + * \retval negative \p s1 is \p NULL or comes before \p s2 + * \retval 0 \p s1 and \p s2 are equal, or \p s1 is found in \p s2 if + * \c pcmk__str_regex is set + * \retval positive \p s2 is \p NULL or \p s1 comes after \p s2, or \p s2 + * is an invalid regular expression, or \p s1 was not found + * in \p s2 if \p pcmk__str_regex is set. + */ +int +pcmk__strcmp(const char *s1, const char *s2, uint32_t flags) +{ + /* If this flag is set, the second string is a regex. */ + if (pcmk_is_set(flags, pcmk__str_regex)) { + regex_t r_patt; + int reg_flags = REG_EXTENDED | REG_NOSUB; + int regcomp_rc = 0; + int rc = 0; + + if (s1 == NULL || s2 == NULL) { + return 1; + } + + if (pcmk_is_set(flags, pcmk__str_casei)) { + reg_flags |= REG_ICASE; + } + regcomp_rc = regcomp(&r_patt, s2, reg_flags); + if (regcomp_rc != 0) { + rc = 1; + crm_err("Bad regex '%s' for update: %s", s2, strerror(regcomp_rc)); + } else { + rc = regexec(&r_patt, s1, 0, NULL, 0); + regfree(&r_patt); + if (rc != 0) { + rc = 1; + } + } + return rc; + } + + /* If the strings are the same pointer, return 0 immediately. */ + if (s1 == s2) { + return 0; + } + + /* If this flag is set, return 0 if either (or both) of the input strings + * are NULL. If neither one is NULL, we need to continue and compare + * them normally. + */ + if (pcmk_is_set(flags, pcmk__str_null_matches)) { + if (s1 == NULL || s2 == NULL) { + return 0; + } + } + + /* Handle the cases where one is NULL and the str_null_matches flag is not set. + * A NULL string always sorts to the beginning. + */ + if (s1 == NULL) { + return -1; + } else if (s2 == NULL) { + return 1; + } + + /* If this flag is set, return 0 if either (or both) of the input strings + * are "*". If neither one is, we need to continue and compare them + * normally. + */ + if (pcmk_is_set(flags, pcmk__str_star_matches)) { + if (strcmp(s1, "*") == 0 || strcmp(s2, "*") == 0) { + return 0; + } + } + + if (pcmk_is_set(flags, pcmk__str_casei)) { + return strcasecmp(s1, s2); + } else { + return strcmp(s1, s2); + } +} + +/*! + * \internal + * \brief Update a dynamically allocated string with a new value + * + * Given a dynamically allocated string and a new value for it, if the string + * is different from the new value, free the string and replace it with either a + * newly allocated duplicate of the value or NULL as appropriate. + * + * \param[in,out] str Pointer to dynamically allocated string + * \param[in] value New value to duplicate (or NULL) + * + * \note The caller remains responsibile for freeing \p *str. + */ +void +pcmk__str_update(char **str, const char *value) +{ + if ((str != NULL) && !pcmk__str_eq(*str, value, pcmk__str_none)) { + free(*str); + if (value == NULL) { + *str = NULL; + } else { + *str = strdup(value); + CRM_ASSERT(*str != NULL); + } + } +} + +/*! + * \internal + * \brief Append a list of strings to a destination \p GString + * + * \param[in,out] buffer Where to append the strings (must not be \p NULL) + * \param[in] ... A NULL-terminated list of strings + * + * \note This tends to be more efficient than a single call to + * \p g_string_append_printf(). + */ +void +pcmk__g_strcat(GString *buffer, ...) +{ + va_list ap; + + CRM_ASSERT(buffer != NULL); + va_start(ap, buffer); + + while (true) { + const char *ele = va_arg(ap, const char *); + + if (ele == NULL) { + break; + } + g_string_append(buffer, ele); + } + va_end(ap); +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +gboolean +safe_str_neq(const char *a, const char *b) +{ + if (a == b) { + return FALSE; + + } else if (a == NULL || b == NULL) { + return TRUE; + + } else if (strcasecmp(a, b) == 0) { + return FALSE; + } + return TRUE; +} + +gboolean +crm_str_eq(const char *a, const char *b, gboolean use_case) +{ + if (use_case) { + return g_strcmp0(a, b) == 0; + + /* TODO - Figure out which calls, if any, really need to be case independent */ + } else if (a == b) { + return TRUE; + + } else if (a == NULL || b == NULL) { + /* shouldn't be comparing NULLs */ + return FALSE; + + } else if (strcasecmp(a, b) == 0) { + return TRUE; + } + return FALSE; +} + +char * +crm_itoa_stack(int an_int, char *buffer, size_t len) +{ + if (buffer != NULL) { + snprintf(buffer, len, "%d", an_int); + } + return buffer; +} + +guint +g_str_hash_traditional(gconstpointer v) +{ + return pcmk__str_hash(v); +} + +gboolean +crm_strcase_equal(gconstpointer a, gconstpointer b) +{ + return pcmk__strcase_equal(a, b); +} + +guint +crm_strcase_hash(gconstpointer v) +{ + return pcmk__strcase_hash(v); +} + +GHashTable * +crm_str_table_dup(GHashTable *old_table) +{ + return pcmk__str_table_dup(old_table); +} + +long long +crm_parse_ll(const char *text, const char *default_text) +{ + long long result; + + if (text == NULL) { + text = default_text; + if (text == NULL) { + crm_err("No default conversion value supplied"); + errno = EINVAL; + return PCMK__PARSE_INT_DEFAULT; + } + } + scan_ll(text, &result, PCMK__PARSE_INT_DEFAULT, NULL); + return result; +} + +int +crm_parse_int(const char *text, const char *default_text) +{ + long long result = crm_parse_ll(text, default_text); + + if (result < INT_MIN) { + // If errno is ERANGE, crm_parse_ll() has already logged a message + if (errno != ERANGE) { + crm_err("Conversion of %s was clipped: %lld", text, result); + errno = ERANGE; + } + return INT_MIN; + + } else if (result > INT_MAX) { + // If errno is ERANGE, crm_parse_ll() has already logged a message + if (errno != ERANGE) { + crm_err("Conversion of %s was clipped: %lld", text, result); + errno = ERANGE; + } + return INT_MAX; + } + + return (int) result; +} + +char * +crm_strip_trailing_newline(char *str) +{ + return pcmk__trim(str); +} + +int +pcmk_numeric_strcasecmp(const char *s1, const char *s2) +{ + return pcmk__numeric_strcasecmp(s1, s2); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/common/tests/Makefile.am b/lib/common/tests/Makefile.am new file mode 100644 index 0000000..b147309 --- /dev/null +++ b/lib/common/tests/Makefile.am @@ -0,0 +1,32 @@ +# +# Copyright 2020-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +SUBDIRS = \ + acl \ + agents \ + cmdline \ + flags \ + health \ + io \ + iso8601 \ + lists \ + nvpair \ + operations \ + options \ + output \ + results \ + scores \ + strings \ + utils \ + xml \ + xpath + +if SUPPORT_PROCFS +SUBDIRS += procfs +endif diff --git a/lib/common/tests/acl/Makefile.am b/lib/common/tests/acl/Makefile.am new file mode 100644 index 0000000..50408f9 --- /dev/null +++ b/lib/common/tests/acl/Makefile.am @@ -0,0 +1,21 @@ +# +# Copyright 2021-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. + +check_PROGRAMS = \ + pcmk__is_user_in_group_test \ + pcmk_acl_required_test \ + xml_acl_denied_test \ + xml_acl_enabled_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/acl/pcmk__is_user_in_group_test.c b/lib/common/tests/acl/pcmk__is_user_in_group_test.c new file mode 100644 index 0000000..917d92e --- /dev/null +++ b/lib/common/tests/acl/pcmk__is_user_in_group_test.c @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include "../../crmcommon_private.h" +#include "mock_private.h" + +static void +is_pcmk__is_user_in_group(void **state) +{ + pcmk__mock_grent = true; + + // null user + assert_false(pcmk__is_user_in_group(NULL, "grp0")); + // null group + assert_false(pcmk__is_user_in_group("user0", NULL)); + // nonexistent group + assert_false(pcmk__is_user_in_group("user0", "nonexistent_group")); + // user is in group + assert_true(pcmk__is_user_in_group("user0", "grp0")); + // user is not in group + assert_false(pcmk__is_user_in_group("user2", "grp0")); + + pcmk__mock_grent = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_pcmk__is_user_in_group)) diff --git a/lib/common/tests/acl/pcmk_acl_required_test.c b/lib/common/tests/acl/pcmk_acl_required_test.c new file mode 100644 index 0000000..bd5b922 --- /dev/null +++ b/lib/common/tests/acl/pcmk_acl_required_test.c @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +is_pcmk_acl_required(void **state) +{ + assert_false(pcmk_acl_required(NULL)); + assert_false(pcmk_acl_required("")); + assert_true(pcmk_acl_required("123")); + assert_false(pcmk_acl_required(CRM_DAEMON_USER)); + assert_false(pcmk_acl_required("root")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_pcmk_acl_required)) diff --git a/lib/common/tests/acl/xml_acl_denied_test.c b/lib/common/tests/acl/xml_acl_denied_test.c new file mode 100644 index 0000000..faf2a39 --- /dev/null +++ b/lib/common/tests/acl/xml_acl_denied_test.c @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include "../../crmcommon_private.h" + +static void +is_xml_acl_denied_without_node(void **state) +{ + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + assert_false(xml_acl_denied(test_xml)); + + test_xml->doc->_private = NULL; + assert_false(xml_acl_denied(test_xml)); + + test_xml->doc = NULL; + assert_false(xml_acl_denied(test_xml)); + + test_xml = NULL; + assert_false(xml_acl_denied(test_xml)); +} + +static void +is_xml_acl_denied_with_node(void **state) +{ + xml_doc_private_t *docpriv; + + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + + // allocate memory for _private, which is NULL by default + test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t)); + + assert_false(xml_acl_denied(test_xml)); + + // cast _private from void* to xml_doc_private_t* + docpriv = test_xml->doc->_private; + + // enable an irrelevant flag + docpriv->flags |= pcmk__xf_acl_enabled; + + assert_false(xml_acl_denied(test_xml)); + + // enable pcmk__xf_acl_denied + docpriv->flags |= pcmk__xf_acl_denied; + + assert_true(xml_acl_denied(test_xml)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_xml_acl_denied_without_node), + cmocka_unit_test(is_xml_acl_denied_with_node)) diff --git a/lib/common/tests/acl/xml_acl_enabled_test.c b/lib/common/tests/acl/xml_acl_enabled_test.c new file mode 100644 index 0000000..28665f4 --- /dev/null +++ b/lib/common/tests/acl/xml_acl_enabled_test.c @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include "../../crmcommon_private.h" + +static void +is_xml_acl_enabled_without_node(void **state) +{ + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + assert_false(xml_acl_enabled(test_xml)); + + test_xml->doc->_private = NULL; + assert_false(xml_acl_enabled(test_xml)); + + test_xml->doc = NULL; + assert_false(xml_acl_enabled(test_xml)); + + test_xml = NULL; + assert_false(xml_acl_enabled(test_xml)); +} + +static void +is_xml_acl_enabled_with_node(void **state) +{ + xml_doc_private_t *docpriv; + + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + + // allocate memory for _private, which is NULL by default + test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t)); + + assert_false(xml_acl_enabled(test_xml)); + + // cast _private from void* to xml_doc_private_t* + docpriv = test_xml->doc->_private; + + // enable an irrelevant flag + docpriv->flags |= pcmk__xf_acl_denied; + + assert_false(xml_acl_enabled(test_xml)); + + // enable pcmk__xf_acl_enabled + docpriv->flags |= pcmk__xf_acl_enabled; + + assert_true(xml_acl_enabled(test_xml)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_xml_acl_enabled_without_node), + cmocka_unit_test(is_xml_acl_enabled_with_node)) diff --git a/lib/common/tests/agents/Makefile.am b/lib/common/tests/agents/Makefile.am new file mode 100644 index 0000000..7a54b7d --- /dev/null +++ b/lib/common/tests/agents/Makefile.am @@ -0,0 +1,20 @@ +# +# Copyright 2020-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = crm_generate_ra_key_test \ + crm_parse_agent_spec_test \ + pcmk__effective_rc_test \ + pcmk_get_ra_caps_test \ + pcmk_stonith_param_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/agents/crm_generate_ra_key_test.c b/lib/common/tests/agents/crm_generate_ra_key_test.c new file mode 100644 index 0000000..f71c1c2 --- /dev/null +++ b/lib/common/tests/agents/crm_generate_ra_key_test.c @@ -0,0 +1,48 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +all_params_null(void **state) { + assert_null(crm_generate_ra_key(NULL, NULL, NULL)); +} + +static void +some_params_null(void **state) { + char *retval; + + assert_null(crm_generate_ra_key("std", "prov", NULL)); + + retval = crm_generate_ra_key("std", NULL, "ty"); + assert_string_equal(retval, "std:ty"); + free(retval); + + assert_null(crm_generate_ra_key(NULL, "prov", "ty")); + assert_null(crm_generate_ra_key("std", NULL, NULL)); + assert_null(crm_generate_ra_key(NULL, "prov", NULL)); + assert_null(crm_generate_ra_key(NULL, NULL, "ty")); +} + +static void +no_params_null(void **state) { + char *retval; + + retval = crm_generate_ra_key("std", "prov", "ty"); + assert_string_equal(retval, "std:prov:ty"); + free(retval); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(all_params_null), + cmocka_unit_test(some_params_null), + cmocka_unit_test(no_params_null)) diff --git a/lib/common/tests/agents/crm_parse_agent_spec_test.c b/lib/common/tests/agents/crm_parse_agent_spec_test.c new file mode 100644 index 0000000..cfd75f0 --- /dev/null +++ b/lib/common/tests/agents/crm_parse_agent_spec_test.c @@ -0,0 +1,87 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +all_params_null(void **state) { + assert_int_equal(crm_parse_agent_spec(NULL, NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec("", NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec(":", NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec("::", NULL, NULL, NULL), -EINVAL); +} + +static void +no_prov_or_type(void **state) { + assert_int_equal(crm_parse_agent_spec("ocf", NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec("ocf:", NULL, NULL, NULL), -EINVAL); + assert_int_equal(crm_parse_agent_spec("ocf::", NULL, NULL, NULL), -EINVAL); +} + +static void +no_type(void **state) { + assert_int_equal(crm_parse_agent_spec("ocf:pacemaker:", NULL, NULL, NULL), -EINVAL); +} + +static void +get_std_and_ty(void **state) { + char *std = NULL; + char *prov = NULL; + char *ty = NULL; + + assert_int_equal(crm_parse_agent_spec("stonith:fence_xvm", &std, &prov, &ty), pcmk_ok); + assert_string_equal(std, "stonith"); + assert_null(prov); + assert_string_equal(ty, "fence_xvm"); + + free(std); + free(ty); +} + +static void +get_all_values(void **state) { + char *std = NULL; + char *prov = NULL; + char *ty = NULL; + + assert_int_equal(crm_parse_agent_spec("ocf:pacemaker:ping", &std, &prov, &ty), pcmk_ok); + assert_string_equal(std, "ocf"); + assert_string_equal(prov, "pacemaker"); + assert_string_equal(ty, "ping"); + + free(std); + free(prov); + free(ty); +} + +static void +get_systemd_values(void **state) { + char *std = NULL; + char *prov = NULL; + char *ty = NULL; + + assert_int_equal(crm_parse_agent_spec("systemd:UNIT@A:B", &std, &prov, &ty), pcmk_ok); + assert_string_equal(std, "systemd"); + assert_null(prov); + assert_string_equal(ty, "UNIT@A:B"); + + free(std); + free(ty); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(all_params_null), + cmocka_unit_test(no_prov_or_type), + cmocka_unit_test(no_type), + cmocka_unit_test(get_std_and_ty), + cmocka_unit_test(get_all_values), + cmocka_unit_test(get_systemd_values)) diff --git a/lib/common/tests/agents/pcmk__effective_rc_test.c b/lib/common/tests/agents/pcmk__effective_rc_test.c new file mode 100644 index 0000000..c9bad97 --- /dev/null +++ b/lib/common/tests/agents/pcmk__effective_rc_test.c @@ -0,0 +1,36 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +pcmk__effective_rc_test(void **state) { + /* All other PCMK_OCF_* values after UNKNOWN are deprecated and no longer used, + * so probably not worth testing them. + */ + assert_int_equal(PCMK_OCF_OK, pcmk__effective_rc(PCMK_OCF_OK)); + assert_int_equal(PCMK_OCF_OK, pcmk__effective_rc(PCMK_OCF_DEGRADED)); + assert_int_equal(PCMK_OCF_RUNNING_PROMOTED, pcmk__effective_rc(PCMK_OCF_DEGRADED_PROMOTED)); + assert_int_equal(PCMK_OCF_UNKNOWN, pcmk__effective_rc(PCMK_OCF_UNKNOWN)); + + /* There's nothing that says pcmk__effective_rc is restricted to PCMK_OCF_* + * values. That's just how it's used. Let's check some values outside + * that range just to be sure. + */ + assert_int_equal(-1, pcmk__effective_rc(-1)); + assert_int_equal(255, pcmk__effective_rc(255)); + assert_int_equal(INT_MAX, pcmk__effective_rc(INT_MAX)); + assert_int_equal(INT_MIN, pcmk__effective_rc(INT_MIN)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(pcmk__effective_rc_test)) diff --git a/lib/common/tests/agents/pcmk_get_ra_caps_test.c b/lib/common/tests/agents/pcmk_get_ra_caps_test.c new file mode 100644 index 0000000..178dce5 --- /dev/null +++ b/lib/common/tests/agents/pcmk_get_ra_caps_test.c @@ -0,0 +1,63 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +ocf_standard(void **state) { + uint32_t expected = pcmk_ra_cap_provider | pcmk_ra_cap_params | + pcmk_ra_cap_unique | pcmk_ra_cap_promotable; + + assert_int_equal(pcmk_get_ra_caps("ocf"), expected); + assert_int_equal(pcmk_get_ra_caps("OCF"), expected); +} + +static void +stonith_standard(void **state) { + uint32_t expected = pcmk_ra_cap_params | pcmk_ra_cap_unique | + pcmk_ra_cap_stdin | pcmk_ra_cap_fence_params; + + assert_int_equal(pcmk_get_ra_caps("stonith"), expected); + assert_int_equal(pcmk_get_ra_caps("StOnItH"), expected); +} + +static void +service_standard(void **state) { + assert_int_equal(pcmk_get_ra_caps("systemd"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("SYSTEMD"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("service"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("SeRvIcE"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("lsb"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("LSB"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("upstart"), pcmk_ra_cap_status); + assert_int_equal(pcmk_get_ra_caps("uPsTaRt"), pcmk_ra_cap_status); +} + +static void +nagios_standard(void **state) { + assert_int_equal(pcmk_get_ra_caps("nagios"), pcmk_ra_cap_params); + assert_int_equal(pcmk_get_ra_caps("NAGios"), pcmk_ra_cap_params); +} + +static void +unknown_standard(void **state) { + assert_int_equal(pcmk_get_ra_caps("blahblah"), pcmk_ra_cap_none); + assert_int_equal(pcmk_get_ra_caps(""), pcmk_ra_cap_none); + assert_int_equal(pcmk_get_ra_caps(NULL), pcmk_ra_cap_none); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(ocf_standard), + cmocka_unit_test(stonith_standard), + cmocka_unit_test(service_standard), + cmocka_unit_test(nagios_standard), + cmocka_unit_test(unknown_standard)) diff --git a/lib/common/tests/agents/pcmk_stonith_param_test.c b/lib/common/tests/agents/pcmk_stonith_param_test.c new file mode 100644 index 0000000..fad431e --- /dev/null +++ b/lib/common/tests/agents/pcmk_stonith_param_test.c @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +is_stonith_param(void **state) +{ + assert_false(pcmk_stonith_param(NULL)); + assert_false(pcmk_stonith_param("")); + assert_false(pcmk_stonith_param("unrecognized")); + assert_false(pcmk_stonith_param("pcmk_unrecognized")); + assert_false(pcmk_stonith_param("x" PCMK_STONITH_ACTION_LIMIT)); + assert_false(pcmk_stonith_param(PCMK_STONITH_ACTION_LIMIT "x")); + + assert_true(pcmk_stonith_param(PCMK_STONITH_ACTION_LIMIT)); + assert_true(pcmk_stonith_param(PCMK_STONITH_DELAY_BASE)); + assert_true(pcmk_stonith_param(PCMK_STONITH_DELAY_MAX)); + assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_ARGUMENT)); + assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_CHECK)); + assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_LIST)); + assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_MAP)); + assert_true(pcmk_stonith_param(PCMK_STONITH_PROVIDES)); + assert_true(pcmk_stonith_param(PCMK_STONITH_STONITH_TIMEOUT)); +} + +static void +is_stonith_action_param(void **state) +{ + /* Currently, the function accepts any string not containing underbars as + * the action name, so we do not need to verify particular action names. + */ + assert_false(pcmk_stonith_param("pcmk_on_unrecognized")); + assert_true(pcmk_stonith_param("pcmk_on_action")); + assert_true(pcmk_stonith_param("pcmk_on_timeout")); + assert_true(pcmk_stonith_param("pcmk_on_retries")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_stonith_param), + cmocka_unit_test(is_stonith_action_param)) diff --git a/lib/common/tests/cmdline/Makefile.am b/lib/common/tests/cmdline/Makefile.am new file mode 100644 index 0000000..d781ed5 --- /dev/null +++ b/lib/common/tests/cmdline/Makefile.am @@ -0,0 +1,17 @@ +# +# Copyright 2020-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = pcmk__cmdline_preproc_test \ + pcmk__quote_cmdline_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c b/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c new file mode 100644 index 0000000..863fbb9 --- /dev/null +++ b/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c @@ -0,0 +1,156 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include + +#define LISTS_EQ(a, b) { \ + assert_int_equal(g_strv_length((gchar **) (a)), g_strv_length((gchar **) (b))); \ + for (int i = 0; i < g_strv_length((a)); i++) { \ + assert_string_equal((a)[i], (b)[i]); \ + } \ +} + +static void +empty_input(void **state) { + assert_null(pcmk__cmdline_preproc(NULL, "")); +} + +static void +no_specials(void **state) { + const char *argv[] = { "crm_mon", "-a", "-b", "-c", "-d", "-1", NULL }; + const gchar *expected[] = { "crm_mon", "-a", "-b", "-c", "-d", "-1", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); + + processed = pcmk__cmdline_preproc((char **) argv, ""); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +single_dash(void **state) { + const char *argv[] = { "crm_mon", "-", NULL }; + const gchar *expected[] = { "crm_mon", "-", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +double_dash(void **state) { + const char *argv[] = { "crm_mon", "-a", "--", "-bc", NULL }; + const gchar *expected[] = { "crm_mon", "-a", "--", "-bc", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +special_args(void **state) { + const char *argv[] = { "crm_mon", "-aX", "-Fval", NULL }; + const gchar *expected[] = { "crm_mon", "-a", "X", "-F", "val", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "aF"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +special_arg_at_end(void **state) { + const char *argv[] = { "crm_mon", "-a", NULL }; + const gchar *expected[] = { "crm_mon", "-a", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "a"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +long_arg(void **state) { + const char *argv[] = { "crm_mon", "--blah=foo", NULL }; + const gchar *expected[] = { "crm_mon", "--blah=foo", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +negative_score(void **state) { + const char *argv[] = { "crm_mon", "-v", "-1000", NULL }; + const gchar *expected[] = { "crm_mon", "-v", "-1000", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +negative_score_2(void **state) { + const char *argv[] = { "crm_mon", "-1i3", NULL }; + const gchar *expected[] = { "crm_mon", "-1", "-i", "-3", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +string_arg_with_dash(void **state) { + const char *argv[] = { "crm_mon", "-n", "crm_mon_options", "-v", "--opt1 --opt2", NULL }; + const gchar *expected[] = { "crm_mon", "-n", "crm_mon_options", "-v", "--opt1 --opt2", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +string_arg_with_dash_2(void **state) { + const char *argv[] = { "crm_mon", "-n", "crm_mon_options", "-v", "-1i3", NULL }; + const gchar *expected[] = { "crm_mon", "-n", "crm_mon_options", "-v", "-1i3", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "v"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +static void +string_arg_with_dash_3(void **state) { + const char *argv[] = { "crm_mon", "-abc", "-1i3", NULL }; + const gchar *expected[] = { "crm_mon", "-a", "-b", "-c", "-1i3", NULL }; + + gchar **processed = pcmk__cmdline_preproc((char **) argv, "c"); + LISTS_EQ(processed, expected); + g_strfreev(processed); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(no_specials), + cmocka_unit_test(single_dash), + cmocka_unit_test(double_dash), + cmocka_unit_test(special_args), + cmocka_unit_test(special_arg_at_end), + cmocka_unit_test(long_arg), + cmocka_unit_test(negative_score), + cmocka_unit_test(negative_score_2), + cmocka_unit_test(string_arg_with_dash), + cmocka_unit_test(string_arg_with_dash_2), + cmocka_unit_test(string_arg_with_dash_3)) diff --git a/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c b/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c new file mode 100644 index 0000000..42bd8ca --- /dev/null +++ b/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c @@ -0,0 +1,56 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include + +static void +empty_input(void **state) { + assert_null(pcmk__quote_cmdline(NULL)); +} + +static void +no_spaces(void **state) { + const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "hello", "--output-as=xml", NULL }; + const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v hello --output-as=xml"; + + gchar *processed = pcmk__quote_cmdline((gchar **) argv); + assert_string_equal(processed, expected); + g_free(processed); +} + +static void +spaces_no_quote(void **state) { + const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "hello world", "--output-as=xml", NULL }; + const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v 'hello world' --output-as=xml"; + + gchar *processed = pcmk__quote_cmdline((gchar **) argv); + assert_string_equal(processed, expected); + g_free(processed); +} + +static void +spaces_with_quote(void **state) { + const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "here's johnny", "--output-as=xml", NULL }; + const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v 'here\\\'s johnny' --output-as=xml"; + + gchar *processed = pcmk__quote_cmdline((gchar **) argv); + assert_string_equal(processed, expected); + g_free(processed); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(no_spaces), + cmocka_unit_test(spaces_no_quote), + cmocka_unit_test(spaces_with_quote)) diff --git a/lib/common/tests/flags/Makefile.am b/lib/common/tests/flags/Makefile.am new file mode 100644 index 0000000..16d8ffb --- /dev/null +++ b/lib/common/tests/flags/Makefile.am @@ -0,0 +1,20 @@ +# +# Copyright 2020-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = \ + pcmk__clear_flags_as_test \ + pcmk__set_flags_as_test \ + pcmk_all_flags_set_test \ + pcmk_any_flags_set_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/flags/pcmk__clear_flags_as_test.c b/lib/common/tests/flags/pcmk__clear_flags_as_test.c new file mode 100644 index 0000000..07dbe28 --- /dev/null +++ b/lib/common/tests/flags/pcmk__clear_flags_as_test.c @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +static void +clear_none(void **state) { + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x00f, NULL), 0x0f0); + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0xf0f, NULL), 0x0f0); +} + +static void +clear_some(void **state) { + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x020, NULL), 0x0d0); + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x030, NULL), 0x0c0); +} + +static void +clear_all(void **state) { + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x0f0, NULL), 0x000); + assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0xfff, NULL), 0x000); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(clear_none), + cmocka_unit_test(clear_some), + cmocka_unit_test(clear_all)) diff --git a/lib/common/tests/flags/pcmk__set_flags_as_test.c b/lib/common/tests/flags/pcmk__set_flags_as_test.c new file mode 100644 index 0000000..cd14c85 --- /dev/null +++ b/lib/common/tests/flags/pcmk__set_flags_as_test.c @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +static void +set_flags(void **state) { + assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0x00f, NULL), 0x0ff); + assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0xf0f, NULL), 0xfff); + assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test", + "test", 0x0f0, 0xfff, NULL), 0xfff); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(set_flags)) diff --git a/lib/common/tests/flags/pcmk_all_flags_set_test.c b/lib/common/tests/flags/pcmk_all_flags_set_test.c new file mode 100644 index 0000000..512ccce --- /dev/null +++ b/lib/common/tests/flags/pcmk_all_flags_set_test.c @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +static void +all_set(void **state) { + assert_false(pcmk_all_flags_set(0x000, 0x003)); + assert_true(pcmk_all_flags_set(0x00f, 0x003)); + assert_false(pcmk_all_flags_set(0x00f, 0x010)); + assert_false(pcmk_all_flags_set(0x00f, 0x011)); + assert_true(pcmk_all_flags_set(0x000, 0x000)); + assert_true(pcmk_all_flags_set(0x00f, 0x000)); +} + +static void +one_is_set(void **state) { + // pcmk_is_set() is a simple macro alias for pcmk_all_flags_set() + assert_true(pcmk_is_set(0x00f, 0x001)); + assert_false(pcmk_is_set(0x00f, 0x010)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(all_set), + cmocka_unit_test(one_is_set)) diff --git a/lib/common/tests/flags/pcmk_any_flags_set_test.c b/lib/common/tests/flags/pcmk_any_flags_set_test.c new file mode 100644 index 0000000..dc3aabc --- /dev/null +++ b/lib/common/tests/flags/pcmk_any_flags_set_test.c @@ -0,0 +1,26 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +static void +any_set(void **state) { + assert_false(pcmk_any_flags_set(0x000, 0x000)); + assert_false(pcmk_any_flags_set(0x000, 0x001)); + assert_true(pcmk_any_flags_set(0x00f, 0x001)); + assert_false(pcmk_any_flags_set(0x00f, 0x010)); + assert_true(pcmk_any_flags_set(0x00f, 0x011)); + assert_false(pcmk_any_flags_set(0x000, 0x000)); + assert_false(pcmk_any_flags_set(0x00f, 0x000)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(any_set)) diff --git a/lib/common/tests/health/Makefile.am b/lib/common/tests/health/Makefile.am new file mode 100644 index 0000000..ad2a2da --- /dev/null +++ b/lib/common/tests/health/Makefile.am @@ -0,0 +1,17 @@ +# +# Copyright 2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = pcmk__parse_health_strategy_test \ + pcmk__validate_health_strategy_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/health/pcmk__parse_health_strategy_test.c b/lib/common/tests/health/pcmk__parse_health_strategy_test.c new file mode 100644 index 0000000..28cc702 --- /dev/null +++ b/lib/common/tests/health/pcmk__parse_health_strategy_test.c @@ -0,0 +1,56 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +static void +valid(void **state) { + assert_int_equal(pcmk__parse_health_strategy(NULL), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("none"), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("NONE"), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("None"), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("nOnE"), + pcmk__health_strategy_none); + + assert_int_equal(pcmk__parse_health_strategy("migrate-on-red"), + pcmk__health_strategy_no_red); + + assert_int_equal(pcmk__parse_health_strategy("only-green"), + pcmk__health_strategy_only_green); + + assert_int_equal(pcmk__parse_health_strategy("progressive"), + pcmk__health_strategy_progressive); + + assert_int_equal(pcmk__parse_health_strategy("custom"), + pcmk__health_strategy_custom); +} + +static void +invalid(void **state) { + assert_int_equal(pcmk__parse_health_strategy("foo"), + pcmk__health_strategy_none); + assert_int_equal(pcmk__parse_health_strategy("custom1"), + pcmk__health_strategy_none); + assert_int_equal(pcmk__parse_health_strategy("not-only-green-here"), + pcmk__health_strategy_none); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(valid), + cmocka_unit_test(invalid)) diff --git a/lib/common/tests/health/pcmk__validate_health_strategy_test.c b/lib/common/tests/health/pcmk__validate_health_strategy_test.c new file mode 100644 index 0000000..c7c60aa --- /dev/null +++ b/lib/common/tests/health/pcmk__validate_health_strategy_test.c @@ -0,0 +1,38 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +// Test functions + +static void +valid_strategy(void **state) { + assert_true(pcmk__validate_health_strategy("none")); + assert_true(pcmk__validate_health_strategy("None")); + assert_true(pcmk__validate_health_strategy("NONE")); + assert_true(pcmk__validate_health_strategy("NoNe")); + assert_true(pcmk__validate_health_strategy("migrate-on-red")); + assert_true(pcmk__validate_health_strategy("only-green")); + assert_true(pcmk__validate_health_strategy("progressive")); + assert_true(pcmk__validate_health_strategy("custom")); +} + +static void +invalid_strategy(void **state) { + assert_false(pcmk__validate_health_strategy(NULL)); + assert_false(pcmk__validate_health_strategy("")); + assert_false(pcmk__validate_health_strategy("none to speak of")); + assert_false(pcmk__validate_health_strategy("customized")); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(valid_strategy), + cmocka_unit_test(invalid_strategy)) diff --git a/lib/common/tests/io/Makefile.am b/lib/common/tests/io/Makefile.am new file mode 100644 index 0000000..c26482c --- /dev/null +++ b/lib/common/tests/io/Makefile.am @@ -0,0 +1,18 @@ +# +# Copyright 2020-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = \ + pcmk__full_path_test \ + pcmk__get_tmpdir_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/io/pcmk__full_path_test.c b/lib/common/tests/io/pcmk__full_path_test.c new file mode 100644 index 0000000..dbbd71b --- /dev/null +++ b/lib/common/tests/io/pcmk__full_path_test.c @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include "mock_private.h" + +static void +function_asserts(void **state) +{ + pcmk__assert_asserts(pcmk__full_path(NULL, "/dir")); + pcmk__assert_asserts(pcmk__full_path("file", NULL)); + + pcmk__assert_asserts( + { + pcmk__mock_strdup = true; // strdup() will return NULL + expect_string(__wrap_strdup, s, "/full/path"); + pcmk__full_path("/full/path", "/dir"); + pcmk__mock_strdup = false; // Use real strdup() + } + ); +} + +static void +full_path(void **state) +{ + char *path = NULL; + + path = pcmk__full_path("file", "/dir"); + assert_int_equal(strcmp(path, "/dir/file"), 0); + free(path); + + path = pcmk__full_path("/full/path", "/dir"); + assert_int_equal(strcmp(path, "/full/path"), 0); + free(path); + + path = pcmk__full_path("../relative/path", "/dir"); + assert_int_equal(strcmp(path, "/dir/../relative/path"), 0); + free(path); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(function_asserts), + cmocka_unit_test(full_path)) diff --git a/lib/common/tests/io/pcmk__get_tmpdir_test.c b/lib/common/tests/io/pcmk__get_tmpdir_test.c new file mode 100644 index 0000000..bc17513 --- /dev/null +++ b/lib/common/tests/io/pcmk__get_tmpdir_test.c @@ -0,0 +1,68 @@ +/* + * Copyright 2021-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include "mock_private.h" + +static void +getenv_returns_invalid(void **state) +{ + const char *result; + + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, NULL); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/tmp"); + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, ""); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/tmp"); + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, "subpath"); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/tmp"); + + pcmk__mock_getenv = false; +} + +static void +getenv_returns_valid(void **state) +{ + const char *result; + + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, "/var/tmp"); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/var/tmp"); + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, "/"); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/"); + + expect_string(__wrap_getenv, name, "TMPDIR"); + will_return(__wrap_getenv, "/tmp/abcd.1234"); // getenv("TMPDIR") return value + result = pcmk__get_tmpdir(); + assert_string_equal(result, "/tmp/abcd.1234"); + + pcmk__mock_getenv = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(getenv_returns_invalid), + cmocka_unit_test(getenv_returns_valid)) diff --git a/lib/common/tests/iso8601/Makefile.am b/lib/common/tests/iso8601/Makefile.am new file mode 100644 index 0000000..5187aec --- /dev/null +++ b/lib/common/tests/iso8601/Makefile.am @@ -0,0 +1,16 @@ +# +# Copyright 2020-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = pcmk__readable_interval_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/iso8601/pcmk__readable_interval_test.c b/lib/common/tests/iso8601/pcmk__readable_interval_test.c new file mode 100644 index 0000000..43b5541 --- /dev/null +++ b/lib/common/tests/iso8601/pcmk__readable_interval_test.c @@ -0,0 +1,27 @@ +/* + * Copyright 2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include + +static void +readable_interval(void **state) +{ + assert_string_equal(pcmk__readable_interval(0), "0s"); + assert_string_equal(pcmk__readable_interval(30000), "30s"); + assert_string_equal(pcmk__readable_interval(150000), "2m30s"); + assert_string_equal(pcmk__readable_interval(3333), "3.333s"); + assert_string_equal(pcmk__readable_interval(UINT_MAX), "49d17h2m47.295s"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(readable_interval)) diff --git a/lib/common/tests/lists/Makefile.am b/lib/common/tests/lists/Makefile.am new file mode 100644 index 0000000..ae0c0b6 --- /dev/null +++ b/lib/common/tests/lists/Makefile.am @@ -0,0 +1,20 @@ +# +# Copyright 2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. + +check_PROGRAMS = \ + pcmk__list_of_1_test \ + pcmk__list_of_multiple_test \ + pcmk__subtract_lists_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/lists/pcmk__list_of_1_test.c b/lib/common/tests/lists/pcmk__list_of_1_test.c new file mode 100644 index 0000000..a3e0bbc --- /dev/null +++ b/lib/common/tests/lists/pcmk__list_of_1_test.c @@ -0,0 +1,45 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include + +static void +empty_list(void **state) { + assert_false(pcmk__list_of_1(NULL)); +} + +static void +singleton_list(void **state) { + GList *lst = NULL; + + lst = g_list_append(lst, strdup("abc")); + assert_true(pcmk__list_of_1(lst)); + + g_list_free_full(lst, free); +} + +static void +longer_list(void **state) { + GList *lst = NULL; + + lst = g_list_append(lst, strdup("abc")); + lst = g_list_append(lst, strdup("xyz")); + assert_false(pcmk__list_of_1(lst)); + + g_list_free_full(lst, free); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_list), + cmocka_unit_test(singleton_list), + cmocka_unit_test(longer_list)) diff --git a/lib/common/tests/lists/pcmk__list_of_multiple_test.c b/lib/common/tests/lists/pcmk__list_of_multiple_test.c new file mode 100644 index 0000000..0966037 --- /dev/null +++ b/lib/common/tests/lists/pcmk__list_of_multiple_test.c @@ -0,0 +1,45 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include + +static void +empty_list(void **state) { + assert_false(pcmk__list_of_multiple(NULL)); +} + +static void +singleton_list(void **state) { + GList *lst = NULL; + + lst = g_list_append(lst, strdup("abc")); + assert_false(pcmk__list_of_multiple(lst)); + + g_list_free_full(lst, free); +} + +static void +longer_list(void **state) { + GList *lst = NULL; + + lst = g_list_append(lst, strdup("abc")); + lst = g_list_append(lst, strdup("xyz")); + assert_true(pcmk__list_of_multiple(lst)); + + g_list_free_full(lst, free); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_list), + cmocka_unit_test(singleton_list), + cmocka_unit_test(longer_list)) diff --git a/lib/common/tests/lists/pcmk__subtract_lists_test.c b/lib/common/tests/lists/pcmk__subtract_lists_test.c new file mode 100644 index 0000000..1198e2b --- /dev/null +++ b/lib/common/tests/lists/pcmk__subtract_lists_test.c @@ -0,0 +1,144 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include + +static void +different_lists(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("123")); + items = g_list_append(items, strdup("456")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 3); + assert_string_equal(g_list_nth_data(result, 0), "abc"); + assert_string_equal(g_list_nth_data(result, 1), "def"); + assert_string_equal(g_list_nth_data(result, 2), "ghi"); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +static void +remove_first_item(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("abc")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 2); + assert_string_equal(g_list_nth_data(result, 0), "def"); + assert_string_equal(g_list_nth_data(result, 1), "ghi"); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +static void +remove_middle_item(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("def")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 2); + assert_string_equal(g_list_nth_data(result, 0), "abc"); + assert_string_equal(g_list_nth_data(result, 1), "ghi"); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +static void +remove_last_item(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("ghi")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 2); + assert_string_equal(g_list_nth_data(result, 0), "abc"); + assert_string_equal(g_list_nth_data(result, 1), "def"); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +static void +remove_all_items(void **state) +{ + GList *from = NULL; + GList *items = NULL; + GList *result = NULL; + + from = g_list_append(from, strdup("abc")); + from = g_list_append(from, strdup("def")); + from = g_list_append(from, strdup("ghi")); + + items = g_list_append(items, strdup("abc")); + items = g_list_append(items, strdup("def")); + items = g_list_append(items, strdup("ghi")); + + result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp); + + assert_int_equal(g_list_length(result), 0); + + g_list_free(result); + g_list_free_full(from, free); + g_list_free_full(items, free); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(different_lists), + cmocka_unit_test(remove_first_item), + cmocka_unit_test(remove_middle_item), + cmocka_unit_test(remove_last_item), + cmocka_unit_test(remove_all_items)) diff --git a/lib/common/tests/nvpair/Makefile.am b/lib/common/tests/nvpair/Makefile.am new file mode 100644 index 0000000..7acaba3 --- /dev/null +++ b/lib/common/tests/nvpair/Makefile.am @@ -0,0 +1,18 @@ +# +# Copyright 2021-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = pcmk__xe_attr_is_true_test \ + pcmk__xe_get_bool_attr_test \ + pcmk__xe_set_bool_attr_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c new file mode 100644 index 0000000..3723707 --- /dev/null +++ b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c @@ -0,0 +1,50 @@ +/* + * Copyright 2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +empty_input(void **state) +{ + xmlNode *node = string2xml(""); + + assert_false(pcmk__xe_attr_is_true(NULL, NULL)); + assert_false(pcmk__xe_attr_is_true(NULL, "whatever")); + assert_false(pcmk__xe_attr_is_true(node, NULL)); + + free_xml(node); +} + +static void +attr_missing(void **state) +{ + xmlNode *node = string2xml(""); + + assert_false(pcmk__xe_attr_is_true(node, "c")); + free_xml(node); +} + +static void +attr_present(void **state) +{ + xmlNode *node = string2xml(""); + + assert_true(pcmk__xe_attr_is_true(node, "a")); + assert_false(pcmk__xe_attr_is_true(node, "b")); + + free_xml(node); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(attr_missing), + cmocka_unit_test(attr_present)) diff --git a/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c new file mode 100644 index 0000000..500d8a6 --- /dev/null +++ b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c @@ -0,0 +1,59 @@ +/* + * Copyright 2021-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +empty_input(void **state) +{ + xmlNode *node = string2xml(""); + bool value; + + assert_int_equal(pcmk__xe_get_bool_attr(NULL, NULL, &value), ENODATA); + assert_int_equal(pcmk__xe_get_bool_attr(NULL, "whatever", &value), ENODATA); + assert_int_equal(pcmk__xe_get_bool_attr(node, NULL, &value), EINVAL); + assert_int_equal(pcmk__xe_get_bool_attr(node, "whatever", NULL), EINVAL); + + free_xml(node); +} + +static void +attr_missing(void **state) +{ + xmlNode *node = string2xml(""); + bool value; + + assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), ENODATA); + free_xml(node); +} + +static void +attr_present(void **state) +{ + xmlNode *node = string2xml(""); + bool value; + + value = false; + assert_int_equal(pcmk__xe_get_bool_attr(node, "a", &value), pcmk_rc_ok); + assert_true(value); + value = true; + assert_int_equal(pcmk__xe_get_bool_attr(node, "b", &value), pcmk_rc_ok); + assert_false(value); + assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), pcmk_rc_bad_input); + + free_xml(node); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_input), + cmocka_unit_test(attr_missing), + cmocka_unit_test(attr_present)) diff --git a/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c new file mode 100644 index 0000000..e1fb9c4 --- /dev/null +++ b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c @@ -0,0 +1,31 @@ +/* + * Copyright 2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include + +static void +set_attr(void **state) +{ + xmlNode *node = string2xml(""); + + pcmk__xe_set_bool_attr(node, "a", true); + pcmk__xe_set_bool_attr(node, "b", false); + + assert_string_equal(crm_element_value(node, "a"), XML_BOOLEAN_TRUE); + assert_string_equal(crm_element_value(node, "b"), XML_BOOLEAN_FALSE); + + free_xml(node); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(set_attr)) diff --git a/lib/common/tests/operations/Makefile.am b/lib/common/tests/operations/Makefile.am new file mode 100644 index 0000000..4687e1b --- /dev/null +++ b/lib/common/tests/operations/Makefile.am @@ -0,0 +1,22 @@ +# +# Copyright 2020-2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = copy_in_properties_test \ + expand_plus_plus_test \ + fix_plus_plus_recursive_test \ + parse_op_key_test \ + pcmk_is_probe_test \ + pcmk_xe_is_probe_test \ + pcmk_xe_mask_probe_failure_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/operations/copy_in_properties_test.c b/lib/common/tests/operations/copy_in_properties_test.c new file mode 100644 index 0000000..7882551 --- /dev/null +++ b/lib/common/tests/operations/copy_in_properties_test.c @@ -0,0 +1,62 @@ + /* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include + +static void +target_is_NULL(void **state) +{ + xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1"); + xmlNode *test_xml_2 = NULL; + + pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL); + + copy_in_properties(test_xml_2, test_xml_1); + + assert_ptr_equal(test_xml_2, NULL); +} + +static void +src_is_NULL(void **state) +{ + xmlNode *test_xml_1 = NULL; + xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2"); + + copy_in_properties(test_xml_2, test_xml_1); + + assert_ptr_equal(test_xml_2->properties, NULL); +} + +static void +copying_is_successful(void **state) +{ + const char *xml_1_value; + const char *xml_2_value; + + xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1"); + xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2"); + + pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL); + + copy_in_properties(test_xml_2, test_xml_1); + + xml_1_value = crm_element_value(test_xml_1, "test_prop"); + xml_2_value = crm_element_value(test_xml_2, "test_prop"); + + assert_string_equal(xml_1_value, xml_2_value); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(target_is_NULL), + cmocka_unit_test(src_is_NULL), + cmocka_unit_test(copying_is_successful)) diff --git a/lib/common/tests/operations/expand_plus_plus_test.c b/lib/common/tests/operations/expand_plus_plus_test.c new file mode 100644 index 0000000..41471f9 --- /dev/null +++ b/lib/common/tests/operations/expand_plus_plus_test.c @@ -0,0 +1,256 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include + +static void +value_is_name_plus_plus(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "6"); +} + +static void +value_is_name_plus_equals_integer(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "X+=2"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "7"); +} + +// NULL input + +static void +target_is_NULL(void **state) +{ + + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(NULL, "X", "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "5"); +} + +static void +name_is_NULL(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, NULL, "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "5"); +} + +static void +value_is_NULL(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", NULL); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "5"); +} + +// the value input doesn't start with the name input + +static void +value_is_wrong_name(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "Y++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "Y++"); +} + +static void +value_is_only_an_integer(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "2"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "2"); +} + +// non-integers + +static void +variable_is_initialized_to_be_NULL(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", NULL); + expand_plus_plus(test_xml, "X", "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "X++"); +} + +static void +variable_is_initialized_to_be_non_numeric(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "hello"); + expand_plus_plus(test_xml, "X", "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "1"); +} + +static void +variable_is_initialized_to_be_non_numeric_2(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "hello"); + expand_plus_plus(test_xml, "X", "X+=2"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "2"); +} + +static void +variable_is_initialized_to_be_numeric_and_decimal_point_containing(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5.01"); + expand_plus_plus(test_xml, "X", "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "6"); +} + +static void +variable_is_initialized_to_be_numeric_and_decimal_point_containing_2(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5.50"); + expand_plus_plus(test_xml, "X", "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "6"); +} + +static void +variable_is_initialized_to_be_numeric_and_decimal_point_containing_3(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5.99"); + expand_plus_plus(test_xml, "X", "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "6"); +} + +static void +value_is_non_numeric(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "X+=hello"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "5"); +} + +static void +value_is_numeric_and_decimal_point_containing(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "X+=2.01"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "7"); +} + +static void +value_is_numeric_and_decimal_point_containing_2(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "X+=1.50"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "6"); +} + +static void +value_is_numeric_and_decimal_point_containing_3(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "X+=1.99"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "6"); +} + +// undefined input + +static void +name_is_undefined(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "Y", "5"); + expand_plus_plus(test_xml, "X", "X++"); + new_value = crm_element_value(test_xml, "X"); + assert_string_equal(new_value, "X++"); +} + +// large input + +static void +assignment_result_is_too_large(void **state) +{ + const char *new_value; + xmlNode *test_xml = create_xml_node(NULL, "test_xml"); + crm_xml_add(test_xml, "X", "5"); + expand_plus_plus(test_xml, "X", "X+=100000000000"); + new_value = crm_element_value(test_xml, "X"); + printf("assignment result is too large %s\n", new_value); + assert_string_equal(new_value, "1000000"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(value_is_name_plus_plus), + cmocka_unit_test(value_is_name_plus_equals_integer), + cmocka_unit_test(target_is_NULL), + cmocka_unit_test(name_is_NULL), + cmocka_unit_test(value_is_NULL), + cmocka_unit_test(value_is_wrong_name), + cmocka_unit_test(value_is_only_an_integer), + cmocka_unit_test(variable_is_initialized_to_be_NULL), + cmocka_unit_test(variable_is_initialized_to_be_non_numeric), + cmocka_unit_test(variable_is_initialized_to_be_non_numeric_2), + cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing), + cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_2), + cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_3), + cmocka_unit_test(value_is_non_numeric), + cmocka_unit_test(value_is_numeric_and_decimal_point_containing), + cmocka_unit_test(value_is_numeric_and_decimal_point_containing_2), + cmocka_unit_test(value_is_numeric_and_decimal_point_containing_3), + cmocka_unit_test(name_is_undefined), + cmocka_unit_test(assignment_result_is_too_large)) diff --git a/lib/common/tests/operations/fix_plus_plus_recursive_test.c b/lib/common/tests/operations/fix_plus_plus_recursive_test.c new file mode 100644 index 0000000..b3c7cc2 --- /dev/null +++ b/lib/common/tests/operations/fix_plus_plus_recursive_test.c @@ -0,0 +1,47 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include + +static void +element_nodes(void **state) +{ + const char *new_value_root; + const char *new_value_child; + const char *new_value_grandchild; + + xmlNode *test_xml_root = create_xml_node(NULL, "test_xml_root"); + xmlNode *test_xml_child = create_xml_node(test_xml_root, "test_xml_child"); + xmlNode *test_xml_grandchild = create_xml_node(test_xml_child, "test_xml_grandchild"); + xmlNode *test_xml_text = pcmk_create_xml_text_node(test_xml_root, "text_xml_text", "content"); + xmlNode *test_xml_comment = string2xml(""); + + crm_xml_add(test_xml_root, "X", "5"); + crm_xml_add(test_xml_child, "X", "X++"); + crm_xml_add(test_xml_grandchild, "X", "X+=2"); + crm_xml_add(test_xml_text, "X", "X++"); + + fix_plus_plus_recursive(test_xml_root); + fix_plus_plus_recursive(test_xml_comment); + + new_value_root = crm_element_value(test_xml_root, "X"); + new_value_child = crm_element_value(test_xml_child, "X"); + new_value_grandchild = crm_element_value(test_xml_grandchild, "X"); + + assert_string_equal(new_value_root, "5"); + assert_string_equal(new_value_child, "1"); + assert_string_equal(new_value_grandchild, "2"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(element_nodes)) diff --git a/lib/common/tests/operations/parse_op_key_test.c b/lib/common/tests/operations/parse_op_key_test.c new file mode 100644 index 0000000..1b1bfff --- /dev/null +++ b/lib/common/tests/operations/parse_op_key_test.c @@ -0,0 +1,275 @@ +/* + * Copyright 2020-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include + +static void +basic(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("Fencing_monitor_60000", &rsc, &ty, &ms)); + assert_string_equal(rsc, "Fencing"); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 60000); + free(rsc); + free(ty); + + // Single-character resource name + assert_true(parse_op_key("R_monitor_100000", &rsc, &ty, &ms)); + assert_string_equal(rsc, "R"); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 100000); + free(rsc); + free(ty); + + // Single-character action name + assert_true(parse_op_key("R_A_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "R"); + assert_string_equal(ty, "A"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +rsc_just_underbars(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("__monitor_1000", &rsc, &ty, &ms)); + assert_string_equal(rsc, "_"); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 1000); + free(rsc); + free(ty); + + assert_true(parse_op_key("___migrate_from_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "__"); + assert_string_equal(ty, "migrate_from"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("____pre_notify_stop_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "___"); + assert_string_equal(ty, "pre_notify_stop"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +colon_in_rsc(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("ClusterIP:0_start_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "ClusterIP:0"); + assert_string_equal(ty, "start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("imagestoreclone:1_post_notify_stop_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "imagestoreclone:1"); + assert_string_equal(ty, "post_notify_stop"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +dashes_in_rsc(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("httpd-bundle-0_monitor_30000", &rsc, &ty, &ms)); + assert_string_equal(rsc, "httpd-bundle-0"); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 30000); + free(rsc); + free(ty); + + assert_true(parse_op_key("httpd-bundle-ip-192.168.122.132_start_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "httpd-bundle-ip-192.168.122.132"); + assert_string_equal(ty, "start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +migrate_to_from(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("vm_migrate_from_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "vm"); + assert_string_equal(ty, "migrate_from"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("vm_migrate_to_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "vm"); + assert_string_equal(ty, "migrate_to"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("vm_idcc_devel_migrate_to_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "vm_idcc_devel"); + assert_string_equal(ty, "migrate_to"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +pre_post(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("rsc_drbd_7788:1_post_notify_start_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "rsc_drbd_7788:1"); + assert_string_equal(ty, "post_notify_start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("rabbitmq-bundle-clone_pre_notify_stop_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "rabbitmq-bundle-clone"); + assert_string_equal(ty, "pre_notify_stop"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("post_notify_start_0", &rsc, &ty, &ms)); + assert_string_equal(rsc, "post_notify"); + assert_string_equal(ty, "start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); + + assert_true(parse_op_key("r_confirmed-post_notify_start_0", + &rsc, &ty, &ms)); + assert_string_equal(rsc, "r"); + assert_string_equal(ty, "confirmed-post_notify_start"); + assert_int_equal(ms, 0); + free(rsc); + free(ty); +} + +static void +skip_rsc(void **state) +{ + char *ty = NULL; + guint ms = 0; + + assert_true(parse_op_key("Fencing_monitor_60000", NULL, &ty, &ms)); + assert_string_equal(ty, "monitor"); + assert_int_equal(ms, 60000); + free(ty); +} + +static void +skip_ty(void **state) +{ + char *rsc = NULL; + guint ms = 0; + + assert_true(parse_op_key("Fencing_monitor_60000", &rsc, NULL, &ms)); + assert_string_equal(rsc, "Fencing"); + assert_int_equal(ms, 60000); + free(rsc); +} + +static void +skip_ms(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + + assert_true(parse_op_key("Fencing_monitor_60000", &rsc, &ty, NULL)); + assert_string_equal(rsc, "Fencing"); + assert_string_equal(ty, "monitor"); + free(rsc); + free(ty); +} + +static void +empty_input(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_false(parse_op_key("", &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); + + assert_false(parse_op_key(NULL, &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); +} + +static void +malformed_input(void **state) +{ + char *rsc = NULL; + char *ty = NULL; + guint ms = 0; + + assert_false(parse_op_key("httpd-bundle-0", &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); + + assert_false(parse_op_key("httpd-bundle-0_monitor", &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); + + assert_false(parse_op_key("httpd-bundle-0_30000", &rsc, &ty, &ms)); + assert_null(rsc); + assert_null(ty); + assert_int_equal(ms, 0); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(basic), + cmocka_unit_test(rsc_just_underbars), + cmocka_unit_test(colon_in_rsc), + cmocka_unit_test(dashes_in_rsc), + cmocka_unit_test(migrate_to_from), + cmocka_unit_test(pre_post), + cmocka_unit_test(skip_rsc), + cmocka_unit_test(skip_ty), + cmocka_unit_test(skip_ms), + cmocka_unit_test(empty_input), + cmocka_unit_test(malformed_input)) diff --git a/lib/common/tests/operations/pcmk_is_probe_test.c b/lib/common/tests/operations/pcmk_is_probe_test.c new file mode 100644 index 0000000..4a65e3f --- /dev/null +++ b/lib/common/tests/operations/pcmk_is_probe_test.c @@ -0,0 +1,25 @@ +/* + * Copyright 2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +static void +is_probe_test(void **state) +{ + assert_false(pcmk_is_probe(NULL, 0)); + assert_false(pcmk_is_probe("", 0)); + assert_false(pcmk_is_probe("blahblah", 0)); + assert_false(pcmk_is_probe("monitor", 1)); + assert_true(pcmk_is_probe("monitor", 0)); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(is_probe_test)) diff --git a/lib/common/tests/operations/pcmk_xe_is_probe_test.c b/lib/common/tests/operations/pcmk_xe_is_probe_test.c new file mode 100644 index 0000000..62b21d9 --- /dev/null +++ b/lib/common/tests/operations/pcmk_xe_is_probe_test.c @@ -0,0 +1,43 @@ +/* + * Copyright 2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +static void +op_is_probe_test(void **state) +{ + xmlNode *node = NULL; + + assert_false(pcmk_xe_is_probe(NULL)); + + node = string2xml(""); + assert_false(pcmk_xe_is_probe(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_is_probe(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_is_probe(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_is_probe(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_is_probe(node)); + free_xml(node); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(op_is_probe_test)) diff --git a/lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c new file mode 100644 index 0000000..9e38019 --- /dev/null +++ b/lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c @@ -0,0 +1,150 @@ +/* + * Copyright 2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +static void +op_is_not_probe_test(void **state) { + xmlNode *node = NULL; + + /* Not worth testing this thoroughly since it's just a duplicate of whether + * pcmk_op_is_probe works or not. + */ + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); +} + +static void +op_does_not_have_right_values_test(void **state) { + xmlNode *node = NULL; + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); +} + +static void +check_values_test(void **state) { + xmlNode *node = NULL; + + /* PCMK_EXEC_NOT_SUPPORTED */ + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + /* PCMK_EXEC_DONE */ + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + /* PCMK_EXEC_NOT_INSTALLED */ + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + /* PCMK_EXEC_ERROR */ + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + /* PCMK_EXEC_ERROR_HARD */ + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + /* PCMK_EXEC_ERROR_FATAL */ + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_true(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); + + node = string2xml(""); + assert_false(pcmk_xe_mask_probe_failure(node)); + free_xml(node); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(op_is_not_probe_test), + cmocka_unit_test(op_does_not_have_right_values_test), + cmocka_unit_test(check_values_test)) diff --git a/lib/common/tests/options/Makefile.am b/lib/common/tests/options/Makefile.am new file mode 100644 index 0000000..9a5fa98 --- /dev/null +++ b/lib/common/tests/options/Makefile.am @@ -0,0 +1,19 @@ +# +# Copyright 2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = \ + pcmk__env_option_test \ + pcmk__set_env_option_test \ + pcmk__env_option_enabled_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/common/tests/options/pcmk__env_option_enabled_test.c b/lib/common/tests/options/pcmk__env_option_enabled_test.c new file mode 100644 index 0000000..b7d5d25 --- /dev/null +++ b/lib/common/tests/options/pcmk__env_option_enabled_test.c @@ -0,0 +1,101 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include +#include + +#include "mock_private.h" + +static void +disabled_null_value(void **state) +{ + // Return false if option value not found (NULL accomplishes this) + assert_false(pcmk__env_option_enabled(NULL, NULL)); + assert_false(pcmk__env_option_enabled("pacemaker-execd", NULL)); +} + +static void +enabled_true_value(void **state) +{ + // Return true if option value is true, with or without daemon name + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "true"); + assert_true(pcmk__env_option_enabled(NULL, "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "true"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + pcmk__mock_getenv = false; +} + +static void +disabled_false_value(void **state) +{ + // Return false if option value is false (no daemon list) + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "false"); + assert_false(pcmk__env_option_enabled(NULL, "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "false"); + assert_false(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + pcmk__mock_getenv = false; +} + +static void +enabled_daemon_in_list(void **state) +{ + // Return true if daemon is in the option's value + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "pacemaker-execd"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "pacemaker-execd,pacemaker-fenced"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "pacemaker-controld,pacemaker-execd"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, + "pacemaker-controld,pacemaker-execd,pacemaker-fenced"); + assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + pcmk__mock_getenv = false; +} + +static void +disabled_daemon_not_in_list(void **state) +{ + // Return false if value is not true and daemon is not in the option's value + pcmk__mock_getenv = true; + + expect_string(__wrap_getenv, name, "PCMK_env_var"); + will_return(__wrap_getenv, "pacemaker-controld,pacemaker-fenced"); + assert_false(pcmk__env_option_enabled("pacemaker-execd", "env_var")); + + pcmk__mock_getenv = false; +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(disabled_null_value), + cmocka_unit_test(enabled_true_value), + cmocka_unit_test(disabled_false_value), + cmocka_unit_test(enabled_daemon_in_list), + cmocka_unit_test(disabled_daemon_not_in_list)) diff --git a/lib/common/tests/options/pcmk__env_option_test.c b/lib/common/tests/options/pcmk__env_option_test.c new file mode 100644 index 0000000..2b85b68 --- /dev/null +++ b/lib/common/tests/options/pcmk__env_option_test.c @@ -0,0 +1,134 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include +#include + +#include "mock_private.h" + +static void +empty_input_string(void **state) +{ + pcmk__mock_getenv = true; + + // getenv() not called + assert_null(pcmk__env_option(NULL)); + assert_null(pcmk__env_option("")); + + pcmk__mock_getenv = false; +} + +static void +input_too_long_for_both(void **state) +{ + /* pcmk__env_option() prepends "PCMK_" before lookup. If the option name is + * too long for the buffer or isn't found in the env, then it prepends "HA_" + * and tries again. A string of length (NAME_MAX - 3) will set us just over + * just over the edge for both tries. + */ + char long_opt[NAME_MAX - 2]; + + for (int i = 0; i < NAME_MAX - 3; i++) { + long_opt[i] = 'a'; + } + long_opt[NAME_MAX - 3] = '\0'; + + pcmk__mock_getenv = true; + + // getenv() not called + assert_null(pcmk__env_option(long_opt)); + + pcmk__mock_getenv = false; +} + +static void +input_too_long_for_pcmk(void **state) +{ + /* If an input is too long for "PCMK_
    • "); + } + rsc->fns->print(rsc, pre_text, options, print_data); + if (options & pe_print_html) { + status_print("
    • \n"); + } + } +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +static void +bundle_print_xml(pe_resource_t *rsc, const char *pre_text, long options, + void *print_data) +{ + pe__bundle_variant_data_t *bundle_data = NULL; + char *child_text = NULL; + CRM_CHECK(rsc != NULL, return); + + if (pre_text == NULL) { + pre_text = ""; + } + child_text = crm_strdup_printf("%s ", pre_text); + + get_bundle_variant_data(bundle_data, rsc); + + status_print("%sid); + status_print("type=\"%s\" ", container_agent_str(bundle_data->agent_type)); + status_print("image=\"%s\" ", bundle_data->image); + status_print("unique=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_unique)); + status_print("managed=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_managed)); + status_print("failed=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_failed)); + status_print(">\n"); + + for (GList *gIter = bundle_data->replicas; gIter != NULL; + gIter = gIter->next) { + pe__bundle_replica_t *replica = gIter->data; + + CRM_ASSERT(replica); + status_print("%s \n", + pre_text, replica->offset); + print_rsc_in_list(replica->ip, child_text, options, print_data); + print_rsc_in_list(replica->child, child_text, options, print_data); + print_rsc_in_list(replica->container, child_text, options, print_data); + print_rsc_in_list(replica->remote, child_text, options, print_data); + status_print("%s \n", pre_text); + } + status_print("%s\n", pre_text); + free(child_text); +} + +PCMK__OUTPUT_ARGS("bundle", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__bundle_xml(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + pe__bundle_variant_data_t *bundle_data = NULL; + int rc = pcmk_rc_no_output; + gboolean printed_header = FALSE; + gboolean print_everything = TRUE; + + const char *desc = NULL; + + CRM_ASSERT(rsc != NULL); + + get_bundle_variant_data(bundle_data, rsc); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return rc; + } + + print_everything = pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches); + + for (GList *gIter = bundle_data->replicas; gIter != NULL; + gIter = gIter->next) { + pe__bundle_replica_t *replica = gIter->data; + char *id = NULL; + gboolean print_ip, print_child, print_ctnr, print_remote; + + CRM_ASSERT(replica); + + if (pcmk__rsc_filtered_by_node(replica->container, only_node)) { + continue; + } + + print_ip = replica->ip != NULL && + !replica->ip->fns->is_filtered(replica->ip, only_rsc, print_everything); + print_child = replica->child != NULL && + !replica->child->fns->is_filtered(replica->child, only_rsc, print_everything); + print_ctnr = !replica->container->fns->is_filtered(replica->container, only_rsc, print_everything); + print_remote = replica->remote != NULL && + !replica->remote->fns->is_filtered(replica->remote, only_rsc, print_everything); + + if (!print_everything && !print_ip && !print_child && !print_ctnr && !print_remote) { + continue; + } + + if (!printed_header) { + printed_header = TRUE; + + desc = pe__resource_description(rsc, show_opts); + + rc = pe__name_and_nvpairs_xml(out, true, "bundle", 8, + "id", rsc->id, + "type", container_agent_str(bundle_data->agent_type), + "image", bundle_data->image, + "unique", pe__rsc_bool_str(rsc, pe_rsc_unique), + "maintenance", pe__rsc_bool_str(rsc, pe_rsc_maintenance), + "managed", pe__rsc_bool_str(rsc, pe_rsc_managed), + "failed", pe__rsc_bool_str(rsc, pe_rsc_failed), + "description", desc); + CRM_ASSERT(rc == pcmk_rc_ok); + } + + id = pcmk__itoa(replica->offset); + rc = pe__name_and_nvpairs_xml(out, true, "replica", 1, "id", id); + free(id); + CRM_ASSERT(rc == pcmk_rc_ok); + + if (print_ip) { + out->message(out, crm_map_element_name(replica->ip->xml), show_opts, + replica->ip, only_node, only_rsc); + } + + if (print_child) { + out->message(out, crm_map_element_name(replica->child->xml), show_opts, + replica->child, only_node, only_rsc); + } + + if (print_ctnr) { + out->message(out, crm_map_element_name(replica->container->xml), show_opts, + replica->container, only_node, only_rsc); + } + + if (print_remote) { + out->message(out, crm_map_element_name(replica->remote->xml), show_opts, + replica->remote, only_node, only_rsc); + } + + pcmk__output_xml_pop_parent(out); // replica + } + + if (printed_header) { + pcmk__output_xml_pop_parent(out); // bundle + } + + return rc; +} + +static void +pe__bundle_replica_output_html(pcmk__output_t *out, pe__bundle_replica_t *replica, + pe_node_t *node, uint32_t show_opts) +{ + pe_resource_t *rsc = replica->child; + + int offset = 0; + char buffer[LINE_MAX]; + + if(rsc == NULL) { + rsc = replica->container; + } + + if (replica->remote) { + offset += snprintf(buffer + offset, LINE_MAX - offset, "%s", + rsc_printable_id(replica->remote)); + } else { + offset += snprintf(buffer + offset, LINE_MAX - offset, "%s", + rsc_printable_id(replica->container)); + } + if (replica->ipaddr) { + offset += snprintf(buffer + offset, LINE_MAX - offset, " (%s)", + replica->ipaddr); + } + + pe__common_output_html(out, rsc, buffer, node, show_opts); +} + +/*! + * \internal + * \brief Get a string describing a resource's unmanaged state or lack thereof + * + * \param[in] rsc Resource to describe + * + * \return A string indicating that a resource is in maintenance mode or + * otherwise unmanaged, or an empty string otherwise + */ +static const char * +get_unmanaged_str(const pe_resource_t *rsc) +{ + if (pcmk_is_set(rsc->flags, pe_rsc_maintenance)) { + return " (maintenance)"; + } + if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + return " (unmanaged)"; + } + return ""; +} + +PCMK__OUTPUT_ARGS("bundle", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__bundle_html(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + const char *desc = NULL; + pe__bundle_variant_data_t *bundle_data = NULL; + int rc = pcmk_rc_no_output; + gboolean print_everything = TRUE; + + CRM_ASSERT(rsc != NULL); + + get_bundle_variant_data(bundle_data, rsc); + + desc = pe__resource_description(rsc, show_opts); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return rc; + } + + print_everything = pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches); + + for (GList *gIter = bundle_data->replicas; gIter != NULL; + gIter = gIter->next) { + pe__bundle_replica_t *replica = gIter->data; + gboolean print_ip, print_child, print_ctnr, print_remote; + + CRM_ASSERT(replica); + + if (pcmk__rsc_filtered_by_node(replica->container, only_node)) { + continue; + } + + print_ip = replica->ip != NULL && + !replica->ip->fns->is_filtered(replica->ip, only_rsc, print_everything); + print_child = replica->child != NULL && + !replica->child->fns->is_filtered(replica->child, only_rsc, print_everything); + print_ctnr = !replica->container->fns->is_filtered(replica->container, only_rsc, print_everything); + print_remote = replica->remote != NULL && + !replica->remote->fns->is_filtered(replica->remote, only_rsc, print_everything); + + if (pcmk_is_set(show_opts, pcmk_show_implicit_rscs) || + (print_everything == FALSE && (print_ip || print_child || print_ctnr || print_remote))) { + /* The text output messages used below require pe_print_implicit to + * be set to do anything. + */ + uint32_t new_show_opts = show_opts | pcmk_show_implicit_rscs; + + PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Container bundle%s: %s [%s]%s%s%s%s%s", + (bundle_data->nreplicas > 1)? " set" : "", + rsc->id, bundle_data->image, + pcmk_is_set(rsc->flags, pe_rsc_unique) ? " (unique)" : "", + desc ? " (" : "", desc ? desc : "", desc ? ")" : "", + get_unmanaged_str(rsc)); + + if (pcmk__list_of_multiple(bundle_data->replicas)) { + out->begin_list(out, NULL, NULL, "Replica[%d]", replica->offset); + } + + if (print_ip) { + out->message(out, crm_map_element_name(replica->ip->xml), + new_show_opts, replica->ip, only_node, only_rsc); + } + + if (print_child) { + out->message(out, crm_map_element_name(replica->child->xml), + new_show_opts, replica->child, only_node, only_rsc); + } + + if (print_ctnr) { + out->message(out, crm_map_element_name(replica->container->xml), + new_show_opts, replica->container, only_node, only_rsc); + } + + if (print_remote) { + out->message(out, crm_map_element_name(replica->remote->xml), + new_show_opts, replica->remote, only_node, only_rsc); + } + + if (pcmk__list_of_multiple(bundle_data->replicas)) { + out->end_list(out); + } + } else if (print_everything == FALSE && !(print_ip || print_child || print_ctnr || print_remote)) { + continue; + } else { + PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Container bundle%s: %s [%s]%s%s%s%s%s", + (bundle_data->nreplicas > 1)? " set" : "", + rsc->id, bundle_data->image, + pcmk_is_set(rsc->flags, pe_rsc_unique) ? " (unique)" : "", + desc ? " (" : "", desc ? desc : "", desc ? ")" : "", + get_unmanaged_str(rsc)); + + pe__bundle_replica_output_html(out, replica, pe__current_node(replica->container), + show_opts); + } + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +static void +pe__bundle_replica_output_text(pcmk__output_t *out, pe__bundle_replica_t *replica, + pe_node_t *node, uint32_t show_opts) +{ + const pe_resource_t *rsc = replica->child; + + int offset = 0; + char buffer[LINE_MAX]; + + if(rsc == NULL) { + rsc = replica->container; + } + + if (replica->remote) { + offset += snprintf(buffer + offset, LINE_MAX - offset, "%s", + rsc_printable_id(replica->remote)); + } else { + offset += snprintf(buffer + offset, LINE_MAX - offset, "%s", + rsc_printable_id(replica->container)); + } + if (replica->ipaddr) { + offset += snprintf(buffer + offset, LINE_MAX - offset, " (%s)", + replica->ipaddr); + } + + pe__common_output_text(out, rsc, buffer, node, show_opts); +} + +PCMK__OUTPUT_ARGS("bundle", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__bundle_text(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + const char *desc = NULL; + pe__bundle_variant_data_t *bundle_data = NULL; + int rc = pcmk_rc_no_output; + gboolean print_everything = TRUE; + + desc = pe__resource_description(rsc, show_opts); + + get_bundle_variant_data(bundle_data, rsc); + + CRM_ASSERT(rsc != NULL); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return rc; + } + + print_everything = pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches); + + for (GList *gIter = bundle_data->replicas; gIter != NULL; + gIter = gIter->next) { + pe__bundle_replica_t *replica = gIter->data; + gboolean print_ip, print_child, print_ctnr, print_remote; + + CRM_ASSERT(replica); + + if (pcmk__rsc_filtered_by_node(replica->container, only_node)) { + continue; + } + + print_ip = replica->ip != NULL && + !replica->ip->fns->is_filtered(replica->ip, only_rsc, print_everything); + print_child = replica->child != NULL && + !replica->child->fns->is_filtered(replica->child, only_rsc, print_everything); + print_ctnr = !replica->container->fns->is_filtered(replica->container, only_rsc, print_everything); + print_remote = replica->remote != NULL && + !replica->remote->fns->is_filtered(replica->remote, only_rsc, print_everything); + + if (pcmk_is_set(show_opts, pcmk_show_implicit_rscs) || + (print_everything == FALSE && (print_ip || print_child || print_ctnr || print_remote))) { + /* The text output messages used below require pe_print_implicit to + * be set to do anything. + */ + uint32_t new_show_opts = show_opts | pcmk_show_implicit_rscs; + + PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Container bundle%s: %s [%s]%s%s%s%s%s", + (bundle_data->nreplicas > 1)? " set" : "", + rsc->id, bundle_data->image, + pcmk_is_set(rsc->flags, pe_rsc_unique) ? " (unique)" : "", + desc ? " (" : "", desc ? desc : "", desc ? ")" : "", + get_unmanaged_str(rsc)); + + if (pcmk__list_of_multiple(bundle_data->replicas)) { + out->list_item(out, NULL, "Replica[%d]", replica->offset); + } + + out->begin_list(out, NULL, NULL, NULL); + + if (print_ip) { + out->message(out, crm_map_element_name(replica->ip->xml), + new_show_opts, replica->ip, only_node, only_rsc); + } + + if (print_child) { + out->message(out, crm_map_element_name(replica->child->xml), + new_show_opts, replica->child, only_node, only_rsc); + } + + if (print_ctnr) { + out->message(out, crm_map_element_name(replica->container->xml), + new_show_opts, replica->container, only_node, only_rsc); + } + + if (print_remote) { + out->message(out, crm_map_element_name(replica->remote->xml), + new_show_opts, replica->remote, only_node, only_rsc); + } + + out->end_list(out); + } else if (print_everything == FALSE && !(print_ip || print_child || print_ctnr || print_remote)) { + continue; + } else { + PCMK__OUTPUT_LIST_HEADER(out, FALSE, rc, "Container bundle%s: %s [%s]%s%s%s%s%s", + (bundle_data->nreplicas > 1)? " set" : "", + rsc->id, bundle_data->image, + pcmk_is_set(rsc->flags, pe_rsc_unique) ? " (unique)" : "", + desc ? " (" : "", desc ? desc : "", desc ? ")" : "", + get_unmanaged_str(rsc)); + + pe__bundle_replica_output_text(out, replica, pe__current_node(replica->container), + show_opts); + } + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +static void +print_bundle_replica(pe__bundle_replica_t *replica, const char *pre_text, + long options, void *print_data) +{ + pe_node_t *node = NULL; + pe_resource_t *rsc = replica->child; + + int offset = 0; + char buffer[LINE_MAX]; + + if(rsc == NULL) { + rsc = replica->container; + } + + if (replica->remote) { + offset += snprintf(buffer + offset, LINE_MAX - offset, "%s", + rsc_printable_id(replica->remote)); + } else { + offset += snprintf(buffer + offset, LINE_MAX - offset, "%s", + rsc_printable_id(replica->container)); + } + if (replica->ipaddr) { + offset += snprintf(buffer + offset, LINE_MAX - offset, " (%s)", + replica->ipaddr); + } + + node = pe__current_node(replica->container); + common_print(rsc, pre_text, buffer, node, options, print_data); +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +void +pe__print_bundle(pe_resource_t *rsc, const char *pre_text, long options, + void *print_data) +{ + pe__bundle_variant_data_t *bundle_data = NULL; + char *child_text = NULL; + CRM_CHECK(rsc != NULL, return); + + if (options & pe_print_xml) { + bundle_print_xml(rsc, pre_text, options, print_data); + return; + } + + get_bundle_variant_data(bundle_data, rsc); + + if (pre_text == NULL) { + pre_text = " "; + } + + status_print("%sContainer bundle%s: %s [%s]%s%s\n", + pre_text, ((bundle_data->nreplicas > 1)? " set" : ""), + rsc->id, bundle_data->image, + pcmk_is_set(rsc->flags, pe_rsc_unique) ? " (unique)" : "", + pcmk_is_set(rsc->flags, pe_rsc_managed) ? "" : " (unmanaged)"); + if (options & pe_print_html) { + status_print("
      \n
        \n"); + } + + + for (GList *gIter = bundle_data->replicas; gIter != NULL; + gIter = gIter->next) { + pe__bundle_replica_t *replica = gIter->data; + + CRM_ASSERT(replica); + if (options & pe_print_html) { + status_print("
      • "); + } + + if (pcmk_is_set(options, pe_print_implicit)) { + child_text = crm_strdup_printf(" %s", pre_text); + if (pcmk__list_of_multiple(bundle_data->replicas)) { + status_print(" %sReplica[%d]\n", pre_text, replica->offset); + } + if (options & pe_print_html) { + status_print("
        \n
          \n"); + } + print_rsc_in_list(replica->ip, child_text, options, print_data); + print_rsc_in_list(replica->container, child_text, options, print_data); + print_rsc_in_list(replica->remote, child_text, options, print_data); + print_rsc_in_list(replica->child, child_text, options, print_data); + if (options & pe_print_html) { + status_print("
        \n"); + } + } else { + child_text = crm_strdup_printf("%s ", pre_text); + print_bundle_replica(replica, child_text, options, print_data); + } + free(child_text); + + if (options & pe_print_html) { + status_print("
      • \n"); + } + } + if (options & pe_print_html) { + status_print("
      \n"); + } +} + +static void +free_bundle_replica(pe__bundle_replica_t *replica) +{ + if (replica == NULL) { + return; + } + + if (replica->node) { + free(replica->node); + replica->node = NULL; + } + + if (replica->ip) { + free_xml(replica->ip->xml); + replica->ip->xml = NULL; + replica->ip->fns->free(replica->ip); + replica->ip = NULL; + } + if (replica->container) { + free_xml(replica->container->xml); + replica->container->xml = NULL; + replica->container->fns->free(replica->container); + replica->container = NULL; + } + if (replica->remote) { + free_xml(replica->remote->xml); + replica->remote->xml = NULL; + replica->remote->fns->free(replica->remote); + replica->remote = NULL; + } + free(replica->ipaddr); + free(replica); +} + +void +pe__free_bundle(pe_resource_t *rsc) +{ + pe__bundle_variant_data_t *bundle_data = NULL; + CRM_CHECK(rsc != NULL, return); + + get_bundle_variant_data(bundle_data, rsc); + pe_rsc_trace(rsc, "Freeing %s", rsc->id); + + free(bundle_data->prefix); + free(bundle_data->image); + free(bundle_data->control_port); + free(bundle_data->host_network); + free(bundle_data->host_netmask); + free(bundle_data->ip_range_start); + free(bundle_data->container_network); + free(bundle_data->launcher_options); + free(bundle_data->container_command); + g_free(bundle_data->container_host_options); + + g_list_free_full(bundle_data->replicas, + (GDestroyNotify) free_bundle_replica); + g_list_free_full(bundle_data->mounts, (GDestroyNotify)mount_free); + g_list_free_full(bundle_data->ports, (GDestroyNotify)port_free); + g_list_free(rsc->children); + + if(bundle_data->child) { + free_xml(bundle_data->child->xml); + bundle_data->child->xml = NULL; + bundle_data->child->fns->free(bundle_data->child); + } + common_free(rsc); +} + +enum rsc_role_e +pe__bundle_resource_state(const pe_resource_t *rsc, gboolean current) +{ + enum rsc_role_e container_role = RSC_ROLE_UNKNOWN; + return container_role; +} + +/*! + * \brief Get the number of configured replicas in a bundle + * + * \param[in] rsc Bundle resource + * + * \return Number of configured replicas, or 0 on error + */ +int +pe_bundle_replicas(const pe_resource_t *rsc) +{ + if ((rsc == NULL) || (rsc->variant != pe_container)) { + return 0; + } else { + pe__bundle_variant_data_t *bundle_data = NULL; + + get_bundle_variant_data(bundle_data, rsc); + return bundle_data->nreplicas; + } +} + +void +pe__count_bundle(pe_resource_t *rsc) +{ + pe__bundle_variant_data_t *bundle_data = NULL; + + get_bundle_variant_data(bundle_data, rsc); + for (GList *item = bundle_data->replicas; item != NULL; item = item->next) { + pe__bundle_replica_t *replica = item->data; + + if (replica->ip) { + replica->ip->fns->count(replica->ip); + } + if (replica->child) { + replica->child->fns->count(replica->child); + } + if (replica->container) { + replica->container->fns->count(replica->container); + } + if (replica->remote) { + replica->remote->fns->count(replica->remote); + } + } +} + +gboolean +pe__bundle_is_filtered(const pe_resource_t *rsc, GList *only_rsc, + gboolean check_parent) +{ + gboolean passes = FALSE; + pe__bundle_variant_data_t *bundle_data = NULL; + + if (pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches)) { + passes = TRUE; + } else { + get_bundle_variant_data(bundle_data, rsc); + + for (GList *gIter = bundle_data->replicas; gIter != NULL; gIter = gIter->next) { + pe__bundle_replica_t *replica = gIter->data; + + if (replica->ip != NULL && !replica->ip->fns->is_filtered(replica->ip, only_rsc, FALSE)) { + passes = TRUE; + break; + } else if (replica->child != NULL && !replica->child->fns->is_filtered(replica->child, only_rsc, FALSE)) { + passes = TRUE; + break; + } else if (!replica->container->fns->is_filtered(replica->container, only_rsc, FALSE)) { + passes = TRUE; + break; + } else if (replica->remote != NULL && !replica->remote->fns->is_filtered(replica->remote, only_rsc, FALSE)) { + passes = TRUE; + break; + } + } + } + + return !passes; +} + +/*! + * \internal + * \brief Get a list of a bundle's containers + * + * \param[in] bundle Bundle resource + * + * \return Newly created list of \p bundle's containers + * \note It is the caller's responsibility to free the result with + * g_list_free(). + */ +GList * +pe__bundle_containers(const pe_resource_t *bundle) +{ + GList *containers = NULL; + const pe__bundle_variant_data_t *data = NULL; + + get_bundle_variant_data(data, bundle); + for (GList *iter = data->replicas; iter != NULL; iter = iter->next) { + pe__bundle_replica_t *replica = iter->data; + + containers = g_list_append(containers, replica->container); + } + return containers; +} + +// Bundle implementation of resource_object_functions_t:active_node() +pe_node_t * +pe__bundle_active_node(const pe_resource_t *rsc, unsigned int *count_all, + unsigned int *count_clean) +{ + pe_node_t *active = NULL; + pe_node_t *node = NULL; + pe_resource_t *container = NULL; + GList *containers = NULL; + GList *iter = NULL; + GHashTable *nodes = NULL; + const pe__bundle_variant_data_t *data = NULL; + + if (count_all != NULL) { + *count_all = 0; + } + if (count_clean != NULL) { + *count_clean = 0; + } + if (rsc == NULL) { + return NULL; + } + + /* For the purposes of this method, we only care about where the bundle's + * containers are active, so build a list of active containers. + */ + get_bundle_variant_data(data, rsc); + for (iter = data->replicas; iter != NULL; iter = iter->next) { + pe__bundle_replica_t *replica = iter->data; + + if (replica->container->running_on != NULL) { + containers = g_list_append(containers, replica->container); + } + } + if (containers == NULL) { + return NULL; + } + + /* If the bundle has only a single active container, just use that + * container's method. If live migration is ever supported for bundle + * containers, this will allow us to prefer the migration source when there + * is only one container and it is migrating. For now, this just lets us + * avoid creating the nodes table. + */ + if (pcmk__list_of_1(containers)) { + container = containers->data; + node = container->fns->active_node(container, count_all, count_clean); + g_list_free(containers); + return node; + } + + // Add all containers' active nodes to a hash table (for uniqueness) + nodes = g_hash_table_new(NULL, NULL); + for (iter = containers; iter != NULL; iter = iter->next) { + container = iter->data; + + for (GList *node_iter = container->running_on; node_iter != NULL; + node_iter = node_iter->next) { + node = node_iter->data; + + // If insert returns true, we haven't counted this node yet + if (g_hash_table_insert(nodes, (gpointer) node->details, + (gpointer) node) + && !pe__count_active_node(rsc, node, &active, count_all, + count_clean)) { + goto done; + } + } + } + +done: + g_list_free(containers); + g_hash_table_destroy(nodes); + return active; +} diff --git a/lib/pengine/clone.c b/lib/pengine/clone.c new file mode 100644 index 0000000..e411f98 --- /dev/null +++ b/lib/pengine/clone.c @@ -0,0 +1,1470 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef PCMK__COMPAT_2_0 +#define PROMOTED_INSTANCES RSC_ROLE_PROMOTED_LEGACY_S "s" +#define UNPROMOTED_INSTANCES RSC_ROLE_UNPROMOTED_LEGACY_S "s" +#else +#define PROMOTED_INSTANCES RSC_ROLE_PROMOTED_S +#define UNPROMOTED_INSTANCES RSC_ROLE_UNPROMOTED_S +#endif + +typedef struct clone_variant_data_s { + int clone_max; + int clone_node_max; + + int promoted_max; + int promoted_node_max; + + int total_clones; + + uint32_t flags; // Group of enum pe__clone_flags + + notify_data_t *stop_notify; + notify_data_t *start_notify; + notify_data_t *demote_notify; + notify_data_t *promote_notify; + + xmlNode *xml_obj_child; +} clone_variant_data_t; + +#define get_clone_variant_data(data, rsc) \ + CRM_ASSERT((rsc != NULL) && (rsc->variant == pe_clone)); \ + data = (clone_variant_data_t *) rsc->variant_opaque; + +/*! + * \internal + * \brief Return the maximum number of clone instances allowed to be run + * + * \param[in] clone Clone or clone instance to check + * + * \return Maximum instances for \p clone + */ +int +pe__clone_max(const pe_resource_t *clone) +{ + const clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, pe__const_top_resource(clone, false)); + return clone_data->clone_max; +} + +/*! + * \internal + * \brief Return the maximum number of clone instances allowed per node + * + * \param[in] clone Promotable clone or clone instance to check + * + * \return Maximum allowed instances per node for \p clone + */ +int +pe__clone_node_max(const pe_resource_t *clone) +{ + const clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, pe__const_top_resource(clone, false)); + return clone_data->clone_node_max; +} + +/*! + * \internal + * \brief Return the maximum number of clone instances allowed to be promoted + * + * \param[in] clone Promotable clone or clone instance to check + * + * \return Maximum promoted instances for \p clone + */ +int +pe__clone_promoted_max(const pe_resource_t *clone) +{ + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, pe__const_top_resource(clone, false)); + return clone_data->promoted_max; +} + +/*! + * \internal + * \brief Return the maximum number of clone instances allowed to be promoted + * + * \param[in] clone Promotable clone or clone instance to check + * + * \return Maximum promoted instances for \p clone + */ +int +pe__clone_promoted_node_max(const pe_resource_t *clone) +{ + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, pe__const_top_resource(clone, false)); + return clone_data->promoted_node_max; +} + +static GList * +sorted_hash_table_values(GHashTable *table) +{ + GList *retval = NULL; + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init(&iter, table); + while (g_hash_table_iter_next(&iter, &key, &value)) { + if (!g_list_find_custom(retval, value, (GCompareFunc) strcmp)) { + retval = g_list_prepend(retval, (char *) value); + } + } + + retval = g_list_sort(retval, (GCompareFunc) strcmp); + return retval; +} + +static GList * +nodes_with_status(GHashTable *table, const char *status) +{ + GList *retval = NULL; + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init(&iter, table); + while (g_hash_table_iter_next(&iter, &key, &value)) { + if (!strcmp((char *) value, status)) { + retval = g_list_prepend(retval, key); + } + } + + retval = g_list_sort(retval, (GCompareFunc) pcmk__numeric_strcasecmp); + return retval; +} + +static GString * +node_list_to_str(const GList *list) +{ + GString *retval = NULL; + + for (const GList *iter = list; iter != NULL; iter = iter->next) { + pcmk__add_word(&retval, 1024, (const char *) iter->data); + } + + return retval; +} + +static void +clone_header(pcmk__output_t *out, int *rc, const pe_resource_t *rsc, + clone_variant_data_t *clone_data, const char *desc) +{ + GString *attrs = NULL; + + if (pcmk_is_set(rsc->flags, pe_rsc_promotable)) { + pcmk__add_separated_word(&attrs, 64, "promotable", ", "); + } + + if (pcmk_is_set(rsc->flags, pe_rsc_unique)) { + pcmk__add_separated_word(&attrs, 64, "unique", ", "); + } + + if (pe__resource_is_disabled(rsc)) { + pcmk__add_separated_word(&attrs, 64, "disabled", ", "); + } + + if (pcmk_is_set(rsc->flags, pe_rsc_maintenance)) { + pcmk__add_separated_word(&attrs, 64, "maintenance", ", "); + + } else if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + pcmk__add_separated_word(&attrs, 64, "unmanaged", ", "); + } + + if (attrs != NULL) { + PCMK__OUTPUT_LIST_HEADER(out, FALSE, *rc, "Clone Set: %s [%s] (%s)%s%s%s", + rsc->id, ID(clone_data->xml_obj_child), + (const char *) attrs->str, desc ? " (" : "", + desc ? desc : "", desc ? ")" : ""); + g_string_free(attrs, TRUE); + } else { + PCMK__OUTPUT_LIST_HEADER(out, FALSE, *rc, "Clone Set: %s [%s]%s%s%s", + rsc->id, ID(clone_data->xml_obj_child), + desc ? " (" : "", desc ? desc : "", + desc ? ")" : ""); + } +} + +void +pe__force_anon(const char *standard, pe_resource_t *rsc, const char *rid, + pe_working_set_t *data_set) +{ + if (pe_rsc_is_clone(rsc)) { + clone_variant_data_t *clone_data = rsc->variant_opaque; + + pe_warn("Ignoring " XML_RSC_ATTR_UNIQUE " for %s because %s resources " + "such as %s can be used only as anonymous clones", + rsc->id, standard, rid); + + clone_data->clone_node_max = 1; + clone_data->clone_max = QB_MIN(clone_data->clone_max, + g_list_length(data_set->nodes)); + } +} + +pe_resource_t * +find_clone_instance(const pe_resource_t *rsc, const char *sub_id) +{ + char *child_id = NULL; + pe_resource_t *child = NULL; + const char *child_base = NULL; + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, rsc); + + child_base = ID(clone_data->xml_obj_child); + child_id = crm_strdup_printf("%s:%s", child_base, sub_id); + child = pe_find_resource(rsc->children, child_id); + + free(child_id); + return child; +} + +pe_resource_t * +pe__create_clone_child(pe_resource_t *rsc, pe_working_set_t *data_set) +{ + gboolean as_orphan = FALSE; + char *inc_num = NULL; + char *inc_max = NULL; + pe_resource_t *child_rsc = NULL; + xmlNode *child_copy = NULL; + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, rsc); + + CRM_CHECK(clone_data->xml_obj_child != NULL, return FALSE); + + if (clone_data->total_clones >= clone_data->clone_max) { + // If we've already used all available instances, this is an orphan + as_orphan = TRUE; + } + + // Allocate instance numbers in numerical order (starting at 0) + inc_num = pcmk__itoa(clone_data->total_clones); + inc_max = pcmk__itoa(clone_data->clone_max); + + child_copy = copy_xml(clone_data->xml_obj_child); + + crm_xml_add(child_copy, XML_RSC_ATTR_INCARNATION, inc_num); + + if (pe__unpack_resource(child_copy, &child_rsc, rsc, + data_set) != pcmk_rc_ok) { + goto bail; + } +/* child_rsc->globally_unique = rsc->globally_unique; */ + + CRM_ASSERT(child_rsc); + clone_data->total_clones += 1; + pe_rsc_trace(child_rsc, "Setting clone attributes for: %s", child_rsc->id); + rsc->children = g_list_append(rsc->children, child_rsc); + if (as_orphan) { + pe__set_resource_flags_recursive(child_rsc, pe_rsc_orphan); + } + + add_hash_param(child_rsc->meta, XML_RSC_ATTR_INCARNATION_MAX, inc_max); + pe_rsc_trace(rsc, "Added %s instance %s", rsc->id, child_rsc->id); + + bail: + free(inc_num); + free(inc_max); + + return child_rsc; +} + +gboolean +clone_unpack(pe_resource_t * rsc, pe_working_set_t * data_set) +{ + int lpc = 0; + xmlNode *a_child = NULL; + xmlNode *xml_obj = rsc->xml; + clone_variant_data_t *clone_data = NULL; + + const char *max_clones = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_INCARNATION_MAX); + const char *max_clones_node = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_INCARNATION_NODEMAX); + + pe_rsc_trace(rsc, "Processing resource %s...", rsc->id); + + clone_data = calloc(1, sizeof(clone_variant_data_t)); + rsc->variant_opaque = clone_data; + + if (pcmk_is_set(rsc->flags, pe_rsc_promotable)) { + const char *promoted_max = NULL; + const char *promoted_node_max = NULL; + + promoted_max = g_hash_table_lookup(rsc->meta, + XML_RSC_ATTR_PROMOTED_MAX); + if (promoted_max == NULL) { + // @COMPAT deprecated since 2.0.0 + promoted_max = g_hash_table_lookup(rsc->meta, + PCMK_XA_PROMOTED_MAX_LEGACY); + } + + promoted_node_max = g_hash_table_lookup(rsc->meta, + XML_RSC_ATTR_PROMOTED_NODEMAX); + if (promoted_node_max == NULL) { + // @COMPAT deprecated since 2.0.0 + promoted_node_max = + g_hash_table_lookup(rsc->meta, + PCMK_XA_PROMOTED_NODE_MAX_LEGACY); + } + + // Use 1 as default but 0 for minimum and invalid + if (promoted_max == NULL) { + clone_data->promoted_max = 1; + } else { + pcmk__scan_min_int(promoted_max, &(clone_data->promoted_max), 0); + } + + // Use 1 as default but 0 for minimum and invalid + if (promoted_node_max == NULL) { + clone_data->promoted_node_max = 1; + } else { + pcmk__scan_min_int(promoted_node_max, + &(clone_data->promoted_node_max), 0); + } + } + + // Implied by calloc() + /* clone_data->xml_obj_child = NULL; */ + + // Use 1 as default but 0 for minimum and invalid + if (max_clones_node == NULL) { + clone_data->clone_node_max = 1; + } else { + pcmk__scan_min_int(max_clones_node, &(clone_data->clone_node_max), 0); + } + + /* Use number of nodes (but always at least 1, which is handy for crm_verify + * for a CIB without nodes) as default, but 0 for minimum and invalid + */ + if (max_clones == NULL) { + clone_data->clone_max = QB_MAX(1, g_list_length(data_set->nodes)); + } else { + pcmk__scan_min_int(max_clones, &(clone_data->clone_max), 0); + } + + if (crm_is_true(g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_ORDERED))) { + clone_data->flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, + "Clone", rsc->id, + clone_data->flags, + pe__clone_ordered, + "pe__clone_ordered"); + } + + if ((rsc->flags & pe_rsc_unique) == 0 && clone_data->clone_node_max > 1) { + pcmk__config_err("Ignoring " XML_RSC_ATTR_PROMOTED_MAX " for %s " + "because anonymous clones support only one instance " + "per node", rsc->id); + clone_data->clone_node_max = 1; + } + + pe_rsc_trace(rsc, "Options for %s", rsc->id); + pe_rsc_trace(rsc, "\tClone max: %d", clone_data->clone_max); + pe_rsc_trace(rsc, "\tClone node max: %d", clone_data->clone_node_max); + pe_rsc_trace(rsc, "\tClone is unique: %s", + pe__rsc_bool_str(rsc, pe_rsc_unique)); + pe_rsc_trace(rsc, "\tClone is promotable: %s", + pe__rsc_bool_str(rsc, pe_rsc_promotable)); + + // Clones may contain a single group or primitive + for (a_child = pcmk__xe_first_child(xml_obj); a_child != NULL; + a_child = pcmk__xe_next(a_child)) { + + if (pcmk__str_any_of((const char *)a_child->name, XML_CIB_TAG_RESOURCE, XML_CIB_TAG_GROUP, NULL)) { + clone_data->xml_obj_child = a_child; + break; + } + } + + if (clone_data->xml_obj_child == NULL) { + pcmk__config_err("%s has nothing to clone", rsc->id); + return FALSE; + } + + /* + * Make clones ever so slightly sticky by default + * + * This helps ensure clone instances are not shuffled around the cluster + * for no benefit in situations when pre-allocation is not appropriate + */ + if (g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_STICKINESS) == NULL) { + add_hash_param(rsc->meta, XML_RSC_ATTR_STICKINESS, "1"); + } + + /* This ensures that the globally-unique value always exists for children to + * inherit when being unpacked, as well as in resource agents' environment. + */ + add_hash_param(rsc->meta, XML_RSC_ATTR_UNIQUE, + pe__rsc_bool_str(rsc, pe_rsc_unique)); + + if (clone_data->clone_max <= 0) { + /* Create one child instance so that unpack_find_resource() will hook up + * any orphans up to the parent correctly. + */ + if (pe__create_clone_child(rsc, data_set) == NULL) { + return FALSE; + } + + } else { + // Create a child instance for each available instance number + for (lpc = 0; lpc < clone_data->clone_max; lpc++) { + if (pe__create_clone_child(rsc, data_set) == NULL) { + return FALSE; + } + } + } + + pe_rsc_trace(rsc, "Added %d children to resource %s...", clone_data->clone_max, rsc->id); + return TRUE; +} + +gboolean +clone_active(pe_resource_t * rsc, gboolean all) +{ + GList *gIter = rsc->children; + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + gboolean child_active = child_rsc->fns->active(child_rsc, all); + + if (all == FALSE && child_active) { + return TRUE; + } else if (all && child_active == FALSE) { + return FALSE; + } + } + + if (all) { + return TRUE; + } else { + return FALSE; + } +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +static void +short_print(const char *list, const char *prefix, const char *type, + const char *suffix, long options, void *print_data) +{ + if(suffix == NULL) { + suffix = ""; + } + + if (!pcmk__str_empty(list)) { + if (options & pe_print_html) { + status_print("
    • "); + } + status_print("%s%s: [ %s ]%s", prefix, type, list, suffix); + + if (options & pe_print_html) { + status_print("
    • \n"); + + } else if (options & pe_print_suppres_nl) { + /* nothing */ + } else if ((options & pe_print_printf) || (options & pe_print_ncurses)) { + status_print("\n"); + } + + } +} + +static const char * +configured_role_str(pe_resource_t * rsc) +{ + const char *target_role = g_hash_table_lookup(rsc->meta, + XML_RSC_ATTR_TARGET_ROLE); + + if ((target_role == NULL) && rsc->children && rsc->children->data) { + target_role = g_hash_table_lookup(((pe_resource_t*)rsc->children->data)->meta, + XML_RSC_ATTR_TARGET_ROLE); + } + return target_role; +} + +static enum rsc_role_e +configured_role(pe_resource_t * rsc) +{ + const char *target_role = configured_role_str(rsc); + + if (target_role) { + return text2role(target_role); + } + return RSC_ROLE_UNKNOWN; +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +static void +clone_print_xml(pe_resource_t *rsc, const char *pre_text, long options, + void *print_data) +{ + char *child_text = crm_strdup_printf("%s ", pre_text); + const char *target_role = configured_role_str(rsc); + GList *gIter = rsc->children; + + status_print("%sid); + status_print("multi_state=\"%s\" ", + pe__rsc_bool_str(rsc, pe_rsc_promotable)); + status_print("unique=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_unique)); + status_print("managed=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_managed)); + status_print("failed=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_failed)); + status_print("failure_ignored=\"%s\" ", + pe__rsc_bool_str(rsc, pe_rsc_failure_ignored)); + if (target_role) { + status_print("target_role=\"%s\" ", target_role); + } + status_print(">\n"); + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + child_rsc->fns->print(child_rsc, child_text, options, print_data); + } + + status_print("%s\n", pre_text); + free(child_text); +} + +bool +is_set_recursive(const pe_resource_t *rsc, long long flag, bool any) +{ + GList *gIter; + bool all = !any; + + if (pcmk_is_set(rsc->flags, flag)) { + if(any) { + return TRUE; + } + } else if(all) { + return FALSE; + } + + for (gIter = rsc->children; gIter != NULL; gIter = gIter->next) { + if(is_set_recursive(gIter->data, flag, any)) { + if(any) { + return TRUE; + } + + } else if(all) { + return FALSE; + } + } + + if(all) { + return TRUE; + } + return FALSE; +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +void +clone_print(pe_resource_t *rsc, const char *pre_text, long options, + void *print_data) +{ + GString *list_text = NULL; + char *child_text = NULL; + GString *stopped_list = NULL; + + GList *promoted_list = NULL; + GList *started_list = NULL; + GList *gIter = rsc->children; + + clone_variant_data_t *clone_data = NULL; + int active_instances = 0; + + if (pre_text == NULL) { + pre_text = " "; + } + + if (options & pe_print_xml) { + clone_print_xml(rsc, pre_text, options, print_data); + return; + } + + get_clone_variant_data(clone_data, rsc); + + child_text = crm_strdup_printf("%s ", pre_text); + + status_print("%sClone Set: %s [%s]%s%s%s", + pre_text ? pre_text : "", rsc->id, ID(clone_data->xml_obj_child), + pcmk_is_set(rsc->flags, pe_rsc_promotable)? " (promotable)" : "", + pcmk_is_set(rsc->flags, pe_rsc_unique)? " (unique)" : "", + pcmk_is_set(rsc->flags, pe_rsc_managed)? "" : " (unmanaged)"); + + if (options & pe_print_html) { + status_print("\n
        \n"); + + } else if ((options & pe_print_log) == 0) { + status_print("\n"); + } + + for (; gIter != NULL; gIter = gIter->next) { + gboolean print_full = FALSE; + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + gboolean partially_active = child_rsc->fns->active(child_rsc, FALSE); + + if (options & pe_print_clone_details) { + print_full = TRUE; + } + + if (pcmk_is_set(rsc->flags, pe_rsc_unique)) { + // Print individual instance when unique (except stopped orphans) + if (partially_active || !pcmk_is_set(rsc->flags, pe_rsc_orphan)) { + print_full = TRUE; + } + + // Everything else in this block is for anonymous clones + + } else if (pcmk_is_set(options, pe_print_pending) + && (child_rsc->pending_task != NULL) + && strcmp(child_rsc->pending_task, "probe")) { + // Print individual instance when non-probe action is pending + print_full = TRUE; + + } else if (partially_active == FALSE) { + // List stopped instances when requested (except orphans) + if (!pcmk_is_set(child_rsc->flags, pe_rsc_orphan) + && !pcmk_is_set(options, pe_print_clone_active)) { + + pcmk__add_word(&stopped_list, 1024, child_rsc->id); + } + + } else if (is_set_recursive(child_rsc, pe_rsc_orphan, TRUE) + || is_set_recursive(child_rsc, pe_rsc_managed, FALSE) == FALSE + || is_set_recursive(child_rsc, pe_rsc_failed, TRUE)) { + + // Print individual instance when active orphaned/unmanaged/failed + print_full = TRUE; + + } else if (child_rsc->fns->active(child_rsc, TRUE)) { + // Instance of fully active anonymous clone + + pe_node_t *location = child_rsc->fns->location(child_rsc, NULL, TRUE); + + if (location) { + // Instance is active on a single node + + enum rsc_role_e a_role = child_rsc->fns->state(child_rsc, TRUE); + + if (location->details->online == FALSE && location->details->unclean) { + print_full = TRUE; + + } else if (a_role > RSC_ROLE_UNPROMOTED) { + promoted_list = g_list_append(promoted_list, location); + + } else { + started_list = g_list_append(started_list, location); + } + + } else { + /* uncolocated group - bleh */ + print_full = TRUE; + } + + } else { + // Instance of partially active anonymous clone + print_full = TRUE; + } + + if (print_full) { + if (options & pe_print_html) { + status_print("
      • \n"); + } + child_rsc->fns->print(child_rsc, child_text, options, print_data); + if (options & pe_print_html) { + status_print("
      • \n"); + } + } + } + + /* Promoted */ + promoted_list = g_list_sort(promoted_list, pe__cmp_node_name); + for (gIter = promoted_list; gIter; gIter = gIter->next) { + pe_node_t *host = gIter->data; + + pcmk__add_word(&list_text, 1024, host->details->uname); + active_instances++; + } + + if (list_text != NULL) { + short_print((const char *) list_text->str, child_text, + PROMOTED_INSTANCES, NULL, options, print_data); + g_string_truncate(list_text, 0); + } + g_list_free(promoted_list); + + /* Started/Unpromoted */ + started_list = g_list_sort(started_list, pe__cmp_node_name); + for (gIter = started_list; gIter; gIter = gIter->next) { + pe_node_t *host = gIter->data; + + pcmk__add_word(&list_text, 1024, host->details->uname); + active_instances++; + } + + if (list_text != NULL) { + if (pcmk_is_set(rsc->flags, pe_rsc_promotable)) { + enum rsc_role_e role = configured_role(rsc); + + if (role == RSC_ROLE_UNPROMOTED) { + short_print((const char *) list_text->str, child_text, + UNPROMOTED_INSTANCES " (target-role)", NULL, + options, print_data); + } else { + short_print((const char *) list_text->str, child_text, + UNPROMOTED_INSTANCES, NULL, options, print_data); + } + + } else { + short_print((const char *) list_text->str, child_text, "Started", + NULL, options, print_data); + } + } + + g_list_free(started_list); + + if (!pcmk_is_set(options, pe_print_clone_active)) { + const char *state = "Stopped"; + enum rsc_role_e role = configured_role(rsc); + + if (role == RSC_ROLE_STOPPED) { + state = "Stopped (disabled)"; + } + + if (!pcmk_is_set(rsc->flags, pe_rsc_unique) + && (clone_data->clone_max > active_instances)) { + + GList *nIter; + GList *list = g_hash_table_get_values(rsc->allowed_nodes); + + /* Custom stopped list for non-unique clones */ + if (stopped_list != NULL) { + g_string_truncate(stopped_list, 0); + } + + if (list == NULL) { + /* Clusters with symmetrical=false haven't calculated allowed_nodes yet + * If we've not probed for them yet, the Stopped list will be empty + */ + list = g_hash_table_get_values(rsc->known_on); + } + + list = g_list_sort(list, pe__cmp_node_name); + for (nIter = list; nIter != NULL; nIter = nIter->next) { + pe_node_t *node = (pe_node_t *)nIter->data; + + if (pe_find_node(rsc->running_on, node->details->uname) == NULL) { + pcmk__add_word(&stopped_list, 1024, node->details->uname); + } + } + g_list_free(list); + } + + if (stopped_list != NULL) { + short_print((const char *) stopped_list->str, child_text, state, + NULL, options, print_data); + } + } + + if (options & pe_print_html) { + status_print("
      \n"); + } + + if (list_text != NULL) { + g_string_free(list_text, TRUE); + } + + if (stopped_list != NULL) { + g_string_free(stopped_list, TRUE); + } + free(child_text); +} + +PCMK__OUTPUT_ARGS("clone", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__clone_xml(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + + const char *desc = NULL; + GList *gIter = rsc->children; + GList *all = NULL; + int rc = pcmk_rc_no_output; + gboolean printed_header = FALSE; + gboolean print_everything = TRUE; + + + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return rc; + } + + print_everything = pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches) || + (strstr(rsc->id, ":") != NULL && pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches)); + + all = g_list_prepend(all, (gpointer) "*"); + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + if (pcmk__rsc_filtered_by_node(child_rsc, only_node)) { + continue; + } + + if (child_rsc->fns->is_filtered(child_rsc, only_rsc, print_everything)) { + continue; + } + + if (!printed_header) { + printed_header = TRUE; + + desc = pe__resource_description(rsc, show_opts); + + rc = pe__name_and_nvpairs_xml(out, true, "clone", 10, + "id", rsc->id, + "multi_state", pe__rsc_bool_str(rsc, pe_rsc_promotable), + "unique", pe__rsc_bool_str(rsc, pe_rsc_unique), + "maintenance", pe__rsc_bool_str(rsc, pe_rsc_maintenance), + "managed", pe__rsc_bool_str(rsc, pe_rsc_managed), + "disabled", pcmk__btoa(pe__resource_is_disabled(rsc)), + "failed", pe__rsc_bool_str(rsc, pe_rsc_failed), + "failure_ignored", pe__rsc_bool_str(rsc, pe_rsc_failure_ignored), + "target_role", configured_role_str(rsc), + "description", desc); + CRM_ASSERT(rc == pcmk_rc_ok); + } + + out->message(out, crm_map_element_name(child_rsc->xml), show_opts, + child_rsc, only_node, all); + } + + if (printed_header) { + pcmk__output_xml_pop_parent(out); + } + + g_list_free(all); + return rc; +} + +PCMK__OUTPUT_ARGS("clone", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__clone_default(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + GHashTable *stopped = NULL; + + GString *list_text = NULL; + + GList *promoted_list = NULL; + GList *started_list = NULL; + GList *gIter = rsc->children; + + const char *desc = NULL; + + clone_variant_data_t *clone_data = NULL; + int active_instances = 0; + int rc = pcmk_rc_no_output; + gboolean print_everything = TRUE; + + desc = pe__resource_description(rsc, show_opts); + + get_clone_variant_data(clone_data, rsc); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return rc; + } + + print_everything = pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches) || + (strstr(rsc->id, ":") != NULL && pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches)); + + for (; gIter != NULL; gIter = gIter->next) { + gboolean print_full = FALSE; + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + gboolean partially_active = child_rsc->fns->active(child_rsc, FALSE); + + if (pcmk__rsc_filtered_by_node(child_rsc, only_node)) { + continue; + } + + if (child_rsc->fns->is_filtered(child_rsc, only_rsc, print_everything)) { + continue; + } + + if (pcmk_is_set(show_opts, pcmk_show_clone_detail)) { + print_full = TRUE; + } + + if (pcmk_is_set(rsc->flags, pe_rsc_unique)) { + // Print individual instance when unique (except stopped orphans) + if (partially_active || !pcmk_is_set(rsc->flags, pe_rsc_orphan)) { + print_full = TRUE; + } + + // Everything else in this block is for anonymous clones + + } else if (pcmk_is_set(show_opts, pcmk_show_pending) + && (child_rsc->pending_task != NULL) + && strcmp(child_rsc->pending_task, "probe")) { + // Print individual instance when non-probe action is pending + print_full = TRUE; + + } else if (partially_active == FALSE) { + // List stopped instances when requested (except orphans) + if (!pcmk_is_set(child_rsc->flags, pe_rsc_orphan) + && !pcmk_is_set(show_opts, pcmk_show_clone_detail) + && pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) { + if (stopped == NULL) { + stopped = pcmk__strkey_table(free, free); + } + g_hash_table_insert(stopped, strdup(child_rsc->id), strdup("Stopped")); + } + + } else if (is_set_recursive(child_rsc, pe_rsc_orphan, TRUE) + || is_set_recursive(child_rsc, pe_rsc_managed, FALSE) == FALSE + || is_set_recursive(child_rsc, pe_rsc_failed, TRUE)) { + + // Print individual instance when active orphaned/unmanaged/failed + print_full = TRUE; + + } else if (child_rsc->fns->active(child_rsc, TRUE)) { + // Instance of fully active anonymous clone + + pe_node_t *location = child_rsc->fns->location(child_rsc, NULL, TRUE); + + if (location) { + // Instance is active on a single node + + enum rsc_role_e a_role = child_rsc->fns->state(child_rsc, TRUE); + + if (location->details->online == FALSE && location->details->unclean) { + print_full = TRUE; + + } else if (a_role > RSC_ROLE_UNPROMOTED) { + promoted_list = g_list_append(promoted_list, location); + + } else { + started_list = g_list_append(started_list, location); + } + + } else { + /* uncolocated group - bleh */ + print_full = TRUE; + } + + } else { + // Instance of partially active anonymous clone + print_full = TRUE; + } + + if (print_full) { + GList *all = NULL; + + clone_header(out, &rc, rsc, clone_data, desc); + + /* Print every resource that's a child of this clone. */ + all = g_list_prepend(all, (gpointer) "*"); + out->message(out, crm_map_element_name(child_rsc->xml), show_opts, + child_rsc, only_node, all); + g_list_free(all); + } + } + + if (pcmk_is_set(show_opts, pcmk_show_clone_detail)) { + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return pcmk_rc_ok; + } + + /* Promoted */ + promoted_list = g_list_sort(promoted_list, pe__cmp_node_name); + for (gIter = promoted_list; gIter; gIter = gIter->next) { + pe_node_t *host = gIter->data; + + if (!pcmk__str_in_list(host->details->uname, only_node, + pcmk__str_star_matches|pcmk__str_casei)) { + continue; + } + + pcmk__add_word(&list_text, 1024, host->details->uname); + active_instances++; + } + g_list_free(promoted_list); + + if ((list_text != NULL) && (list_text->len > 0)) { + clone_header(out, &rc, rsc, clone_data, desc); + + out->list_item(out, NULL, PROMOTED_INSTANCES ": [ %s ]", + (const char *) list_text->str); + g_string_truncate(list_text, 0); + } + + /* Started/Unpromoted */ + started_list = g_list_sort(started_list, pe__cmp_node_name); + for (gIter = started_list; gIter; gIter = gIter->next) { + pe_node_t *host = gIter->data; + + if (!pcmk__str_in_list(host->details->uname, only_node, + pcmk__str_star_matches|pcmk__str_casei)) { + continue; + } + + pcmk__add_word(&list_text, 1024, host->details->uname); + active_instances++; + } + g_list_free(started_list); + + if ((list_text != NULL) && (list_text->len > 0)) { + clone_header(out, &rc, rsc, clone_data, desc); + + if (pcmk_is_set(rsc->flags, pe_rsc_promotable)) { + enum rsc_role_e role = configured_role(rsc); + + if (role == RSC_ROLE_UNPROMOTED) { + out->list_item(out, NULL, + UNPROMOTED_INSTANCES " (target-role): [ %s ]", + (const char *) list_text->str); + } else { + out->list_item(out, NULL, UNPROMOTED_INSTANCES ": [ %s ]", + (const char *) list_text->str); + } + + } else { + out->list_item(out, NULL, "Started: [ %s ]", + (const char *) list_text->str); + } + } + + if (list_text != NULL) { + g_string_free(list_text, TRUE); + } + + if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) { + if (!pcmk_is_set(rsc->flags, pe_rsc_unique) + && (clone_data->clone_max > active_instances)) { + + GList *nIter; + GList *list = g_hash_table_get_values(rsc->allowed_nodes); + + /* Custom stopped table for non-unique clones */ + if (stopped != NULL) { + g_hash_table_destroy(stopped); + stopped = NULL; + } + + if (list == NULL) { + /* Clusters with symmetrical=false haven't calculated allowed_nodes yet + * If we've not probed for them yet, the Stopped list will be empty + */ + list = g_hash_table_get_values(rsc->known_on); + } + + list = g_list_sort(list, pe__cmp_node_name); + for (nIter = list; nIter != NULL; nIter = nIter->next) { + pe_node_t *node = (pe_node_t *)nIter->data; + + if (pe_find_node(rsc->running_on, node->details->uname) == NULL && + pcmk__str_in_list(node->details->uname, only_node, + pcmk__str_star_matches|pcmk__str_casei)) { + xmlNode *probe_op = pe__failed_probe_for_rsc(rsc, node->details->uname); + const char *state = "Stopped"; + + if (configured_role(rsc) == RSC_ROLE_STOPPED) { + state = "Stopped (disabled)"; + } + + if (stopped == NULL) { + stopped = pcmk__strkey_table(free, free); + } + if (probe_op != NULL) { + int rc; + + pcmk__scan_min_int(crm_element_value(probe_op, XML_LRM_ATTR_RC), &rc, 0); + g_hash_table_insert(stopped, strdup(node->details->uname), + crm_strdup_printf("Stopped (%s)", services_ocf_exitcode_str(rc))); + } else { + g_hash_table_insert(stopped, strdup(node->details->uname), + strdup(state)); + } + } + } + g_list_free(list); + } + + if (stopped != NULL) { + GList *list = sorted_hash_table_values(stopped); + + clone_header(out, &rc, rsc, clone_data, desc); + + for (GList *status_iter = list; status_iter != NULL; status_iter = status_iter->next) { + const char *status = status_iter->data; + GList *nodes = nodes_with_status(stopped, status); + GString *nodes_str = node_list_to_str(nodes); + + if (nodes_str != NULL) { + if (nodes_str->len > 0) { + out->list_item(out, NULL, "%s: [ %s ]", status, + (const char *) nodes_str->str); + } + g_string_free(nodes_str, TRUE); + } + + g_list_free(nodes); + } + + g_list_free(list); + g_hash_table_destroy(stopped); + + /* If there are no instances of this clone (perhaps because there are no + * nodes configured), simply output the clone header by itself. This can + * come up in PCS testing. + */ + } else if (active_instances == 0) { + clone_header(out, &rc, rsc, clone_data, desc); + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; + } + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +void +clone_free(pe_resource_t * rsc) +{ + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, rsc); + + pe_rsc_trace(rsc, "Freeing %s", rsc->id); + + for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + CRM_ASSERT(child_rsc); + pe_rsc_trace(child_rsc, "Freeing child %s", child_rsc->id); + free_xml(child_rsc->xml); + child_rsc->xml = NULL; + /* There could be a saved unexpanded xml */ + free_xml(child_rsc->orig_xml); + child_rsc->orig_xml = NULL; + child_rsc->fns->free(child_rsc); + } + + g_list_free(rsc->children); + + if (clone_data) { + CRM_ASSERT(clone_data->demote_notify == NULL); + CRM_ASSERT(clone_data->stop_notify == NULL); + CRM_ASSERT(clone_data->start_notify == NULL); + CRM_ASSERT(clone_data->promote_notify == NULL); + } + + common_free(rsc); +} + +enum rsc_role_e +clone_resource_state(const pe_resource_t * rsc, gboolean current) +{ + enum rsc_role_e clone_role = RSC_ROLE_UNKNOWN; + GList *gIter = rsc->children; + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + enum rsc_role_e a_role = child_rsc->fns->state(child_rsc, current); + + if (a_role > clone_role) { + clone_role = a_role; + } + } + + pe_rsc_trace(rsc, "%s role: %s", rsc->id, role2text(clone_role)); + return clone_role; +} + +/*! + * \internal + * \brief Check whether a clone has an instance for every node + * + * \param[in] rsc Clone to check + * \param[in] data_set Cluster state + */ +bool +pe__is_universal_clone(const pe_resource_t *rsc, + const pe_working_set_t *data_set) +{ + if (pe_rsc_is_clone(rsc)) { + clone_variant_data_t *clone_data = rsc->variant_opaque; + + if (clone_data->clone_max == g_list_length(data_set->nodes)) { + return TRUE; + } + } + return FALSE; +} + +gboolean +pe__clone_is_filtered(const pe_resource_t *rsc, GList *only_rsc, + gboolean check_parent) +{ + gboolean passes = FALSE; + clone_variant_data_t *clone_data = NULL; + + if (pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches)) { + passes = TRUE; + } else { + get_clone_variant_data(clone_data, rsc); + passes = pcmk__str_in_list(ID(clone_data->xml_obj_child), only_rsc, pcmk__str_star_matches); + + if (!passes) { + for (const GList *iter = rsc->children; + iter != NULL; iter = iter->next) { + + const pe_resource_t *child_rsc = NULL; + + child_rsc = (const pe_resource_t *) iter->data; + if (!child_rsc->fns->is_filtered(child_rsc, only_rsc, FALSE)) { + passes = TRUE; + break; + } + } + } + } + return !passes; +} + +const char * +pe__clone_child_id(const pe_resource_t *rsc) +{ + clone_variant_data_t *clone_data = NULL; + get_clone_variant_data(clone_data, rsc); + return ID(clone_data->xml_obj_child); +} + +/*! + * \internal + * \brief Check whether a clone is ordered + * + * \param[in] clone Clone resource to check + * + * \return true if clone is ordered, otherwise false + */ +bool +pe__clone_is_ordered(const pe_resource_t *clone) +{ + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, clone); + return pcmk_is_set(clone_data->flags, pe__clone_ordered); +} + +/*! + * \internal + * \brief Set a clone flag + * + * \param[in,out] clone Clone resource to set flag for + * \param[in] flag Clone flag to set + * + * \return Standard Pacemaker return code (either pcmk_rc_ok if flag was not + * already set or pcmk_rc_already if it was) + */ +int +pe__set_clone_flag(pe_resource_t *clone, enum pe__clone_flags flag) +{ + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, clone); + if (pcmk_is_set(clone_data->flags, flag)) { + return pcmk_rc_already; + } + clone_data->flags = pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, + "Clone", clone->id, + clone_data->flags, flag, "flag"); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Create pseudo-actions needed for promotable clones + * + * \param[in,out] clone Promotable clone to create actions for + * \param[in] any_promoting Whether any instances will be promoted + * \param[in] any_demoting Whether any instance will be demoted + */ +void +pe__create_promotable_pseudo_ops(pe_resource_t *clone, bool any_promoting, + bool any_demoting) +{ + pe_action_t *action = NULL; + pe_action_t *action_complete = NULL; + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, clone); + + // Create a "promote" action for the clone itself + action = pe__new_rsc_pseudo_action(clone, RSC_PROMOTE, !any_promoting, + true); + + // Create a "promoted" action for when all promotions are done + action_complete = pe__new_rsc_pseudo_action(clone, RSC_PROMOTED, + !any_promoting, true); + action_complete->priority = INFINITY; + + // Create notification pseudo-actions for promotion + if (clone_data->promote_notify == NULL) { + clone_data->promote_notify = pe__action_notif_pseudo_ops(clone, + RSC_PROMOTE, + action, + action_complete); + } + + // Create a "demote" action for the clone itself + action = pe__new_rsc_pseudo_action(clone, RSC_DEMOTE, !any_demoting, true); + + // Create a "demoted" action for when all demotions are done + action_complete = pe__new_rsc_pseudo_action(clone, RSC_DEMOTED, + !any_demoting, true); + action_complete->priority = INFINITY; + + // Create notification pseudo-actions for demotion + if (clone_data->demote_notify == NULL) { + clone_data->demote_notify = pe__action_notif_pseudo_ops(clone, + RSC_DEMOTE, + action, + action_complete); + + if (clone_data->promote_notify != NULL) { + order_actions(clone_data->stop_notify->post_done, + clone_data->promote_notify->pre, + pe_order_optional); + order_actions(clone_data->start_notify->post_done, + clone_data->promote_notify->pre, + pe_order_optional); + order_actions(clone_data->demote_notify->post_done, + clone_data->promote_notify->pre, + pe_order_optional); + order_actions(clone_data->demote_notify->post_done, + clone_data->start_notify->pre, + pe_order_optional); + order_actions(clone_data->demote_notify->post_done, + clone_data->stop_notify->pre, + pe_order_optional); + } + } +} + +/*! + * \internal + * \brief Create all notification data and actions for a clone + * + * \param[in,out] clone Clone to create notifications for + */ +void +pe__create_clone_notifications(pe_resource_t *clone) +{ + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, clone); + + pe__create_action_notifications(clone, clone_data->start_notify); + pe__create_action_notifications(clone, clone_data->stop_notify); + pe__create_action_notifications(clone, clone_data->promote_notify); + pe__create_action_notifications(clone, clone_data->demote_notify); +} + +/*! + * \internal + * \brief Free all notification data for a clone + * + * \param[in,out] clone Clone to free notification data for + */ +void +pe__free_clone_notification_data(pe_resource_t *clone) +{ + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, clone); + + pe__free_action_notification_data(clone_data->demote_notify); + clone_data->demote_notify = NULL; + + pe__free_action_notification_data(clone_data->stop_notify); + clone_data->stop_notify = NULL; + + pe__free_action_notification_data(clone_data->start_notify); + clone_data->start_notify = NULL; + + pe__free_action_notification_data(clone_data->promote_notify); + clone_data->promote_notify = NULL; +} + +/*! + * \internal + * \brief Create pseudo-actions for clone start/stop notifications + * + * \param[in,out] clone Clone to create pseudo-actions for + * \param[in,out] start Start action for \p clone + * \param[in,out] stop Stop action for \p clone + * \param[in,out] started Started action for \p clone + * \param[in,out] stopped Stopped action for \p clone + */ +void +pe__create_clone_notif_pseudo_ops(pe_resource_t *clone, + pe_action_t *start, pe_action_t *started, + pe_action_t *stop, pe_action_t *stopped) +{ + clone_variant_data_t *clone_data = NULL; + + get_clone_variant_data(clone_data, clone); + + if (clone_data->start_notify == NULL) { + clone_data->start_notify = pe__action_notif_pseudo_ops(clone, RSC_START, + start, started); + } + + if (clone_data->stop_notify == NULL) { + clone_data->stop_notify = pe__action_notif_pseudo_ops(clone, RSC_STOP, + stop, stopped); + if ((clone_data->start_notify != NULL) + && (clone_data->stop_notify != NULL)) { + order_actions(clone_data->stop_notify->post_done, + clone_data->start_notify->pre, pe_order_optional); + } + } +} diff --git a/lib/pengine/common.c b/lib/pengine/common.c new file mode 100644 index 0000000..6c69bfc --- /dev/null +++ b/lib/pengine/common.c @@ -0,0 +1,564 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include + +#include + +#include + +gboolean was_processing_error = FALSE; +gboolean was_processing_warning = FALSE; + +static bool +check_placement_strategy(const char *value) +{ + return pcmk__strcase_any_of(value, "default", "utilization", "minimal", + "balanced", NULL); +} + +static pcmk__cluster_option_t pe_opts[] = { + /* name, old name, type, allowed values, + * default value, validator, + * short description, + * long description + */ + { + "no-quorum-policy", NULL, "select", "stop, freeze, ignore, demote, suicide", + "stop", pcmk__valid_quorum, + N_("What to do when the cluster does not have quorum"), + NULL + }, + { + "symmetric-cluster", NULL, "boolean", NULL, + "true", pcmk__valid_boolean, + N_("Whether resources can run on any node by default"), + NULL + }, + { + "maintenance-mode", NULL, "boolean", NULL, + "false", pcmk__valid_boolean, + N_("Whether the cluster should refrain from monitoring, starting, " + "and stopping resources"), + NULL + }, + { + "start-failure-is-fatal", NULL, "boolean", NULL, + "true", pcmk__valid_boolean, + N_("Whether a start failure should prevent a resource from being " + "recovered on the same node"), + N_("When true, the cluster will immediately ban a resource from a node " + "if it fails to start there. When false, the cluster will instead " + "check the resource's fail count against its migration-threshold.") + }, + { + "enable-startup-probes", NULL, "boolean", NULL, + "true", pcmk__valid_boolean, + N_("Whether the cluster should check for active resources during start-up"), + NULL + }, + { + XML_CONFIG_ATTR_SHUTDOWN_LOCK, NULL, "boolean", NULL, + "false", pcmk__valid_boolean, + N_("Whether to lock resources to a cleanly shut down node"), + N_("When true, resources active on a node when it is cleanly shut down " + "are kept \"locked\" to that node (not allowed to run elsewhere) " + "until they start again on that node after it rejoins (or for at " + "most shutdown-lock-limit, if set). Stonith resources and " + "Pacemaker Remote connections are never locked. Clone and bundle " + "instances and the promoted role of promotable clones are " + "currently never locked, though support could be added in a future " + "release.") + }, + { + XML_CONFIG_ATTR_SHUTDOWN_LOCK_LIMIT, NULL, "time", NULL, + "0", pcmk__valid_interval_spec, + N_("Do not lock resources to a cleanly shut down node longer than " + "this"), + N_("If shutdown-lock is true and this is set to a nonzero time " + "duration, shutdown locks will expire after this much time has " + "passed since the shutdown was initiated, even if the node has not " + "rejoined.") + }, + + // Fencing-related options + { + "stonith-enabled", NULL, "boolean", NULL, + "true", pcmk__valid_boolean, + N_("*** Advanced Use Only *** " + "Whether nodes may be fenced as part of recovery"), + N_("If false, unresponsive nodes are immediately assumed to be harmless, " + "and resources that were active on them may be recovered " + "elsewhere. This can result in a \"split-brain\" situation, " + "potentially leading to data loss and/or service unavailability.") + }, + { + "stonith-action", NULL, "select", "reboot, off, poweroff", + "reboot", pcmk__is_fencing_action, + N_("Action to send to fence device when a node needs to be fenced " + "(\"poweroff\" is a deprecated alias for \"off\")"), + NULL + }, + { + "stonith-timeout", NULL, "time", NULL, + "60s", pcmk__valid_interval_spec, + N_("*** Advanced Use Only *** Unused by Pacemaker"), + N_("This value is not used by Pacemaker, but is kept for backward " + "compatibility, and certain legacy fence agents might use it.") + }, + { + XML_ATTR_HAVE_WATCHDOG, NULL, "boolean", NULL, + "false", pcmk__valid_boolean, + N_("Whether watchdog integration is enabled"), + N_("This is set automatically by the cluster according to whether SBD " + "is detected to be in use. User-configured values are ignored. " + "The value `true` is meaningful if diskless SBD is used and " + "`stonith-watchdog-timeout` is nonzero. In that case, if fencing " + "is required, watchdog-based self-fencing will be performed via " + "SBD without requiring a fencing resource explicitly configured.") + }, + { + "concurrent-fencing", NULL, "boolean", NULL, + PCMK__CONCURRENT_FENCING_DEFAULT, pcmk__valid_boolean, + N_("Allow performing fencing operations in parallel"), + NULL + }, + { + "startup-fencing", NULL, "boolean", NULL, + "true", pcmk__valid_boolean, + N_("*** Advanced Use Only *** Whether to fence unseen nodes at start-up"), + N_("Setting this to false may lead to a \"split-brain\" situation," + "potentially leading to data loss and/or service unavailability.") + }, + { + XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY, NULL, "time", NULL, + "0", pcmk__valid_interval_spec, + N_("Apply fencing delay targeting the lost nodes with the highest total resource priority"), + N_("Apply specified delay for the fencings that are targeting the lost " + "nodes with the highest total resource priority in case we don't " + "have the majority of the nodes in our cluster partition, so that " + "the more significant nodes potentially win any fencing match, " + "which is especially meaningful under split-brain of 2-node " + "cluster. A promoted resource instance takes the base priority + 1 " + "on calculation if the base priority is not 0. Any static/random " + "delays that are introduced by `pcmk_delay_base/max` configured " + "for the corresponding fencing resources will be added to this " + "delay. This delay should be significantly greater than, safely " + "twice, the maximum `pcmk_delay_base/max`. By default, priority " + "fencing delay is disabled.") + }, + + { + "cluster-delay", NULL, "time", NULL, + "60s", pcmk__valid_interval_spec, + N_("Maximum time for node-to-node communication"), + N_("The node elected Designated Controller (DC) will consider an action " + "failed if it does not get a response from the node executing the " + "action within this time (after considering the action's own " + "timeout). The \"correct\" value will depend on the speed and " + "load of your network and cluster nodes.") + }, + { + "batch-limit", NULL, "integer", NULL, + "0", pcmk__valid_number, + N_("Maximum number of jobs that the cluster may execute in parallel " + "across all nodes"), + N_("The \"correct\" value will depend on the speed and load of your " + "network and cluster nodes. If set to 0, the cluster will " + "impose a dynamically calculated limit when any node has a " + "high load.") + }, + { + "migration-limit", NULL, "integer", NULL, + "-1", pcmk__valid_number, + N_("The number of live migration actions that the cluster is allowed " + "to execute in parallel on a node (-1 means no limit)") + }, + + /* Orphans and stopping */ + { + "stop-all-resources", NULL, "boolean", NULL, + "false", pcmk__valid_boolean, + N_("Whether the cluster should stop all active resources"), + NULL + }, + { + "stop-orphan-resources", NULL, "boolean", NULL, + "true", pcmk__valid_boolean, + N_("Whether to stop resources that were removed from the configuration"), + NULL + }, + { + "stop-orphan-actions", NULL, "boolean", NULL, + "true", pcmk__valid_boolean, + N_("Whether to cancel recurring actions removed from the configuration"), + NULL + }, + { + "remove-after-stop", NULL, "boolean", NULL, + "false", pcmk__valid_boolean, + N_("*** Deprecated *** Whether to remove stopped resources from " + "the executor"), + N_("Values other than default are poorly tested and potentially dangerous." + " This option will be removed in a future release.") + }, + + /* Storing inputs */ + { + "pe-error-series-max", NULL, "integer", NULL, + "-1", pcmk__valid_number, + N_("The number of scheduler inputs resulting in errors to save"), + N_("Zero to disable, -1 to store unlimited.") + }, + { + "pe-warn-series-max", NULL, "integer", NULL, + "5000", pcmk__valid_number, + N_("The number of scheduler inputs resulting in warnings to save"), + N_("Zero to disable, -1 to store unlimited.") + }, + { + "pe-input-series-max", NULL, "integer", NULL, + "4000", pcmk__valid_number, + N_("The number of scheduler inputs without errors or warnings to save"), + N_("Zero to disable, -1 to store unlimited.") + }, + + /* Node health */ + { + PCMK__OPT_NODE_HEALTH_STRATEGY, NULL, "select", + PCMK__VALUE_NONE ", " PCMK__VALUE_MIGRATE_ON_RED ", " + PCMK__VALUE_ONLY_GREEN ", " PCMK__VALUE_PROGRESSIVE ", " + PCMK__VALUE_CUSTOM, + PCMK__VALUE_NONE, pcmk__validate_health_strategy, + N_("How cluster should react to node health attributes"), + N_("Requires external entities to create node attributes (named with " + "the prefix \"#health\") with values \"red\", " + "\"yellow\", or \"green\".") + }, + { + PCMK__OPT_NODE_HEALTH_BASE, NULL, "integer", NULL, + "0", pcmk__valid_number, + N_("Base health score assigned to a node"), + N_("Only used when \"node-health-strategy\" is set to \"progressive\".") + }, + { + PCMK__OPT_NODE_HEALTH_GREEN, NULL, "integer", NULL, + "0", pcmk__valid_number, + N_("The score to use for a node health attribute whose value is \"green\""), + N_("Only used when \"node-health-strategy\" is set to \"custom\" or \"progressive\".") + }, + { + PCMK__OPT_NODE_HEALTH_YELLOW, NULL, "integer", NULL, + "0", pcmk__valid_number, + N_("The score to use for a node health attribute whose value is \"yellow\""), + N_("Only used when \"node-health-strategy\" is set to \"custom\" or \"progressive\".") + }, + { + PCMK__OPT_NODE_HEALTH_RED, NULL, "integer", NULL, + "-INFINITY", pcmk__valid_number, + N_("The score to use for a node health attribute whose value is \"red\""), + N_("Only used when \"node-health-strategy\" is set to \"custom\" or \"progressive\".") + }, + + /*Placement Strategy*/ + { + "placement-strategy", NULL, "select", + "default, utilization, minimal, balanced", + "default", check_placement_strategy, + N_("How the cluster should allocate resources to nodes"), + NULL + }, +}; + +void +pe_metadata(pcmk__output_t *out) +{ + const char *desc_short = "Pacemaker scheduler options"; + const char *desc_long = "Cluster options used by Pacemaker's scheduler"; + + gchar *s = pcmk__format_option_metadata("pacemaker-schedulerd", desc_short, + desc_long, pe_opts, + PCMK__NELEM(pe_opts)); + out->output_xml(out, "metadata", s); + g_free(s); +} + +void +verify_pe_options(GHashTable * options) +{ + pcmk__validate_cluster_options(options, pe_opts, PCMK__NELEM(pe_opts)); +} + +const char * +pe_pref(GHashTable * options, const char *name) +{ + return pcmk__cluster_option(options, pe_opts, PCMK__NELEM(pe_opts), name); +} + +const char * +fail2text(enum action_fail_response fail) +{ + const char *result = ""; + + switch (fail) { + case action_fail_ignore: + result = "ignore"; + break; + case action_fail_demote: + result = "demote"; + break; + case action_fail_block: + result = "block"; + break; + case action_fail_recover: + result = "recover"; + break; + case action_fail_migrate: + result = "migrate"; + break; + case action_fail_stop: + result = "stop"; + break; + case action_fail_fence: + result = "fence"; + break; + case action_fail_standby: + result = "standby"; + break; + case action_fail_restart_container: + result = "restart-container"; + break; + case action_fail_reset_remote: + result = "reset-remote"; + break; + } + return result; +} + +enum action_tasks +text2task(const char *task) +{ + if (pcmk__str_eq(task, CRMD_ACTION_STOP, pcmk__str_casei)) { + return stop_rsc; + } else if (pcmk__str_eq(task, CRMD_ACTION_STOPPED, pcmk__str_casei)) { + return stopped_rsc; + } else if (pcmk__str_eq(task, CRMD_ACTION_START, pcmk__str_casei)) { + return start_rsc; + } else if (pcmk__str_eq(task, CRMD_ACTION_STARTED, pcmk__str_casei)) { + return started_rsc; + } else if (pcmk__str_eq(task, CRM_OP_SHUTDOWN, pcmk__str_casei)) { + return shutdown_crm; + } else if (pcmk__str_eq(task, CRM_OP_FENCE, pcmk__str_casei)) { + return stonith_node; + } else if (pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_casei)) { + return monitor_rsc; + } else if (pcmk__str_eq(task, CRMD_ACTION_NOTIFY, pcmk__str_casei)) { + return action_notify; + } else if (pcmk__str_eq(task, CRMD_ACTION_NOTIFIED, pcmk__str_casei)) { + return action_notified; + } else if (pcmk__str_eq(task, CRMD_ACTION_PROMOTE, pcmk__str_casei)) { + return action_promote; + } else if (pcmk__str_eq(task, CRMD_ACTION_DEMOTE, pcmk__str_casei)) { + return action_demote; + } else if (pcmk__str_eq(task, CRMD_ACTION_PROMOTED, pcmk__str_casei)) { + return action_promoted; + } else if (pcmk__str_eq(task, CRMD_ACTION_DEMOTED, pcmk__str_casei)) { + return action_demoted; + } +#if SUPPORT_TRACING + if (pcmk__str_eq(task, CRMD_ACTION_CANCEL, pcmk__str_casei)) { + return no_action; + } else if (pcmk__str_eq(task, CRMD_ACTION_DELETE, pcmk__str_casei)) { + return no_action; + } else if (pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_casei)) { + return no_action; + } else if (pcmk__str_eq(task, CRMD_ACTION_MIGRATE, pcmk__str_casei)) { + return no_action; + } else if (pcmk__str_eq(task, CRMD_ACTION_MIGRATED, pcmk__str_casei)) { + return no_action; + } + crm_trace("Unsupported action: %s", task); +#endif + + return no_action; +} + +const char * +task2text(enum action_tasks task) +{ + const char *result = ""; + + switch (task) { + case no_action: + result = "no_action"; + break; + case stop_rsc: + result = CRMD_ACTION_STOP; + break; + case stopped_rsc: + result = CRMD_ACTION_STOPPED; + break; + case start_rsc: + result = CRMD_ACTION_START; + break; + case started_rsc: + result = CRMD_ACTION_STARTED; + break; + case shutdown_crm: + result = CRM_OP_SHUTDOWN; + break; + case stonith_node: + result = CRM_OP_FENCE; + break; + case monitor_rsc: + result = CRMD_ACTION_STATUS; + break; + case action_notify: + result = CRMD_ACTION_NOTIFY; + break; + case action_notified: + result = CRMD_ACTION_NOTIFIED; + break; + case action_promote: + result = CRMD_ACTION_PROMOTE; + break; + case action_promoted: + result = CRMD_ACTION_PROMOTED; + break; + case action_demote: + result = CRMD_ACTION_DEMOTE; + break; + case action_demoted: + result = CRMD_ACTION_DEMOTED; + break; + } + + return result; +} + +const char * +role2text(enum rsc_role_e role) +{ + switch (role) { + case RSC_ROLE_UNKNOWN: + return RSC_ROLE_UNKNOWN_S; + case RSC_ROLE_STOPPED: + return RSC_ROLE_STOPPED_S; + case RSC_ROLE_STARTED: + return RSC_ROLE_STARTED_S; + case RSC_ROLE_UNPROMOTED: +#ifdef PCMK__COMPAT_2_0 + return RSC_ROLE_UNPROMOTED_LEGACY_S; +#else + return RSC_ROLE_UNPROMOTED_S; +#endif + case RSC_ROLE_PROMOTED: +#ifdef PCMK__COMPAT_2_0 + return RSC_ROLE_PROMOTED_LEGACY_S; +#else + return RSC_ROLE_PROMOTED_S; +#endif + } + CRM_CHECK(role >= RSC_ROLE_UNKNOWN, return RSC_ROLE_UNKNOWN_S); + CRM_CHECK(role < RSC_ROLE_MAX, return RSC_ROLE_UNKNOWN_S); + // coverity[dead_error_line] + return RSC_ROLE_UNKNOWN_S; +} + +enum rsc_role_e +text2role(const char *role) +{ + CRM_ASSERT(role != NULL); + if (pcmk__str_eq(role, RSC_ROLE_STOPPED_S, pcmk__str_casei)) { + return RSC_ROLE_STOPPED; + } else if (pcmk__str_eq(role, RSC_ROLE_STARTED_S, pcmk__str_casei)) { + return RSC_ROLE_STARTED; + } else if (pcmk__strcase_any_of(role, RSC_ROLE_UNPROMOTED_S, + RSC_ROLE_UNPROMOTED_LEGACY_S, NULL)) { + return RSC_ROLE_UNPROMOTED; + } else if (pcmk__strcase_any_of(role, RSC_ROLE_PROMOTED_S, + RSC_ROLE_PROMOTED_LEGACY_S, NULL)) { + return RSC_ROLE_PROMOTED; + } else if (pcmk__str_eq(role, RSC_ROLE_UNKNOWN_S, pcmk__str_casei)) { + return RSC_ROLE_UNKNOWN; + } + crm_err("Unknown role: %s", role); + return RSC_ROLE_UNKNOWN; +} + +void +add_hash_param(GHashTable * hash, const char *name, const char *value) +{ + CRM_CHECK(hash != NULL, return); + + crm_trace("Adding name='%s' value='%s' to hash table", + pcmk__s(name, ""), pcmk__s(value, "")); + if (name == NULL || value == NULL) { + return; + + } else if (pcmk__str_eq(value, "#default", pcmk__str_casei)) { + return; + + } else if (g_hash_table_lookup(hash, name) == NULL) { + g_hash_table_insert(hash, strdup(name), strdup(value)); + } +} + +const char * +pe_node_attribute_calculated(const pe_node_t *node, const char *name, + const pe_resource_t *rsc) +{ + const char *source; + + if(node == NULL) { + return NULL; + + } else if(rsc == NULL) { + return g_hash_table_lookup(node->details->attrs, name); + } + + source = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET); + if(source == NULL || !pcmk__str_eq("host", source, pcmk__str_casei)) { + return g_hash_table_lookup(node->details->attrs, name); + } + + /* Use attributes set for the containers location + * instead of for the container itself + * + * Useful when the container is using the host's local + * storage + */ + + CRM_ASSERT(node->details->remote_rsc); + CRM_ASSERT(node->details->remote_rsc->container); + + if(node->details->remote_rsc->container->running_on) { + pe_node_t *host = node->details->remote_rsc->container->running_on->data; + pe_rsc_trace(rsc, "%s: Looking for %s on the container host %s", + rsc->id, name, pe__node_name(host)); + return g_hash_table_lookup(host->details->attrs, name); + } + + pe_rsc_trace(rsc, "%s: Not looking for %s on the container host: %s is inactive", + rsc->id, name, node->details->remote_rsc->container->id); + return NULL; +} + +const char * +pe_node_attribute_raw(const pe_node_t *node, const char *name) +{ + if(node == NULL) { + return NULL; + } + return g_hash_table_lookup(node->details->attrs, name); +} diff --git a/lib/pengine/complex.c b/lib/pengine/complex.c new file mode 100644 index 0000000..f168124 --- /dev/null +++ b/lib/pengine/complex.c @@ -0,0 +1,1174 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include + +#include "pe_status_private.h" + +void populate_hash(xmlNode * nvpair_list, GHashTable * hash, const char **attrs, int attrs_length); + +static pe_node_t *active_node(const pe_resource_t *rsc, unsigned int *count_all, + unsigned int *count_clean); + +resource_object_functions_t resource_class_functions[] = { + { + native_unpack, + native_find_rsc, + native_parameter, + native_print, + native_active, + native_resource_state, + native_location, + native_free, + pe__count_common, + pe__native_is_filtered, + active_node, + }, + { + group_unpack, + native_find_rsc, + native_parameter, + group_print, + group_active, + group_resource_state, + native_location, + group_free, + pe__count_common, + pe__group_is_filtered, + active_node, + }, + { + clone_unpack, + native_find_rsc, + native_parameter, + clone_print, + clone_active, + clone_resource_state, + native_location, + clone_free, + pe__count_common, + pe__clone_is_filtered, + active_node, + }, + { + pe__unpack_bundle, + native_find_rsc, + native_parameter, + pe__print_bundle, + pe__bundle_active, + pe__bundle_resource_state, + native_location, + pe__free_bundle, + pe__count_bundle, + pe__bundle_is_filtered, + pe__bundle_active_node, + } +}; + +static enum pe_obj_types +get_resource_type(const char *name) +{ + if (pcmk__str_eq(name, XML_CIB_TAG_RESOURCE, pcmk__str_casei)) { + return pe_native; + + } else if (pcmk__str_eq(name, XML_CIB_TAG_GROUP, pcmk__str_casei)) { + return pe_group; + + } else if (pcmk__str_eq(name, XML_CIB_TAG_INCARNATION, pcmk__str_casei)) { + return pe_clone; + + } else if (pcmk__str_eq(name, PCMK_XE_PROMOTABLE_LEGACY, pcmk__str_casei)) { + // @COMPAT deprecated since 2.0.0 + return pe_clone; + + } else if (pcmk__str_eq(name, XML_CIB_TAG_CONTAINER, pcmk__str_casei)) { + return pe_container; + } + + return pe_unknown; +} + +static void +dup_attr(gpointer key, gpointer value, gpointer user_data) +{ + add_hash_param(user_data, key, value); +} + +static void +expand_parents_fixed_nvpairs(pe_resource_t * rsc, pe_rule_eval_data_t * rule_data, GHashTable * meta_hash, pe_working_set_t * data_set) +{ + GHashTable *parent_orig_meta = pcmk__strkey_table(free, free); + pe_resource_t *p = rsc->parent; + + if (p == NULL) { + return ; + } + + /* Search all parent resources, get the fixed value of "meta_attributes" set only in the original xml, and stack it in the hash table. */ + /* The fixed value of the lower parent resource takes precedence and is not overwritten. */ + while(p != NULL) { + /* A hash table for comparison is generated, including the id-ref. */ + pe__unpack_dataset_nvpairs(p->xml, XML_TAG_META_SETS, + rule_data, parent_orig_meta, NULL, FALSE, data_set); + p = p->parent; + } + + /* If there is a fixed value of "meta_attributes" of the parent resource, it will be processed. */ + if (parent_orig_meta != NULL) { + GHashTableIter iter; + char *key = NULL; + char *value = NULL; + + g_hash_table_iter_init(&iter, parent_orig_meta); + while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value)) { + /* Parameters set in the original xml of the parent resource will also try to overwrite the child resource. */ + /* Attributes that already exist in the child lease are not updated. */ + dup_attr(key, value, meta_hash); + } + } + + if (parent_orig_meta != NULL) { + g_hash_table_destroy(parent_orig_meta); + } + + return ; + +} +void +get_meta_attributes(GHashTable * meta_hash, pe_resource_t * rsc, + pe_node_t * node, pe_working_set_t * data_set) +{ + pe_rsc_eval_data_t rsc_rule_data = { + .standard = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS), + .provider = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER), + .agent = crm_element_value(rsc->xml, XML_EXPR_ATTR_TYPE) + }; + + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = &rsc_rule_data, + .op_data = NULL + }; + + if (node) { + rule_data.node_hash = node->details->attrs; + } + + for (xmlAttrPtr a = pcmk__xe_first_attr(rsc->xml); a != NULL; a = a->next) { + const char *prop_name = (const char *) a->name; + const char *prop_value = crm_element_value(rsc->xml, prop_name); + + add_hash_param(meta_hash, prop_name, prop_value); + } + + pe__unpack_dataset_nvpairs(rsc->xml, XML_TAG_META_SETS, &rule_data, + meta_hash, NULL, FALSE, data_set); + + /* Set the "meta_attributes" explicitly set in the parent resource to the hash table of the child resource. */ + /* If it is already explicitly set as a child, it will not be overwritten. */ + if (rsc->parent != NULL) { + expand_parents_fixed_nvpairs(rsc, &rule_data, meta_hash, data_set); + } + + /* check the defaults */ + pe__unpack_dataset_nvpairs(data_set->rsc_defaults, XML_TAG_META_SETS, + &rule_data, meta_hash, NULL, FALSE, data_set); + + /* If there is "meta_attributes" that the parent resource has not explicitly set, set a value that is not set from rsc_default either. */ + /* The values already set up to this point will not be overwritten. */ + if (rsc->parent) { + g_hash_table_foreach(rsc->parent->meta, dup_attr, meta_hash); + } +} + +void +get_rsc_attributes(GHashTable *meta_hash, const pe_resource_t *rsc, + const pe_node_t *node, pe_working_set_t *data_set) +{ + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + if (node) { + rule_data.node_hash = node->details->attrs; + } + + pe__unpack_dataset_nvpairs(rsc->xml, XML_TAG_ATTR_SETS, &rule_data, + meta_hash, NULL, FALSE, data_set); + + /* set anything else based on the parent */ + if (rsc->parent != NULL) { + get_rsc_attributes(meta_hash, rsc->parent, node, data_set); + + } else { + /* and finally check the defaults */ + pe__unpack_dataset_nvpairs(data_set->rsc_defaults, XML_TAG_ATTR_SETS, + &rule_data, meta_hash, NULL, FALSE, data_set); + } +} + +static char * +template_op_key(xmlNode * op) +{ + const char *name = crm_element_value(op, "name"); + const char *role = crm_element_value(op, "role"); + char *key = NULL; + + if ((role == NULL) + || pcmk__strcase_any_of(role, RSC_ROLE_STARTED_S, RSC_ROLE_UNPROMOTED_S, + RSC_ROLE_UNPROMOTED_LEGACY_S, NULL)) { + role = RSC_ROLE_UNKNOWN_S; + } + + key = crm_strdup_printf("%s-%s", name, role); + return key; +} + +static gboolean +unpack_template(xmlNode * xml_obj, xmlNode ** expanded_xml, pe_working_set_t * data_set) +{ + xmlNode *cib_resources = NULL; + xmlNode *template = NULL; + xmlNode *new_xml = NULL; + xmlNode *child_xml = NULL; + xmlNode *rsc_ops = NULL; + xmlNode *template_ops = NULL; + const char *template_ref = NULL; + const char *clone = NULL; + const char *id = NULL; + + if (xml_obj == NULL) { + pe_err("No resource object for template unpacking"); + return FALSE; + } + + template_ref = crm_element_value(xml_obj, XML_CIB_TAG_RSC_TEMPLATE); + if (template_ref == NULL) { + return TRUE; + } + + id = ID(xml_obj); + if (id == NULL) { + pe_err("'%s' object must have a id", crm_element_name(xml_obj)); + return FALSE; + } + + if (pcmk__str_eq(template_ref, id, pcmk__str_none)) { + pe_err("The resource object '%s' should not reference itself", id); + return FALSE; + } + + cib_resources = get_xpath_object("//"XML_CIB_TAG_RESOURCES, data_set->input, LOG_TRACE); + if (cib_resources == NULL) { + pe_err("No resources configured"); + return FALSE; + } + + template = pcmk__xe_match(cib_resources, XML_CIB_TAG_RSC_TEMPLATE, + XML_ATTR_ID, template_ref); + if (template == NULL) { + pe_err("No template named '%s'", template_ref); + return FALSE; + } + + new_xml = copy_xml(template); + xmlNodeSetName(new_xml, xml_obj->name); + crm_xml_replace(new_xml, XML_ATTR_ID, id); + + clone = crm_element_value(xml_obj, XML_RSC_ATTR_INCARNATION); + if(clone) { + crm_xml_add(new_xml, XML_RSC_ATTR_INCARNATION, clone); + } + + template_ops = find_xml_node(new_xml, "operations", FALSE); + + for (child_xml = pcmk__xe_first_child(xml_obj); child_xml != NULL; + child_xml = pcmk__xe_next(child_xml)) { + xmlNode *new_child = NULL; + + new_child = add_node_copy(new_xml, child_xml); + + if (pcmk__str_eq((const char *)new_child->name, "operations", pcmk__str_none)) { + rsc_ops = new_child; + } + } + + if (template_ops && rsc_ops) { + xmlNode *op = NULL; + GHashTable *rsc_ops_hash = pcmk__strkey_table(free, NULL); + + for (op = pcmk__xe_first_child(rsc_ops); op != NULL; + op = pcmk__xe_next(op)) { + + char *key = template_op_key(op); + + g_hash_table_insert(rsc_ops_hash, key, op); + } + + for (op = pcmk__xe_first_child(template_ops); op != NULL; + op = pcmk__xe_next(op)) { + + char *key = template_op_key(op); + + if (g_hash_table_lookup(rsc_ops_hash, key) == NULL) { + add_node_copy(rsc_ops, op); + } + + free(key); + } + + if (rsc_ops_hash) { + g_hash_table_destroy(rsc_ops_hash); + } + + free_xml(template_ops); + } + + /*free_xml(*expanded_xml); */ + *expanded_xml = new_xml; + + /* Disable multi-level templates for now */ + /*if(unpack_template(new_xml, expanded_xml, data_set) == FALSE) { + free_xml(*expanded_xml); + *expanded_xml = NULL; + + return FALSE; + } */ + + return TRUE; +} + +static gboolean +add_template_rsc(xmlNode * xml_obj, pe_working_set_t * data_set) +{ + const char *template_ref = NULL; + const char *id = NULL; + + if (xml_obj == NULL) { + pe_err("No resource object for processing resource list of template"); + return FALSE; + } + + template_ref = crm_element_value(xml_obj, XML_CIB_TAG_RSC_TEMPLATE); + if (template_ref == NULL) { + return TRUE; + } + + id = ID(xml_obj); + if (id == NULL) { + pe_err("'%s' object must have a id", crm_element_name(xml_obj)); + return FALSE; + } + + if (pcmk__str_eq(template_ref, id, pcmk__str_none)) { + pe_err("The resource object '%s' should not reference itself", id); + return FALSE; + } + + if (add_tag_ref(data_set->template_rsc_sets, template_ref, id) == FALSE) { + return FALSE; + } + + return TRUE; +} + +static bool +detect_promotable(pe_resource_t *rsc) +{ + const char *promotable = g_hash_table_lookup(rsc->meta, + XML_RSC_ATTR_PROMOTABLE); + + if (crm_is_true(promotable)) { + return TRUE; + } + + // @COMPAT deprecated since 2.0.0 + if (pcmk__str_eq(crm_element_name(rsc->xml), PCMK_XE_PROMOTABLE_LEGACY, + pcmk__str_casei)) { + /* @TODO in some future version, pe_warn_once() here, + * then drop support in even later version + */ + g_hash_table_insert(rsc->meta, strdup(XML_RSC_ATTR_PROMOTABLE), + strdup(XML_BOOLEAN_TRUE)); + return TRUE; + } + return FALSE; +} + +static void +free_params_table(gpointer data) +{ + g_hash_table_destroy((GHashTable *) data); +} + +/*! + * \brief Get a table of resource parameters + * + * \param[in,out] rsc Resource to query + * \param[in] node Node for evaluating rules (NULL for defaults) + * \param[in,out] data_set Cluster working set + * + * \return Hash table containing resource parameter names and values + * (or NULL if \p rsc or \p data_set is NULL) + * \note The returned table will be destroyed when the resource is freed, so + * callers should not destroy it. + */ +GHashTable * +pe_rsc_params(pe_resource_t *rsc, const pe_node_t *node, + pe_working_set_t *data_set) +{ + GHashTable *params_on_node = NULL; + + /* A NULL node is used to request the resource's default parameters + * (not evaluated for node), but we always want something non-NULL + * as a hash table key. + */ + const char *node_name = ""; + + // Sanity check + if ((rsc == NULL) || (data_set == NULL)) { + return NULL; + } + if ((node != NULL) && (node->details->uname != NULL)) { + node_name = node->details->uname; + } + + // Find the parameter table for given node + if (rsc->parameter_cache == NULL) { + rsc->parameter_cache = pcmk__strikey_table(free, free_params_table); + } else { + params_on_node = g_hash_table_lookup(rsc->parameter_cache, node_name); + } + + // If none exists yet, create one with parameters evaluated for node + if (params_on_node == NULL) { + params_on_node = pcmk__strkey_table(free, free); + get_rsc_attributes(params_on_node, rsc, node, data_set); + g_hash_table_insert(rsc->parameter_cache, strdup(node_name), + params_on_node); + } + return params_on_node; +} + +/*! + * \internal + * \brief Unpack a resource's "requires" meta-attribute + * + * \param[in,out] rsc Resource being unpacked + * \param[in] value Value of "requires" meta-attribute + * \param[in] is_default Whether \p value was selected by default + */ +static void +unpack_requires(pe_resource_t *rsc, const char *value, bool is_default) +{ + if (pcmk__str_eq(value, PCMK__VALUE_NOTHING, pcmk__str_casei)) { + + } else if (pcmk__str_eq(value, PCMK__VALUE_QUORUM, pcmk__str_casei)) { + pe__set_resource_flags(rsc, pe_rsc_needs_quorum); + + } else if (pcmk__str_eq(value, PCMK__VALUE_FENCING, pcmk__str_casei)) { + pe__set_resource_flags(rsc, pe_rsc_needs_fencing); + if (!pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled)) { + pcmk__config_warn("%s requires fencing but fencing is disabled", + rsc->id); + } + + } else if (pcmk__str_eq(value, PCMK__VALUE_UNFENCING, pcmk__str_casei)) { + if (pcmk_is_set(rsc->flags, pe_rsc_fence_device)) { + pcmk__config_warn("Resetting \"" XML_RSC_ATTR_REQUIRES "\" for %s " + "to \"" PCMK__VALUE_QUORUM "\" because fencing " + "devices cannot require unfencing", rsc->id); + unpack_requires(rsc, PCMK__VALUE_QUORUM, true); + return; + + } else if (!pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled)) { + pcmk__config_warn("Resetting \"" XML_RSC_ATTR_REQUIRES "\" for %s " + "to \"" PCMK__VALUE_QUORUM "\" because fencing " + "is disabled", rsc->id); + unpack_requires(rsc, PCMK__VALUE_QUORUM, true); + return; + + } else { + pe__set_resource_flags(rsc, + pe_rsc_needs_fencing|pe_rsc_needs_unfencing); + } + + } else { + const char *orig_value = value; + + if (pcmk_is_set(rsc->flags, pe_rsc_fence_device)) { + value = PCMK__VALUE_QUORUM; + + } else if ((rsc->variant == pe_native) + && xml_contains_remote_node(rsc->xml)) { + value = PCMK__VALUE_QUORUM; + + } else if (pcmk_is_set(rsc->cluster->flags, pe_flag_enable_unfencing)) { + value = PCMK__VALUE_UNFENCING; + + } else if (pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled)) { + value = PCMK__VALUE_FENCING; + + } else if (rsc->cluster->no_quorum_policy == no_quorum_ignore) { + value = PCMK__VALUE_NOTHING; + + } else { + value = PCMK__VALUE_QUORUM; + } + + if (orig_value != NULL) { + pcmk__config_err("Resetting '" XML_RSC_ATTR_REQUIRES "' for %s " + "to '%s' because '%s' is not valid", + rsc->id, value, orig_value); + } + unpack_requires(rsc, value, true); + return; + } + + pe_rsc_trace(rsc, "\tRequired to start: %s%s", value, + (is_default? " (default)" : "")); +} + +#ifndef PCMK__COMPAT_2_0 +static void +warn_about_deprecated_classes(pe_resource_t *rsc) +{ + const char *std = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS); + + if (pcmk__str_eq(std, PCMK_RESOURCE_CLASS_UPSTART, pcmk__str_none)) { + pe_warn_once(pe_wo_upstart, + "Support for Upstart resources (such as %s) is deprecated " + "and will be removed in a future release of Pacemaker", + rsc->id); + + } else if (pcmk__str_eq(std, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_none)) { + pe_warn_once(pe_wo_nagios, + "Support for Nagios resources (such as %s) is deprecated " + "and will be removed in a future release of Pacemaker", + rsc->id); + } +} +#endif + +/*! + * \internal + * \brief Unpack configuration XML for a given resource + * + * Unpack the XML object containing a resource's configuration into a new + * \c pe_resource_t object. + * + * \param[in] xml_obj XML node containing the resource's configuration + * \param[out] rsc Where to store the unpacked resource information + * \param[in] parent Resource's parent, if any + * \param[in,out] data_set Cluster working set + * + * \return Standard Pacemaker return code + * \note If pcmk_rc_ok is returned, \p *rsc is guaranteed to be non-NULL, and + * the caller is responsible for freeing it using its variant-specific + * free() method. Otherwise, \p *rsc is guaranteed to be NULL. + */ +int +pe__unpack_resource(xmlNode *xml_obj, pe_resource_t **rsc, + pe_resource_t *parent, pe_working_set_t *data_set) +{ + xmlNode *expanded_xml = NULL; + xmlNode *ops = NULL; + const char *value = NULL; + const char *id = NULL; + bool guest_node = false; + bool remote_node = false; + + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = NULL, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + CRM_CHECK(rsc != NULL, return EINVAL); + CRM_CHECK((xml_obj != NULL) && (data_set != NULL), + *rsc = NULL; + return EINVAL); + + rule_data.now = data_set->now; + + crm_log_xml_trace(xml_obj, "[raw XML]"); + + id = crm_element_value(xml_obj, XML_ATTR_ID); + if (id == NULL) { + pe_err("Ignoring <%s> configuration without " XML_ATTR_ID, + crm_element_name(xml_obj)); + return pcmk_rc_unpack_error; + } + + if (unpack_template(xml_obj, &expanded_xml, data_set) == FALSE) { + return pcmk_rc_unpack_error; + } + + *rsc = calloc(1, sizeof(pe_resource_t)); + if (*rsc == NULL) { + crm_crit("Unable to allocate memory for resource '%s'", id); + return ENOMEM; + } + (*rsc)->cluster = data_set; + + if (expanded_xml) { + crm_log_xml_trace(expanded_xml, "[expanded XML]"); + (*rsc)->xml = expanded_xml; + (*rsc)->orig_xml = xml_obj; + + } else { + (*rsc)->xml = xml_obj; + (*rsc)->orig_xml = NULL; + } + + /* Do not use xml_obj from here on, use (*rsc)->xml in case templates are involved */ + + (*rsc)->parent = parent; + + ops = find_xml_node((*rsc)->xml, "operations", FALSE); + (*rsc)->ops_xml = expand_idref(ops, data_set->input); + + (*rsc)->variant = get_resource_type(crm_element_name((*rsc)->xml)); + if ((*rsc)->variant == pe_unknown) { + pe_err("Ignoring resource '%s' of unknown type '%s'", + id, crm_element_name((*rsc)->xml)); + common_free(*rsc); + *rsc = NULL; + return pcmk_rc_unpack_error; + } + +#ifndef PCMK__COMPAT_2_0 + warn_about_deprecated_classes(*rsc); +#endif + + (*rsc)->meta = pcmk__strkey_table(free, free); + (*rsc)->allowed_nodes = pcmk__strkey_table(NULL, free); + (*rsc)->known_on = pcmk__strkey_table(NULL, free); + + value = crm_element_value((*rsc)->xml, XML_RSC_ATTR_INCARNATION); + if (value) { + (*rsc)->id = crm_strdup_printf("%s:%s", id, value); + add_hash_param((*rsc)->meta, XML_RSC_ATTR_INCARNATION, value); + + } else { + (*rsc)->id = strdup(id); + } + + (*rsc)->fns = &resource_class_functions[(*rsc)->variant]; + + get_meta_attributes((*rsc)->meta, *rsc, NULL, data_set); + (*rsc)->parameters = pe_rsc_params(*rsc, NULL, data_set); // \deprecated + + (*rsc)->flags = 0; + pe__set_resource_flags(*rsc, pe_rsc_runnable|pe_rsc_provisional); + + if (!pcmk_is_set(data_set->flags, pe_flag_maintenance_mode)) { + pe__set_resource_flags(*rsc, pe_rsc_managed); + } + + (*rsc)->rsc_cons = NULL; + (*rsc)->rsc_tickets = NULL; + (*rsc)->actions = NULL; + (*rsc)->role = RSC_ROLE_STOPPED; + (*rsc)->next_role = RSC_ROLE_UNKNOWN; + + (*rsc)->recovery_type = recovery_stop_start; + (*rsc)->stickiness = 0; + (*rsc)->migration_threshold = INFINITY; + (*rsc)->failure_timeout = 0; + + value = g_hash_table_lookup((*rsc)->meta, XML_CIB_ATTR_PRIORITY); + (*rsc)->priority = char2score(value); + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_CRITICAL); + if ((value == NULL) || crm_is_true(value)) { + pe__set_resource_flags(*rsc, pe_rsc_critical); + } + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_NOTIFY); + if (crm_is_true(value)) { + pe__set_resource_flags(*rsc, pe_rsc_notify); + } + + if (xml_contains_remote_node((*rsc)->xml)) { + (*rsc)->is_remote_node = TRUE; + if (g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_CONTAINER)) { + guest_node = true; + } else { + remote_node = true; + } + } + + value = g_hash_table_lookup((*rsc)->meta, XML_OP_ATTR_ALLOW_MIGRATE); + if (crm_is_true(value)) { + pe__set_resource_flags(*rsc, pe_rsc_allow_migrate); + } else if ((value == NULL) && remote_node) { + /* By default, we want remote nodes to be able + * to float around the cluster without having to stop all the + * resources within the remote-node before moving. Allowing + * migration support enables this feature. If this ever causes + * problems, migration support can be explicitly turned off with + * allow-migrate=false. + */ + pe__set_resource_flags(*rsc, pe_rsc_allow_migrate); + } + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_MANAGED); + if (value != NULL && !pcmk__str_eq("default", value, pcmk__str_casei)) { + if (crm_is_true(value)) { + pe__set_resource_flags(*rsc, pe_rsc_managed); + } else { + pe__clear_resource_flags(*rsc, pe_rsc_managed); + } + } + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_MAINTENANCE); + if (crm_is_true(value)) { + pe__clear_resource_flags(*rsc, pe_rsc_managed); + pe__set_resource_flags(*rsc, pe_rsc_maintenance); + } + if (pcmk_is_set(data_set->flags, pe_flag_maintenance_mode)) { + pe__clear_resource_flags(*rsc, pe_rsc_managed); + pe__set_resource_flags(*rsc, pe_rsc_maintenance); + } + + if (pe_rsc_is_clone(pe__const_top_resource(*rsc, false))) { + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_UNIQUE); + if (crm_is_true(value)) { + pe__set_resource_flags(*rsc, pe_rsc_unique); + } + if (detect_promotable(*rsc)) { + pe__set_resource_flags(*rsc, pe_rsc_promotable); + } + } else { + pe__set_resource_flags(*rsc, pe_rsc_unique); + } + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_RESTART); + if (pcmk__str_eq(value, "restart", pcmk__str_casei)) { + (*rsc)->restart_type = pe_restart_restart; + pe_rsc_trace((*rsc), "%s dependency restart handling: restart", + (*rsc)->id); + pe_warn_once(pe_wo_restart_type, + "Support for restart-type is deprecated and will be removed in a future release"); + + } else { + (*rsc)->restart_type = pe_restart_ignore; + pe_rsc_trace((*rsc), "%s dependency restart handling: ignore", + (*rsc)->id); + } + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_MULTIPLE); + if (pcmk__str_eq(value, "stop_only", pcmk__str_casei)) { + (*rsc)->recovery_type = recovery_stop_only; + pe_rsc_trace((*rsc), "%s multiple running resource recovery: stop only", + (*rsc)->id); + + } else if (pcmk__str_eq(value, "block", pcmk__str_casei)) { + (*rsc)->recovery_type = recovery_block; + pe_rsc_trace((*rsc), "%s multiple running resource recovery: block", + (*rsc)->id); + + } else if (pcmk__str_eq(value, "stop_unexpected", pcmk__str_casei)) { + (*rsc)->recovery_type = recovery_stop_unexpected; + pe_rsc_trace((*rsc), "%s multiple running resource recovery: " + "stop unexpected instances", + (*rsc)->id); + + } else { // "stop_start" + if (!pcmk__str_eq(value, "stop_start", + pcmk__str_casei|pcmk__str_null_matches)) { + pe_warn("%s is not a valid value for " XML_RSC_ATTR_MULTIPLE + ", using default of \"stop_start\"", value); + } + (*rsc)->recovery_type = recovery_stop_start; + pe_rsc_trace((*rsc), "%s multiple running resource recovery: " + "stop/start", (*rsc)->id); + } + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_STICKINESS); + if (value != NULL && !pcmk__str_eq("default", value, pcmk__str_casei)) { + (*rsc)->stickiness = char2score(value); + } + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_FAIL_STICKINESS); + if (value != NULL && !pcmk__str_eq("default", value, pcmk__str_casei)) { + (*rsc)->migration_threshold = char2score(value); + if ((*rsc)->migration_threshold < 0) { + /* @TODO We use 1 here to preserve previous behavior, but this + * should probably use the default (INFINITY) or 0 (to disable) + * instead. + */ + pe_warn_once(pe_wo_neg_threshold, + XML_RSC_ATTR_FAIL_STICKINESS + " must be non-negative, using 1 instead"); + (*rsc)->migration_threshold = 1; + } + } + + if (pcmk__str_eq(crm_element_value((*rsc)->xml, XML_AGENT_ATTR_CLASS), + PCMK_RESOURCE_CLASS_STONITH, pcmk__str_casei)) { + pe__set_working_set_flags(data_set, pe_flag_have_stonith_resource); + pe__set_resource_flags(*rsc, pe_rsc_fence_device); + } + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_REQUIRES); + unpack_requires(*rsc, value, false); + + value = g_hash_table_lookup((*rsc)->meta, XML_RSC_ATTR_FAIL_TIMEOUT); + if (value != NULL) { + // Stored as seconds + (*rsc)->failure_timeout = (int) (crm_parse_interval_spec(value) / 1000); + } + + if (remote_node) { + GHashTable *params = pe_rsc_params(*rsc, NULL, data_set); + + /* Grabbing the value now means that any rules based on node attributes + * will evaluate to false, so such rules should not be used with + * reconnect_interval. + * + * @TODO Evaluate per node before using + */ + value = g_hash_table_lookup(params, XML_REMOTE_ATTR_RECONNECT_INTERVAL); + if (value) { + /* reconnect delay works by setting failure_timeout and preventing the + * connection from starting until the failure is cleared. */ + (*rsc)->remote_reconnect_ms = crm_parse_interval_spec(value); + /* we want to override any default failure_timeout in use when remote + * reconnect_interval is in use. */ + (*rsc)->failure_timeout = (*rsc)->remote_reconnect_ms / 1000; + } + } + + get_target_role(*rsc, &((*rsc)->next_role)); + pe_rsc_trace((*rsc), "%s desired next state: %s", (*rsc)->id, + (*rsc)->next_role != RSC_ROLE_UNKNOWN ? role2text((*rsc)->next_role) : "default"); + + if ((*rsc)->fns->unpack(*rsc, data_set) == FALSE) { + (*rsc)->fns->free(*rsc); + *rsc = NULL; + return pcmk_rc_unpack_error; + } + + if (pcmk_is_set(data_set->flags, pe_flag_symmetric_cluster)) { + // This tag must stay exactly the same because it is tested elsewhere + resource_location(*rsc, NULL, 0, "symmetric_default", data_set); + } else if (guest_node) { + /* remote resources tied to a container resource must always be allowed + * to opt-in to the cluster. Whether the connection resource is actually + * allowed to be placed on a node is dependent on the container resource */ + resource_location(*rsc, NULL, 0, "remote_connection_default", data_set); + } + + pe_rsc_trace((*rsc), "%s action notification: %s", (*rsc)->id, + pcmk_is_set((*rsc)->flags, pe_rsc_notify)? "required" : "not required"); + + (*rsc)->utilization = pcmk__strkey_table(free, free); + + pe__unpack_dataset_nvpairs((*rsc)->xml, XML_TAG_UTILIZATION, &rule_data, + (*rsc)->utilization, NULL, FALSE, data_set); + + if (expanded_xml) { + if (add_template_rsc(xml_obj, data_set) == FALSE) { + (*rsc)->fns->free(*rsc); + *rsc = NULL; + return pcmk_rc_unpack_error; + } + } + return pcmk_rc_ok; +} + +gboolean +is_parent(pe_resource_t *child, pe_resource_t *rsc) +{ + pe_resource_t *parent = child; + + if (parent == NULL || rsc == NULL) { + return FALSE; + } + while (parent->parent != NULL) { + if (parent->parent == rsc) { + return TRUE; + } + parent = parent->parent; + } + return FALSE; +} + +pe_resource_t * +uber_parent(pe_resource_t * rsc) +{ + pe_resource_t *parent = rsc; + + if (parent == NULL) { + return NULL; + } + while (parent->parent != NULL && parent->parent->variant != pe_container) { + parent = parent->parent; + } + return parent; +} + +/*! + * \internal + * \brief Get the topmost parent of a resource as a const pointer + * + * \param[in] rsc Resource to check + * \param[in] include_bundle If true, go all the way to bundle + * + * \return \p NULL if \p rsc is NULL, \p rsc if \p rsc has no parent, + * the bundle if \p rsc is bundled and \p include_bundle is true, + * otherwise the topmost parent of \p rsc up to a clone + */ +const pe_resource_t * +pe__const_top_resource(const pe_resource_t *rsc, bool include_bundle) +{ + const pe_resource_t *parent = rsc; + + if (parent == NULL) { + return NULL; + } + while (parent->parent != NULL) { + if (!include_bundle && (parent->parent->variant == pe_container)) { + break; + } + parent = parent->parent; + } + return parent; +} + +void +common_free(pe_resource_t * rsc) +{ + if (rsc == NULL) { + return; + } + + pe_rsc_trace(rsc, "Freeing %s %d", rsc->id, rsc->variant); + + g_list_free(rsc->rsc_cons); + g_list_free(rsc->rsc_cons_lhs); + g_list_free(rsc->rsc_tickets); + g_list_free(rsc->dangling_migrations); + + if (rsc->parameter_cache != NULL) { + g_hash_table_destroy(rsc->parameter_cache); + } + if (rsc->meta != NULL) { + g_hash_table_destroy(rsc->meta); + } + if (rsc->utilization != NULL) { + g_hash_table_destroy(rsc->utilization); + } + + if ((rsc->parent == NULL) && pcmk_is_set(rsc->flags, pe_rsc_orphan)) { + free_xml(rsc->xml); + rsc->xml = NULL; + free_xml(rsc->orig_xml); + rsc->orig_xml = NULL; + + /* if rsc->orig_xml, then rsc->xml is an expanded xml from a template */ + } else if (rsc->orig_xml) { + free_xml(rsc->xml); + rsc->xml = NULL; + } + if (rsc->running_on) { + g_list_free(rsc->running_on); + rsc->running_on = NULL; + } + if (rsc->known_on) { + g_hash_table_destroy(rsc->known_on); + rsc->known_on = NULL; + } + if (rsc->actions) { + g_list_free(rsc->actions); + rsc->actions = NULL; + } + if (rsc->allowed_nodes) { + g_hash_table_destroy(rsc->allowed_nodes); + rsc->allowed_nodes = NULL; + } + g_list_free(rsc->fillers); + g_list_free(rsc->rsc_location); + pe_rsc_trace(rsc, "Resource freed"); + free(rsc->id); + free(rsc->clone_name); + free(rsc->allocated_to); + free(rsc->variant_opaque); + free(rsc->pending_task); + free(rsc); +} + +/*! + * \internal + * \brief Count a node and update most preferred to it as appropriate + * + * \param[in] rsc An active resource + * \param[in] node A node that \p rsc is active on + * \param[in,out] active This will be set to \p node if \p node is more + * preferred than the current value + * \param[in,out] count_all If not NULL, this will be incremented + * \param[in,out] count_clean If not NULL, this will be incremented if \p node + * is online and clean + * + * \return true if the count should continue, or false if sufficiently known + */ +bool +pe__count_active_node(const pe_resource_t *rsc, pe_node_t *node, + pe_node_t **active, unsigned int *count_all, + unsigned int *count_clean) +{ + bool keep_looking = false; + bool is_happy = false; + + CRM_CHECK((rsc != NULL) && (node != NULL) && (active != NULL), + return false); + + is_happy = node->details->online && !node->details->unclean; + + if (count_all != NULL) { + ++*count_all; + } + if ((count_clean != NULL) && is_happy) { + ++*count_clean; + } + if ((count_all != NULL) || (count_clean != NULL)) { + keep_looking = true; // We're counting, so go through entire list + } + + if (rsc->partial_migration_source != NULL) { + if (node->details == rsc->partial_migration_source->details) { + *active = node; // This is the migration source + } else { + keep_looking = true; + } + } else if (!pcmk_is_set(rsc->flags, pe_rsc_needs_fencing)) { + if (is_happy && ((*active == NULL) || !(*active)->details->online + || (*active)->details->unclean)) { + *active = node; // This is the first clean node + } else { + keep_looking = true; + } + } + if (*active == NULL) { + *active = node; // This is the first node checked + } + return keep_looking; +} + +// Shared implementation of resource_object_functions_t:active_node() +static pe_node_t * +active_node(const pe_resource_t *rsc, unsigned int *count_all, + unsigned int *count_clean) +{ + pe_node_t *active = NULL; + + if (count_all != NULL) { + *count_all = 0; + } + if (count_clean != NULL) { + *count_clean = 0; + } + if (rsc == NULL) { + return NULL; + } + for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) { + if (!pe__count_active_node(rsc, (pe_node_t *) iter->data, &active, + count_all, count_clean)) { + break; // Don't waste time iterating if we don't have to + } + } + return active; +} + +/*! + * \brief + * \internal Find and count active nodes according to "requires" + * + * \param[in] rsc Resource to check + * \param[out] count If not NULL, will be set to count of active nodes + * + * \return An active node (or NULL if resource is not active anywhere) + * + * \note This is a convenience wrapper for active_node() where the count of all + * active nodes or only clean active nodes is desired according to the + * "requires" meta-attribute. + */ +pe_node_t * +pe__find_active_requires(const pe_resource_t *rsc, unsigned int *count) +{ + if (rsc == NULL) { + if (count != NULL) { + *count = 0; + } + return NULL; + + } else if (pcmk_is_set(rsc->flags, pe_rsc_needs_fencing)) { + return rsc->fns->active_node(rsc, count, NULL); + + } else { + return rsc->fns->active_node(rsc, NULL, count); + } +} + +void +pe__count_common(pe_resource_t *rsc) +{ + if (rsc->children != NULL) { + for (GList *item = rsc->children; item != NULL; item = item->next) { + ((pe_resource_t *) item->data)->fns->count(item->data); + } + + } else if (!pcmk_is_set(rsc->flags, pe_rsc_orphan) + || (rsc->role > RSC_ROLE_STOPPED)) { + rsc->cluster->ninstances++; + if (pe__resource_is_disabled(rsc)) { + rsc->cluster->disabled_resources++; + } + if (pcmk_is_set(rsc->flags, pe_rsc_block)) { + rsc->cluster->blocked_resources++; + } + } +} + +/*! + * \internal + * \brief Update a resource's next role + * + * \param[in,out] rsc Resource to be updated + * \param[in] role Resource's new next role + * \param[in] why Human-friendly reason why role is changing (for logs) + */ +void +pe__set_next_role(pe_resource_t *rsc, enum rsc_role_e role, const char *why) +{ + CRM_ASSERT((rsc != NULL) && (why != NULL)); + if (rsc->next_role != role) { + pe_rsc_trace(rsc, "Resetting next role for %s from %s to %s (%s)", + rsc->id, role2text(rsc->next_role), role2text(role), why); + rsc->next_role = role; + } +} diff --git a/lib/pengine/failcounts.c b/lib/pengine/failcounts.c new file mode 100644 index 0000000..a4a3e11 --- /dev/null +++ b/lib/pengine/failcounts.c @@ -0,0 +1,403 @@ +/* + * Copyright 2008-2023 the Pacemaker project contributors + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +static gboolean +is_matched_failure(const char *rsc_id, const xmlNode *conf_op_xml, + const xmlNode *lrm_op_xml) +{ + gboolean matched = FALSE; + const char *conf_op_name = NULL; + const char *lrm_op_task = NULL; + const char *conf_op_interval_spec = NULL; + guint conf_op_interval_ms = 0; + guint lrm_op_interval_ms = 0; + const char *lrm_op_id = NULL; + char *last_failure_key = NULL; + + if (rsc_id == NULL || conf_op_xml == NULL || lrm_op_xml == NULL) { + return FALSE; + } + + // Get name and interval from configured op + conf_op_name = crm_element_value(conf_op_xml, "name"); + conf_op_interval_spec = crm_element_value(conf_op_xml, + XML_LRM_ATTR_INTERVAL); + conf_op_interval_ms = crm_parse_interval_spec(conf_op_interval_spec); + + // Get name and interval from op history entry + lrm_op_task = crm_element_value(lrm_op_xml, XML_LRM_ATTR_TASK); + crm_element_value_ms(lrm_op_xml, XML_LRM_ATTR_INTERVAL_MS, + &lrm_op_interval_ms); + + if ((conf_op_interval_ms != lrm_op_interval_ms) + || !pcmk__str_eq(conf_op_name, lrm_op_task, pcmk__str_casei)) { + return FALSE; + } + + lrm_op_id = ID(lrm_op_xml); + last_failure_key = pcmk__op_key(rsc_id, "last_failure", 0); + + if (pcmk__str_eq(last_failure_key, lrm_op_id, pcmk__str_casei)) { + matched = TRUE; + + } else { + char *expected_op_key = pcmk__op_key(rsc_id, conf_op_name, + conf_op_interval_ms); + + if (pcmk__str_eq(expected_op_key, lrm_op_id, pcmk__str_casei)) { + int rc = 0; + int target_rc = pe__target_rc_from_xml(lrm_op_xml); + + crm_element_value_int(lrm_op_xml, XML_LRM_ATTR_RC, &rc); + if (rc != target_rc) { + matched = TRUE; + } + } + free(expected_op_key); + } + + free(last_failure_key); + return matched; +} + +static gboolean +block_failure(const pe_node_t *node, pe_resource_t *rsc, const xmlNode *xml_op) +{ + char *xml_name = clone_strip(rsc->id); + + /* @TODO This xpath search occurs after template expansion, but it is unable + * to properly detect on-fail in id-ref, operation meta-attributes, or + * op_defaults, or evaluate rules. + * + * Also, on-fail defaults to block (in unpack_operation()) for stop actions + * when stonith is disabled. + * + * Ideally, we'd unpack the operation before this point, and pass in a + * meta-attributes table that takes all that into consideration. + */ + char *xpath = crm_strdup_printf("//" XML_CIB_TAG_RESOURCE + "[@" XML_ATTR_ID "='%s']" + "//" XML_ATTR_OP + "[@" XML_OP_ATTR_ON_FAIL "='block']", + xml_name); + + xmlXPathObject *xpathObj = xpath_search(rsc->xml, xpath); + gboolean should_block = FALSE; + + free(xpath); + + if (xpathObj) { + int max = numXpathResults(xpathObj); + int lpc = 0; + + for (lpc = 0; lpc < max; lpc++) { + xmlNode *pref = getXpathResult(xpathObj, lpc); + + if (xml_op) { + should_block = is_matched_failure(xml_name, pref, xml_op); + if (should_block) { + break; + } + + } else { + const char *conf_op_name = NULL; + const char *conf_op_interval_spec = NULL; + guint conf_op_interval_ms = 0; + char *lrm_op_xpath = NULL; + xmlXPathObject *lrm_op_xpathObj = NULL; + + // Get name and interval from configured op + conf_op_name = crm_element_value(pref, "name"); + conf_op_interval_spec = crm_element_value(pref, XML_LRM_ATTR_INTERVAL); + conf_op_interval_ms = crm_parse_interval_spec(conf_op_interval_spec); + +#define XPATH_FMT "//" XML_CIB_TAG_STATE "[@" XML_ATTR_UNAME "='%s']" \ + "//" XML_LRM_TAG_RESOURCE "[@" XML_ATTR_ID "='%s']" \ + "/" XML_LRM_TAG_RSC_OP "[@" XML_LRM_ATTR_TASK "='%s']" \ + "[@" XML_LRM_ATTR_INTERVAL "='%u']" + + lrm_op_xpath = crm_strdup_printf(XPATH_FMT, + node->details->uname, xml_name, + conf_op_name, + conf_op_interval_ms); + lrm_op_xpathObj = xpath_search(rsc->cluster->input, lrm_op_xpath); + + free(lrm_op_xpath); + + if (lrm_op_xpathObj) { + int max2 = numXpathResults(lrm_op_xpathObj); + int lpc2 = 0; + + for (lpc2 = 0; lpc2 < max2; lpc2++) { + xmlNode *lrm_op_xml = getXpathResult(lrm_op_xpathObj, + lpc2); + + should_block = is_matched_failure(xml_name, pref, + lrm_op_xml); + if (should_block) { + break; + } + } + } + freeXpathObject(lrm_op_xpathObj); + + if (should_block) { + break; + } + } + } + } + + free(xml_name); + freeXpathObject(xpathObj); + + return should_block; +} + +/*! + * \internal + * \brief Get resource name as used in failure-related node attributes + * + * \param[in] rsc Resource to check + * + * \return Newly allocated string containing resource's fail name + * \note The caller is responsible for freeing the result. + */ +static inline char * +rsc_fail_name(const pe_resource_t *rsc) +{ + const char *name = (rsc->clone_name? rsc->clone_name : rsc->id); + + return pcmk_is_set(rsc->flags, pe_rsc_unique)? strdup(name) : clone_strip(name); +} + +/*! + * \internal + * \brief Compile regular expression to match a failure-related node attribute + * + * \param[in] prefix Attribute prefix to match + * \param[in] rsc_name Resource name to match as used in failure attributes + * \param[in] is_legacy Whether DC uses per-resource fail counts + * \param[in] is_unique Whether the resource is a globally unique clone + * \param[out] re Where to store resulting regular expression + * + * \return Standard Pacemaker return code + * \note Fail attributes are named like PREFIX-RESOURCE#OP_INTERVAL. + * The caller is responsible for freeing re with regfree(). + */ +static int +generate_fail_regex(const char *prefix, const char *rsc_name, + gboolean is_legacy, gboolean is_unique, regex_t *re) +{ + char *pattern; + + /* @COMPAT DC < 1.1.17: Fail counts used to be per-resource rather than + * per-operation. + */ + const char *op_pattern = (is_legacy? "" : "#.+_[0-9]+"); + + /* Ignore instance numbers for anything other than globally unique clones. + * Anonymous clone fail counts could contain an instance number if the + * clone was initially unique, failed, then was converted to anonymous. + * @COMPAT Also, before 1.1.8, anonymous clone fail counts always contained + * clone instance numbers. + */ + const char *instance_pattern = (is_unique? "" : "(:[0-9]+)?"); + + pattern = crm_strdup_printf("^%s-%s%s%s$", prefix, rsc_name, + instance_pattern, op_pattern); + if (regcomp(re, pattern, REG_EXTENDED|REG_NOSUB) != 0) { + free(pattern); + return EINVAL; + } + + free(pattern); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Compile regular expressions to match failure-related node attributes + * + * \param[in] rsc Resource being checked for failures + * \param[in] data_set Data set (for CRM feature set version) + * \param[out] failcount_re Storage for regular expression for fail count + * \param[out] lastfailure_re Storage for regular expression for last failure + * + * \return Standard Pacemaker return code + * \note On success, the caller is responsible for freeing the expressions with + * regfree(). + */ +static int +generate_fail_regexes(const pe_resource_t *rsc, + const pe_working_set_t *data_set, + regex_t *failcount_re, regex_t *lastfailure_re) +{ + char *rsc_name = rsc_fail_name(rsc); + const char *version = crm_element_value(data_set->input, XML_ATTR_CRM_VERSION); + gboolean is_legacy = (compare_version(version, "3.0.13") < 0); + int rc = pcmk_rc_ok; + + if (generate_fail_regex(PCMK__FAIL_COUNT_PREFIX, rsc_name, is_legacy, + pcmk_is_set(rsc->flags, pe_rsc_unique), + failcount_re) != pcmk_rc_ok) { + rc = EINVAL; + + } else if (generate_fail_regex(PCMK__LAST_FAILURE_PREFIX, rsc_name, + is_legacy, + pcmk_is_set(rsc->flags, pe_rsc_unique), + lastfailure_re) != pcmk_rc_ok) { + rc = EINVAL; + regfree(failcount_re); + } + + free(rsc_name); + return rc; +} + +int +pe_get_failcount(const pe_node_t *node, pe_resource_t *rsc, + time_t *last_failure, uint32_t flags, const xmlNode *xml_op) +{ + char *key = NULL; + const char *value = NULL; + regex_t failcount_re, lastfailure_re; + int failcount = 0; + time_t last = 0; + GHashTableIter iter; + + CRM_CHECK(generate_fail_regexes(rsc, rsc->cluster, &failcount_re, + &lastfailure_re) == pcmk_rc_ok, + return 0); + + /* Resource fail count is sum of all matching operation fail counts */ + g_hash_table_iter_init(&iter, node->details->attrs); + while (g_hash_table_iter_next(&iter, (gpointer *) &key, (gpointer *) &value)) { + if (regexec(&failcount_re, key, 0, NULL, 0) == 0) { + failcount = pcmk__add_scores(failcount, char2score(value)); + crm_trace("Added %s (%s) to %s fail count (now %s)", + key, value, rsc->id, pcmk_readable_score(failcount)); + } else if (regexec(&lastfailure_re, key, 0, NULL, 0) == 0) { + long long last_ll; + + if (pcmk__scan_ll(value, &last_ll, 0LL) == pcmk_rc_ok) { + last = (time_t) QB_MAX(last, last_ll); + } + } + } + + regfree(&failcount_re); + regfree(&lastfailure_re); + + if ((failcount > 0) && (last > 0) && (last_failure != NULL)) { + *last_failure = last; + } + + /* If failure blocks the resource, disregard any failure timeout */ + if ((failcount > 0) && rsc->failure_timeout + && block_failure(node, rsc, xml_op)) { + + pe_warn("Ignoring failure timeout %d for %s because it conflicts with on-fail=block", + rsc->failure_timeout, rsc->id); + rsc->failure_timeout = 0; + } + + /* If all failures have expired, ignore fail count */ + if (pcmk_is_set(flags, pe_fc_effective) && (failcount > 0) && (last > 0) + && rsc->failure_timeout) { + + time_t now = get_effective_time(rsc->cluster); + + if (now > (last + rsc->failure_timeout)) { + crm_debug("Failcount for %s on %s expired after %ds", + rsc->id, pe__node_name(node), rsc->failure_timeout); + failcount = 0; + } + } + + /* We never want the fail counts of a bundle container's fillers to + * count towards the container's fail count. + * + * Most importantly, a Pacemaker Remote connection to a bundle container + * is a filler of the container, but can reside on a different node than the + * container itself. Counting its fail count on its node towards the + * container's fail count on that node could lead to attempting to stop the + * container on the wrong node. + */ + + if (pcmk_is_set(flags, pe_fc_fillers) && rsc->fillers + && !pe_rsc_is_bundled(rsc)) { + + GList *gIter = NULL; + + for (gIter = rsc->fillers; gIter != NULL; gIter = gIter->next) { + pe_resource_t *filler = (pe_resource_t *) gIter->data; + time_t filler_last_failure = 0; + + failcount += pe_get_failcount(node, filler, &filler_last_failure, + flags, xml_op); + + if (last_failure && filler_last_failure > *last_failure) { + *last_failure = filler_last_failure; + } + } + + if (failcount > 0) { + crm_info("Container %s and the resources within it " + "have failed %s time%s on %s", + rsc->id, pcmk_readable_score(failcount), + pcmk__plural_s(failcount), pe__node_name(node)); + } + + } else if (failcount > 0) { + crm_info("%s has failed %s time%s on %s", + rsc->id, pcmk_readable_score(failcount), + pcmk__plural_s(failcount), pe__node_name(node)); + } + + return failcount; +} + +/*! + * \brief Schedule a controller operation to clear a fail count + * + * \param[in,out] rsc Resource with failure + * \param[in] node Node failure occurred on + * \param[in] reason Readable description why needed (for logging) + * \param[in,out] data_set Working set for cluster + * + * \return Scheduled action + */ +pe_action_t * +pe__clear_failcount(pe_resource_t *rsc, const pe_node_t *node, + const char *reason, pe_working_set_t *data_set) +{ + char *key = NULL; + pe_action_t *clear = NULL; + + CRM_CHECK(rsc && node && reason && data_set, return NULL); + + key = pcmk__op_key(rsc->id, CRM_OP_CLEAR_FAILCOUNT, 0); + clear = custom_action(rsc, key, CRM_OP_CLEAR_FAILCOUNT, node, FALSE, TRUE, + data_set); + add_hash_param(clear->meta, XML_ATTR_TE_NOWAIT, XML_BOOLEAN_TRUE); + crm_notice("Clearing failure of %s on %s because %s " CRM_XS " %s", + rsc->id, pe__node_name(node), reason, clear->uuid); + return clear; +} diff --git a/lib/pengine/group.c b/lib/pengine/group.c new file mode 100644 index 0000000..d54b01a --- /dev/null +++ b/lib/pengine/group.c @@ -0,0 +1,521 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct group_variant_data_s { + pe_resource_t *last_child; // Last group member + uint32_t flags; // Group of enum pe__group_flags +} group_variant_data_t; + +/*! + * \internal + * \brief Get a group's last member + * + * \param[in] group Group resource to check + * + * \return Last member of \p group if any, otherwise NULL + */ +pe_resource_t * +pe__last_group_member(const pe_resource_t *group) +{ + if (group != NULL) { + CRM_CHECK((group->variant == pe_group) + && (group->variant_opaque != NULL), return NULL); + return ((group_variant_data_t *) group->variant_opaque)->last_child; + } + return NULL; +} + +/*! + * \internal + * \brief Check whether a group flag is set + * + * \param[in] group Group resource to check + * \param[in] flags Flag or flags to check + * + * \return true if all \p flags are set for \p group, otherwise false + */ +bool +pe__group_flag_is_set(const pe_resource_t *group, uint32_t flags) +{ + group_variant_data_t *group_data = NULL; + + CRM_CHECK((group != NULL) && (group->variant == pe_group) + && (group->variant_opaque != NULL), return false); + group_data = (group_variant_data_t *) group->variant_opaque; + return pcmk_all_flags_set(group_data->flags, flags); +} + +/*! + * \internal + * \brief Set a (deprecated) group flag + * + * \param[in,out] group Group resource to check + * \param[in] option Name of boolean configuration option + * \param[in] flag Flag to set if \p option is true (which is default) + * \param[in] wo_bit "Warn once" flag to use for deprecation warning + */ +static void +set_group_flag(pe_resource_t *group, const char *option, uint32_t flag, + uint32_t wo_bit) +{ + const char *value_s = NULL; + int value = 0; + + value_s = g_hash_table_lookup(group->meta, option); + + // We don't actually need the null check but it speeds up the common case + if ((value_s == NULL) || (crm_str_to_boolean(value_s, &value) < 0) + || (value != 0)) { + + ((group_variant_data_t *) group->variant_opaque)->flags |= flag; + + } else { + pe_warn_once(wo_bit, + "Support for the '%s' group meta-attribute is deprecated " + "and will be removed in a future release " + "(use a resource set instead)", option); + } +} + +static int +inactive_resources(pe_resource_t *rsc) +{ + int retval = 0; + + for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + if (!child_rsc->fns->active(child_rsc, TRUE)) { + retval++; + } + } + + return retval; +} + +static void +group_header(pcmk__output_t *out, int *rc, const pe_resource_t *rsc, + int n_inactive, bool show_inactive, const char *desc) +{ + GString *attrs = NULL; + + if (n_inactive > 0 && !show_inactive) { + attrs = g_string_sized_new(64); + g_string_append_printf(attrs, "%d member%s inactive", n_inactive, + pcmk__plural_s(n_inactive)); + } + + if (pe__resource_is_disabled(rsc)) { + pcmk__add_separated_word(&attrs, 64, "disabled", ", "); + } + + if (pcmk_is_set(rsc->flags, pe_rsc_maintenance)) { + pcmk__add_separated_word(&attrs, 64, "maintenance", ", "); + + } else if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + pcmk__add_separated_word(&attrs, 64, "unmanaged", ", "); + } + + if (attrs != NULL) { + PCMK__OUTPUT_LIST_HEADER(out, FALSE, *rc, "Resource Group: %s (%s)%s%s%s", + rsc->id, + (const char *) attrs->str, desc ? " (" : "", + desc ? desc : "", desc ? ")" : ""); + g_string_free(attrs, TRUE); + } else { + PCMK__OUTPUT_LIST_HEADER(out, FALSE, *rc, "Resource Group: %s%s%s%s", + rsc->id, + desc ? " (" : "", desc ? desc : "", + desc ? ")" : ""); + } +} + +static bool +skip_child_rsc(pe_resource_t *rsc, pe_resource_t *child, gboolean parent_passes, + GList *only_rsc, uint32_t show_opts) +{ + bool star_list = pcmk__list_of_1(only_rsc) && + pcmk__str_eq("*", g_list_first(only_rsc)->data, pcmk__str_none); + bool child_filtered = child->fns->is_filtered(child, only_rsc, FALSE); + bool child_active = child->fns->active(child, FALSE); + bool show_inactive = pcmk_is_set(show_opts, pcmk_show_inactive_rscs); + + /* If the resource is in only_rsc by name (so, ignoring "*") then allow + * it regardless of if it's active or not. + */ + if (!star_list && !child_filtered) { + return false; + + } else if (!child_filtered && (child_active || show_inactive)) { + return false; + + } else if (parent_passes && (child_active || show_inactive)) { + return false; + + } + + return true; +} + +gboolean +group_unpack(pe_resource_t * rsc, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = rsc->xml; + xmlNode *xml_native_rsc = NULL; + group_variant_data_t *group_data = NULL; + const char *clone_id = NULL; + + pe_rsc_trace(rsc, "Processing resource %s...", rsc->id); + + group_data = calloc(1, sizeof(group_variant_data_t)); + group_data->last_child = NULL; + rsc->variant_opaque = group_data; + + // @COMPAT These are deprecated since 2.1.5 + set_group_flag(rsc, XML_RSC_ATTR_ORDERED, pe__group_ordered, + pe_wo_group_order); + set_group_flag(rsc, "collocated", pe__group_colocated, pe_wo_group_coloc); + + clone_id = crm_element_value(rsc->xml, XML_RSC_ATTR_INCARNATION); + + for (xml_native_rsc = pcmk__xe_first_child(xml_obj); xml_native_rsc != NULL; + xml_native_rsc = pcmk__xe_next(xml_native_rsc)) { + + if (pcmk__str_eq((const char *)xml_native_rsc->name, + XML_CIB_TAG_RESOURCE, pcmk__str_none)) { + pe_resource_t *new_rsc = NULL; + + crm_xml_add(xml_native_rsc, XML_RSC_ATTR_INCARNATION, clone_id); + if (pe__unpack_resource(xml_native_rsc, &new_rsc, rsc, + data_set) != pcmk_rc_ok) { + continue; + } + + rsc->children = g_list_append(rsc->children, new_rsc); + group_data->last_child = new_rsc; + pe_rsc_trace(rsc, "Added %s member %s", rsc->id, new_rsc->id); + } + } + + if (rsc->children == NULL) { + /* The schema does not allow empty groups, but if validation is + * disabled, we allow them (members can be added later). + * + * @COMPAT At a major release bump, we should consider this a failure so + * that group methods can assume children is not NULL, and there + * are no strange effects from phantom groups due to their + * presence or meta-attributes. + */ + pcmk__config_warn("Group %s will be ignored because it does not have " + "any members", rsc->id); + } + return TRUE; +} + +gboolean +group_active(pe_resource_t * rsc, gboolean all) +{ + gboolean c_all = TRUE; + gboolean c_any = FALSE; + GList *gIter = rsc->children; + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + if (child_rsc->fns->active(child_rsc, all)) { + c_any = TRUE; + } else { + c_all = FALSE; + } + } + + if (c_any == FALSE) { + return FALSE; + } else if (all && c_all == FALSE) { + return FALSE; + } + return TRUE; +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +static void +group_print_xml(pe_resource_t *rsc, const char *pre_text, long options, + void *print_data) +{ + GList *gIter = rsc->children; + char *child_text = crm_strdup_printf("%s ", pre_text); + + status_print("%sid); + status_print("number_resources=\"%d\" ", g_list_length(rsc->children)); + status_print(">\n"); + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + child_rsc->fns->print(child_rsc, child_text, options, print_data); + } + + status_print("%s\n", pre_text); + free(child_text); +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +void +group_print(pe_resource_t *rsc, const char *pre_text, long options, + void *print_data) +{ + char *child_text = NULL; + GList *gIter = rsc->children; + + if (pre_text == NULL) { + pre_text = " "; + } + + if (options & pe_print_xml) { + group_print_xml(rsc, pre_text, options, print_data); + return; + } + + child_text = crm_strdup_printf("%s ", pre_text); + + status_print("%sResource Group: %s", pre_text ? pre_text : "", rsc->id); + + if (options & pe_print_html) { + status_print("\n
        \n"); + + } else if ((options & pe_print_log) == 0) { + status_print("\n"); + } + + if (options & pe_print_brief) { + print_rscs_brief(rsc->children, child_text, options, print_data, TRUE); + + } else { + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + if (options & pe_print_html) { + status_print("
      • \n"); + } + child_rsc->fns->print(child_rsc, child_text, options, print_data); + if (options & pe_print_html) { + status_print("
      • \n"); + } + } + } + + if (options & pe_print_html) { + status_print("
      \n"); + } + free(child_text); +} + +PCMK__OUTPUT_ARGS("group", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__group_xml(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + const char *desc = NULL; + GList *gIter = rsc->children; + + int rc = pcmk_rc_no_output; + + gboolean parent_passes = pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches) || + (strstr(rsc->id, ":") != NULL && pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches)); + + desc = pe__resource_description(rsc, show_opts); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return rc; + } + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + if (skip_child_rsc(rsc, child_rsc, parent_passes, only_rsc, show_opts)) { + continue; + } + + if (rc == pcmk_rc_no_output) { + char *count = pcmk__itoa(g_list_length(gIter)); + const char *maint_s = pe__rsc_bool_str(rsc, pe_rsc_maintenance); + const char *managed_s = pe__rsc_bool_str(rsc, pe_rsc_managed); + const char *disabled_s = pcmk__btoa(pe__resource_is_disabled(rsc)); + + rc = pe__name_and_nvpairs_xml(out, true, "group", 5, + XML_ATTR_ID, rsc->id, + "number_resources", count, + "maintenance", maint_s, + "managed", managed_s, + "disabled", disabled_s, + "description", desc); + free(count); + CRM_ASSERT(rc == pcmk_rc_ok); + } + + out->message(out, crm_map_element_name(child_rsc->xml), show_opts, child_rsc, + only_node, only_rsc); + } + + if (rc == pcmk_rc_ok) { + pcmk__output_xml_pop_parent(out); + } + + return rc; +} + +PCMK__OUTPUT_ARGS("group", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__group_default(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + const char *desc = NULL; + int rc = pcmk_rc_no_output; + + gboolean parent_passes = pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches) || + (strstr(rsc->id, ":") != NULL && pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches)); + + gboolean active = rsc->fns->active(rsc, TRUE); + gboolean partially_active = rsc->fns->active(rsc, FALSE); + + desc = pe__resource_description(rsc, show_opts); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return rc; + } + + if (pcmk_is_set(show_opts, pcmk_show_brief)) { + GList *rscs = pe__filter_rsc_list(rsc->children, only_rsc); + + if (rscs != NULL) { + group_header(out, &rc, rsc, !active && partially_active ? inactive_resources(rsc) : 0, + pcmk_is_set(show_opts, pcmk_show_inactive_rscs), desc); + pe__rscs_brief_output(out, rscs, show_opts | pcmk_show_inactive_rscs); + + rc = pcmk_rc_ok; + g_list_free(rscs); + } + + } else { + for (GList *gIter = rsc->children; gIter; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + if (skip_child_rsc(rsc, child_rsc, parent_passes, only_rsc, show_opts)) { + continue; + } + + group_header(out, &rc, rsc, !active && partially_active ? inactive_resources(rsc) : 0, + pcmk_is_set(show_opts, pcmk_show_inactive_rscs), desc); + out->message(out, crm_map_element_name(child_rsc->xml), show_opts, + child_rsc, only_node, only_rsc); + } + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + + return rc; +} + +void +group_free(pe_resource_t * rsc) +{ + CRM_CHECK(rsc != NULL, return); + + pe_rsc_trace(rsc, "Freeing %s", rsc->id); + + for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + CRM_ASSERT(child_rsc); + pe_rsc_trace(child_rsc, "Freeing child %s", child_rsc->id); + child_rsc->fns->free(child_rsc); + } + + pe_rsc_trace(rsc, "Freeing child list"); + g_list_free(rsc->children); + + common_free(rsc); +} + +enum rsc_role_e +group_resource_state(const pe_resource_t * rsc, gboolean current) +{ + enum rsc_role_e group_role = RSC_ROLE_UNKNOWN; + GList *gIter = rsc->children; + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + enum rsc_role_e role = child_rsc->fns->state(child_rsc, current); + + if (role > group_role) { + group_role = role; + } + } + + pe_rsc_trace(rsc, "%s role: %s", rsc->id, role2text(group_role)); + return group_role; +} + +gboolean +pe__group_is_filtered(const pe_resource_t *rsc, GList *only_rsc, + gboolean check_parent) +{ + gboolean passes = FALSE; + + if (check_parent + && pcmk__str_in_list(rsc_printable_id(pe__const_top_resource(rsc, + false)), + only_rsc, pcmk__str_star_matches)) { + passes = TRUE; + } else if (pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches)) { + passes = TRUE; + } else if (strstr(rsc->id, ":") != NULL && pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches)) { + passes = TRUE; + } else { + for (const GList *iter = rsc->children; + iter != NULL; iter = iter->next) { + + const pe_resource_t *child_rsc = (const pe_resource_t *) iter->data; + + if (!child_rsc->fns->is_filtered(child_rsc, only_rsc, FALSE)) { + passes = TRUE; + break; + } + } + } + + return !passes; +} diff --git a/lib/pengine/native.c b/lib/pengine/native.c new file mode 100644 index 0000000..5e92ddc --- /dev/null +++ b/lib/pengine/native.c @@ -0,0 +1,1414 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#ifdef PCMK__COMPAT_2_0 +#define PROVIDER_SEP "::" +#else +#define PROVIDER_SEP ":" +#endif + +/*! + * \internal + * \brief Check whether a resource is active on multiple nodes + */ +static bool +is_multiply_active(const pe_resource_t *rsc) +{ + unsigned int count = 0; + + if (rsc->variant == pe_native) { + pe__find_active_requires(rsc, &count); + } + return count > 1; +} + +static void +native_priority_to_node(pe_resource_t * rsc, pe_node_t * node, gboolean failed) +{ + int priority = 0; + + if ((rsc->priority == 0) || (failed == TRUE)) { + return; + } + + if (rsc->role == RSC_ROLE_PROMOTED) { + // Promoted instance takes base priority + 1 + priority = rsc->priority + 1; + + } else { + priority = rsc->priority; + } + + node->details->priority += priority; + pe_rsc_trace(rsc, "%s now has priority %d with %s'%s' (priority: %d%s)", + pe__node_name(node), node->details->priority, + (rsc->role == RSC_ROLE_PROMOTED)? "promoted " : "", + rsc->id, rsc->priority, + (rsc->role == RSC_ROLE_PROMOTED)? " + 1" : ""); + + /* Priority of a resource running on a guest node is added to the cluster + * node as well. */ + if (node->details->remote_rsc + && node->details->remote_rsc->container) { + GList *gIter = node->details->remote_rsc->container->running_on; + + for (; gIter != NULL; gIter = gIter->next) { + pe_node_t *a_node = gIter->data; + + a_node->details->priority += priority; + pe_rsc_trace(rsc, "%s now has priority %d with %s'%s' (priority: %d%s) " + "from guest node %s", + pe__node_name(a_node), a_node->details->priority, + (rsc->role == RSC_ROLE_PROMOTED)? "promoted " : "", + rsc->id, rsc->priority, + (rsc->role == RSC_ROLE_PROMOTED)? " + 1" : "", + pe__node_name(node)); + } + } +} + +void +native_add_running(pe_resource_t * rsc, pe_node_t * node, pe_working_set_t * data_set, gboolean failed) +{ + GList *gIter = rsc->running_on; + + CRM_CHECK(node != NULL, return); + for (; gIter != NULL; gIter = gIter->next) { + pe_node_t *a_node = (pe_node_t *) gIter->data; + + CRM_CHECK(a_node != NULL, return); + if (pcmk__str_eq(a_node->details->id, node->details->id, pcmk__str_casei)) { + return; + } + } + + pe_rsc_trace(rsc, "Adding %s to %s %s", rsc->id, pe__node_name(node), + pcmk_is_set(rsc->flags, pe_rsc_managed)? "" : "(unmanaged)"); + + rsc->running_on = g_list_append(rsc->running_on, node); + if (rsc->variant == pe_native) { + node->details->running_rsc = g_list_append(node->details->running_rsc, rsc); + + native_priority_to_node(rsc, node, failed); + } + + if (rsc->variant == pe_native && node->details->maintenance) { + pe__clear_resource_flags(rsc, pe_rsc_managed); + pe__set_resource_flags(rsc, pe_rsc_maintenance); + } + + if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + pe_resource_t *p = rsc->parent; + + pe_rsc_info(rsc, "resource %s isn't managed", rsc->id); + resource_location(rsc, node, INFINITY, "not_managed_default", data_set); + + while(p && node->details->online) { + /* add without the additional location constraint */ + p->running_on = g_list_append(p->running_on, node); + p = p->parent; + } + return; + } + + if (is_multiply_active(rsc)) { + switch (rsc->recovery_type) { + case recovery_stop_only: + { + GHashTableIter gIter; + pe_node_t *local_node = NULL; + + /* make sure it doesn't come up again */ + if (rsc->allowed_nodes != NULL) { + g_hash_table_destroy(rsc->allowed_nodes); + } + rsc->allowed_nodes = pe__node_list2table(data_set->nodes); + g_hash_table_iter_init(&gIter, rsc->allowed_nodes); + while (g_hash_table_iter_next(&gIter, NULL, (void **)&local_node)) { + local_node->weight = -INFINITY; + } + } + break; + case recovery_block: + pe__clear_resource_flags(rsc, pe_rsc_managed); + pe__set_resource_flags(rsc, pe_rsc_block); + + /* If the resource belongs to a group or bundle configured with + * multiple-active=block, block the entire entity. + */ + if (rsc->parent + && (rsc->parent->variant == pe_group || rsc->parent->variant == pe_container) + && rsc->parent->recovery_type == recovery_block) { + GList *gIter = rsc->parent->children; + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child = (pe_resource_t *) gIter->data; + + pe__clear_resource_flags(child, pe_rsc_managed); + pe__set_resource_flags(child, pe_rsc_block); + } + } + break; + default: // recovery_stop_start, recovery_stop_unexpected + /* The scheduler will do the right thing because the relevant + * variables and flags are set when unpacking the history. + */ + break; + } + crm_debug("%s is active on multiple nodes including %s: %s", + rsc->id, pe__node_name(node), + recovery2text(rsc->recovery_type)); + + } else { + pe_rsc_trace(rsc, "Resource %s is active on %s", + rsc->id, pe__node_name(node)); + } + + if (rsc->parent != NULL) { + native_add_running(rsc->parent, node, data_set, FALSE); + } +} + +static void +recursive_clear_unique(pe_resource_t *rsc, gpointer user_data) +{ + pe__clear_resource_flags(rsc, pe_rsc_unique); + add_hash_param(rsc->meta, XML_RSC_ATTR_UNIQUE, XML_BOOLEAN_FALSE); + g_list_foreach(rsc->children, (GFunc) recursive_clear_unique, NULL); +} + +gboolean +native_unpack(pe_resource_t * rsc, pe_working_set_t * data_set) +{ + pe_resource_t *parent = uber_parent(rsc); + const char *standard = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS); + uint32_t ra_caps = pcmk_get_ra_caps(standard); + + pe_rsc_trace(rsc, "Processing resource %s...", rsc->id); + + // Only some agent standards support unique and promotable clones + if (!pcmk_is_set(ra_caps, pcmk_ra_cap_unique) + && pcmk_is_set(rsc->flags, pe_rsc_unique) && pe_rsc_is_clone(parent)) { + + /* @COMPAT We should probably reject this situation as an error (as we + * do for promotable below) rather than warn and convert, but that would + * be a backward-incompatible change that we should probably do with a + * transform at a schema major version bump. + */ + pe__force_anon(standard, parent, rsc->id, data_set); + + /* Clear globally-unique on the parent and all its descendants unpacked + * so far (clearing the parent should make any future children unpacking + * correct). We have to clear this resource explicitly because it isn't + * hooked into the parent's children yet. + */ + recursive_clear_unique(parent, NULL); + recursive_clear_unique(rsc, NULL); + } + if (!pcmk_is_set(ra_caps, pcmk_ra_cap_promotable) + && pcmk_is_set(parent->flags, pe_rsc_promotable)) { + + pe_err("Resource %s is of type %s and therefore " + "cannot be used as a promotable clone resource", + rsc->id, standard); + return FALSE; + } + return TRUE; +} + +static bool +rsc_is_on_node(pe_resource_t *rsc, const pe_node_t *node, int flags) +{ + pe_rsc_trace(rsc, "Checking whether %s is on %s", + rsc->id, pe__node_name(node)); + + if (pcmk_is_set(flags, pe_find_current) && rsc->running_on) { + + for (GList *iter = rsc->running_on; iter; iter = iter->next) { + pe_node_t *loc = (pe_node_t *) iter->data; + + if (loc->details == node->details) { + return true; + } + } + + } else if (pcmk_is_set(flags, pe_find_inactive) + && (rsc->running_on == NULL)) { + return true; + + } else if (!pcmk_is_set(flags, pe_find_current) && rsc->allocated_to + && (rsc->allocated_to->details == node->details)) { + return true; + } + return false; +} + +pe_resource_t * +native_find_rsc(pe_resource_t * rsc, const char *id, const pe_node_t *on_node, + int flags) +{ + bool match = false; + pe_resource_t *result = NULL; + + CRM_CHECK(id && rsc && rsc->id, return NULL); + + if (flags & pe_find_clone) { + const char *rid = ID(rsc->xml); + + if (!pe_rsc_is_clone(pe__const_top_resource(rsc, false))) { + match = false; + + } else if (!strcmp(id, rsc->id) || pcmk__str_eq(id, rid, pcmk__str_none)) { + match = true; + } + + } else if (!strcmp(id, rsc->id)) { + match = true; + + } else if (pcmk_is_set(flags, pe_find_renamed) + && rsc->clone_name && strcmp(rsc->clone_name, id) == 0) { + match = true; + + } else if (pcmk_is_set(flags, pe_find_any) + || (pcmk_is_set(flags, pe_find_anon) + && !pcmk_is_set(rsc->flags, pe_rsc_unique))) { + match = pe_base_name_eq(rsc, id); + } + + if (match && on_node) { + if (!rsc_is_on_node(rsc, on_node, flags)) { + match = false; + } + } + + if (match) { + return rsc; + } + + for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child = (pe_resource_t *) gIter->data; + + result = rsc->fns->find_rsc(child, id, on_node, flags); + if (result) { + return result; + } + } + return NULL; +} + +// create is ignored +char * +native_parameter(pe_resource_t * rsc, pe_node_t * node, gboolean create, const char *name, + pe_working_set_t * data_set) +{ + char *value_copy = NULL; + const char *value = NULL; + GHashTable *params = NULL; + + CRM_CHECK(rsc != NULL, return NULL); + CRM_CHECK(name != NULL && strlen(name) != 0, return NULL); + + pe_rsc_trace(rsc, "Looking up %s in %s", name, rsc->id); + params = pe_rsc_params(rsc, node, data_set); + value = g_hash_table_lookup(params, name); + if (value == NULL) { + /* try meta attributes instead */ + value = g_hash_table_lookup(rsc->meta, name); + } + pcmk__str_update(&value_copy, value); + return value_copy; +} + +gboolean +native_active(pe_resource_t * rsc, gboolean all) +{ + for (GList *gIter = rsc->running_on; gIter != NULL; gIter = gIter->next) { + pe_node_t *a_node = (pe_node_t *) gIter->data; + + if (a_node->details->unclean) { + pe_rsc_trace(rsc, "Resource %s: %s is unclean", + rsc->id, pe__node_name(a_node)); + return TRUE; + } else if (a_node->details->online == FALSE && pcmk_is_set(rsc->flags, pe_rsc_managed)) { + pe_rsc_trace(rsc, "Resource %s: %s is offline", + rsc->id, pe__node_name(a_node)); + } else { + pe_rsc_trace(rsc, "Resource %s active on %s", + rsc->id, pe__node_name(a_node)); + return TRUE; + } + } + return FALSE; +} + +struct print_data_s { + long options; + void *print_data; +}; + +static const char * +native_pending_state(const pe_resource_t *rsc) +{ + const char *pending_state = NULL; + + if (pcmk__str_eq(rsc->pending_task, CRMD_ACTION_START, pcmk__str_casei)) { + pending_state = "Starting"; + + } else if (pcmk__str_eq(rsc->pending_task, CRMD_ACTION_STOP, pcmk__str_casei)) { + pending_state = "Stopping"; + + } else if (pcmk__str_eq(rsc->pending_task, CRMD_ACTION_MIGRATE, pcmk__str_casei)) { + pending_state = "Migrating"; + + } else if (pcmk__str_eq(rsc->pending_task, CRMD_ACTION_MIGRATED, pcmk__str_casei)) { + /* Work might be done in here. */ + pending_state = "Migrating"; + + } else if (pcmk__str_eq(rsc->pending_task, CRMD_ACTION_PROMOTE, pcmk__str_casei)) { + pending_state = "Promoting"; + + } else if (pcmk__str_eq(rsc->pending_task, CRMD_ACTION_DEMOTE, pcmk__str_casei)) { + pending_state = "Demoting"; + } + + return pending_state; +} + +static const char * +native_pending_task(const pe_resource_t *rsc) +{ + const char *pending_task = NULL; + + if (pcmk__str_eq(rsc->pending_task, CRMD_ACTION_STATUS, pcmk__str_casei)) { + pending_task = "Monitoring"; + + /* Pending probes are not printed, even if pending + * operations are requested. If someone ever requests that + * behavior, uncomment this and the corresponding part of + * unpack.c:unpack_rsc_op(). + */ + /* + } else if (pcmk__str_eq(rsc->pending_task, "probe", pcmk__str_casei)) { + pending_task = "Checking"; + */ + } + + return pending_task; +} + +static enum rsc_role_e +native_displayable_role(const pe_resource_t *rsc) +{ + enum rsc_role_e role = rsc->role; + + if ((role == RSC_ROLE_STARTED) + && pcmk_is_set(pe__const_top_resource(rsc, false)->flags, + pe_rsc_promotable)) { + + role = RSC_ROLE_UNPROMOTED; + } + return role; +} + +static const char * +native_displayable_state(const pe_resource_t *rsc, bool print_pending) +{ + const char *rsc_state = NULL; + + if (print_pending) { + rsc_state = native_pending_state(rsc); + } + if (rsc_state == NULL) { + rsc_state = role2text(native_displayable_role(rsc)); + } + return rsc_state; +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +static void +native_print_xml(pe_resource_t *rsc, const char *pre_text, long options, + void *print_data) +{ + const char *class = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS); + const char *prov = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER); + const char *rsc_state = native_displayable_state(rsc, pcmk_is_set(options, pe_print_pending)); + const char *target_role = NULL; + + /* resource information. */ + status_print("%sxml, XML_ATTR_TYPE)); + + status_print("role=\"%s\" ", rsc_state); + if (rsc->meta) { + target_role = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE); + } + if (target_role) { + status_print("target_role=\"%s\" ", target_role); + } + status_print("active=\"%s\" ", pcmk__btoa(rsc->fns->active(rsc, TRUE))); + status_print("orphaned=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_orphan)); + status_print("blocked=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_block)); + status_print("managed=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_managed)); + status_print("failed=\"%s\" ", pe__rsc_bool_str(rsc, pe_rsc_failed)); + status_print("failure_ignored=\"%s\" ", + pe__rsc_bool_str(rsc, pe_rsc_failure_ignored)); + status_print("nodes_running_on=\"%d\" ", g_list_length(rsc->running_on)); + + if (options & pe_print_pending) { + const char *pending_task = native_pending_task(rsc); + + if (pending_task) { + status_print("pending=\"%s\" ", pending_task); + } + } + + /* print out the nodes this resource is running on */ + if (options & pe_print_rsconly) { + status_print("/>\n"); + /* do nothing */ + } else if (rsc->running_on != NULL) { + GList *gIter = rsc->running_on; + + status_print(">\n"); + for (; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = (pe_node_t *) gIter->data; + + status_print("%s \n", + pre_text, pcmk__s(node->details->uname, ""), + node->details->id, pcmk__btoa(!node->details->online)); + } + status_print("%s\n", pre_text); + } else { + status_print("/>\n"); + } +} + +// Append a flag to resource description string's flags list +static bool +add_output_flag(GString *s, const char *flag_desc, bool have_flags) +{ + g_string_append(s, (have_flags? ", " : " (")); + g_string_append(s, flag_desc); + return true; +} + +// Append a node name to resource description string's node list +static bool +add_output_node(GString *s, const char *node, bool have_nodes) +{ + g_string_append(s, (have_nodes? " " : " [ ")); + g_string_append(s, node); + return true; +} + +/*! + * \internal + * \brief Create a string description of a resource + * + * \param[in] rsc Resource to describe + * \param[in] name Desired identifier for the resource + * \param[in] node If not NULL, node that resource is "on" + * \param[in] show_opts Bitmask of pcmk_show_opt_e. + * \param[in] target_role Resource's target role + * \param[in] show_nodes Whether to display nodes when multiply active + * + * \return Newly allocated string description of resource + * \note Caller must free the result with g_free(). + */ +gchar * +pcmk__native_output_string(const pe_resource_t *rsc, const char *name, + const pe_node_t *node, uint32_t show_opts, + const char *target_role, bool show_nodes) +{ + const char *class = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS); + const char *provider = NULL; + const char *kind = crm_element_value(rsc->xml, XML_ATTR_TYPE); + GString *outstr = NULL; + bool have_flags = false; + + if (rsc->variant != pe_native) { + return NULL; + } + + CRM_CHECK(name != NULL, name = "unknown"); + CRM_CHECK(kind != NULL, kind = "unknown"); + CRM_CHECK(class != NULL, class = "unknown"); + + if (pcmk_is_set(pcmk_get_ra_caps(class), pcmk_ra_cap_provider)) { + provider = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER); + } + + if ((node == NULL) && (rsc->lock_node != NULL)) { + node = rsc->lock_node; + } + if (pcmk_any_flags_set(show_opts, pcmk_show_rsc_only) + || pcmk__list_of_multiple(rsc->running_on)) { + node = NULL; + } + + outstr = g_string_sized_new(128); + + // Resource name and agent + pcmk__g_strcat(outstr, + name, "\t(", class, ((provider == NULL)? "" : PROVIDER_SEP), + pcmk__s(provider, ""), ":", kind, "):\t", NULL); + + // State on node + if (pcmk_is_set(rsc->flags, pe_rsc_orphan)) { + g_string_append(outstr, " ORPHANED"); + } + if (pcmk_is_set(rsc->flags, pe_rsc_failed)) { + enum rsc_role_e role = native_displayable_role(rsc); + + g_string_append(outstr, " FAILED"); + if (role > RSC_ROLE_UNPROMOTED) { + pcmk__add_word(&outstr, 0, role2text(role)); + } + } else { + bool show_pending = pcmk_is_set(show_opts, pcmk_show_pending); + + pcmk__add_word(&outstr, 0, native_displayable_state(rsc, show_pending)); + } + if (node) { + pcmk__add_word(&outstr, 0, pe__node_name(node)); + } + + // Failed probe operation + if (native_displayable_role(rsc) == RSC_ROLE_STOPPED) { + xmlNode *probe_op = pe__failed_probe_for_rsc(rsc, node ? node->details->uname : NULL); + if (probe_op != NULL) { + int rc; + + pcmk__scan_min_int(crm_element_value(probe_op, XML_LRM_ATTR_RC), &rc, 0); + pcmk__g_strcat(outstr, " (", services_ocf_exitcode_str(rc), ") ", + NULL); + } + } + + // Flags, as: ( [...]) + if (node && !(node->details->online) && node->details->unclean) { + have_flags = add_output_flag(outstr, "UNCLEAN", have_flags); + } + if (node && (node == rsc->lock_node)) { + have_flags = add_output_flag(outstr, "LOCKED", have_flags); + } + if (pcmk_is_set(show_opts, pcmk_show_pending)) { + const char *pending_task = native_pending_task(rsc); + + if (pending_task) { + have_flags = add_output_flag(outstr, pending_task, have_flags); + } + } + if (target_role) { + enum rsc_role_e target_role_e = text2role(target_role); + + /* Only show target role if it limits our abilities (i.e. ignore + * Started, as it is the default anyways, and doesn't prevent the + * resource from becoming promoted). + */ + if (target_role_e == RSC_ROLE_STOPPED) { + have_flags = add_output_flag(outstr, "disabled", have_flags); + + } else if (pcmk_is_set(pe__const_top_resource(rsc, false)->flags, + pe_rsc_promotable) + && target_role_e == RSC_ROLE_UNPROMOTED) { + have_flags = add_output_flag(outstr, "target-role:", have_flags); + g_string_append(outstr, target_role); + } + } + + // Blocked or maintenance implies unmanaged + if (pcmk_any_flags_set(rsc->flags, pe_rsc_block|pe_rsc_maintenance)) { + if (pcmk_is_set(rsc->flags, pe_rsc_block)) { + have_flags = add_output_flag(outstr, "blocked", have_flags); + + } else if (pcmk_is_set(rsc->flags, pe_rsc_maintenance)) { + have_flags = add_output_flag(outstr, "maintenance", have_flags); + } + } else if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + have_flags = add_output_flag(outstr, "unmanaged", have_flags); + } + + if (pcmk_is_set(rsc->flags, pe_rsc_failure_ignored)) { + have_flags = add_output_flag(outstr, "failure ignored", have_flags); + } + + + if (have_flags) { + g_string_append_c(outstr, ')'); + } + + // User-supplied description + if (pcmk_any_flags_set(show_opts, pcmk_show_rsc_only|pcmk_show_description) + || pcmk__list_of_multiple(rsc->running_on)) { + const char *desc = crm_element_value(rsc->xml, XML_ATTR_DESC); + + if (desc) { + g_string_append(outstr, " ("); + g_string_append(outstr, desc); + g_string_append(outstr, ")"); + + } + } + + if (show_nodes && !pcmk_is_set(show_opts, pcmk_show_rsc_only) + && pcmk__list_of_multiple(rsc->running_on)) { + bool have_nodes = false; + + for (GList *iter = rsc->running_on; iter != NULL; iter = iter->next) { + pe_node_t *n = (pe_node_t *) iter->data; + + have_nodes = add_output_node(outstr, n->details->uname, have_nodes); + } + if (have_nodes) { + g_string_append(outstr, " ]"); + } + } + + return g_string_free(outstr, FALSE); +} + +int +pe__common_output_html(pcmk__output_t *out, const pe_resource_t *rsc, + const char *name, const pe_node_t *node, + uint32_t show_opts) +{ + const char *kind = crm_element_value(rsc->xml, XML_ATTR_TYPE); + const char *target_role = NULL; + + xmlNodePtr list_node = NULL; + const char *cl = NULL; + + CRM_ASSERT(rsc->variant == pe_native); + CRM_ASSERT(kind != NULL); + + if (rsc->meta) { + const char *is_internal = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_INTERNAL_RSC); + + if (crm_is_true(is_internal) + && !pcmk_is_set(show_opts, pcmk_show_implicit_rscs)) { + + crm_trace("skipping print of internal resource %s", rsc->id); + return pcmk_rc_no_output; + } + target_role = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE); + } + + if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + cl = "rsc-managed"; + + } else if (pcmk_is_set(rsc->flags, pe_rsc_failed)) { + cl = "rsc-failed"; + + } else if (rsc->variant == pe_native && (rsc->running_on == NULL)) { + cl = "rsc-failed"; + + } else if (pcmk__list_of_multiple(rsc->running_on)) { + cl = "rsc-multiple"; + + } else if (pcmk_is_set(rsc->flags, pe_rsc_failure_ignored)) { + cl = "rsc-failure-ignored"; + + } else { + cl = "rsc-ok"; + } + + { + gchar *s = pcmk__native_output_string(rsc, name, node, show_opts, + target_role, true); + + list_node = pcmk__output_create_html_node(out, "li", NULL, NULL, NULL); + pcmk_create_html_node(list_node, "span", NULL, cl, s); + g_free(s); + } + + return pcmk_rc_ok; +} + +int +pe__common_output_text(pcmk__output_t *out, const pe_resource_t *rsc, + const char *name, const pe_node_t *node, + uint32_t show_opts) +{ + const char *target_role = NULL; + + CRM_ASSERT(rsc->variant == pe_native); + + if (rsc->meta) { + const char *is_internal = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_INTERNAL_RSC); + + if (crm_is_true(is_internal) + && !pcmk_is_set(show_opts, pcmk_show_implicit_rscs)) { + + crm_trace("skipping print of internal resource %s", rsc->id); + return pcmk_rc_no_output; + } + target_role = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE); + } + + { + gchar *s = pcmk__native_output_string(rsc, name, node, show_opts, + target_role, true); + + out->list_item(out, NULL, "%s", s); + g_free(s); + } + + return pcmk_rc_ok; +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +void +common_print(pe_resource_t *rsc, const char *pre_text, const char *name, + const pe_node_t *node, long options, void *print_data) +{ + const char *target_role = NULL; + + CRM_ASSERT(rsc->variant == pe_native); + + if (rsc->meta) { + const char *is_internal = g_hash_table_lookup(rsc->meta, + XML_RSC_ATTR_INTERNAL_RSC); + + if (crm_is_true(is_internal) + && !pcmk_is_set(options, pe_print_implicit)) { + + crm_trace("skipping print of internal resource %s", rsc->id); + return; + } + target_role = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE); + } + + if (options & pe_print_xml) { + native_print_xml(rsc, pre_text, options, print_data); + return; + } + + if ((pre_text == NULL) && (options & pe_print_printf)) { + pre_text = " "; + } + + if (options & pe_print_html) { + if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + status_print(""); + + } else if (pcmk_is_set(rsc->flags, pe_rsc_failed)) { + status_print(""); + + } else if (rsc->running_on == NULL) { + status_print(""); + + } else if (pcmk__list_of_multiple(rsc->running_on)) { + status_print(""); + + } else if (pcmk_is_set(rsc->flags, pe_rsc_failure_ignored)) { + status_print(""); + + } else { + status_print(""); + } + } + + { + gchar *resource_s = pcmk__native_output_string(rsc, name, node, options, + target_role, false); + status_print("%s%s", (pre_text? pre_text : ""), resource_s); + g_free(resource_s); + } + + if (pcmk_is_set(options, pe_print_html)) { + status_print(" "); + } + + if (!pcmk_is_set(options, pe_print_rsconly) + && pcmk__list_of_multiple(rsc->running_on)) { + + GList *gIter = rsc->running_on; + int counter = 0; + + if (options & pe_print_html) { + status_print("
        \n"); + } else if ((options & pe_print_printf) + || (options & pe_print_ncurses)) { + status_print("["); + } + + for (; gIter != NULL; gIter = gIter->next) { + pe_node_t *n = (pe_node_t *) gIter->data; + + counter++; + + if (options & pe_print_html) { + status_print("
      • \n%s", pe__node_name(n)); + + } else if ((options & pe_print_printf) + || (options & pe_print_ncurses)) { + status_print(" %s", pe__node_name(n)); + + } else if ((options & pe_print_log)) { + status_print("\t%d : %s", counter, pe__node_name(n)); + + } else { + status_print("%s", pe__node_name(n)); + } + if (options & pe_print_html) { + status_print("
      • \n"); + + } + } + + if (options & pe_print_html) { + status_print("
      \n"); + } else if ((options & pe_print_printf) + || (options & pe_print_ncurses)) { + status_print(" ]"); + } + } + + if (options & pe_print_html) { + status_print("
      \n"); + } else if (options & pe_print_suppres_nl) { + /* nothing */ + } else if ((options & pe_print_printf) || (options & pe_print_ncurses)) { + status_print("\n"); + } +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +void +native_print(pe_resource_t *rsc, const char *pre_text, long options, + void *print_data) +{ + const pe_node_t *node = NULL; + + CRM_ASSERT(rsc->variant == pe_native); + if (options & pe_print_xml) { + native_print_xml(rsc, pre_text, options, print_data); + return; + } + + node = pe__current_node(rsc); + + if (node == NULL) { + // This is set only if a non-probe action is pending on this node + node = rsc->pending_node; + } + + common_print(rsc, pre_text, rsc_printable_id(rsc), node, options, print_data); +} + +PCMK__OUTPUT_ARGS("primitive", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__resource_xml(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node G_GNUC_UNUSED = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + bool print_pending = pcmk_is_set(show_opts, pcmk_show_pending); + const char *class = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS); + const char *prov = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER); + const char *rsc_state = native_displayable_state(rsc, print_pending); + + const char *desc = NULL; + char ra_name[LINE_MAX]; + char *nodes_running_on = NULL; + const char *lock_node_name = NULL; + int rc = pcmk_rc_no_output; + const char *target_role = NULL; + + desc = pe__resource_description(rsc, show_opts); + + if (rsc->meta != NULL) { + target_role = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE); + } + + CRM_ASSERT(rsc->variant == pe_native); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return pcmk_rc_no_output; + } + + /* resource information. */ + snprintf(ra_name, LINE_MAX, "%s%s%s:%s", class, + ((prov == NULL)? "" : PROVIDER_SEP), ((prov == NULL)? "" : prov), + crm_element_value(rsc->xml, XML_ATTR_TYPE)); + + nodes_running_on = pcmk__itoa(g_list_length(rsc->running_on)); + + if (rsc->lock_node != NULL) { + lock_node_name = rsc->lock_node->details->uname; + } + + rc = pe__name_and_nvpairs_xml(out, true, "resource", 15, + "id", rsc_printable_id(rsc), + "resource_agent", ra_name, + "role", rsc_state, + "target_role", target_role, + "active", pcmk__btoa(rsc->fns->active(rsc, TRUE)), + "orphaned", pe__rsc_bool_str(rsc, pe_rsc_orphan), + "blocked", pe__rsc_bool_str(rsc, pe_rsc_block), + "maintenance", pe__rsc_bool_str(rsc, pe_rsc_maintenance), + "managed", pe__rsc_bool_str(rsc, pe_rsc_managed), + "failed", pe__rsc_bool_str(rsc, pe_rsc_failed), + "failure_ignored", pe__rsc_bool_str(rsc, pe_rsc_failure_ignored), + "nodes_running_on", nodes_running_on, + "pending", (print_pending? native_pending_task(rsc) : NULL), + "locked_to", lock_node_name, + "description", desc); + free(nodes_running_on); + + CRM_ASSERT(rc == pcmk_rc_ok); + + if (rsc->running_on != NULL) { + GList *gIter = rsc->running_on; + + for (; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = (pe_node_t *) gIter->data; + + rc = pe__name_and_nvpairs_xml(out, false, "node", 3, + "name", node->details->uname, + "id", node->details->id, + "cached", pcmk__btoa(node->details->online)); + CRM_ASSERT(rc == pcmk_rc_ok); + } + } + + pcmk__output_xml_pop_parent(out); + return rc; +} + +PCMK__OUTPUT_ARGS("primitive", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__resource_html(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node G_GNUC_UNUSED = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + const pe_node_t *node = pe__current_node(rsc); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return pcmk_rc_no_output; + } + + CRM_ASSERT(rsc->variant == pe_native); + + if (node == NULL) { + // This is set only if a non-probe action is pending on this node + node = rsc->pending_node; + } + return pe__common_output_html(out, rsc, rsc_printable_id(rsc), node, show_opts); +} + +PCMK__OUTPUT_ARGS("primitive", "uint32_t", "pe_resource_t *", "GList *", "GList *") +int +pe__resource_text(pcmk__output_t *out, va_list args) +{ + uint32_t show_opts = va_arg(args, uint32_t); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + GList *only_node G_GNUC_UNUSED = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + const pe_node_t *node = pe__current_node(rsc); + + CRM_ASSERT(rsc->variant == pe_native); + + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + return pcmk_rc_no_output; + } + + if (node == NULL) { + // This is set only if a non-probe action is pending on this node + node = rsc->pending_node; + } + return pe__common_output_text(out, rsc, rsc_printable_id(rsc), node, show_opts); +} + +void +native_free(pe_resource_t * rsc) +{ + pe_rsc_trace(rsc, "Freeing resource action list (not the data)"); + common_free(rsc); +} + +enum rsc_role_e +native_resource_state(const pe_resource_t * rsc, gboolean current) +{ + enum rsc_role_e role = rsc->next_role; + + if (current) { + role = rsc->role; + } + pe_rsc_trace(rsc, "%s state: %s", rsc->id, role2text(role)); + return role; +} + +/*! + * \internal + * \brief List nodes where a resource (or any of its children) is + * + * \param[in] rsc Resource to check + * \param[out] list List to add result to + * \param[in] current 0 = where allocated, 1 = where running, + * 2 = where running or pending + * + * \return If list contains only one node, that node, or NULL otherwise + */ +pe_node_t * +native_location(const pe_resource_t *rsc, GList **list, int current) +{ + pe_node_t *one = NULL; + GList *result = NULL; + + if (rsc->children) { + GList *gIter = rsc->children; + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child = (pe_resource_t *) gIter->data; + + child->fns->location(child, &result, current); + } + + } else if (current) { + + if (rsc->running_on) { + result = g_list_copy(rsc->running_on); + } + if ((current == 2) && rsc->pending_node + && !pe_find_node_id(result, rsc->pending_node->details->id)) { + result = g_list_append(result, rsc->pending_node); + } + + } else if (current == FALSE && rsc->allocated_to) { + result = g_list_append(NULL, rsc->allocated_to); + } + + if (result && (result->next == NULL)) { + one = result->data; + } + + if (list) { + GList *gIter = result; + + for (; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = (pe_node_t *) gIter->data; + + if (*list == NULL || pe_find_node_id(*list, node->details->id) == NULL) { + *list = g_list_append(*list, node); + } + } + } + + g_list_free(result); + return one; +} + +static void +get_rscs_brief(GList *rsc_list, GHashTable * rsc_table, GHashTable * active_table) +{ + GList *gIter = rsc_list; + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *rsc = (pe_resource_t *) gIter->data; + + const char *class = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS); + const char *kind = crm_element_value(rsc->xml, XML_ATTR_TYPE); + + int offset = 0; + char buffer[LINE_MAX]; + + int *rsc_counter = NULL; + int *active_counter = NULL; + + if (rsc->variant != pe_native) { + continue; + } + + offset += snprintf(buffer + offset, LINE_MAX - offset, "%s", class); + if (pcmk_is_set(pcmk_get_ra_caps(class), pcmk_ra_cap_provider)) { + const char *prov = crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER); + + if (prov != NULL) { + offset += snprintf(buffer + offset, LINE_MAX - offset, + PROVIDER_SEP "%s", prov); + } + } + offset += snprintf(buffer + offset, LINE_MAX - offset, ":%s", kind); + CRM_LOG_ASSERT(offset > 0); + + if (rsc_table) { + rsc_counter = g_hash_table_lookup(rsc_table, buffer); + if (rsc_counter == NULL) { + rsc_counter = calloc(1, sizeof(int)); + *rsc_counter = 0; + g_hash_table_insert(rsc_table, strdup(buffer), rsc_counter); + } + (*rsc_counter)++; + } + + if (active_table) { + GList *gIter2 = rsc->running_on; + + for (; gIter2 != NULL; gIter2 = gIter2->next) { + pe_node_t *node = (pe_node_t *) gIter2->data; + GHashTable *node_table = NULL; + + if (node->details->unclean == FALSE && node->details->online == FALSE && + pcmk_is_set(rsc->flags, pe_rsc_managed)) { + continue; + } + + node_table = g_hash_table_lookup(active_table, node->details->uname); + if (node_table == NULL) { + node_table = pcmk__strkey_table(free, free); + g_hash_table_insert(active_table, strdup(node->details->uname), node_table); + } + + active_counter = g_hash_table_lookup(node_table, buffer); + if (active_counter == NULL) { + active_counter = calloc(1, sizeof(int)); + *active_counter = 0; + g_hash_table_insert(node_table, strdup(buffer), active_counter); + } + (*active_counter)++; + } + } + } +} + +static void +destroy_node_table(gpointer data) +{ + GHashTable *node_table = data; + + if (node_table) { + g_hash_table_destroy(node_table); + } +} + +/*! + * \internal + * \deprecated This function will be removed in a future release + */ +void +print_rscs_brief(GList *rsc_list, const char *pre_text, long options, + void *print_data, gboolean print_all) +{ + GHashTable *rsc_table = pcmk__strkey_table(free, free); + GHashTable *active_table = pcmk__strkey_table(free, destroy_node_table); + GHashTableIter hash_iter; + char *type = NULL; + int *rsc_counter = NULL; + + get_rscs_brief(rsc_list, rsc_table, active_table); + + g_hash_table_iter_init(&hash_iter, rsc_table); + while (g_hash_table_iter_next(&hash_iter, (gpointer *)&type, (gpointer *)&rsc_counter)) { + GHashTableIter hash_iter2; + char *node_name = NULL; + GHashTable *node_table = NULL; + int active_counter_all = 0; + + g_hash_table_iter_init(&hash_iter2, active_table); + while (g_hash_table_iter_next(&hash_iter2, (gpointer *)&node_name, (gpointer *)&node_table)) { + int *active_counter = g_hash_table_lookup(node_table, type); + + if (active_counter == NULL || *active_counter == 0) { + continue; + + } else { + active_counter_all += *active_counter; + } + + if (options & pe_print_rsconly) { + node_name = NULL; + } + + if (options & pe_print_html) { + status_print("
    • \n"); + } + + if (print_all) { + status_print("%s%d/%d\t(%s):\tActive %s\n", pre_text ? pre_text : "", + active_counter ? *active_counter : 0, + rsc_counter ? *rsc_counter : 0, type, + active_counter && (*active_counter > 0) && node_name ? node_name : ""); + } else { + status_print("%s%d\t(%s):\tActive %s\n", pre_text ? pre_text : "", + active_counter ? *active_counter : 0, type, + active_counter && (*active_counter > 0) && node_name ? node_name : ""); + } + + if (options & pe_print_html) { + status_print("
    • \n"); + } + } + + if (print_all && active_counter_all == 0) { + if (options & pe_print_html) { + status_print("
    • \n"); + } + + status_print("%s%d/%d\t(%s):\tActive\n", pre_text ? pre_text : "", + active_counter_all, + rsc_counter ? *rsc_counter : 0, type); + + if (options & pe_print_html) { + status_print("
    • \n"); + } + } + } + + if (rsc_table) { + g_hash_table_destroy(rsc_table); + rsc_table = NULL; + } + if (active_table) { + g_hash_table_destroy(active_table); + active_table = NULL; + } +} + +int +pe__rscs_brief_output(pcmk__output_t *out, GList *rsc_list, uint32_t show_opts) +{ + GHashTable *rsc_table = pcmk__strkey_table(free, free); + GHashTable *active_table = pcmk__strkey_table(free, destroy_node_table); + GList *sorted_rscs; + int rc = pcmk_rc_no_output; + + get_rscs_brief(rsc_list, rsc_table, active_table); + + /* Make a list of the rsc_table keys so that it can be sorted. This is to make sure + * output order stays consistent between systems. + */ + sorted_rscs = g_hash_table_get_keys(rsc_table); + sorted_rscs = g_list_sort(sorted_rscs, (GCompareFunc) strcmp); + + for (GList *gIter = sorted_rscs; gIter; gIter = gIter->next) { + char *type = (char *) gIter->data; + int *rsc_counter = g_hash_table_lookup(rsc_table, type); + + GList *sorted_nodes = NULL; + int active_counter_all = 0; + + /* Also make a list of the active_table keys so it can be sorted. If there's + * more than one instance of a type of resource running, we need the nodes to + * be sorted to make sure output order stays consistent between systems. + */ + sorted_nodes = g_hash_table_get_keys(active_table); + sorted_nodes = g_list_sort(sorted_nodes, (GCompareFunc) pcmk__numeric_strcasecmp); + + for (GList *gIter2 = sorted_nodes; gIter2; gIter2 = gIter2->next) { + char *node_name = (char *) gIter2->data; + GHashTable *node_table = g_hash_table_lookup(active_table, node_name); + int *active_counter = NULL; + + if (node_table == NULL) { + continue; + } + + active_counter = g_hash_table_lookup(node_table, type); + + if (active_counter == NULL || *active_counter == 0) { + continue; + + } else { + active_counter_all += *active_counter; + } + + if (pcmk_is_set(show_opts, pcmk_show_rsc_only)) { + node_name = NULL; + } + + if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) { + out->list_item(out, NULL, "%d/%d\t(%s):\tActive %s", + *active_counter, + rsc_counter ? *rsc_counter : 0, type, + (*active_counter > 0) && node_name ? node_name : ""); + } else { + out->list_item(out, NULL, "%d\t(%s):\tActive %s", + *active_counter, type, + (*active_counter > 0) && node_name ? node_name : ""); + } + + rc = pcmk_rc_ok; + } + + if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs) && active_counter_all == 0) { + out->list_item(out, NULL, "%d/%d\t(%s):\tActive", + active_counter_all, + rsc_counter ? *rsc_counter : 0, type); + rc = pcmk_rc_ok; + } + + if (sorted_nodes) { + g_list_free(sorted_nodes); + } + } + + if (rsc_table) { + g_hash_table_destroy(rsc_table); + rsc_table = NULL; + } + if (active_table) { + g_hash_table_destroy(active_table); + active_table = NULL; + } + if (sorted_rscs) { + g_list_free(sorted_rscs); + } + + return rc; +} + +gboolean +pe__native_is_filtered(const pe_resource_t *rsc, GList *only_rsc, + gboolean check_parent) +{ + if (pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, pcmk__str_star_matches) || + pcmk__str_in_list(rsc->id, only_rsc, pcmk__str_star_matches)) { + return FALSE; + } else if (check_parent && rsc->parent) { + const pe_resource_t *up = pe__const_top_resource(rsc, true); + + return up->fns->is_filtered(up, only_rsc, FALSE); + } + + return TRUE; +} diff --git a/lib/pengine/pe_actions.c b/lib/pengine/pe_actions.c new file mode 100644 index 0000000..ed7f0da --- /dev/null +++ b/lib/pengine/pe_actions.c @@ -0,0 +1,1686 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include +#include +#include "pe_status_private.h" + +static void unpack_operation(pe_action_t *action, const xmlNode *xml_obj, + const pe_resource_t *container, + pe_working_set_t *data_set, guint interval_ms); + +static void +add_singleton(pe_working_set_t *data_set, pe_action_t *action) +{ + if (data_set->singletons == NULL) { + data_set->singletons = pcmk__strkey_table(NULL, NULL); + } + g_hash_table_insert(data_set->singletons, action->uuid, action); +} + +static pe_action_t * +lookup_singleton(pe_working_set_t *data_set, const char *action_uuid) +{ + if (data_set->singletons == NULL) { + return NULL; + } + return g_hash_table_lookup(data_set->singletons, action_uuid); +} + +/*! + * \internal + * \brief Find an existing action that matches arguments + * + * \param[in] key Action key to match + * \param[in] rsc Resource to match (if any) + * \param[in] node Node to match (if any) + * \param[in] data_set Cluster working set + * + * \return Existing action that matches arguments (or NULL if none) + */ +static pe_action_t * +find_existing_action(const char *key, const pe_resource_t *rsc, + const pe_node_t *node, const pe_working_set_t *data_set) +{ + GList *matches = NULL; + pe_action_t *action = NULL; + + /* When rsc is NULL, it would be quicker to check data_set->singletons, + * but checking all data_set->actions takes the node into account. + */ + matches = find_actions(((rsc == NULL)? data_set->actions : rsc->actions), + key, node); + if (matches == NULL) { + return NULL; + } + CRM_LOG_ASSERT(!pcmk__list_of_multiple(matches)); + + action = matches->data; + g_list_free(matches); + return action; +} + +static xmlNode * +find_rsc_op_entry_helper(const pe_resource_t *rsc, const char *key, + gboolean include_disabled) +{ + guint interval_ms = 0; + gboolean do_retry = TRUE; + char *local_key = NULL; + const char *name = NULL; + const char *interval_spec = NULL; + char *match_key = NULL; + xmlNode *op = NULL; + xmlNode *operation = NULL; + + retry: + for (operation = pcmk__xe_first_child(rsc->ops_xml); operation != NULL; + operation = pcmk__xe_next(operation)) { + + if (pcmk__str_eq((const char *)operation->name, "op", pcmk__str_none)) { + bool enabled = false; + + name = crm_element_value(operation, "name"); + interval_spec = crm_element_value(operation, XML_LRM_ATTR_INTERVAL); + if (!include_disabled && pcmk__xe_get_bool_attr(operation, "enabled", &enabled) == pcmk_rc_ok && + !enabled) { + continue; + } + + interval_ms = crm_parse_interval_spec(interval_spec); + match_key = pcmk__op_key(rsc->id, name, interval_ms); + if (pcmk__str_eq(key, match_key, pcmk__str_casei)) { + op = operation; + } + free(match_key); + + if (rsc->clone_name) { + match_key = pcmk__op_key(rsc->clone_name, name, interval_ms); + if (pcmk__str_eq(key, match_key, pcmk__str_casei)) { + op = operation; + } + free(match_key); + } + + if (op != NULL) { + free(local_key); + return op; + } + } + } + + free(local_key); + if (do_retry == FALSE) { + return NULL; + } + + do_retry = FALSE; + if (strstr(key, CRMD_ACTION_MIGRATE) || strstr(key, CRMD_ACTION_MIGRATED)) { + local_key = pcmk__op_key(rsc->id, "migrate", 0); + key = local_key; + goto retry; + + } else if (strstr(key, "_notify_")) { + local_key = pcmk__op_key(rsc->id, "notify", 0); + key = local_key; + goto retry; + } + + return NULL; +} + +xmlNode * +find_rsc_op_entry(const pe_resource_t *rsc, const char *key) +{ + return find_rsc_op_entry_helper(rsc, key, FALSE); +} + +/*! + * \internal + * \brief Create a new action object + * + * \param[in] key Action key + * \param[in] task Action name + * \param[in,out] rsc Resource that action is for (if any) + * \param[in] node Node that action is on (if any) + * \param[in] optional Whether action should be considered optional + * \param[in] for_graph Whether action should be recorded in transition graph + * \param[in,out] data_set Cluster working set + * + * \return Newly allocated action + * \note This function takes ownership of \p key. It is the caller's + * responsibility to free the return value with pe_free_action(). + */ +static pe_action_t * +new_action(char *key, const char *task, pe_resource_t *rsc, + const pe_node_t *node, bool optional, bool for_graph, + pe_working_set_t *data_set) +{ + pe_action_t *action = calloc(1, sizeof(pe_action_t)); + + CRM_ASSERT(action != NULL); + + action->rsc = rsc; + action->task = strdup(task); CRM_ASSERT(action->task != NULL); + action->uuid = key; + action->extra = pcmk__strkey_table(free, free); + action->meta = pcmk__strkey_table(free, free); + + if (node) { + action->node = pe__copy_node(node); + } + + if (pcmk__str_eq(task, CRM_OP_LRM_DELETE, pcmk__str_casei)) { + // Resource history deletion for a node can be done on the DC + pe__set_action_flags(action, pe_action_dc); + } + + pe__set_action_flags(action, pe_action_runnable); + if (optional) { + pe__set_action_flags(action, pe_action_optional); + } else { + pe__clear_action_flags(action, pe_action_optional); + } + + if (rsc != NULL) { + guint interval_ms = 0; + + action->op_entry = find_rsc_op_entry_helper(rsc, key, TRUE); + parse_op_key(key, NULL, NULL, &interval_ms); + unpack_operation(action, action->op_entry, rsc->container, data_set, + interval_ms); + } + + if (for_graph) { + pe_rsc_trace(rsc, "Created %s action %d (%s): %s for %s on %s", + (optional? "optional" : "required"), + data_set->action_id, key, task, + ((rsc == NULL)? "no resource" : rsc->id), + pe__node_name(node)); + action->id = data_set->action_id++; + + data_set->actions = g_list_prepend(data_set->actions, action); + if (rsc == NULL) { + add_singleton(data_set, action); + } else { + rsc->actions = g_list_prepend(rsc->actions, action); + } + } + return action; +} + +/*! + * \internal + * \brief Evaluate node attribute values for an action + * + * \param[in,out] action Action to unpack attributes for + * \param[in,out] data_set Cluster working set + */ +static void +unpack_action_node_attributes(pe_action_t *action, pe_working_set_t *data_set) +{ + if (!pcmk_is_set(action->flags, pe_action_have_node_attrs) + && (action->op_entry != NULL)) { + + pe_rule_eval_data_t rule_data = { + .node_hash = action->node->details->attrs, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + pe__set_action_flags(action, pe_action_have_node_attrs); + pe__unpack_dataset_nvpairs(action->op_entry, XML_TAG_ATTR_SETS, + &rule_data, action->extra, NULL, + FALSE, data_set); + } +} + +/*! + * \internal + * \brief Update an action's optional flag + * + * \param[in,out] action Action to update + * \param[in] optional Requested optional status + */ +static void +update_action_optional(pe_action_t *action, gboolean optional) +{ + // Force a non-recurring action to be optional if its resource is unmanaged + if ((action->rsc != NULL) && (action->node != NULL) + && !pcmk_is_set(action->flags, pe_action_pseudo) + && !pcmk_is_set(action->rsc->flags, pe_rsc_managed) + && (g_hash_table_lookup(action->meta, + XML_LRM_ATTR_INTERVAL_MS) == NULL)) { + pe_rsc_debug(action->rsc, "%s on %s is optional (%s is unmanaged)", + action->uuid, pe__node_name(action->node), + action->rsc->id); + pe__set_action_flags(action, pe_action_optional); + // We shouldn't clear runnable here because ... something + + // Otherwise require the action if requested + } else if (!optional) { + pe__clear_action_flags(action, pe_action_optional); + } +} + +static enum pe_quorum_policy +effective_quorum_policy(pe_resource_t *rsc, pe_working_set_t *data_set) +{ + enum pe_quorum_policy policy = data_set->no_quorum_policy; + + if (pcmk_is_set(data_set->flags, pe_flag_have_quorum)) { + policy = no_quorum_ignore; + + } else if (data_set->no_quorum_policy == no_quorum_demote) { + switch (rsc->role) { + case RSC_ROLE_PROMOTED: + case RSC_ROLE_UNPROMOTED: + if (rsc->next_role > RSC_ROLE_UNPROMOTED) { + pe__set_next_role(rsc, RSC_ROLE_UNPROMOTED, + "no-quorum-policy=demote"); + } + policy = no_quorum_ignore; + break; + default: + policy = no_quorum_stop; + break; + } + } + return policy; +} + +/*! + * \internal + * \brief Update a resource action's runnable flag + * + * \param[in,out] action Action to update + * \param[in] for_graph Whether action should be recorded in transition graph + * \param[in,out] data_set Cluster working set + * + * \note This may also schedule fencing if a stop is unrunnable. + */ +static void +update_resource_action_runnable(pe_action_t *action, bool for_graph, + pe_working_set_t *data_set) +{ + if (pcmk_is_set(action->flags, pe_action_pseudo)) { + return; + } + + if (action->node == NULL) { + pe_rsc_trace(action->rsc, "%s is unrunnable (unallocated)", + action->uuid); + pe__clear_action_flags(action, pe_action_runnable); + + } else if (!pcmk_is_set(action->flags, pe_action_dc) + && !(action->node->details->online) + && (!pe__is_guest_node(action->node) + || action->node->details->remote_requires_reset)) { + pe__clear_action_flags(action, pe_action_runnable); + do_crm_log((for_graph? LOG_WARNING: LOG_TRACE), + "%s on %s is unrunnable (node is offline)", + action->uuid, pe__node_name(action->node)); + if (pcmk_is_set(action->rsc->flags, pe_rsc_managed) + && for_graph + && pcmk__str_eq(action->task, CRMD_ACTION_STOP, pcmk__str_casei) + && !(action->node->details->unclean)) { + pe_fence_node(data_set, action->node, "stop is unrunnable", false); + } + + } else if (!pcmk_is_set(action->flags, pe_action_dc) + && action->node->details->pending) { + pe__clear_action_flags(action, pe_action_runnable); + do_crm_log((for_graph? LOG_WARNING: LOG_TRACE), + "Action %s on %s is unrunnable (node is pending)", + action->uuid, pe__node_name(action->node)); + + } else if (action->needs == rsc_req_nothing) { + pe_action_set_reason(action, NULL, TRUE); + if (pe__is_guest_node(action->node) + && !pe_can_fence(data_set, action->node)) { + /* An action that requires nothing usually does not require any + * fencing in order to be runnable. However, there is an exception: + * such an action cannot be completed if it is on a guest node whose + * host is unclean and cannot be fenced. + */ + pe_rsc_debug(action->rsc, "%s on %s is unrunnable " + "(node's host cannot be fenced)", + action->uuid, pe__node_name(action->node)); + pe__clear_action_flags(action, pe_action_runnable); + } else { + pe_rsc_trace(action->rsc, + "%s on %s does not require fencing or quorum", + action->uuid, pe__node_name(action->node)); + pe__set_action_flags(action, pe_action_runnable); + } + + } else { + switch (effective_quorum_policy(action->rsc, data_set)) { + case no_quorum_stop: + pe_rsc_debug(action->rsc, "%s on %s is unrunnable (no quorum)", + action->uuid, pe__node_name(action->node)); + pe__clear_action_flags(action, pe_action_runnable); + pe_action_set_reason(action, "no quorum", true); + break; + + case no_quorum_freeze: + if (!action->rsc->fns->active(action->rsc, TRUE) + || (action->rsc->next_role > action->rsc->role)) { + pe_rsc_debug(action->rsc, + "%s on %s is unrunnable (no quorum)", + action->uuid, pe__node_name(action->node)); + pe__clear_action_flags(action, pe_action_runnable); + pe_action_set_reason(action, "quorum freeze", true); + } + break; + + default: + //pe_action_set_reason(action, NULL, TRUE); + pe__set_action_flags(action, pe_action_runnable); + break; + } + } +} + +/*! + * \internal + * \brief Update a resource object's flags for a new action on it + * + * \param[in,out] rsc Resource that action is for (if any) + * \param[in] action New action + */ +static void +update_resource_flags_for_action(pe_resource_t *rsc, const pe_action_t *action) +{ + /* @COMPAT pe_rsc_starting and pe_rsc_stopping are not actually used + * within Pacemaker, and should be deprecated and eventually removed + */ + if (pcmk__str_eq(action->task, CRMD_ACTION_STOP, pcmk__str_casei)) { + pe__set_resource_flags(rsc, pe_rsc_stopping); + + } else if (pcmk__str_eq(action->task, CRMD_ACTION_START, pcmk__str_casei)) { + if (pcmk_is_set(action->flags, pe_action_runnable)) { + pe__set_resource_flags(rsc, pe_rsc_starting); + } else { + pe__clear_resource_flags(rsc, pe_rsc_starting); + } + } +} + +static bool +valid_stop_on_fail(const char *value) +{ + return !pcmk__strcase_any_of(value, "standby", "demote", "stop", NULL); +} + +static const char * +unpack_operation_on_fail(pe_action_t * action) +{ + const char *name = NULL; + const char *role = NULL; + const char *on_fail = NULL; + const char *interval_spec = NULL; + const char *value = g_hash_table_lookup(action->meta, XML_OP_ATTR_ON_FAIL); + + if (pcmk__str_eq(action->task, CRMD_ACTION_STOP, pcmk__str_casei) + && !valid_stop_on_fail(value)) { + + pcmk__config_err("Resetting '" XML_OP_ATTR_ON_FAIL "' for %s stop " + "action to default value because '%s' is not " + "allowed for stop", action->rsc->id, value); + return NULL; + + } else if (pcmk__str_eq(action->task, CRMD_ACTION_DEMOTE, pcmk__str_casei) && !value) { + // demote on_fail defaults to monitor value for promoted role if present + xmlNode *operation = NULL; + + CRM_CHECK(action->rsc != NULL, return NULL); + + for (operation = pcmk__xe_first_child(action->rsc->ops_xml); + (operation != NULL) && (value == NULL); + operation = pcmk__xe_next(operation)) { + bool enabled = false; + + if (!pcmk__str_eq((const char *)operation->name, "op", pcmk__str_none)) { + continue; + } + name = crm_element_value(operation, "name"); + role = crm_element_value(operation, "role"); + on_fail = crm_element_value(operation, XML_OP_ATTR_ON_FAIL); + interval_spec = crm_element_value(operation, XML_LRM_ATTR_INTERVAL); + if (!on_fail) { + continue; + } else if (pcmk__xe_get_bool_attr(operation, "enabled", &enabled) == pcmk_rc_ok && !enabled) { + continue; + } else if (!pcmk__str_eq(name, "monitor", pcmk__str_casei) + || !pcmk__strcase_any_of(role, RSC_ROLE_PROMOTED_S, + RSC_ROLE_PROMOTED_LEGACY_S, + NULL)) { + continue; + } else if (crm_parse_interval_spec(interval_spec) == 0) { + continue; + } else if (pcmk__str_eq(on_fail, "demote", pcmk__str_casei)) { + continue; + } + + value = on_fail; + } + } else if (pcmk__str_eq(action->task, CRM_OP_LRM_DELETE, pcmk__str_casei)) { + value = "ignore"; + + } else if (pcmk__str_eq(value, "demote", pcmk__str_casei)) { + name = crm_element_value(action->op_entry, "name"); + role = crm_element_value(action->op_entry, "role"); + interval_spec = crm_element_value(action->op_entry, + XML_LRM_ATTR_INTERVAL); + + if (!pcmk__str_eq(name, CRMD_ACTION_PROMOTE, pcmk__str_casei) + && (!pcmk__str_eq(name, CRMD_ACTION_STATUS, pcmk__str_casei) + || !pcmk__strcase_any_of(role, RSC_ROLE_PROMOTED_S, + RSC_ROLE_PROMOTED_LEGACY_S, NULL) + || (crm_parse_interval_spec(interval_spec) == 0))) { + pcmk__config_err("Resetting '" XML_OP_ATTR_ON_FAIL "' for %s %s " + "action to default value because 'demote' is not " + "allowed for it", action->rsc->id, name); + return NULL; + } + } + + return value; +} + +static int +unpack_timeout(const char *value) +{ + int timeout_ms = crm_get_msec(value); + + if (timeout_ms < 0) { + timeout_ms = crm_get_msec(CRM_DEFAULT_OP_TIMEOUT_S); + } + return timeout_ms; +} + +// true if value contains valid, non-NULL interval origin for recurring op +static bool +unpack_interval_origin(const char *value, const xmlNode *xml_obj, + guint interval_ms, const crm_time_t *now, + long long *start_delay) +{ + long long result = 0; + guint interval_sec = interval_ms / 1000; + crm_time_t *origin = NULL; + + // Ignore unspecified values and non-recurring operations + if ((value == NULL) || (interval_ms == 0) || (now == NULL)) { + return false; + } + + // Parse interval origin from text + origin = crm_time_new(value); + if (origin == NULL) { + pcmk__config_err("Ignoring '" XML_OP_ATTR_ORIGIN "' for operation " + "'%s' because '%s' is not valid", + (ID(xml_obj)? ID(xml_obj) : "(missing ID)"), value); + return false; + } + + // Get seconds since origin (negative if origin is in the future) + result = crm_time_get_seconds(now) - crm_time_get_seconds(origin); + crm_time_free(origin); + + // Calculate seconds from closest interval to now + result = result % interval_sec; + + // Calculate seconds remaining until next interval + result = ((result <= 0)? 0 : interval_sec) - result; + crm_info("Calculated a start delay of %llds for operation '%s'", + result, + (ID(xml_obj)? ID(xml_obj) : "(unspecified)")); + + if (start_delay != NULL) { + *start_delay = result * 1000; // milliseconds + } + return true; +} + +static int +unpack_start_delay(const char *value, GHashTable *meta) +{ + int start_delay = 0; + + if (value != NULL) { + start_delay = crm_get_msec(value); + + if (start_delay < 0) { + start_delay = 0; + } + + if (meta) { + g_hash_table_replace(meta, strdup(XML_OP_ATTR_START_DELAY), + pcmk__itoa(start_delay)); + } + } + + return start_delay; +} + +static xmlNode * +find_min_interval_mon(pe_resource_t * rsc, gboolean include_disabled) +{ + guint interval_ms = 0; + guint min_interval_ms = G_MAXUINT; + const char *name = NULL; + const char *interval_spec = NULL; + xmlNode *op = NULL; + xmlNode *operation = NULL; + + for (operation = pcmk__xe_first_child(rsc->ops_xml); + operation != NULL; + operation = pcmk__xe_next(operation)) { + + if (pcmk__str_eq((const char *)operation->name, "op", pcmk__str_none)) { + bool enabled = false; + + name = crm_element_value(operation, "name"); + interval_spec = crm_element_value(operation, XML_LRM_ATTR_INTERVAL); + if (!include_disabled && pcmk__xe_get_bool_attr(operation, "enabled", &enabled) == pcmk_rc_ok && + !enabled) { + continue; + } + + if (!pcmk__str_eq(name, RSC_STATUS, pcmk__str_casei)) { + continue; + } + + interval_ms = crm_parse_interval_spec(interval_spec); + + if (interval_ms && (interval_ms < min_interval_ms)) { + min_interval_ms = interval_ms; + op = operation; + } + } + } + + return op; +} + +/*! + * \brief Unpack operation XML into an action structure + * + * Unpack an operation's meta-attributes (normalizing the interval, timeout, + * and start delay values as integer milliseconds), requirements, and + * failure policy. + * + * \param[in,out] action Action to unpack into + * \param[in] xml_obj Operation XML (or NULL if all defaults) + * \param[in] container Resource that contains affected resource, if any + * \param[in,out] data_set Cluster state + * \param[in] interval_ms How frequently to perform the operation + */ +static void +unpack_operation(pe_action_t *action, const xmlNode *xml_obj, + const pe_resource_t *container, + pe_working_set_t *data_set, guint interval_ms) +{ + int timeout_ms = 0; + const char *value = NULL; + bool is_probe = false; + + pe_rsc_eval_data_t rsc_rule_data = { + .standard = crm_element_value(action->rsc->xml, XML_AGENT_ATTR_CLASS), + .provider = crm_element_value(action->rsc->xml, XML_AGENT_ATTR_PROVIDER), + .agent = crm_element_value(action->rsc->xml, XML_EXPR_ATTR_TYPE) + }; + + pe_op_eval_data_t op_rule_data = { + .op_name = action->task, + .interval = interval_ms + }; + + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = &rsc_rule_data, + .op_data = &op_rule_data + }; + + CRM_CHECK(action && action->rsc, return); + + is_probe = pcmk_is_probe(action->task, interval_ms); + + // Cluster-wide + pe__unpack_dataset_nvpairs(data_set->op_defaults, XML_TAG_META_SETS, &rule_data, + action->meta, NULL, FALSE, data_set); + + // Determine probe default timeout differently + if (is_probe) { + xmlNode *min_interval_mon = find_min_interval_mon(action->rsc, FALSE); + + if (min_interval_mon) { + value = crm_element_value(min_interval_mon, XML_ATTR_TIMEOUT); + if (value) { + crm_trace("\t%s: Setting default timeout to minimum-interval " + "monitor's timeout '%s'", action->uuid, value); + g_hash_table_replace(action->meta, strdup(XML_ATTR_TIMEOUT), + strdup(value)); + } + } + } + + if (xml_obj) { + xmlAttrPtr xIter = NULL; + + // take precedence over defaults + pe__unpack_dataset_nvpairs(xml_obj, XML_TAG_META_SETS, &rule_data, + action->meta, NULL, TRUE, data_set); + + /* Anything set as an XML property has highest precedence. + * This ensures we use the name and interval from the tag. + */ + for (xIter = xml_obj->properties; xIter; xIter = xIter->next) { + const char *prop_name = (const char *)xIter->name; + const char *prop_value = crm_element_value(xml_obj, prop_name); + + g_hash_table_replace(action->meta, strdup(prop_name), strdup(prop_value)); + } + } + + g_hash_table_remove(action->meta, "id"); + + // Normalize interval to milliseconds + if (interval_ms > 0) { + g_hash_table_replace(action->meta, strdup(XML_LRM_ATTR_INTERVAL), + crm_strdup_printf("%u", interval_ms)); + } else { + g_hash_table_remove(action->meta, XML_LRM_ATTR_INTERVAL); + } + + /* + * Timeout order of precedence: + * 1. pcmk_monitor_timeout (if rsc has pcmk_ra_cap_fence_params + * and task is start or a probe; pcmk_monitor_timeout works + * by default for a recurring monitor) + * 2. explicit op timeout on the primitive + * 3. default op timeout + * a. if probe, then min-interval monitor's timeout + * b. else, in XML_CIB_TAG_OPCONFIG + * 4. CRM_DEFAULT_OP_TIMEOUT_S + * + * #1 overrides general rule of XML property having highest + * precedence. + */ + if (pcmk_is_set(pcmk_get_ra_caps(rsc_rule_data.standard), + pcmk_ra_cap_fence_params) + && (pcmk__str_eq(action->task, RSC_START, pcmk__str_casei) + || is_probe)) { + + GHashTable *params = pe_rsc_params(action->rsc, action->node, data_set); + + value = g_hash_table_lookup(params, "pcmk_monitor_timeout"); + + if (value) { + crm_trace("\t%s: Setting timeout to pcmk_monitor_timeout '%s', " + "overriding default", action->uuid, value); + g_hash_table_replace(action->meta, strdup(XML_ATTR_TIMEOUT), + strdup(value)); + } + } + + // Normalize timeout to positive milliseconds + value = g_hash_table_lookup(action->meta, XML_ATTR_TIMEOUT); + timeout_ms = unpack_timeout(value); + g_hash_table_replace(action->meta, strdup(XML_ATTR_TIMEOUT), + pcmk__itoa(timeout_ms)); + + if (!pcmk__strcase_any_of(action->task, RSC_START, RSC_PROMOTE, NULL)) { + action->needs = rsc_req_nothing; + value = "nothing (not start or promote)"; + + } else if (pcmk_is_set(action->rsc->flags, pe_rsc_needs_fencing)) { + action->needs = rsc_req_stonith; + value = "fencing"; + + } else if (pcmk_is_set(action->rsc->flags, pe_rsc_needs_quorum)) { + action->needs = rsc_req_quorum; + value = "quorum"; + + } else { + action->needs = rsc_req_nothing; + value = "nothing"; + } + pe_rsc_trace(action->rsc, "%s requires %s", action->uuid, value); + + value = unpack_operation_on_fail(action); + + if (value == NULL) { + + } else if (pcmk__str_eq(value, "block", pcmk__str_casei)) { + action->on_fail = action_fail_block; + g_hash_table_insert(action->meta, strdup(XML_OP_ATTR_ON_FAIL), strdup("block")); + value = "block"; // The above could destroy the original string + + } else if (pcmk__str_eq(value, "fence", pcmk__str_casei)) { + action->on_fail = action_fail_fence; + value = "node fencing"; + + if (!pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + pcmk__config_err("Resetting '" XML_OP_ATTR_ON_FAIL "' for " + "operation '%s' to 'stop' because 'fence' is not " + "valid when fencing is disabled", action->uuid); + action->on_fail = action_fail_stop; + action->fail_role = RSC_ROLE_STOPPED; + value = "stop resource"; + } + + } else if (pcmk__str_eq(value, "standby", pcmk__str_casei)) { + action->on_fail = action_fail_standby; + value = "node standby"; + + } else if (pcmk__strcase_any_of(value, "ignore", PCMK__VALUE_NOTHING, + NULL)) { + action->on_fail = action_fail_ignore; + value = "ignore"; + + } else if (pcmk__str_eq(value, "migrate", pcmk__str_casei)) { + action->on_fail = action_fail_migrate; + value = "force migration"; + + } else if (pcmk__str_eq(value, "stop", pcmk__str_casei)) { + action->on_fail = action_fail_stop; + action->fail_role = RSC_ROLE_STOPPED; + value = "stop resource"; + + } else if (pcmk__str_eq(value, "restart", pcmk__str_casei)) { + action->on_fail = action_fail_recover; + value = "restart (and possibly migrate)"; + + } else if (pcmk__str_eq(value, "restart-container", pcmk__str_casei)) { + if (container) { + action->on_fail = action_fail_restart_container; + value = "restart container (and possibly migrate)"; + + } else { + value = NULL; + } + + } else if (pcmk__str_eq(value, "demote", pcmk__str_casei)) { + action->on_fail = action_fail_demote; + value = "demote instance"; + + } else { + pe_err("Resource %s: Unknown failure type (%s)", action->rsc->id, value); + value = NULL; + } + + /* defaults */ + if (value == NULL && container) { + action->on_fail = action_fail_restart_container; + value = "restart container (and possibly migrate) (default)"; + + /* For remote nodes, ensure that any failure that results in dropping an + * active connection to the node results in fencing of the node. + * + * There are only two action failures that don't result in fencing. + * 1. probes - probe failures are expected. + * 2. start - a start failure indicates that an active connection does not already + * exist. The user can set op on-fail=fence if they really want to fence start + * failures. */ + } else if (((value == NULL) || !pcmk_is_set(action->rsc->flags, pe_rsc_managed)) + && pe__resource_is_remote_conn(action->rsc, data_set) + && !(pcmk__str_eq(action->task, CRMD_ACTION_STATUS, pcmk__str_casei) + && (interval_ms == 0)) + && !pcmk__str_eq(action->task, CRMD_ACTION_START, pcmk__str_casei)) { + + if (!pcmk_is_set(action->rsc->flags, pe_rsc_managed)) { + action->on_fail = action_fail_stop; + action->fail_role = RSC_ROLE_STOPPED; + value = "stop unmanaged remote node (enforcing default)"; + + } else { + if (pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + value = "fence remote node (default)"; + } else { + value = "recover remote node connection (default)"; + } + + if (action->rsc->remote_reconnect_ms) { + action->fail_role = RSC_ROLE_STOPPED; + } + action->on_fail = action_fail_reset_remote; + } + + } else if (value == NULL && pcmk__str_eq(action->task, CRMD_ACTION_STOP, pcmk__str_casei)) { + if (pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + action->on_fail = action_fail_fence; + value = "resource fence (default)"; + + } else { + action->on_fail = action_fail_block; + value = "resource block (default)"; + } + + } else if (value == NULL) { + action->on_fail = action_fail_recover; + value = "restart (and possibly migrate) (default)"; + } + + pe_rsc_trace(action->rsc, "%s failure handling: %s", + action->uuid, value); + + value = NULL; + if (xml_obj != NULL) { + value = g_hash_table_lookup(action->meta, "role_after_failure"); + if (value) { + pe_warn_once(pe_wo_role_after, + "Support for role_after_failure is deprecated and will be removed in a future release"); + } + } + if (value != NULL && action->fail_role == RSC_ROLE_UNKNOWN) { + action->fail_role = text2role(value); + } + /* defaults */ + if (action->fail_role == RSC_ROLE_UNKNOWN) { + if (pcmk__str_eq(action->task, CRMD_ACTION_PROMOTE, pcmk__str_casei)) { + action->fail_role = RSC_ROLE_UNPROMOTED; + } else { + action->fail_role = RSC_ROLE_STARTED; + } + } + pe_rsc_trace(action->rsc, "%s failure results in: %s", + action->uuid, role2text(action->fail_role)); + + value = g_hash_table_lookup(action->meta, XML_OP_ATTR_START_DELAY); + if (value) { + unpack_start_delay(value, action->meta); + } else { + long long start_delay = 0; + + value = g_hash_table_lookup(action->meta, XML_OP_ATTR_ORIGIN); + if (unpack_interval_origin(value, xml_obj, interval_ms, data_set->now, + &start_delay)) { + g_hash_table_replace(action->meta, strdup(XML_OP_ATTR_START_DELAY), + crm_strdup_printf("%lld", start_delay)); + } + } +} + +/*! + * \brief Create or update an action object + * + * \param[in,out] rsc Resource that action is for (if any) + * \param[in,out] key Action key (must be non-NULL) + * \param[in] task Action name (must be non-NULL) + * \param[in] on_node Node that action is on (if any) + * \param[in] optional Whether action should be considered optional + * \param[in] save_action Whether action should be recorded in transition graph + * \param[in,out] data_set Cluster working set + * + * \return Action object corresponding to arguments + * \note This function takes ownership of (and might free) \p key. If + * \p save_action is true, \p data_set will own the returned action, + * otherwise it is the caller's responsibility to free the return value + * with pe_free_action(). + */ +pe_action_t * +custom_action(pe_resource_t *rsc, char *key, const char *task, + const pe_node_t *on_node, gboolean optional, gboolean save_action, + pe_working_set_t *data_set) +{ + pe_action_t *action = NULL; + + CRM_ASSERT((key != NULL) && (task != NULL) && (data_set != NULL)); + + if (save_action) { + action = find_existing_action(key, rsc, on_node, data_set); + } + + if (action == NULL) { + action = new_action(key, task, rsc, on_node, optional, save_action, + data_set); + } else { + free(key); + } + + update_action_optional(action, optional); + + if (rsc != NULL) { + if (action->node != NULL) { + unpack_action_node_attributes(action, data_set); + } + + update_resource_action_runnable(action, save_action, data_set); + + if (save_action) { + update_resource_flags_for_action(rsc, action); + } + } + + return action; +} + +pe_action_t * +get_pseudo_op(const char *name, pe_working_set_t * data_set) +{ + pe_action_t *op = lookup_singleton(data_set, name); + + if (op == NULL) { + op = custom_action(NULL, strdup(name), name, NULL, TRUE, TRUE, data_set); + pe__set_action_flags(op, pe_action_pseudo|pe_action_runnable); + } + return op; +} + +static GList * +find_unfencing_devices(GList *candidates, GList *matches) +{ + for (GList *gIter = candidates; gIter != NULL; gIter = gIter->next) { + pe_resource_t *candidate = gIter->data; + + if (candidate->children != NULL) { + matches = find_unfencing_devices(candidate->children, matches); + + } else if (!pcmk_is_set(candidate->flags, pe_rsc_fence_device)) { + continue; + + } else if (pcmk_is_set(candidate->flags, pe_rsc_needs_unfencing)) { + matches = g_list_prepend(matches, candidate); + + } else if (pcmk__str_eq(g_hash_table_lookup(candidate->meta, + PCMK_STONITH_PROVIDES), + PCMK__VALUE_UNFENCING, + pcmk__str_casei)) { + matches = g_list_prepend(matches, candidate); + } + } + return matches; +} + +static int +node_priority_fencing_delay(const pe_node_t *node, + const pe_working_set_t *data_set) +{ + int member_count = 0; + int online_count = 0; + int top_priority = 0; + int lowest_priority = 0; + GList *gIter = NULL; + + // `priority-fencing-delay` is disabled + if (data_set->priority_fencing_delay <= 0) { + return 0; + } + + /* No need to request a delay if the fencing target is not a normal cluster + * member, for example if it's a remote node or a guest node. */ + if (node->details->type != node_member) { + return 0; + } + + // No need to request a delay if the fencing target is in our partition + if (node->details->online) { + return 0; + } + + for (gIter = data_set->nodes; gIter != NULL; gIter = gIter->next) { + pe_node_t *n = gIter->data; + + if (n->details->type != node_member) { + continue; + } + + member_count ++; + + if (n->details->online) { + online_count++; + } + + if (member_count == 1 + || n->details->priority > top_priority) { + top_priority = n->details->priority; + } + + if (member_count == 1 + || n->details->priority < lowest_priority) { + lowest_priority = n->details->priority; + } + } + + // No need to delay if we have more than half of the cluster members + if (online_count > member_count / 2) { + return 0; + } + + /* All the nodes have equal priority. + * Any configured corresponding `pcmk_delay_base/max` will be applied. */ + if (lowest_priority == top_priority) { + return 0; + } + + if (node->details->priority < top_priority) { + return 0; + } + + return data_set->priority_fencing_delay; +} + +pe_action_t * +pe_fence_op(pe_node_t *node, const char *op, bool optional, + const char *reason, bool priority_delay, pe_working_set_t *data_set) +{ + char *op_key = NULL; + pe_action_t *stonith_op = NULL; + + if(op == NULL) { + op = data_set->stonith_action; + } + + op_key = crm_strdup_printf("%s-%s-%s", CRM_OP_FENCE, node->details->uname, op); + + stonith_op = lookup_singleton(data_set, op_key); + if(stonith_op == NULL) { + stonith_op = custom_action(NULL, op_key, CRM_OP_FENCE, node, TRUE, TRUE, data_set); + + add_hash_param(stonith_op->meta, XML_LRM_ATTR_TARGET, node->details->uname); + add_hash_param(stonith_op->meta, XML_LRM_ATTR_TARGET_UUID, node->details->id); + add_hash_param(stonith_op->meta, "stonith_action", op); + + if (pcmk_is_set(data_set->flags, pe_flag_enable_unfencing)) { + /* Extra work to detect device changes + */ + GString *digests_all = g_string_sized_new(1024); + GString *digests_secure = g_string_sized_new(1024); + + GList *matches = find_unfencing_devices(data_set->resources, NULL); + + char *key = NULL; + char *value = NULL; + + for (GList *gIter = matches; gIter != NULL; gIter = gIter->next) { + pe_resource_t *match = gIter->data; + const char *agent = g_hash_table_lookup(match->meta, + XML_ATTR_TYPE); + op_digest_cache_t *data = NULL; + + data = pe__compare_fencing_digest(match, agent, node, data_set); + if(data->rc == RSC_DIGEST_ALL) { + optional = FALSE; + crm_notice("Unfencing node %s because the definition of " + "%s changed", pe__node_name(node), match->id); + if (!pcmk__is_daemon && data_set->priv != NULL) { + pcmk__output_t *out = data_set->priv; + + out->info(out, + "notice: Unfencing node %s because the " + "definition of %s changed", + pe__node_name(node), match->id); + } + } + + pcmk__g_strcat(digests_all, + match->id, ":", agent, ":", + data->digest_all_calc, ",", NULL); + pcmk__g_strcat(digests_secure, + match->id, ":", agent, ":", + data->digest_secure_calc, ",", NULL); + } + key = strdup(XML_OP_ATTR_DIGESTS_ALL); + value = strdup((const char *) digests_all->str); + CRM_ASSERT((key != NULL) && (value != NULL)); + g_hash_table_insert(stonith_op->meta, key, value); + g_string_free(digests_all, TRUE); + + key = strdup(XML_OP_ATTR_DIGESTS_SECURE); + value = strdup((const char *) digests_secure->str); + CRM_ASSERT((key != NULL) && (value != NULL)); + g_hash_table_insert(stonith_op->meta, key, value); + g_string_free(digests_secure, TRUE); + } + + } else { + free(op_key); + } + + if (data_set->priority_fencing_delay > 0 + + /* It's a suitable case where `priority-fencing-delay` applies. + * At least add `priority-fencing-delay` field as an indicator. */ + && (priority_delay + + /* The priority delay needs to be recalculated if this function has + * been called by schedule_fencing_and_shutdowns() after node + * priority has already been calculated by native_add_running(). + */ + || g_hash_table_lookup(stonith_op->meta, + XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY) != NULL)) { + + /* Add `priority-fencing-delay` to the fencing op even if it's 0 for + * the targeting node. So that it takes precedence over any possible + * `pcmk_delay_base/max`. + */ + char *delay_s = pcmk__itoa(node_priority_fencing_delay(node, data_set)); + + g_hash_table_insert(stonith_op->meta, + strdup(XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY), + delay_s); + } + + if(optional == FALSE && pe_can_fence(data_set, node)) { + pe__clear_action_flags(stonith_op, pe_action_optional); + pe_action_set_reason(stonith_op, reason, false); + + } else if(reason && stonith_op->reason == NULL) { + stonith_op->reason = strdup(reason); + } + + return stonith_op; +} + +void +pe_free_action(pe_action_t * action) +{ + if (action == NULL) { + return; + } + g_list_free_full(action->actions_before, free); /* pe_action_wrapper_t* */ + g_list_free_full(action->actions_after, free); /* pe_action_wrapper_t* */ + if (action->extra) { + g_hash_table_destroy(action->extra); + } + if (action->meta) { + g_hash_table_destroy(action->meta); + } + free(action->cancel_task); + free(action->reason); + free(action->task); + free(action->uuid); + free(action->node); + free(action); +} + +int +pe_get_configured_timeout(pe_resource_t *rsc, const char *action, pe_working_set_t *data_set) +{ + xmlNode *child = NULL; + GHashTable *action_meta = NULL; + const char *timeout_spec = NULL; + int timeout_ms = 0; + + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + for (child = first_named_child(rsc->ops_xml, XML_ATTR_OP); + child != NULL; child = crm_next_same_xml(child)) { + if (pcmk__str_eq(action, crm_element_value(child, XML_NVPAIR_ATTR_NAME), + pcmk__str_casei)) { + timeout_spec = crm_element_value(child, XML_ATTR_TIMEOUT); + break; + } + } + + if (timeout_spec == NULL && data_set->op_defaults) { + action_meta = pcmk__strkey_table(free, free); + pe__unpack_dataset_nvpairs(data_set->op_defaults, XML_TAG_META_SETS, + &rule_data, action_meta, NULL, FALSE, data_set); + timeout_spec = g_hash_table_lookup(action_meta, XML_ATTR_TIMEOUT); + } + + // @TODO check meta-attributes + // @TODO maybe use min-interval monitor timeout as default for monitors + + timeout_ms = crm_get_msec(timeout_spec); + if (timeout_ms < 0) { + timeout_ms = crm_get_msec(CRM_DEFAULT_OP_TIMEOUT_S); + } + + if (action_meta != NULL) { + g_hash_table_destroy(action_meta); + } + return timeout_ms; +} + +enum action_tasks +get_complex_task(const pe_resource_t *rsc, const char *name) +{ + enum action_tasks task = text2task(name); + + if ((rsc != NULL) && (rsc->variant == pe_native)) { + switch (task) { + case stopped_rsc: + case started_rsc: + case action_demoted: + case action_promoted: + crm_trace("Folding %s back into its atomic counterpart for %s", + name, rsc->id); + --task; + break; + default: + break; + } + } + return task; +} + +/*! + * \internal + * \brief Find first matching action in a list + * + * \param[in] input List of actions to search + * \param[in] uuid If not NULL, action must have this UUID + * \param[in] task If not NULL, action must have this action name + * \param[in] on_node If not NULL, action must be on this node + * + * \return First action in list that matches criteria, or NULL if none + */ +pe_action_t * +find_first_action(const GList *input, const char *uuid, const char *task, + const pe_node_t *on_node) +{ + CRM_CHECK(uuid || task, return NULL); + + for (const GList *gIter = input; gIter != NULL; gIter = gIter->next) { + pe_action_t *action = (pe_action_t *) gIter->data; + + if (uuid != NULL && !pcmk__str_eq(uuid, action->uuid, pcmk__str_casei)) { + continue; + + } else if (task != NULL && !pcmk__str_eq(task, action->task, pcmk__str_casei)) { + continue; + + } else if (on_node == NULL) { + return action; + + } else if (action->node == NULL) { + continue; + + } else if (on_node->details == action->node->details) { + return action; + } + } + + return NULL; +} + +GList * +find_actions(GList *input, const char *key, const pe_node_t *on_node) +{ + GList *gIter = input; + GList *result = NULL; + + CRM_CHECK(key != NULL, return NULL); + + for (; gIter != NULL; gIter = gIter->next) { + pe_action_t *action = (pe_action_t *) gIter->data; + + if (!pcmk__str_eq(key, action->uuid, pcmk__str_casei)) { + continue; + + } else if (on_node == NULL) { + crm_trace("Action %s matches (ignoring node)", key); + result = g_list_prepend(result, action); + + } else if (action->node == NULL) { + crm_trace("Action %s matches (unallocated, assigning to %s)", + key, pe__node_name(on_node)); + + action->node = pe__copy_node(on_node); + result = g_list_prepend(result, action); + + } else if (on_node->details == action->node->details) { + crm_trace("Action %s on %s matches", key, pe__node_name(on_node)); + result = g_list_prepend(result, action); + } + } + + return result; +} + +GList * +find_actions_exact(GList *input, const char *key, const pe_node_t *on_node) +{ + GList *result = NULL; + + CRM_CHECK(key != NULL, return NULL); + + if (on_node == NULL) { + return NULL; + } + + for (GList *gIter = input; gIter != NULL; gIter = gIter->next) { + pe_action_t *action = (pe_action_t *) gIter->data; + + if ((action->node != NULL) + && pcmk__str_eq(key, action->uuid, pcmk__str_casei) + && pcmk__str_eq(on_node->details->id, action->node->details->id, + pcmk__str_casei)) { + + crm_trace("Action %s on %s matches", key, pe__node_name(on_node)); + result = g_list_prepend(result, action); + } + } + + return result; +} + +/*! + * \brief Find all actions of given type for a resource + * + * \param[in] rsc Resource to search + * \param[in] node Find only actions scheduled on this node + * \param[in] task Action name to search for + * \param[in] require_node If TRUE, NULL node or action node will not match + * + * \return List of actions found (or NULL if none) + * \note If node is not NULL and require_node is FALSE, matching actions + * without a node will be assigned to node. + */ +GList * +pe__resource_actions(const pe_resource_t *rsc, const pe_node_t *node, + const char *task, bool require_node) +{ + GList *result = NULL; + char *key = pcmk__op_key(rsc->id, task, 0); + + if (require_node) { + result = find_actions_exact(rsc->actions, key, node); + } else { + result = find_actions(rsc->actions, key, node); + } + free(key); + return result; +} + +/*! + * \internal + * \brief Create an action reason string based on the action itself + * + * \param[in] action Action to create reason string for + * \param[in] flag Action flag that was cleared + * + * \return Newly allocated string suitable for use as action reason + * \note It is the caller's responsibility to free() the result. + */ +char * +pe__action2reason(const pe_action_t *action, enum pe_action_flags flag) +{ + const char *change = NULL; + + switch (flag) { + case pe_action_runnable: + case pe_action_migrate_runnable: + change = "unrunnable"; + break; + case pe_action_optional: + change = "required"; + break; + default: + // Bug: caller passed unsupported flag + CRM_CHECK(change != NULL, change = ""); + break; + } + return crm_strdup_printf("%s%s%s %s", change, + (action->rsc == NULL)? "" : " ", + (action->rsc == NULL)? "" : action->rsc->id, + action->task); +} + +void pe_action_set_reason(pe_action_t *action, const char *reason, bool overwrite) +{ + if (action->reason != NULL && overwrite) { + pe_rsc_trace(action->rsc, "Changing %s reason from '%s' to '%s'", + action->uuid, action->reason, pcmk__s(reason, "(none)")); + } else if (action->reason == NULL) { + pe_rsc_trace(action->rsc, "Set %s reason to '%s'", + action->uuid, pcmk__s(reason, "(none)")); + } else { + // crm_assert(action->reason != NULL && !overwrite); + return; + } + + pcmk__str_update(&action->reason, reason); +} + +/*! + * \internal + * \brief Create an action to clear a resource's history from CIB + * + * \param[in,out] rsc Resource to clear + * \param[in] node Node to clear history on + * \param[in,out] data_set Cluster working set + * + * \return New action to clear resource history + */ +pe_action_t * +pe__clear_resource_history(pe_resource_t *rsc, const pe_node_t *node, + pe_working_set_t *data_set) +{ + char *key = NULL; + + CRM_ASSERT(rsc && node); + key = pcmk__op_key(rsc->id, CRM_OP_LRM_DELETE, 0); + return custom_action(rsc, key, CRM_OP_LRM_DELETE, node, FALSE, TRUE, + data_set); +} + +#define sort_return(an_int, why) do { \ + free(a_uuid); \ + free(b_uuid); \ + crm_trace("%s (%d) %c %s (%d) : %s", \ + a_xml_id, a_call_id, an_int>0?'>':an_int<0?'<':'=', \ + b_xml_id, b_call_id, why); \ + return an_int; \ + } while(0) + +int +pe__is_newer_op(const xmlNode *xml_a, const xmlNode *xml_b, + bool same_node_default) +{ + int a_call_id = -1; + int b_call_id = -1; + + char *a_uuid = NULL; + char *b_uuid = NULL; + + const char *a_xml_id = crm_element_value(xml_a, XML_ATTR_ID); + const char *b_xml_id = crm_element_value(xml_b, XML_ATTR_ID); + + const char *a_node = crm_element_value(xml_a, XML_LRM_ATTR_TARGET); + const char *b_node = crm_element_value(xml_b, XML_LRM_ATTR_TARGET); + bool same_node = true; + + /* @COMPAT The on_node attribute was added to last_failure as of 1.1.13 (via + * 8b3ca1c) and the other entries as of 1.1.12 (via 0b07b5c). + * + * In case that any of the lrm_rsc_op entries doesn't have on_node + * attribute, we need to explicitly tell whether the two operations are on + * the same node. + */ + if (a_node == NULL || b_node == NULL) { + same_node = same_node_default; + + } else { + same_node = pcmk__str_eq(a_node, b_node, pcmk__str_casei); + } + + if (same_node && pcmk__str_eq(a_xml_id, b_xml_id, pcmk__str_none)) { + /* We have duplicate lrm_rsc_op entries in the status + * section which is unlikely to be a good thing + * - we can handle it easily enough, but we need to get + * to the bottom of why it's happening. + */ + pe_err("Duplicate lrm_rsc_op entries named %s", a_xml_id); + sort_return(0, "duplicate"); + } + + crm_element_value_int(xml_a, XML_LRM_ATTR_CALLID, &a_call_id); + crm_element_value_int(xml_b, XML_LRM_ATTR_CALLID, &b_call_id); + + if (a_call_id == -1 && b_call_id == -1) { + /* both are pending ops so it doesn't matter since + * stops are never pending + */ + sort_return(0, "pending"); + + } else if (same_node && a_call_id >= 0 && a_call_id < b_call_id) { + sort_return(-1, "call id"); + + } else if (same_node && b_call_id >= 0 && a_call_id > b_call_id) { + sort_return(1, "call id"); + + } else if (a_call_id >= 0 && b_call_id >= 0 + && (!same_node || a_call_id == b_call_id)) { + /* + * The op and last_failed_op are the same + * Order on last-rc-change + */ + time_t last_a = -1; + time_t last_b = -1; + + crm_element_value_epoch(xml_a, XML_RSC_OP_LAST_CHANGE, &last_a); + crm_element_value_epoch(xml_b, XML_RSC_OP_LAST_CHANGE, &last_b); + + crm_trace("rc-change: %lld vs %lld", + (long long) last_a, (long long) last_b); + if (last_a >= 0 && last_a < last_b) { + sort_return(-1, "rc-change"); + + } else if (last_b >= 0 && last_a > last_b) { + sort_return(1, "rc-change"); + } + sort_return(0, "rc-change"); + + } else { + /* One of the inputs is a pending operation + * Attempt to use XML_ATTR_TRANSITION_MAGIC to determine its age relative to the other + */ + + int a_id = -1; + int b_id = -1; + + const char *a_magic = crm_element_value(xml_a, XML_ATTR_TRANSITION_MAGIC); + const char *b_magic = crm_element_value(xml_b, XML_ATTR_TRANSITION_MAGIC); + + CRM_CHECK(a_magic != NULL && b_magic != NULL, sort_return(0, "No magic")); + if (!decode_transition_magic(a_magic, &a_uuid, &a_id, NULL, NULL, NULL, + NULL)) { + sort_return(0, "bad magic a"); + } + if (!decode_transition_magic(b_magic, &b_uuid, &b_id, NULL, NULL, NULL, + NULL)) { + sort_return(0, "bad magic b"); + } + /* try to determine the relative age of the operation... + * some pending operations (e.g. a start) may have been superseded + * by a subsequent stop + * + * [a|b]_id == -1 means it's a shutdown operation and _always_ comes last + */ + if (!pcmk__str_eq(a_uuid, b_uuid, pcmk__str_casei) || a_id == b_id) { + /* + * some of the logic in here may be redundant... + * + * if the UUID from the TE doesn't match then one better + * be a pending operation. + * pending operations don't survive between elections and joins + * because we query the LRM directly + */ + + if (b_call_id == -1) { + sort_return(-1, "transition + call"); + + } else if (a_call_id == -1) { + sort_return(1, "transition + call"); + } + + } else if ((a_id >= 0 && a_id < b_id) || b_id == -1) { + sort_return(-1, "transition"); + + } else if ((b_id >= 0 && a_id > b_id) || a_id == -1) { + sort_return(1, "transition"); + } + } + + /* we should never end up here */ + CRM_CHECK(FALSE, sort_return(0, "default")); +} + +gint +sort_op_by_callid(gconstpointer a, gconstpointer b) +{ + const xmlNode *xml_a = a; + const xmlNode *xml_b = b; + + return pe__is_newer_op(xml_a, xml_b, true); +} + +/*! + * \internal + * \brief Create a new pseudo-action for a resource + * + * \param[in,out] rsc Resource to create action for + * \param[in] task Action name + * \param[in] optional Whether action should be considered optional + * \param[in] runnable Whethe action should be considered runnable + * + * \return New action object corresponding to arguments + */ +pe_action_t * +pe__new_rsc_pseudo_action(pe_resource_t *rsc, const char *task, bool optional, + bool runnable) +{ + pe_action_t *action = NULL; + + CRM_ASSERT((rsc != NULL) && (task != NULL)); + + action = custom_action(rsc, pcmk__op_key(rsc->id, task, 0), task, NULL, + optional, TRUE, rsc->cluster); + pe__set_action_flags(action, pe_action_pseudo); + if (runnable) { + pe__set_action_flags(action, pe_action_runnable); + } + return action; +} + +/*! + * \internal + * \brief Add the expected result to an action + * + * \param[in,out] action Action to add expected result to + * \param[in] expected_result Expected result to add + * + * \note This is more efficient than calling add_hash_param(). + */ +void +pe__add_action_expected_result(pe_action_t *action, int expected_result) +{ + char *name = NULL; + + CRM_ASSERT((action != NULL) && (action->meta != NULL)); + + name = strdup(XML_ATTR_TE_TARGET_RC); + CRM_ASSERT (name != NULL); + + g_hash_table_insert(action->meta, name, pcmk__itoa(expected_result)); +} diff --git a/lib/pengine/pe_digest.c b/lib/pengine/pe_digest.c new file mode 100644 index 0000000..b8047da --- /dev/null +++ b/lib/pengine/pe_digest.c @@ -0,0 +1,592 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include "pe_status_private.h" + +extern bool pcmk__is_daemon; + +/*! + * \internal + * \brief Free an operation digest cache entry + * + * \param[in,out] ptr Pointer to cache entry to free + * + * \note The argument is a gpointer so this can be used as a hash table + * free function. + */ +void +pe__free_digests(gpointer ptr) +{ + op_digest_cache_t *data = ptr; + + if (data != NULL) { + free_xml(data->params_all); + free_xml(data->params_secure); + free_xml(data->params_restart); + + free(data->digest_all_calc); + free(data->digest_restart_calc); + free(data->digest_secure_calc); + + free(data); + } +} + +// Return true if XML attribute name is not substring of a given string +static bool +attr_not_in_string(xmlAttrPtr a, void *user_data) +{ + bool filter = false; + char *name = crm_strdup_printf(" %s ", (const char *) a->name); + + if (strstr((const char *) user_data, name) == NULL) { + crm_trace("Filtering %s (not found in '%s')", + (const char *) a->name, (const char *) user_data); + filter = true; + } + free(name); + return filter; +} + +// Return true if XML attribute name is substring of a given string +static bool +attr_in_string(xmlAttrPtr a, void *user_data) +{ + bool filter = false; + char *name = crm_strdup_printf(" %s ", (const char *) a->name); + + if (strstr((const char *) user_data, name) != NULL) { + crm_trace("Filtering %s (found in '%s')", + (const char *) a->name, (const char *) user_data); + filter = true; + } + free(name); + return filter; +} + +/*! + * \internal + * \brief Add digest of all parameters to a digest cache entry + * + * \param[out] data Digest cache entry to modify + * \param[in,out] rsc Resource that action was for + * \param[in] node Node action was performed on + * \param[in] params Resource parameters evaluated for node + * \param[in] task Name of action performed + * \param[in,out] interval_ms Action's interval (will be reset if in overrides) + * \param[in] xml_op Unused + * \param[in] op_version CRM feature set to use for digest calculation + * \param[in] overrides Key/value table to override resource parameters + * \param[in,out] data_set Cluster working set + */ +static void +calculate_main_digest(op_digest_cache_t *data, pe_resource_t *rsc, + const pe_node_t *node, GHashTable *params, + const char *task, guint *interval_ms, + const xmlNode *xml_op, const char *op_version, + GHashTable *overrides, pe_working_set_t *data_set) +{ + pe_action_t *action = NULL; + + data->params_all = create_xml_node(NULL, XML_TAG_PARAMS); + + /* REMOTE_CONTAINER_HACK: Allow Pacemaker Remote nodes to run containers + * that themselves are Pacemaker Remote nodes + */ + (void) pe__add_bundle_remote_name(rsc, data_set, data->params_all, + XML_RSC_ATTR_REMOTE_RA_ADDR); + + // If interval was overridden, reset it + if (overrides != NULL) { + const char *interval_s = g_hash_table_lookup(overrides, CRM_META "_" + XML_LRM_ATTR_INTERVAL); + + if (interval_s != NULL) { + long long value_ll; + + if ((pcmk__scan_ll(interval_s, &value_ll, 0LL) == pcmk_rc_ok) + && (value_ll >= 0) && (value_ll <= G_MAXUINT)) { + *interval_ms = (guint) value_ll; + } + } + } + + action = custom_action(rsc, pcmk__op_key(rsc->id, task, *interval_ms), + task, node, TRUE, FALSE, data_set); + if (overrides != NULL) { + g_hash_table_foreach(overrides, hash2field, data->params_all); + } + g_hash_table_foreach(params, hash2field, data->params_all); + g_hash_table_foreach(action->extra, hash2field, data->params_all); + g_hash_table_foreach(action->meta, hash2metafield, data->params_all); + + pcmk__filter_op_for_digest(data->params_all); + + /* Given a non-recurring operation with extra parameters configured, + * in case that the main digest doesn't match, even if the restart + * digest matches, enforce a restart rather than a reload-agent anyway. + * So that it ensures any changes of the extra parameters get applied + * for this specific operation, and the digests calculated for the + * resulting lrm_rsc_op will be correct. + * Mark the implied rc RSC_DIGEST_RESTART for the case that the main + * digest doesn't match. + */ + if (*interval_ms == 0 + && g_hash_table_size(action->extra) > 0) { + data->rc = RSC_DIGEST_RESTART; + } + + pe_free_action(action); + + data->digest_all_calc = calculate_operation_digest(data->params_all, + op_version); +} + +// Return true if XML attribute name is a Pacemaker-defined fencing parameter +static bool +is_fence_param(xmlAttrPtr attr, void *user_data) +{ + return pcmk_stonith_param((const char *) attr->name); +} + +/*! + * \internal + * \brief Add secure digest to a digest cache entry + * + * \param[out] data Digest cache entry to modify + * \param[in] rsc Resource that action was for + * \param[in] params Resource parameters evaluated for node + * \param[in] xml_op XML of operation in CIB status (if available) + * \param[in] op_version CRM feature set to use for digest calculation + * \param[in] overrides Key/value hash table to override resource parameters + */ +static void +calculate_secure_digest(op_digest_cache_t *data, const pe_resource_t *rsc, + GHashTable *params, const xmlNode *xml_op, + const char *op_version, GHashTable *overrides) +{ + const char *class = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS); + const char *secure_list = NULL; + bool old_version = (compare_version(op_version, "3.16.0") < 0); + + if (xml_op == NULL) { + secure_list = " passwd password user "; + } else { + secure_list = crm_element_value(xml_op, XML_LRM_ATTR_OP_SECURE); + } + + if (old_version) { + data->params_secure = create_xml_node(NULL, XML_TAG_PARAMS); + if (overrides != NULL) { + g_hash_table_foreach(overrides, hash2field, data->params_secure); + } + + g_hash_table_foreach(params, hash2field, data->params_secure); + + } else { + // Start with a copy of all parameters + data->params_secure = copy_xml(data->params_all); + } + + if (secure_list != NULL) { + pcmk__xe_remove_matching_attrs(data->params_secure, attr_in_string, + (void *) secure_list); + } + if (old_version + && pcmk_is_set(pcmk_get_ra_caps(class), + pcmk_ra_cap_fence_params)) { + /* For stonith resources, Pacemaker adds special parameters, + * but these are not listed in fence agent meta-data, so with older + * versions of DC, the controller will not hash them. That means we have + * to filter them out before calculating our hash for comparison. + */ + pcmk__xe_remove_matching_attrs(data->params_secure, is_fence_param, + NULL); + } + pcmk__filter_op_for_digest(data->params_secure); + + /* CRM_meta_timeout *should* be part of a digest for recurring operations. + * However, with older versions of DC, the controller does not add timeout + * to secure digests, because it only includes parameters declared by the + * resource agent. + * Remove any timeout that made it this far, to match. + */ + if (old_version) { + xml_remove_prop(data->params_secure, CRM_META "_" XML_ATTR_TIMEOUT); + } + + data->digest_secure_calc = calculate_operation_digest(data->params_secure, + op_version); +} + +/*! + * \internal + * \brief Add restart digest to a digest cache entry + * + * \param[out] data Digest cache entry to modify + * \param[in] xml_op XML of operation in CIB status (if available) + * \param[in] op_version CRM feature set to use for digest calculation + * + * \note This function doesn't need to handle overrides because it starts with + * data->params_all, which already has overrides applied. + */ +static void +calculate_restart_digest(op_digest_cache_t *data, const xmlNode *xml_op, + const char *op_version) +{ + const char *value = NULL; + + // We must have XML of resource operation history + if (xml_op == NULL) { + return; + } + + // And the history must have a restart digest to compare against + if (crm_element_value(xml_op, XML_LRM_ATTR_RESTART_DIGEST) == NULL) { + return; + } + + // Start with a copy of all parameters + data->params_restart = copy_xml(data->params_all); + + // Then filter out reloadable parameters, if any + value = crm_element_value(xml_op, XML_LRM_ATTR_OP_RESTART); + if (value != NULL) { + pcmk__xe_remove_matching_attrs(data->params_restart, attr_not_in_string, + (void *) value); + } + + value = crm_element_value(xml_op, XML_ATTR_CRM_VERSION); + data->digest_restart_calc = calculate_operation_digest(data->params_restart, + value); +} + +/*! + * \internal + * \brief Create a new digest cache entry with calculated digests + * + * \param[in,out] rsc Resource that action was for + * \param[in] task Name of action performed + * \param[in,out] interval_ms Action's interval (will be reset if in overrides) + * \param[in] node Node action was performed on + * \param[in] xml_op XML of operation in CIB status (if available) + * \param[in] overrides Key/value table to override resource parameters + * \param[in] calc_secure Whether to calculate secure digest + * \param[in,out] data_set Cluster working set + * + * \return Pointer to new digest cache entry (or NULL on memory error) + * \note It is the caller's responsibility to free the result using + * pe__free_digests(). + */ +op_digest_cache_t * +pe__calculate_digests(pe_resource_t *rsc, const char *task, guint *interval_ms, + const pe_node_t *node, const xmlNode *xml_op, + GHashTable *overrides, bool calc_secure, + pe_working_set_t *data_set) +{ + op_digest_cache_t *data = calloc(1, sizeof(op_digest_cache_t)); + const char *op_version = NULL; + GHashTable *params = NULL; + + if (data == NULL) { + return NULL; + } + + data->rc = RSC_DIGEST_MATCH; + + if (xml_op != NULL) { + op_version = crm_element_value(xml_op, XML_ATTR_CRM_VERSION); + } + + if (op_version == NULL && data_set != NULL && data_set->input != NULL) { + op_version = crm_element_value(data_set->input, XML_ATTR_CRM_VERSION); + } + + if (op_version == NULL) { + op_version = CRM_FEATURE_SET; + } + + params = pe_rsc_params(rsc, node, data_set); + calculate_main_digest(data, rsc, node, params, task, interval_ms, xml_op, + op_version, overrides, data_set); + if (calc_secure) { + calculate_secure_digest(data, rsc, params, xml_op, op_version, + overrides); + } + calculate_restart_digest(data, xml_op, op_version); + return data; +} + +/*! + * \internal + * \brief Calculate action digests and store in node's digest cache + * + * \param[in,out] rsc Resource that action was for + * \param[in] task Name of action performed + * \param[in] interval_ms Action's interval + * \param[in,out] node Node action was performed on + * \param[in] xml_op XML of operation in CIB status (if available) + * \param[in] calc_secure Whether to calculate secure digest + * \param[in,out] data_set Cluster working set + * + * \return Pointer to node's digest cache entry + */ +static op_digest_cache_t * +rsc_action_digest(pe_resource_t *rsc, const char *task, guint interval_ms, + pe_node_t *node, const xmlNode *xml_op, + bool calc_secure, pe_working_set_t *data_set) +{ + op_digest_cache_t *data = NULL; + char *key = pcmk__op_key(rsc->id, task, interval_ms); + + data = g_hash_table_lookup(node->details->digest_cache, key); + if (data == NULL) { + data = pe__calculate_digests(rsc, task, &interval_ms, node, xml_op, + NULL, calc_secure, data_set); + CRM_ASSERT(data != NULL); + g_hash_table_insert(node->details->digest_cache, strdup(key), data); + } + free(key); + return data; +} + +/*! + * \internal + * \brief Calculate operation digests and compare against an XML history entry + * + * \param[in,out] rsc Resource to check + * \param[in] xml_op Resource history XML + * \param[in,out] node Node to use for digest calculation + * \param[in,out] data_set Cluster working set + * + * \return Pointer to node's digest cache entry, with comparison result set + */ +op_digest_cache_t * +rsc_action_digest_cmp(pe_resource_t *rsc, const xmlNode *xml_op, + pe_node_t *node, pe_working_set_t *data_set) +{ + op_digest_cache_t *data = NULL; + guint interval_ms = 0; + + const char *op_version; + const char *task = crm_element_value(xml_op, XML_LRM_ATTR_TASK); + const char *digest_all; + const char *digest_restart; + + CRM_ASSERT(node != NULL); + + op_version = crm_element_value(xml_op, XML_ATTR_CRM_VERSION); + digest_all = crm_element_value(xml_op, XML_LRM_ATTR_OP_DIGEST); + digest_restart = crm_element_value(xml_op, XML_LRM_ATTR_RESTART_DIGEST); + + crm_element_value_ms(xml_op, XML_LRM_ATTR_INTERVAL_MS, &interval_ms); + data = rsc_action_digest(rsc, task, interval_ms, node, xml_op, + pcmk_is_set(data_set->flags, pe_flag_sanitized), + data_set); + + if (digest_restart && data->digest_restart_calc && strcmp(data->digest_restart_calc, digest_restart) != 0) { + pe_rsc_info(rsc, "Parameters to %ums-interval %s action for %s on %s " + "changed: hash was %s vs. now %s (restart:%s) %s", + interval_ms, task, rsc->id, pe__node_name(node), + pcmk__s(digest_restart, "missing"), + data->digest_restart_calc, + op_version, + crm_element_value(xml_op, XML_ATTR_TRANSITION_MAGIC)); + data->rc = RSC_DIGEST_RESTART; + + } else if (digest_all == NULL) { + /* it is unknown what the previous op digest was */ + data->rc = RSC_DIGEST_UNKNOWN; + + } else if (strcmp(digest_all, data->digest_all_calc) != 0) { + /* Given a non-recurring operation with extra parameters configured, + * in case that the main digest doesn't match, even if the restart + * digest matches, enforce a restart rather than a reload-agent anyway. + * So that it ensures any changes of the extra parameters get applied + * for this specific operation, and the digests calculated for the + * resulting lrm_rsc_op will be correct. + * Preserve the implied rc RSC_DIGEST_RESTART for the case that the main + * digest doesn't match. + */ + if (interval_ms == 0 + && data->rc == RSC_DIGEST_RESTART) { + pe_rsc_info(rsc, "Parameters containing extra ones to %ums-interval" + " %s action for %s on %s " + "changed: hash was %s vs. now %s (restart:%s) %s", + interval_ms, task, rsc->id, pe__node_name(node), + pcmk__s(digest_all, "missing"), data->digest_all_calc, + op_version, + crm_element_value(xml_op, XML_ATTR_TRANSITION_MAGIC)); + + } else { + pe_rsc_info(rsc, "Parameters to %ums-interval %s action for %s on %s " + "changed: hash was %s vs. now %s (%s:%s) %s", + interval_ms, task, rsc->id, pe__node_name(node), + pcmk__s(digest_all, "missing"), data->digest_all_calc, + (interval_ms > 0)? "reschedule" : "reload", + op_version, + crm_element_value(xml_op, XML_ATTR_TRANSITION_MAGIC)); + data->rc = RSC_DIGEST_ALL; + } + + } else { + data->rc = RSC_DIGEST_MATCH; + } + return data; +} + +/*! + * \internal + * \brief Create an unfencing summary for use in special node attribute + * + * Create a string combining a fence device's resource ID, agent type, and + * parameter digest (whether for all parameters or just non-private parameters). + * This can be stored in a special node attribute, allowing us to detect changes + * in either the agent type or parameters, to know whether unfencing must be + * redone or can be safely skipped when the device's history is cleaned. + * + * \param[in] rsc_id Fence device resource ID + * \param[in] agent_type Fence device agent + * \param[in] param_digest Fence device parameter digest + * + * \return Newly allocated string with unfencing digest + * \note The caller is responsible for freeing the result. + */ +static inline char * +create_unfencing_summary(const char *rsc_id, const char *agent_type, + const char *param_digest) +{ + return crm_strdup_printf("%s:%s:%s", rsc_id, agent_type, param_digest); +} + +/*! + * \internal + * \brief Check whether a node can skip unfencing + * + * Check whether a fence device's current definition matches a node's + * stored summary of when it was last unfenced by the device. + * + * \param[in] rsc_id Fence device's resource ID + * \param[in] agent Fence device's agent type + * \param[in] digest_calc Fence device's current parameter digest + * \param[in] node_summary Value of node's special unfencing node attribute + * (a comma-separated list of unfencing summaries for + * all devices that have unfenced this node) + * + * \return TRUE if digest matches, FALSE otherwise + */ +static bool +unfencing_digest_matches(const char *rsc_id, const char *agent, + const char *digest_calc, const char *node_summary) +{ + bool matches = FALSE; + + if (rsc_id && agent && digest_calc && node_summary) { + char *search_secure = create_unfencing_summary(rsc_id, agent, + digest_calc); + + /* The digest was calculated including the device ID and agent, + * so there is no risk of collision using strstr(). + */ + matches = (strstr(node_summary, search_secure) != NULL); + crm_trace("Calculated unfencing digest '%s' %sfound in '%s'", + search_secure, matches? "" : "not ", node_summary); + free(search_secure); + } + return matches; +} + +/* Magic string to use as action name for digest cache entries used for + * unfencing checks. This is not a real action name (i.e. "on"), so + * pcmk__check_action_config() won't confuse these entries with real actions. + */ +#define STONITH_DIGEST_TASK "stonith-on" + +/*! + * \internal + * \brief Calculate fence device digests and digest comparison result + * + * \param[in,out] rsc Fence device resource + * \param[in] agent Fence device's agent type + * \param[in,out] node Node with digest cache to use + * \param[in,out] data_set Cluster working set + * + * \return Node's digest cache entry + */ +op_digest_cache_t * +pe__compare_fencing_digest(pe_resource_t *rsc, const char *agent, + pe_node_t *node, pe_working_set_t *data_set) +{ + const char *node_summary = NULL; + + // Calculate device's current parameter digests + op_digest_cache_t *data = rsc_action_digest(rsc, STONITH_DIGEST_TASK, 0U, + node, NULL, TRUE, data_set); + + // Check whether node has special unfencing summary node attribute + node_summary = pe_node_attribute_raw(node, CRM_ATTR_DIGESTS_ALL); + if (node_summary == NULL) { + data->rc = RSC_DIGEST_UNKNOWN; + return data; + } + + // Check whether full parameter digest matches + if (unfencing_digest_matches(rsc->id, agent, data->digest_all_calc, + node_summary)) { + data->rc = RSC_DIGEST_MATCH; + return data; + } + + // Check whether secure parameter digest matches + node_summary = pe_node_attribute_raw(node, CRM_ATTR_DIGESTS_SECURE); + if (unfencing_digest_matches(rsc->id, agent, data->digest_secure_calc, + node_summary)) { + data->rc = RSC_DIGEST_MATCH; + if (!pcmk__is_daemon && data_set->priv != NULL) { + pcmk__output_t *out = data_set->priv; + out->info(out, "Only 'private' parameters to %s " + "for unfencing %s changed", rsc->id, + pe__node_name(node)); + } + return data; + } + + // Parameters don't match + data->rc = RSC_DIGEST_ALL; + if (pcmk_is_set(data_set->flags, pe_flag_sanitized) && data->digest_secure_calc) { + if (data_set->priv != NULL) { + pcmk__output_t *out = data_set->priv; + char *digest = create_unfencing_summary(rsc->id, agent, + data->digest_secure_calc); + + out->info(out, "Parameters to %s for unfencing " + "%s changed, try '%s'", rsc->id, + pe__node_name(node), digest); + free(digest); + } else if (!pcmk__is_daemon) { + char *digest = create_unfencing_summary(rsc->id, agent, + data->digest_secure_calc); + + printf("Parameters to %s for unfencing %s changed, try '%s'\n", + rsc->id, pe__node_name(node), digest); + free(digest); + } + } + return data; +} diff --git a/lib/pengine/pe_health.c b/lib/pengine/pe_health.c new file mode 100644 index 0000000..6419fdf --- /dev/null +++ b/lib/pengine/pe_health.c @@ -0,0 +1,157 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include "pe_status_private.h" + +/*! + * \internal + * \brief Set the node health values to use for "red", "yellow", and "green" + * + * \param[in,out] data_set Cluster working set + */ +void +pe__unpack_node_health_scores(pe_working_set_t *data_set) +{ + switch (pe__health_strategy(data_set)) { + case pcmk__health_strategy_none: + pcmk__score_red = 0; + pcmk__score_yellow = 0; + pcmk__score_green = 0; + break; + + case pcmk__health_strategy_no_red: + pcmk__score_red = -INFINITY; + pcmk__score_yellow = 0; + pcmk__score_green = 0; + break; + + case pcmk__health_strategy_only_green: + pcmk__score_red = -INFINITY; + pcmk__score_yellow = -INFINITY; + pcmk__score_green = 0; + break; + + default: // progressive or custom + pcmk__score_red = pe__health_score(PCMK__OPT_NODE_HEALTH_RED, + data_set); + pcmk__score_green = pe__health_score(PCMK__OPT_NODE_HEALTH_GREEN, + data_set); + pcmk__score_yellow = pe__health_score(PCMK__OPT_NODE_HEALTH_YELLOW, + data_set); + break; + } + + if ((pcmk__score_red != 0) || (pcmk__score_yellow != 0) + || (pcmk__score_green != 0)) { + crm_debug("Values of node health scores: " + PCMK__VALUE_RED "=%d " + PCMK__VALUE_YELLOW "=%d " + PCMK__VALUE_GREEN "=%d", + pcmk__score_red, pcmk__score_yellow, pcmk__score_green); + } +} + +/*! + * \internal + * \brief Add node attribute value to an integer, if it is a health attribute + * + * \param[in] key Name of node attribute + * \param[in] value String value of node attribute + * \param[in,out] user_data Address of integer to which \p value should be + * added if \p key is a node health attribute + */ +static void +add_node_health_value(gpointer key, gpointer value, gpointer user_data) +{ + if (pcmk__starts_with((const char *) key, "#health")) { + int score = char2score((const char *) value); + int *health = (int *) user_data; + + *health = pcmk__add_scores(score, *health); + crm_trace("Combined '%s' into node health score (now %s)", + (const char *) value, pcmk_readable_score(*health)); + } +} + +/*! + * \internal + * \brief Sum a node's health attribute scores + * + * \param[in] node Node whose health attributes should be added + * \param[in] base_health Add this number to the total + * + * \return Sum of all health attribute scores of \p node plus \p base_health + */ +int +pe__sum_node_health_scores(const pe_node_t *node, int base_health) +{ + CRM_ASSERT(node != NULL); + g_hash_table_foreach(node->details->attrs, add_node_health_value, + &base_health); + return base_health; +} + +/*! + * \internal + * \brief Check the general health status for a node + * + * \param[in,out] node Node to check + * + * \return A negative value if any health attribute for \p node is red, + * otherwise 0 if any attribute is yellow, otherwise a positive value. + */ +int +pe__node_health(pe_node_t *node) +{ + GHashTableIter iter; + const char *name = NULL; + const char *value = NULL; + enum pcmk__health_strategy strategy; + int score = 0; + int rc = 1; + + CRM_ASSERT(node != NULL); + + strategy = pe__health_strategy(node->details->data_set); + if (strategy == pcmk__health_strategy_none) { + return rc; + } + + g_hash_table_iter_init(&iter, node->details->attrs); + while (g_hash_table_iter_next(&iter, (gpointer *) &name, + (gpointer *) &value)) { + if (pcmk__starts_with(name, "#health")) { + /* It's possible that pcmk__score_red equals pcmk__score_yellow, + * or pcmk__score_yellow equals pcmk__score_green, so check the + * textual value first to be able to distinguish those. + */ + if (pcmk__str_eq(value, PCMK__VALUE_RED, pcmk__str_casei)) { + return -1; + } else if (pcmk__str_eq(value, PCMK__VALUE_YELLOW, + pcmk__str_casei)) { + rc = 0; + continue; + } + + // The value is an integer, so compare numerically + score = char2score(value); + if (score <= pcmk__score_red) { + return -1; + } else if ((score <= pcmk__score_yellow) + && (pcmk__score_yellow != pcmk__score_green)) { + rc = 0; + } + } + } + return rc; +} diff --git a/lib/pengine/pe_notif.c b/lib/pengine/pe_notif.c new file mode 100644 index 0000000..7ed490f --- /dev/null +++ b/lib/pengine/pe_notif.c @@ -0,0 +1,996 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include + +#include "pe_status_private.h" + +typedef struct notify_entry_s { + const pe_resource_t *rsc; + const pe_node_t *node; +} notify_entry_t; + +/*! + * \internal + * \brief Compare two notification entries + * + * Compare two notification entries, where the one with the alphabetically first + * resource name (or if equal, node name) sorts as first, with NULL sorting as + * less than non-NULL. + * + * \param[in] a First notification entry to compare + * \param[in] b Second notification entry to compare + * + * \return -1 if \p a sorts before \p b, 0 if they are equal, otherwise 1 + */ +static gint +compare_notify_entries(gconstpointer a, gconstpointer b) +{ + int tmp; + const notify_entry_t *entry_a = a; + const notify_entry_t *entry_b = b; + + // NULL a or b is not actually possible + if ((entry_a == NULL) && (entry_b == NULL)) { + return 0; + } + if (entry_a == NULL) { + return 1; + } + if (entry_b == NULL) { + return -1; + } + + // NULL resources sort first + if ((entry_a->rsc == NULL) && (entry_b->rsc == NULL)) { + return 0; + } + if (entry_a->rsc == NULL) { + return 1; + } + if (entry_b->rsc == NULL) { + return -1; + } + + // Compare resource names + tmp = strcmp(entry_a->rsc->id, entry_b->rsc->id); + if (tmp != 0) { + return tmp; + } + + // Otherwise NULL nodes sort first + if ((entry_a->node == NULL) && (entry_b->node == NULL)) { + return 0; + } + if (entry_a->node == NULL) { + return 1; + } + if (entry_b->node == NULL) { + return -1; + } + + // Finally, compare node names + return strcmp(entry_a->node->details->id, entry_b->node->details->id); +} + +/*! + * \internal + * \brief Duplicate a notification entry + * + * \param[in] entry Entry to duplicate + * + * \return Newly allocated duplicate of \p entry + * \note It is the caller's responsibility to free the return value. + */ +static notify_entry_t * +dup_notify_entry(const notify_entry_t *entry) +{ + notify_entry_t *dup = calloc(1, sizeof(notify_entry_t)); + + CRM_ASSERT(dup != NULL); + dup->rsc = entry->rsc; + dup->node = entry->node; + return dup; +} + +/*! + * \internal + * \brief Given a list of nodes, create strings with node names + * + * \param[in] list List of nodes (as pe_node_t *) + * \param[out] all_node_names If not NULL, will be set to space-separated list + * of the names of all nodes in \p list + * \param[out] host_node_names Same as \p all_node_names, except active + * guest nodes will list the name of their host + * + * \note The caller is responsible for freeing the output argument values using + * \p g_string_free(). + */ +static void +get_node_names(const GList *list, GString **all_node_names, + GString **host_node_names) +{ + if (all_node_names != NULL) { + *all_node_names = NULL; + } + if (host_node_names != NULL) { + *host_node_names = NULL; + } + + for (const GList *iter = list; iter != NULL; iter = iter->next) { + const pe_node_t *node = (const pe_node_t *) iter->data; + + if (node->details->uname == NULL) { + continue; + } + + // Always add to list of all node names + if (all_node_names != NULL) { + pcmk__add_word(all_node_names, 1024, node->details->uname); + } + + // Add to host node name list if appropriate + if (host_node_names != NULL) { + if (pe__is_guest_node(node) + && (node->details->remote_rsc->container->running_on != NULL)) { + node = pe__current_node(node->details->remote_rsc->container); + if (node->details->uname == NULL) { + continue; + } + } + pcmk__add_word(host_node_names, 1024, node->details->uname); + } + } + + if ((all_node_names != NULL) && (*all_node_names == NULL)) { + *all_node_names = g_string_new(" "); + } + if ((host_node_names != NULL) && (*host_node_names == NULL)) { + *host_node_names = g_string_new(" "); + } +} + +/*! + * \internal + * \brief Create strings of instance and node names from notification entries + * + * \param[in,out] list List of notification entries (will be sorted here) + * \param[out] rsc_names If not NULL, will be set to space-separated list + * of clone instances from \p list + * \param[out] node_names If not NULL, will be set to space-separated list + * of node names from \p list + * + * \return (Possibly new) head of sorted \p list + * \note The caller is responsible for freeing the output argument values using + * \p g_list_free_full() and \p g_string_free(). + */ +static GList * +notify_entries_to_strings(GList *list, GString **rsc_names, + GString **node_names) +{ + const char *last_rsc_id = NULL; + + // Initialize output lists to NULL + if (rsc_names != NULL) { + *rsc_names = NULL; + } + if (node_names != NULL) { + *node_names = NULL; + } + + // Sort input list for user-friendliness (and ease of filtering duplicates) + list = g_list_sort(list, compare_notify_entries); + + for (GList *gIter = list; gIter != NULL; gIter = gIter->next) { + notify_entry_t *entry = (notify_entry_t *) gIter->data; + + // Entry must have a resource (with ID) + CRM_LOG_ASSERT((entry != NULL) && (entry->rsc != NULL) + && (entry->rsc->id != NULL)); + if ((entry == NULL) || (entry->rsc == NULL) + || (entry->rsc->id == NULL)) { + continue; + } + + // Entry must have a node unless listing inactive resources + CRM_LOG_ASSERT((node_names == NULL) || (entry->node != NULL)); + if ((node_names != NULL) && (entry->node == NULL)) { + continue; + } + + // Don't add duplicates of a particular clone instance + if (pcmk__str_eq(entry->rsc->id, last_rsc_id, pcmk__str_none)) { + continue; + } + last_rsc_id = entry->rsc->id; + + if (rsc_names != NULL) { + pcmk__add_word(rsc_names, 1024, entry->rsc->id); + } + if ((node_names != NULL) && (entry->node->details->uname != NULL)) { + pcmk__add_word(node_names, 1024, entry->node->details->uname); + } + } + + // If there are no entries, return "empty" lists + if ((rsc_names != NULL) && (*rsc_names == NULL)) { + *rsc_names = g_string_new(" "); + } + if ((node_names != NULL) && (*node_names == NULL)) { + *node_names = g_string_new(" "); + } + + return list; +} + +/*! + * \internal + * \brief Copy a meta-attribute into a notify action + * + * \param[in] key Name of meta-attribute to copy + * \param[in] value Value of meta-attribute to copy + * \param[in,out] user_data Notify action to copy into + */ +static void +copy_meta_to_notify(gpointer key, gpointer value, gpointer user_data) +{ + pe_action_t *notify = (pe_action_t *) user_data; + + /* Any existing meta-attributes (for example, the action timeout) are for + * the notify action itself, so don't override those. + */ + if (g_hash_table_lookup(notify->meta, (const char *) key) != NULL) { + return; + } + + g_hash_table_insert(notify->meta, strdup((const char *) key), + strdup((const char *) value)); +} + +static void +add_notify_data_to_action_meta(const notify_data_t *n_data, pe_action_t *action) +{ + for (const GSList *item = n_data->keys; item; item = item->next) { + const pcmk_nvpair_t *nvpair = (const pcmk_nvpair_t *) item->data; + + add_hash_param(action->meta, nvpair->name, nvpair->value); + } +} + +/*! + * \internal + * \brief Create a new notify pseudo-action for a clone resource + * + * \param[in,out] rsc Clone resource that notification is for + * \param[in] action Action to use in notify action key + * \param[in] notif_action RSC_NOTIFY or RSC_NOTIFIED + * \param[in] notif_type "pre", "post", "confirmed-pre", "confirmed-post" + * + * \return Newly created notify pseudo-action + */ +static pe_action_t * +new_notify_pseudo_action(pe_resource_t *rsc, const pe_action_t *action, + const char *notif_action, const char *notif_type) +{ + pe_action_t *notify = NULL; + + notify = custom_action(rsc, + pcmk__notify_key(rsc->id, notif_type, action->task), + notif_action, NULL, + pcmk_is_set(action->flags, pe_action_optional), + TRUE, rsc->cluster); + pe__set_action_flags(notify, pe_action_pseudo); + add_hash_param(notify->meta, "notify_key_type", notif_type); + add_hash_param(notify->meta, "notify_key_operation", action->task); + return notify; +} + +/*! + * \internal + * \brief Create a new notify action for a clone instance + * + * \param[in,out] rsc Clone instance that notification is for + * \param[in] node Node that notification is for + * \param[in,out] op Action that notification is for + * \param[in,out] notify_done Parent pseudo-action for notifications complete + * \param[in] n_data Notification values to add to action meta-data + * + * \return Newly created notify action + */ +static pe_action_t * +new_notify_action(pe_resource_t *rsc, const pe_node_t *node, pe_action_t *op, + pe_action_t *notify_done, const notify_data_t *n_data) +{ + char *key = NULL; + pe_action_t *notify_action = NULL; + const char *value = NULL; + const char *task = NULL; + const char *skip_reason = NULL; + + CRM_CHECK((rsc != NULL) && (node != NULL), return NULL); + + // Ensure we have all the info we need + if (op == NULL) { + skip_reason = "no action"; + } else if (notify_done == NULL) { + skip_reason = "no parent notification"; + } else if (!node->details->online) { + skip_reason = "node offline"; + } else if (!pcmk_is_set(op->flags, pe_action_runnable)) { + skip_reason = "original action not runnable"; + } + if (skip_reason != NULL) { + pe_rsc_trace(rsc, "Skipping notify action for %s on %s: %s", + rsc->id, pe__node_name(node), skip_reason); + return NULL; + } + + value = g_hash_table_lookup(op->meta, "notify_type"); // "pre" or "post" + task = g_hash_table_lookup(op->meta, "notify_operation"); // original action + + pe_rsc_trace(rsc, "Creating notify action for %s on %s (%s-%s)", + rsc->id, pe__node_name(node), value, task); + + // Create the notify action + key = pcmk__notify_key(rsc->id, value, task); + notify_action = custom_action(rsc, key, op->task, node, + pcmk_is_set(op->flags, pe_action_optional), + TRUE, rsc->cluster); + + // Add meta-data to notify action + g_hash_table_foreach(op->meta, copy_meta_to_notify, notify_action); + add_notify_data_to_action_meta(n_data, notify_action); + + // Order notify after original action and before parent notification + order_actions(op, notify_action, pe_order_optional); + order_actions(notify_action, notify_done, pe_order_optional); + return notify_action; +} + +/*! + * \internal + * \brief Create a new "post-" notify action for a clone instance + * + * \param[in,out] rsc Clone instance that notification is for + * \param[in] node Node that notification is for + * \param[in,out] n_data Notification values to add to action meta-data + */ +static void +new_post_notify_action(pe_resource_t *rsc, const pe_node_t *node, + notify_data_t *n_data) +{ + pe_action_t *notify = NULL; + + CRM_ASSERT(n_data != NULL); + + // Create the "post-" notify action for specified instance + notify = new_notify_action(rsc, node, n_data->post, n_data->post_done, + n_data); + if (notify != NULL) { + notify->priority = INFINITY; + } + + // Order recurring monitors after all "post-" notifications complete + if (n_data->post_done == NULL) { + return; + } + for (GList *iter = rsc->actions; iter != NULL; iter = iter->next) { + pe_action_t *mon = (pe_action_t *) iter->data; + const char *interval_ms_s = NULL; + + interval_ms_s = g_hash_table_lookup(mon->meta, + XML_LRM_ATTR_INTERVAL_MS); + if (pcmk__str_eq(interval_ms_s, "0", pcmk__str_null_matches) + || pcmk__str_eq(mon->task, RSC_CANCEL, pcmk__str_none)) { + continue; // Not a recurring monitor + } + order_actions(n_data->post_done, mon, pe_order_optional); + } +} + +/*! + * \internal + * \brief Create and order notification pseudo-actions for a clone action + * + * In addition to the actual notify actions needed for each clone instance, + * clone notifications also require pseudo-actions to provide ordering points + * in the notification process. This creates the notification data, along with + * appropriate pseudo-actions and their orderings. + * + * For example, the ordering sequence for starting a clone is: + * + * "pre-" notify pseudo-action for clone + * -> "pre-" notify actions for each clone instance + * -> "pre-" notifications complete pseudo-action for clone + * -> start actions for each clone instance + * -> "started" pseudo-action for clone + * -> "post-" notify pseudo-action for clone + * -> "post-" notify actions for each clone instance + * -> "post-" notifications complete pseudo-action for clone + * + * \param[in,out] rsc Clone that notifications are for + * \param[in] task Name of action that notifications are for + * \param[in,out] action If not NULL, create a "pre-" pseudo-action ordered + * before a "pre-" complete pseudo-action, ordered + * before this action + * \param[in,out] complete If not NULL, create a "post-" pseudo-action ordered + * after this action, and a "post-" complete + * pseudo-action ordered after that + * + * \return Newly created notification data + */ +notify_data_t * +pe__action_notif_pseudo_ops(pe_resource_t *rsc, const char *task, + pe_action_t *action, pe_action_t *complete) +{ + notify_data_t *n_data = NULL; + + if (!pcmk_is_set(rsc->flags, pe_rsc_notify)) { + return NULL; + } + + n_data = calloc(1, sizeof(notify_data_t)); + CRM_ASSERT(n_data != NULL); + + n_data->action = task; + + if (action != NULL) { // Need "pre-" pseudo-actions + + // Create "pre-" notify pseudo-action for clone + n_data->pre = new_notify_pseudo_action(rsc, action, RSC_NOTIFY, "pre"); + pe__set_action_flags(n_data->pre, pe_action_runnable); + add_hash_param(n_data->pre->meta, "notify_type", "pre"); + add_hash_param(n_data->pre->meta, "notify_operation", n_data->action); + + // Create "pre-" notifications complete pseudo-action for clone + n_data->pre_done = new_notify_pseudo_action(rsc, action, RSC_NOTIFIED, + "confirmed-pre"); + pe__set_action_flags(n_data->pre_done, pe_action_runnable); + add_hash_param(n_data->pre_done->meta, "notify_type", "pre"); + add_hash_param(n_data->pre_done->meta, + "notify_operation", n_data->action); + + // Order "pre-" -> "pre-" complete -> original action + order_actions(n_data->pre, n_data->pre_done, pe_order_optional); + order_actions(n_data->pre_done, action, pe_order_optional); + } + + if (complete != NULL) { // Need "post-" pseudo-actions + + // Create "post-" notify pseudo-action for clone + n_data->post = new_notify_pseudo_action(rsc, complete, RSC_NOTIFY, + "post"); + n_data->post->priority = INFINITY; + if (pcmk_is_set(complete->flags, pe_action_runnable)) { + pe__set_action_flags(n_data->post, pe_action_runnable); + } else { + pe__clear_action_flags(n_data->post, pe_action_runnable); + } + add_hash_param(n_data->post->meta, "notify_type", "post"); + add_hash_param(n_data->post->meta, "notify_operation", n_data->action); + + // Create "post-" notifications complete pseudo-action for clone + n_data->post_done = new_notify_pseudo_action(rsc, complete, + RSC_NOTIFIED, + "confirmed-post"); + n_data->post_done->priority = INFINITY; + if (pcmk_is_set(complete->flags, pe_action_runnable)) { + pe__set_action_flags(n_data->post_done, pe_action_runnable); + } else { + pe__clear_action_flags(n_data->post_done, pe_action_runnable); + } + add_hash_param(n_data->post_done->meta, "notify_type", "post"); + add_hash_param(n_data->post_done->meta, + "notify_operation", n_data->action); + + // Order original action complete -> "post-" -> "post-" complete + order_actions(complete, n_data->post, pe_order_implies_then); + order_actions(n_data->post, n_data->post_done, pe_order_implies_then); + } + + // If we created both, order "pre-" complete -> "post-" + if ((action != NULL) && (complete != NULL)) { + order_actions(n_data->pre_done, n_data->post, pe_order_optional); + } + return n_data; +} + +/*! + * \internal + * \brief Create a new notification entry + * + * \param[in] rsc Resource for notification + * \param[in] node Node for notification + * + * \return Newly allocated notification entry + * \note The caller is responsible for freeing the return value. + */ +static notify_entry_t * +new_notify_entry(const pe_resource_t *rsc, const pe_node_t *node) +{ + notify_entry_t *entry = calloc(1, sizeof(notify_entry_t)); + + CRM_ASSERT(entry != NULL); + entry->rsc = rsc; + entry->node = node; + return entry; +} + +/*! + * \internal + * \brief Add notification data for resource state and optionally actions + * + * \param[in] rsc Clone or clone instance being notified + * \param[in] activity Whether to add notification entries for actions + * \param[in,out] n_data Notification data for clone + */ +static void +collect_resource_data(const pe_resource_t *rsc, bool activity, + notify_data_t *n_data) +{ + const GList *iter = NULL; + notify_entry_t *entry = NULL; + const pe_node_t *node = NULL; + + if (n_data == NULL) { + return; + } + + if (n_data->allowed_nodes == NULL) { + n_data->allowed_nodes = rsc->allowed_nodes; + } + + // If this is a clone, call recursively for each instance + if (rsc->children != NULL) { + for (iter = rsc->children; iter != NULL; iter = iter->next) { + const pe_resource_t *child = (const pe_resource_t *) iter->data; + + collect_resource_data(child, activity, n_data); + } + return; + } + + // This is a notification for a single clone instance + + if (rsc->running_on != NULL) { + node = rsc->running_on->data; // First is sufficient + } + entry = new_notify_entry(rsc, node); + + // Add notification indicating the resource state + switch (rsc->role) { + case RSC_ROLE_STOPPED: + n_data->inactive = g_list_prepend(n_data->inactive, entry); + break; + + case RSC_ROLE_STARTED: + n_data->active = g_list_prepend(n_data->active, entry); + break; + + case RSC_ROLE_UNPROMOTED: + n_data->unpromoted = g_list_prepend(n_data->unpromoted, entry); + n_data->active = g_list_prepend(n_data->active, + dup_notify_entry(entry)); + break; + + case RSC_ROLE_PROMOTED: + n_data->promoted = g_list_prepend(n_data->promoted, entry); + n_data->active = g_list_prepend(n_data->active, + dup_notify_entry(entry)); + break; + + default: + crm_err("Resource %s role on %s (%s) is not supported for " + "notifications (bug?)", + rsc->id, pe__node_name(node), role2text(rsc->role)); + free(entry); + break; + } + + if (!activity) { + return; + } + + // Add notification entries for each of the resource's actions + for (iter = rsc->actions; iter != NULL; iter = iter->next) { + const pe_action_t *op = (const pe_action_t *) iter->data; + + if (!pcmk_is_set(op->flags, pe_action_optional) && (op->node != NULL)) { + enum action_tasks task = text2task(op->task); + + if ((task == stop_rsc) && op->node->details->unclean) { + // Create anyway (additional noise if node can't be fenced) + } else if (!pcmk_is_set(op->flags, pe_action_runnable)) { + continue; + } + + entry = new_notify_entry(rsc, op->node); + + switch (task) { + case start_rsc: + n_data->start = g_list_prepend(n_data->start, entry); + break; + case stop_rsc: + n_data->stop = g_list_prepend(n_data->stop, entry); + break; + case action_promote: + n_data->promote = g_list_prepend(n_data->promote, entry); + break; + case action_demote: + n_data->demote = g_list_prepend(n_data->demote, entry); + break; + default: + free(entry); + break; + } + } + } +} + +// For (char *) value +#define add_notify_env(n_data, key, value) do { \ + n_data->keys = pcmk_prepend_nvpair(n_data->keys, key, value); \ + } while (0) + +// For (GString *) value +#define add_notify_env_gs(n_data, key, value) do { \ + n_data->keys = pcmk_prepend_nvpair(n_data->keys, key, \ + (const char *) value->str); \ + } while (0) + +// For (GString *) value +#define add_notify_env_free_gs(n_data, key, value) do { \ + n_data->keys = pcmk_prepend_nvpair(n_data->keys, key, \ + (const char *) value->str); \ + g_string_free(value, TRUE); value = NULL; \ + } while (0) + +/*! + * \internal + * \brief Create notification name/value pairs from structured data + * + * \param[in] rsc Resource that notification is for + * \param[in,out] n_data Notification data + */ +static void +add_notif_keys(const pe_resource_t *rsc, notify_data_t *n_data) +{ + bool required = false; // Whether to make notify actions required + GString *rsc_list = NULL; + GString *node_list = NULL; + GString *metal_list = NULL; + const char *source = NULL; + GList *nodes = NULL; + + n_data->stop = notify_entries_to_strings(n_data->stop, + &rsc_list, &node_list); + if ((strcmp(" ", (const char *) rsc_list->str) != 0) + && pcmk__str_eq(n_data->action, RSC_STOP, pcmk__str_none)) { + required = true; + } + add_notify_env_free_gs(n_data, "notify_stop_resource", rsc_list); + add_notify_env_free_gs(n_data, "notify_stop_uname", node_list); + + if ((n_data->start != NULL) + && pcmk__str_eq(n_data->action, RSC_START, pcmk__str_none)) { + required = true; + } + n_data->start = notify_entries_to_strings(n_data->start, + &rsc_list, &node_list); + add_notify_env_free_gs(n_data, "notify_start_resource", rsc_list); + add_notify_env_free_gs(n_data, "notify_start_uname", node_list); + + if ((n_data->demote != NULL) + && pcmk__str_eq(n_data->action, RSC_DEMOTE, pcmk__str_none)) { + required = true; + } + n_data->demote = notify_entries_to_strings(n_data->demote, + &rsc_list, &node_list); + add_notify_env_free_gs(n_data, "notify_demote_resource", rsc_list); + add_notify_env_free_gs(n_data, "notify_demote_uname", node_list); + + if ((n_data->promote != NULL) + && pcmk__str_eq(n_data->action, RSC_PROMOTE, pcmk__str_none)) { + required = true; + } + n_data->promote = notify_entries_to_strings(n_data->promote, + &rsc_list, &node_list); + add_notify_env_free_gs(n_data, "notify_promote_resource", rsc_list); + add_notify_env_free_gs(n_data, "notify_promote_uname", node_list); + + n_data->active = notify_entries_to_strings(n_data->active, + &rsc_list, &node_list); + add_notify_env_free_gs(n_data, "notify_active_resource", rsc_list); + add_notify_env_free_gs(n_data, "notify_active_uname", node_list); + + n_data->unpromoted = notify_entries_to_strings(n_data->unpromoted, + &rsc_list, &node_list); + add_notify_env_gs(n_data, "notify_unpromoted_resource", rsc_list); + add_notify_env_gs(n_data, "notify_unpromoted_uname", node_list); + + // Deprecated: kept for backward compatibility with older resource agents + add_notify_env_free_gs(n_data, "notify_slave_resource", rsc_list); + add_notify_env_free_gs(n_data, "notify_slave_uname", node_list); + + n_data->promoted = notify_entries_to_strings(n_data->promoted, + &rsc_list, &node_list); + add_notify_env_gs(n_data, "notify_promoted_resource", rsc_list); + add_notify_env_gs(n_data, "notify_promoted_uname", node_list); + + // Deprecated: kept for backward compatibility with older resource agents + add_notify_env_free_gs(n_data, "notify_master_resource", rsc_list); + add_notify_env_free_gs(n_data, "notify_master_uname", node_list); + + n_data->inactive = notify_entries_to_strings(n_data->inactive, + &rsc_list, NULL); + add_notify_env_free_gs(n_data, "notify_inactive_resource", rsc_list); + + nodes = g_hash_table_get_values(n_data->allowed_nodes); + if (!pcmk__is_daemon) { + /* For display purposes, sort the node list, for consistent + * regression test output (while avoiding the performance hit + * for the live cluster). + */ + nodes = g_list_sort(nodes, pe__cmp_node_name); + } + get_node_names(nodes, &node_list, NULL); + add_notify_env_free_gs(n_data, "notify_available_uname", node_list); + g_list_free(nodes); + + source = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET); + if (pcmk__str_eq("host", source, pcmk__str_none)) { + get_node_names(rsc->cluster->nodes, &node_list, &metal_list); + add_notify_env_free_gs(n_data, "notify_all_hosts", metal_list); + } else { + get_node_names(rsc->cluster->nodes, &node_list, NULL); + } + add_notify_env_free_gs(n_data, "notify_all_uname", node_list); + + if (required && (n_data->pre != NULL)) { + pe__clear_action_flags(n_data->pre, pe_action_optional); + pe__clear_action_flags(n_data->pre_done, pe_action_optional); + } + + if (required && (n_data->post != NULL)) { + pe__clear_action_flags(n_data->post, pe_action_optional); + pe__clear_action_flags(n_data->post_done, pe_action_optional); + } +} + +/* + * \internal + * \brief Find any remote connection start relevant to an action + * + * \param[in] action Action to check + * + * \return If action is behind a remote connection, connection's start + */ +static pe_action_t * +find_remote_start(pe_action_t *action) +{ + if ((action != NULL) && (action->node != NULL)) { + pe_resource_t *remote_rsc = action->node->details->remote_rsc; + + if (remote_rsc != NULL) { + return find_first_action(remote_rsc->actions, NULL, RSC_START, + NULL); + } + } + return NULL; +} + +/*! + * \internal + * \brief Create notify actions, and add notify data to original actions + * + * \param[in,out] rsc Clone or clone instance that notification is for + * \param[in,out] n_data Clone notification data for some action + */ +static void +create_notify_actions(pe_resource_t *rsc, notify_data_t *n_data) +{ + GList *iter = NULL; + pe_action_t *stop = NULL; + pe_action_t *start = NULL; + enum action_tasks task = text2task(n_data->action); + + // If this is a clone, call recursively for each instance + if (rsc->children != NULL) { + g_list_foreach(rsc->children, (GFunc) create_notify_actions, n_data); + return; + } + + // Add notification meta-attributes to original actions + for (iter = rsc->actions; iter != NULL; iter = iter->next) { + pe_action_t *op = (pe_action_t *) iter->data; + + if (!pcmk_is_set(op->flags, pe_action_optional) && (op->node != NULL)) { + switch (text2task(op->task)) { + case start_rsc: + case stop_rsc: + case action_promote: + case action_demote: + add_notify_data_to_action_meta(n_data, op); + break; + default: + break; + } + } + } + + // Skip notify action itself if original action was not needed + switch (task) { + case start_rsc: + if (n_data->start == NULL) { + pe_rsc_trace(rsc, "No notify action needed for %s %s", + rsc->id, n_data->action); + return; + } + break; + + case action_promote: + if (n_data->promote == NULL) { + pe_rsc_trace(rsc, "No notify action needed for %s %s", + rsc->id, n_data->action); + return; + } + break; + + case action_demote: + if (n_data->demote == NULL) { + pe_rsc_trace(rsc, "No notify action needed for %s %s", + rsc->id, n_data->action); + return; + } + break; + + default: + // We cannot do same for stop because it might be implied by fencing + break; + } + + pe_rsc_trace(rsc, "Creating notify actions for %s %s", + rsc->id, n_data->action); + + // Create notify actions for stop or demote + if ((rsc->role != RSC_ROLE_STOPPED) + && ((task == stop_rsc) || (task == action_demote))) { + + stop = find_first_action(rsc->actions, NULL, RSC_STOP, NULL); + + for (iter = rsc->running_on; iter != NULL; iter = iter->next) { + pe_node_t *current_node = (pe_node_t *) iter->data; + + /* If a stop is a pseudo-action implied by fencing, don't try to + * notify the node getting fenced. + */ + if ((stop != NULL) && pcmk_is_set(stop->flags, pe_action_pseudo) + && (current_node->details->unclean + || current_node->details->remote_requires_reset)) { + continue; + } + + new_notify_action(rsc, current_node, n_data->pre, + n_data->pre_done, n_data); + + if ((task == action_demote) || (stop == NULL) + || pcmk_is_set(stop->flags, pe_action_optional)) { + new_post_notify_action(rsc, current_node, n_data); + } + } + } + + // Create notify actions for start or promote + if ((rsc->next_role != RSC_ROLE_STOPPED) + && ((task == start_rsc) || (task == action_promote))) { + + start = find_first_action(rsc->actions, NULL, RSC_START, NULL); + if (start != NULL) { + pe_action_t *remote_start = find_remote_start(start); + + if ((remote_start != NULL) + && !pcmk_is_set(remote_start->flags, pe_action_runnable)) { + /* Start and promote actions for a clone instance behind + * a Pacemaker Remote connection happen after the + * connection starts. If the connection start is blocked, do + * not schedule notifications for these actions. + */ + return; + } + } + if (rsc->allocated_to == NULL) { + pe_proc_err("Next role '%s' but %s is not allocated", + role2text(rsc->next_role), rsc->id); + return; + } + if ((task != start_rsc) || (start == NULL) + || pcmk_is_set(start->flags, pe_action_optional)) { + + new_notify_action(rsc, rsc->allocated_to, n_data->pre, + n_data->pre_done, n_data); + } + new_post_notify_action(rsc, rsc->allocated_to, n_data); + } +} + +/*! + * \internal + * \brief Create notification data and actions for one clone action + * + * \param[in,out] rsc Clone resource that notification is for + * \param[in,out] n_data Clone notification data for some action + */ +void +pe__create_action_notifications(pe_resource_t *rsc, notify_data_t *n_data) +{ + if ((rsc == NULL) || (n_data == NULL)) { + return; + } + collect_resource_data(rsc, true, n_data); + add_notif_keys(rsc, n_data); + create_notify_actions(rsc, n_data); +} + +/*! + * \internal + * \brief Free notification data for one action + * + * \param[in,out] n_data Notification data to free + */ +void +pe__free_action_notification_data(notify_data_t *n_data) +{ + if (n_data == NULL) { + return; + } + g_list_free_full(n_data->stop, free); + g_list_free_full(n_data->start, free); + g_list_free_full(n_data->demote, free); + g_list_free_full(n_data->promote, free); + g_list_free_full(n_data->promoted, free); + g_list_free_full(n_data->unpromoted, free); + g_list_free_full(n_data->active, free); + g_list_free_full(n_data->inactive, free); + pcmk_free_nvpairs(n_data->keys); + free(n_data); +} + +/*! + * \internal + * \brief Order clone "notifications complete" pseudo-action after fencing + * + * If a stop action is implied by fencing, the usual notification pseudo-actions + * will not be sufficient to order things properly, or even create all needed + * notifications if the clone is also stopping on another node, and another + * clone is ordered after it. This function creates new notification + * pseudo-actions relative to the fencing to ensure everything works properly. + * + * \param[in] stop Stop action implied by fencing + * \param[in,out] rsc Clone resource that notification is for + * \param[in,out] stonith_op Fencing action that implies \p stop + */ +void +pe__order_notifs_after_fencing(const pe_action_t *stop, pe_resource_t *rsc, + pe_action_t *stonith_op) +{ + notify_data_t *n_data; + + crm_info("Ordering notifications for implied %s after fencing", stop->uuid); + n_data = pe__action_notif_pseudo_ops(rsc, RSC_STOP, NULL, stonith_op); + + if (n_data != NULL) { + collect_resource_data(rsc, false, n_data); + add_notify_env(n_data, "notify_stop_resource", rsc->id); + add_notify_env(n_data, "notify_stop_uname", stop->node->details->uname); + create_notify_actions(uber_parent(rsc), n_data); + pe__free_action_notification_data(n_data); + } +} diff --git a/lib/pengine/pe_output.c b/lib/pengine/pe_output.c new file mode 100644 index 0000000..68cc867 --- /dev/null +++ b/lib/pengine/pe_output.c @@ -0,0 +1,3108 @@ +/* + * Copyright 2019-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include +#include +#include + +const char * +pe__resource_description(const pe_resource_t *rsc, uint32_t show_opts) +{ + const char * desc = NULL; + // User-supplied description + if (pcmk_any_flags_set(show_opts, pcmk_show_rsc_only|pcmk_show_description) + || pcmk__list_of_multiple(rsc->running_on)) { + desc = crm_element_value(rsc->xml, XML_ATTR_DESC); + } + return desc; +} + +/* Never display node attributes whose name starts with one of these prefixes */ +#define FILTER_STR { PCMK__FAIL_COUNT_PREFIX, PCMK__LAST_FAILURE_PREFIX, \ + "shutdown", "terminate", "standby", "#", NULL } + +static int +compare_attribute(gconstpointer a, gconstpointer b) +{ + int rc; + + rc = strcmp((const char *)a, (const char *)b); + + return rc; +} + +/*! + * \internal + * \brief Determine whether extended information about an attribute should be added. + * + * \param[in] node Node that ran this resource + * \param[in,out] rsc_list List of resources for this node + * \param[in,out] data_set Cluster working set + * \param[in] attrname Attribute to find + * \param[out] expected_score Expected value for this attribute + * + * \return true if extended information should be printed, false otherwise + * \note Currently, extended information is only supported for ping/pingd + * resources, for which a message will be printed if connectivity is lost + * or degraded. + */ +static bool +add_extra_info(const pe_node_t *node, GList *rsc_list, pe_working_set_t *data_set, + const char *attrname, int *expected_score) +{ + GList *gIter = NULL; + + for (gIter = rsc_list; gIter != NULL; gIter = gIter->next) { + pe_resource_t *rsc = (pe_resource_t *) gIter->data; + const char *type = g_hash_table_lookup(rsc->meta, "type"); + const char *name = NULL; + GHashTable *params = NULL; + + if (rsc->children != NULL) { + if (add_extra_info(node, rsc->children, data_set, attrname, + expected_score)) { + return true; + } + } + + if (!pcmk__strcase_any_of(type, "ping", "pingd", NULL)) { + continue; + } + + params = pe_rsc_params(rsc, node, data_set); + name = g_hash_table_lookup(params, "name"); + + if (name == NULL) { + name = "pingd"; + } + + /* To identify the resource with the attribute name. */ + if (pcmk__str_eq(name, attrname, pcmk__str_casei)) { + int host_list_num = 0; + const char *hosts = g_hash_table_lookup(params, "host_list"); + const char *multiplier = g_hash_table_lookup(params, "multiplier"); + int multiplier_i; + + if (hosts) { + char **host_list = g_strsplit(hosts, " ", 0); + host_list_num = g_strv_length(host_list); + g_strfreev(host_list); + } + + if ((multiplier == NULL) + || (pcmk__scan_min_int(multiplier, &multiplier_i, + INT_MIN) != pcmk_rc_ok)) { + /* The ocf:pacemaker:ping resource agent defaults multiplier to + * 1. The agent currently does not handle invalid text, but it + * should, and this would be a reasonable choice ... + */ + multiplier_i = 1; + } + *expected_score = host_list_num * multiplier_i; + + return true; + } + } + return false; +} + +static GList * +filter_attr_list(GList *attr_list, char *name) +{ + int i; + const char *filt_str[] = FILTER_STR; + + CRM_CHECK(name != NULL, return attr_list); + + /* filtering automatic attributes */ + for (i = 0; filt_str[i] != NULL; i++) { + if (g_str_has_prefix(name, filt_str[i])) { + return attr_list; + } + } + + return g_list_insert_sorted(attr_list, name, compare_attribute); +} + +static GList * +get_operation_list(xmlNode *rsc_entry) { + GList *op_list = NULL; + xmlNode *rsc_op = NULL; + + for (rsc_op = pcmk__xe_first_child(rsc_entry); rsc_op != NULL; + rsc_op = pcmk__xe_next(rsc_op)) { + const char *task = crm_element_value(rsc_op, XML_LRM_ATTR_TASK); + const char *interval_ms_s = crm_element_value(rsc_op, + XML_LRM_ATTR_INTERVAL_MS); + const char *op_rc = crm_element_value(rsc_op, XML_LRM_ATTR_RC); + int op_rc_i; + + pcmk__scan_min_int(op_rc, &op_rc_i, 0); + + /* Display 0-interval monitors as "probe" */ + if (pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_casei) + && pcmk__str_eq(interval_ms_s, "0", pcmk__str_null_matches | pcmk__str_casei)) { + task = "probe"; + } + + /* Ignore notifies and some probes */ + if (pcmk__str_eq(task, CRMD_ACTION_NOTIFY, pcmk__str_casei) || (pcmk__str_eq(task, "probe", pcmk__str_casei) && (op_rc_i == 7))) { + continue; + } + + if (pcmk__str_eq((const char *)rsc_op->name, XML_LRM_TAG_RSC_OP, pcmk__str_none)) { + op_list = g_list_append(op_list, rsc_op); + } + } + + op_list = g_list_sort(op_list, sort_op_by_callid); + return op_list; +} + +static void +add_dump_node(gpointer key, gpointer value, gpointer user_data) +{ + xmlNodePtr node = user_data; + pcmk_create_xml_text_node(node, (const char *) key, (const char *) value); +} + +static void +append_dump_text(gpointer key, gpointer value, gpointer user_data) +{ + char **dump_text = user_data; + char *new_text = crm_strdup_printf("%s %s=%s", + *dump_text, (char *)key, (char *)value); + + free(*dump_text); + *dump_text = new_text; +} + +static const char * +get_cluster_stack(pe_working_set_t *data_set) +{ + xmlNode *stack = get_xpath_object("//nvpair[@name='cluster-infrastructure']", + data_set->input, LOG_DEBUG); + return stack? crm_element_value(stack, XML_NVPAIR_ATTR_VALUE) : "unknown"; +} + +static char * +last_changed_string(const char *last_written, const char *user, + const char *client, const char *origin) { + if (last_written != NULL || user != NULL || client != NULL || origin != NULL) { + return crm_strdup_printf("%s%s%s%s%s%s%s", + last_written ? last_written : "", + user ? " by " : "", + user ? user : "", + client ? " via " : "", + client ? client : "", + origin ? " on " : "", + origin ? origin : ""); + } else { + return strdup(""); + } +} + +static char * +op_history_string(xmlNode *xml_op, const char *task, const char *interval_ms_s, + int rc, bool print_timing) { + const char *call = crm_element_value(xml_op, XML_LRM_ATTR_CALLID); + char *interval_str = NULL; + char *buf = NULL; + + if (interval_ms_s && !pcmk__str_eq(interval_ms_s, "0", pcmk__str_casei)) { + char *pair = pcmk__format_nvpair("interval", interval_ms_s, "ms"); + interval_str = crm_strdup_printf(" %s", pair); + free(pair); + } + + if (print_timing) { + char *last_change_str = NULL; + char *exec_str = NULL; + char *queue_str = NULL; + + const char *value = NULL; + + time_t epoch = 0; + + if ((crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, &epoch) == pcmk_ok) + && (epoch > 0)) { + char *epoch_str = pcmk__epoch2str(&epoch, 0); + + last_change_str = crm_strdup_printf(" %s=\"%s\"", + XML_RSC_OP_LAST_CHANGE, + pcmk__s(epoch_str, "")); + free(epoch_str); + } + + value = crm_element_value(xml_op, XML_RSC_OP_T_EXEC); + if (value) { + char *pair = pcmk__format_nvpair(XML_RSC_OP_T_EXEC, value, "ms"); + exec_str = crm_strdup_printf(" %s", pair); + free(pair); + } + + value = crm_element_value(xml_op, XML_RSC_OP_T_QUEUE); + if (value) { + char *pair = pcmk__format_nvpair(XML_RSC_OP_T_QUEUE, value, "ms"); + queue_str = crm_strdup_printf(" %s", pair); + free(pair); + } + + buf = crm_strdup_printf("(%s) %s:%s%s%s%s rc=%d (%s)", call, task, + interval_str ? interval_str : "", + last_change_str ? last_change_str : "", + exec_str ? exec_str : "", + queue_str ? queue_str : "", + rc, services_ocf_exitcode_str(rc)); + + if (last_change_str) { + free(last_change_str); + } + + if (exec_str) { + free(exec_str); + } + + if (queue_str) { + free(queue_str); + } + } else { + buf = crm_strdup_printf("(%s) %s%s%s", call, task, + interval_str ? ":" : "", + interval_str ? interval_str : ""); + } + + if (interval_str) { + free(interval_str); + } + + return buf; +} + +static char * +resource_history_string(pe_resource_t *rsc, const char *rsc_id, bool all, + int failcount, time_t last_failure) { + char *buf = NULL; + + if (rsc == NULL) { + buf = crm_strdup_printf("%s: orphan", rsc_id); + } else if (all || failcount || last_failure > 0) { + char *failcount_s = NULL; + char *lastfail_s = NULL; + + if (failcount > 0) { + failcount_s = crm_strdup_printf(" %s=%d", PCMK__FAIL_COUNT_PREFIX, + failcount); + } else { + failcount_s = strdup(""); + } + if (last_failure > 0) { + buf = pcmk__epoch2str(&last_failure, 0); + lastfail_s = crm_strdup_printf(" %s='%s'", + PCMK__LAST_FAILURE_PREFIX, buf); + free(buf); + } + + buf = crm_strdup_printf("%s: migration-threshold=%d%s%s", + rsc_id, rsc->migration_threshold, failcount_s, + lastfail_s? lastfail_s : ""); + free(failcount_s); + free(lastfail_s); + } else { + buf = crm_strdup_printf("%s:", rsc_id); + } + + return buf; +} + +static const char * +get_node_feature_set(pe_node_t *node) { + const char *feature_set = NULL; + + if (node->details->online && !pe__is_guest_or_remote_node(node)) { + feature_set = g_hash_table_lookup(node->details->attrs, + CRM_ATTR_FEATURE_SET); + /* The feature set attribute is present since 3.15.1. If it is missing + * then the node must be running an earlier version. */ + if (feature_set == NULL) { + feature_set = "<3.15.1"; + } + } + return feature_set; +} + +static bool +is_mixed_version(pe_working_set_t *data_set) { + const char *feature_set = NULL; + for (GList *gIter = data_set->nodes; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = gIter->data; + const char *node_feature_set = get_node_feature_set(node); + if (node_feature_set != NULL) { + if (feature_set == NULL) { + feature_set = node_feature_set; + } else if (strcmp(feature_set, node_feature_set) != 0) { + return true; + } + } + } + return false; +} + +static char * +formatted_xml_buf(pe_resource_t *rsc, bool raw) +{ + if (raw) { + return dump_xml_formatted(rsc->orig_xml ? rsc->orig_xml : rsc->xml); + } else { + return dump_xml_formatted(rsc->xml); + } +} + +PCMK__OUTPUT_ARGS("cluster-summary", "pe_working_set_t *", + "enum pcmk_pacemakerd_state", "uint32_t", "uint32_t") +static int +cluster_summary(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + enum pcmk_pacemakerd_state pcmkd_state = + (enum pcmk_pacemakerd_state) va_arg(args, int); + uint32_t section_opts = va_arg(args, uint32_t); + uint32_t show_opts = va_arg(args, uint32_t); + + int rc = pcmk_rc_no_output; + const char *stack_s = get_cluster_stack(data_set); + + if (pcmk_is_set(section_opts, pcmk_section_stack)) { + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-stack", stack_s, pcmkd_state); + } + + if (pcmk_is_set(section_opts, pcmk_section_dc)) { + xmlNode *dc_version = get_xpath_object("//nvpair[@name='dc-version']", + data_set->input, LOG_DEBUG); + const char *dc_version_s = dc_version? + crm_element_value(dc_version, XML_NVPAIR_ATTR_VALUE) + : NULL; + const char *quorum = crm_element_value(data_set->input, XML_ATTR_HAVE_QUORUM); + char *dc_name = data_set->dc_node ? pe__node_display_name(data_set->dc_node, pcmk_is_set(show_opts, pcmk_show_node_id)) : NULL; + bool mixed_version = is_mixed_version(data_set); + + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-dc", data_set->dc_node, quorum, + dc_version_s, dc_name, mixed_version); + free(dc_name); + } + + if (pcmk_is_set(section_opts, pcmk_section_times)) { + const char *last_written = crm_element_value(data_set->input, XML_CIB_ATTR_WRITTEN); + const char *user = crm_element_value(data_set->input, XML_ATTR_UPDATE_USER); + const char *client = crm_element_value(data_set->input, XML_ATTR_UPDATE_CLIENT); + const char *origin = crm_element_value(data_set->input, XML_ATTR_UPDATE_ORIG); + + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-times", + data_set->localhost, last_written, user, client, origin); + } + + if (pcmk_is_set(section_opts, pcmk_section_counts)) { + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-counts", g_list_length(data_set->nodes), + data_set->ninstances, data_set->disabled_resources, + data_set->blocked_resources); + } + + if (pcmk_is_set(section_opts, pcmk_section_options)) { + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-options", data_set); + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + + if (pcmk_is_set(section_opts, pcmk_section_maint_mode)) { + if (out->message(out, "maint-mode", data_set->flags) == pcmk_rc_ok) { + rc = pcmk_rc_ok; + } + } + + return rc; +} + +PCMK__OUTPUT_ARGS("cluster-summary", "pe_working_set_t *", + "enum pcmk_pacemakerd_state", "uint32_t", "uint32_t") +static int +cluster_summary_html(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + enum pcmk_pacemakerd_state pcmkd_state = + (enum pcmk_pacemakerd_state) va_arg(args, int); + uint32_t section_opts = va_arg(args, uint32_t); + uint32_t show_opts = va_arg(args, uint32_t); + + int rc = pcmk_rc_no_output; + const char *stack_s = get_cluster_stack(data_set); + + if (pcmk_is_set(section_opts, pcmk_section_stack)) { + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-stack", stack_s, pcmkd_state); + } + + /* Always print DC if none, even if not requested */ + if (data_set->dc_node == NULL || pcmk_is_set(section_opts, pcmk_section_dc)) { + xmlNode *dc_version = get_xpath_object("//nvpair[@name='dc-version']", + data_set->input, LOG_DEBUG); + const char *dc_version_s = dc_version? + crm_element_value(dc_version, XML_NVPAIR_ATTR_VALUE) + : NULL; + const char *quorum = crm_element_value(data_set->input, XML_ATTR_HAVE_QUORUM); + char *dc_name = data_set->dc_node ? pe__node_display_name(data_set->dc_node, pcmk_is_set(show_opts, pcmk_show_node_id)) : NULL; + bool mixed_version = is_mixed_version(data_set); + + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-dc", data_set->dc_node, quorum, + dc_version_s, dc_name, mixed_version); + free(dc_name); + } + + if (pcmk_is_set(section_opts, pcmk_section_times)) { + const char *last_written = crm_element_value(data_set->input, XML_CIB_ATTR_WRITTEN); + const char *user = crm_element_value(data_set->input, XML_ATTR_UPDATE_USER); + const char *client = crm_element_value(data_set->input, XML_ATTR_UPDATE_CLIENT); + const char *origin = crm_element_value(data_set->input, XML_ATTR_UPDATE_ORIG); + + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-times", + data_set->localhost, last_written, user, client, origin); + } + + if (pcmk_is_set(section_opts, pcmk_section_counts)) { + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Cluster Summary"); + out->message(out, "cluster-counts", g_list_length(data_set->nodes), + data_set->ninstances, data_set->disabled_resources, + data_set->blocked_resources); + } + + if (pcmk_is_set(section_opts, pcmk_section_options)) { + /* Kind of a hack - close the list we may have opened earlier in this + * function so we can put all the options into their own list. We + * only want to do this on HTML output, though. + */ + PCMK__OUTPUT_LIST_FOOTER(out, rc); + + out->begin_list(out, NULL, NULL, "Config Options"); + out->message(out, "cluster-options", data_set); + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + + if (pcmk_is_set(section_opts, pcmk_section_maint_mode)) { + if (out->message(out, "maint-mode", data_set->flags) == pcmk_rc_ok) { + rc = pcmk_rc_ok; + } + } + + return rc; +} + +char * +pe__node_display_name(pe_node_t *node, bool print_detail) +{ + char *node_name; + const char *node_host = NULL; + const char *node_id = NULL; + int name_len; + + CRM_ASSERT((node != NULL) && (node->details != NULL) && (node->details->uname != NULL)); + + /* Host is displayed only if this is a guest node and detail is requested */ + if (print_detail && pe__is_guest_node(node)) { + const pe_resource_t *container = node->details->remote_rsc->container; + const pe_node_t *host_node = pe__current_node(container); + + if (host_node && host_node->details) { + node_host = host_node->details->uname; + } + if (node_host == NULL) { + node_host = ""; /* so we at least get "uname@" to indicate guest */ + } + } + + /* Node ID is displayed if different from uname and detail is requested */ + if (print_detail && !pcmk__str_eq(node->details->uname, node->details->id, pcmk__str_casei)) { + node_id = node->details->id; + } + + /* Determine name length */ + name_len = strlen(node->details->uname) + 1; + if (node_host) { + name_len += strlen(node_host) + 1; /* "@node_host" */ + } + if (node_id) { + name_len += strlen(node_id) + 3; /* + " (node_id)" */ + } + + /* Allocate and populate display name */ + node_name = malloc(name_len); + CRM_ASSERT(node_name != NULL); + strcpy(node_name, node->details->uname); + if (node_host) { + strcat(node_name, "@"); + strcat(node_name, node_host); + } + if (node_id) { + strcat(node_name, " ("); + strcat(node_name, node_id); + strcat(node_name, ")"); + } + return node_name; +} + +int +pe__name_and_nvpairs_xml(pcmk__output_t *out, bool is_list, const char *tag_name + , size_t pairs_count, ...) +{ + xmlNodePtr xml_node = NULL; + va_list args; + + CRM_ASSERT(tag_name != NULL); + + xml_node = pcmk__output_xml_peek_parent(out); + CRM_ASSERT(xml_node != NULL); + xml_node = is_list + ? create_xml_node(xml_node, tag_name) + : xmlNewChild(xml_node, NULL, (pcmkXmlStr) tag_name, NULL); + + va_start(args, pairs_count); + while(pairs_count--) { + const char *param_name = va_arg(args, const char *); + const char *param_value = va_arg(args, const char *); + if (param_name && param_value) { + crm_xml_add(xml_node, param_name, param_value); + } + }; + va_end(args); + + if (is_list) { + pcmk__output_xml_push_parent(out, xml_node); + } + return pcmk_rc_ok; +} + +static const char * +role_desc(enum rsc_role_e role) +{ + if (role == RSC_ROLE_PROMOTED) { +#ifdef PCMK__COMPAT_2_0 + return "as " RSC_ROLE_PROMOTED_LEGACY_S " "; +#else + return "in " RSC_ROLE_PROMOTED_S " role "; +#endif + } + return ""; +} + +PCMK__OUTPUT_ARGS("ban", "pe_node_t *", "pe__location_t *", "uint32_t") +static int +ban_html(pcmk__output_t *out, va_list args) { + pe_node_t *pe_node = va_arg(args, pe_node_t *); + pe__location_t *location = va_arg(args, pe__location_t *); + uint32_t show_opts = va_arg(args, uint32_t); + + char *node_name = pe__node_display_name(pe_node, + pcmk_is_set(show_opts, pcmk_show_node_id)); + char *buf = crm_strdup_printf("%s\tprevents %s from running %son %s", + location->id, location->rsc_lh->id, + role_desc(location->role_filter), node_name); + + pcmk__output_create_html_node(out, "li", NULL, NULL, buf); + + free(node_name); + free(buf); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("ban", "pe_node_t *", "pe__location_t *", "uint32_t") +static int +ban_text(pcmk__output_t *out, va_list args) { + pe_node_t *pe_node = va_arg(args, pe_node_t *); + pe__location_t *location = va_arg(args, pe__location_t *); + uint32_t show_opts = va_arg(args, uint32_t); + + char *node_name = pe__node_display_name(pe_node, + pcmk_is_set(show_opts, pcmk_show_node_id)); + out->list_item(out, NULL, "%s\tprevents %s from running %son %s", + location->id, location->rsc_lh->id, + role_desc(location->role_filter), node_name); + + free(node_name); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("ban", "pe_node_t *", "pe__location_t *", "uint32_t") +static int +ban_xml(pcmk__output_t *out, va_list args) { + pe_node_t *pe_node = va_arg(args, pe_node_t *); + pe__location_t *location = va_arg(args, pe__location_t *); + uint32_t show_opts G_GNUC_UNUSED = va_arg(args, uint32_t); + + const char *promoted_only = pcmk__btoa(location->role_filter == RSC_ROLE_PROMOTED); + char *weight_s = pcmk__itoa(pe_node->weight); + + pcmk__output_create_xml_node(out, "ban", + "id", location->id, + "resource", location->rsc_lh->id, + "node", pe_node->details->uname, + "weight", weight_s, + "promoted-only", promoted_only, + /* This is a deprecated alias for + * promoted_only. Removing it will break + * backward compatibility of the API schema, + * which will require an API schema major + * version bump. + */ + "master_only", promoted_only, + NULL); + + free(weight_s); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("ban-list", "pe_working_set_t *", "const char *", "GList *", + "uint32_t", "bool") +static int +ban_list(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + const char *prefix = va_arg(args, const char *); + GList *only_rsc = va_arg(args, GList *); + uint32_t show_opts = va_arg(args, uint32_t); + bool print_spacer = va_arg(args, int); + + GList *gIter, *gIter2; + int rc = pcmk_rc_no_output; + + /* Print each ban */ + for (gIter = data_set->placement_constraints; gIter != NULL; gIter = gIter->next) { + pe__location_t *location = gIter->data; + const pe_resource_t *rsc = location->rsc_lh; + + if (prefix != NULL && !g_str_has_prefix(location->id, prefix)) { + continue; + } + + if (!pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, + pcmk__str_star_matches) + && !pcmk__str_in_list(rsc_printable_id(pe__const_top_resource(rsc, false)), + only_rsc, pcmk__str_star_matches)) { + continue; + } + + for (gIter2 = location->node_list_rh; gIter2 != NULL; gIter2 = gIter2->next) { + pe_node_t *node = (pe_node_t *) gIter2->data; + + if (node->weight < 0) { + PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Negative Location Constraints"); + out->message(out, "ban", node, location, show_opts); + } + } + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +PCMK__OUTPUT_ARGS("cluster-counts", "unsigned int", "int", "int", "int") +static int +cluster_counts_html(pcmk__output_t *out, va_list args) { + unsigned int nnodes = va_arg(args, unsigned int); + int nresources = va_arg(args, int); + int ndisabled = va_arg(args, int); + int nblocked = va_arg(args, int); + + xmlNodePtr nodes_node = pcmk__output_create_xml_node(out, "li", NULL); + xmlNodePtr resources_node = pcmk__output_create_xml_node(out, "li", NULL); + + char *nnodes_str = crm_strdup_printf("%d node%s configured", + nnodes, pcmk__plural_s(nnodes)); + + pcmk_create_html_node(nodes_node, "span", NULL, NULL, nnodes_str); + free(nnodes_str); + + if (ndisabled && nblocked) { + char *s = crm_strdup_printf("%d resource instance%s configured (%d ", + nresources, pcmk__plural_s(nresources), + ndisabled); + pcmk_create_html_node(resources_node, "span", NULL, NULL, s); + free(s); + + pcmk_create_html_node(resources_node, "span", NULL, "bold", "DISABLED"); + + s = crm_strdup_printf(", %d ", nblocked); + pcmk_create_html_node(resources_node, "span", NULL, NULL, s); + free(s); + + pcmk_create_html_node(resources_node, "span", NULL, "bold", "BLOCKED"); + pcmk_create_html_node(resources_node, "span", NULL, NULL, + " from further action due to failure)"); + } else if (ndisabled && !nblocked) { + char *s = crm_strdup_printf("%d resource instance%s configured (%d ", + nresources, pcmk__plural_s(nresources), + ndisabled); + pcmk_create_html_node(resources_node, "span", NULL, NULL, s); + free(s); + + pcmk_create_html_node(resources_node, "span", NULL, "bold", "DISABLED"); + pcmk_create_html_node(resources_node, "span", NULL, NULL, ")"); + } else if (!ndisabled && nblocked) { + char *s = crm_strdup_printf("%d resource instance%s configured (%d ", + nresources, pcmk__plural_s(nresources), + nblocked); + pcmk_create_html_node(resources_node, "span", NULL, NULL, s); + free(s); + + pcmk_create_html_node(resources_node, "span", NULL, "bold", "BLOCKED"); + pcmk_create_html_node(resources_node, "span", NULL, NULL, + " from further action due to failure)"); + } else { + char *s = crm_strdup_printf("%d resource instance%s configured", + nresources, pcmk__plural_s(nresources)); + pcmk_create_html_node(resources_node, "span", NULL, NULL, s); + free(s); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-counts", "unsigned int", "int", "int", "int") +static int +cluster_counts_text(pcmk__output_t *out, va_list args) { + unsigned int nnodes = va_arg(args, unsigned int); + int nresources = va_arg(args, int); + int ndisabled = va_arg(args, int); + int nblocked = va_arg(args, int); + + out->list_item(out, NULL, "%d node%s configured", + nnodes, pcmk__plural_s(nnodes)); + + if (ndisabled && nblocked) { + out->list_item(out, NULL, "%d resource instance%s configured " + "(%d DISABLED, %d BLOCKED from " + "further action due to failure)", + nresources, pcmk__plural_s(nresources), ndisabled, + nblocked); + } else if (ndisabled && !nblocked) { + out->list_item(out, NULL, "%d resource instance%s configured " + "(%d DISABLED)", + nresources, pcmk__plural_s(nresources), ndisabled); + } else if (!ndisabled && nblocked) { + out->list_item(out, NULL, "%d resource instance%s configured " + "(%d BLOCKED from further action " + "due to failure)", + nresources, pcmk__plural_s(nresources), nblocked); + } else { + out->list_item(out, NULL, "%d resource instance%s configured", + nresources, pcmk__plural_s(nresources)); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-counts", "unsigned int", "int", "int", "int") +static int +cluster_counts_xml(pcmk__output_t *out, va_list args) { + unsigned int nnodes = va_arg(args, unsigned int); + int nresources = va_arg(args, int); + int ndisabled = va_arg(args, int); + int nblocked = va_arg(args, int); + + xmlNodePtr nodes_node = pcmk__output_create_xml_node(out, "nodes_configured", NULL); + xmlNodePtr resources_node = pcmk__output_create_xml_node(out, "resources_configured", NULL); + + char *s = pcmk__itoa(nnodes); + crm_xml_add(nodes_node, "number", s); + free(s); + + s = pcmk__itoa(nresources); + crm_xml_add(resources_node, "number", s); + free(s); + + s = pcmk__itoa(ndisabled); + crm_xml_add(resources_node, "disabled", s); + free(s); + + s = pcmk__itoa(nblocked); + crm_xml_add(resources_node, "blocked", s); + free(s); + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-dc", "pe_node_t *", "const char *", "const char *", + "char *", "int") +static int +cluster_dc_html(pcmk__output_t *out, va_list args) { + pe_node_t *dc = va_arg(args, pe_node_t *); + const char *quorum = va_arg(args, const char *); + const char *dc_version_s = va_arg(args, const char *); + char *dc_name = va_arg(args, char *); + bool mixed_version = va_arg(args, int); + + xmlNodePtr node = pcmk__output_create_xml_node(out, "li", NULL); + + pcmk_create_html_node(node, "span", NULL, "bold", "Current DC: "); + + if (dc) { + char *buf = crm_strdup_printf("%s (version %s) -", dc_name, + dc_version_s ? dc_version_s : "unknown"); + pcmk_create_html_node(node, "span", NULL, NULL, buf); + free(buf); + + if (mixed_version) { + pcmk_create_html_node(node, "span", NULL, "warning", + " MIXED-VERSION"); + } + pcmk_create_html_node(node, "span", NULL, NULL, " partition"); + if (crm_is_true(quorum)) { + pcmk_create_html_node(node, "span", NULL, NULL, " with"); + } else { + pcmk_create_html_node(node, "span", NULL, "warning", " WITHOUT"); + } + pcmk_create_html_node(node, "span", NULL, NULL, " quorum"); + } else { + pcmk_create_html_node(node, "span", NULL, "warning", "NONE"); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-dc", "pe_node_t *", "const char *", "const char *", + "char *", "int") +static int +cluster_dc_text(pcmk__output_t *out, va_list args) { + pe_node_t *dc = va_arg(args, pe_node_t *); + const char *quorum = va_arg(args, const char *); + const char *dc_version_s = va_arg(args, const char *); + char *dc_name = va_arg(args, char *); + bool mixed_version = va_arg(args, int); + + if (dc) { + out->list_item(out, "Current DC", + "%s (version %s) - %spartition %s quorum", + dc_name, dc_version_s ? dc_version_s : "unknown", + mixed_version ? "MIXED-VERSION " : "", + crm_is_true(quorum) ? "with" : "WITHOUT"); + } else { + out->list_item(out, "Current DC", "NONE"); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-dc", "pe_node_t *", "const char *", "const char *", + "char *", "int") +static int +cluster_dc_xml(pcmk__output_t *out, va_list args) { + pe_node_t *dc = va_arg(args, pe_node_t *); + const char *quorum = va_arg(args, const char *); + const char *dc_version_s = va_arg(args, const char *); + char *dc_name G_GNUC_UNUSED = va_arg(args, char *); + bool mixed_version = va_arg(args, int); + + if (dc) { + pcmk__output_create_xml_node(out, "current_dc", + "present", "true", + "version", dc_version_s ? dc_version_s : "", + "name", dc->details->uname, + "id", dc->details->id, + "with_quorum", pcmk__btoa(crm_is_true(quorum)), + "mixed_version", pcmk__btoa(mixed_version), + NULL); + } else { + pcmk__output_create_xml_node(out, "current_dc", + "present", "false", + NULL); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("maint-mode", "unsigned long long int") +static int +cluster_maint_mode_text(pcmk__output_t *out, va_list args) { + unsigned long long flags = va_arg(args, unsigned long long); + + if (pcmk_is_set(flags, pe_flag_maintenance_mode)) { + pcmk__formatted_printf(out, "\n *** Resource management is DISABLED ***\n"); + pcmk__formatted_printf(out, " The cluster will not attempt to start, stop or recover services\n"); + return pcmk_rc_ok; + } else if (pcmk_is_set(flags, pe_flag_stop_everything)) { + pcmk__formatted_printf(out, "\n *** Resource management is DISABLED ***\n"); + pcmk__formatted_printf(out, " The cluster will keep all resources stopped\n"); + return pcmk_rc_ok; + } else { + return pcmk_rc_no_output; + } +} + +PCMK__OUTPUT_ARGS("cluster-options", "pe_working_set_t *") +static int +cluster_options_html(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + + out->list_item(out, NULL, "STONITH of failed nodes %s", + pcmk_is_set(data_set->flags, pe_flag_stonith_enabled) ? "enabled" : "disabled"); + + out->list_item(out, NULL, "Cluster is %s", + pcmk_is_set(data_set->flags, pe_flag_symmetric_cluster) ? "symmetric" : "asymmetric"); + + switch (data_set->no_quorum_policy) { + case no_quorum_freeze: + out->list_item(out, NULL, "No quorum policy: Freeze resources"); + break; + + case no_quorum_stop: + out->list_item(out, NULL, "No quorum policy: Stop ALL resources"); + break; + + case no_quorum_demote: + out->list_item(out, NULL, "No quorum policy: Demote promotable " + "resources and stop all other resources"); + break; + + case no_quorum_ignore: + out->list_item(out, NULL, "No quorum policy: Ignore"); + break; + + case no_quorum_suicide: + out->list_item(out, NULL, "No quorum policy: Suicide"); + break; + } + + if (pcmk_is_set(data_set->flags, pe_flag_maintenance_mode)) { + xmlNodePtr node = pcmk__output_create_xml_node(out, "li", NULL); + + pcmk_create_html_node(node, "span", NULL, NULL, "Resource management: "); + pcmk_create_html_node(node, "span", NULL, "bold", "DISABLED"); + pcmk_create_html_node(node, "span", NULL, NULL, + " (the cluster will not attempt to start, stop, or recover services)"); + } else if (pcmk_is_set(data_set->flags, pe_flag_stop_everything)) { + xmlNodePtr node = pcmk__output_create_xml_node(out, "li", NULL); + + pcmk_create_html_node(node, "span", NULL, NULL, "Resource management: "); + pcmk_create_html_node(node, "span", NULL, "bold", "STOPPED"); + pcmk_create_html_node(node, "span", NULL, NULL, + " (the cluster will keep all resources stopped)"); + } else { + out->list_item(out, NULL, "Resource management: enabled"); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-options", "pe_working_set_t *") +static int +cluster_options_log(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + + if (pcmk_is_set(data_set->flags, pe_flag_maintenance_mode)) { + return out->info(out, "Resource management is DISABLED. The cluster will not attempt to start, stop or recover services."); + } else if (pcmk_is_set(data_set->flags, pe_flag_stop_everything)) { + return out->info(out, "Resource management is DISABLED. The cluster has stopped all resources."); + } else { + return pcmk_rc_no_output; + } +} + +PCMK__OUTPUT_ARGS("cluster-options", "pe_working_set_t *") +static int +cluster_options_text(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + + out->list_item(out, NULL, "STONITH of failed nodes %s", + pcmk_is_set(data_set->flags, pe_flag_stonith_enabled) ? "enabled" : "disabled"); + + out->list_item(out, NULL, "Cluster is %s", + pcmk_is_set(data_set->flags, pe_flag_symmetric_cluster) ? "symmetric" : "asymmetric"); + + switch (data_set->no_quorum_policy) { + case no_quorum_freeze: + out->list_item(out, NULL, "No quorum policy: Freeze resources"); + break; + + case no_quorum_stop: + out->list_item(out, NULL, "No quorum policy: Stop ALL resources"); + break; + + case no_quorum_demote: + out->list_item(out, NULL, "No quorum policy: Demote promotable " + "resources and stop all other resources"); + break; + + case no_quorum_ignore: + out->list_item(out, NULL, "No quorum policy: Ignore"); + break; + + case no_quorum_suicide: + out->list_item(out, NULL, "No quorum policy: Suicide"); + break; + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-options", "pe_working_set_t *") +static int +cluster_options_xml(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + + const char *no_quorum_policy = NULL; + char *stonith_timeout_str = pcmk__itoa(data_set->stonith_timeout); + char *priority_fencing_delay_str = pcmk__itoa(data_set->priority_fencing_delay * 1000); + + switch (data_set->no_quorum_policy) { + case no_quorum_freeze: + no_quorum_policy = "freeze"; + break; + + case no_quorum_stop: + no_quorum_policy = "stop"; + break; + + case no_quorum_demote: + no_quorum_policy = "demote"; + break; + + case no_quorum_ignore: + no_quorum_policy = "ignore"; + break; + + case no_quorum_suicide: + no_quorum_policy = "suicide"; + break; + } + + pcmk__output_create_xml_node(out, "cluster_options", + "stonith-enabled", pcmk__btoa(pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)), + "symmetric-cluster", pcmk__btoa(pcmk_is_set(data_set->flags, pe_flag_symmetric_cluster)), + "no-quorum-policy", no_quorum_policy, + "maintenance-mode", pcmk__btoa(pcmk_is_set(data_set->flags, pe_flag_maintenance_mode)), + "stop-all-resources", pcmk__btoa(pcmk_is_set(data_set->flags, pe_flag_stop_everything)), + "stonith-timeout-ms", stonith_timeout_str, + "priority-fencing-delay-ms", priority_fencing_delay_str, + NULL); + free(stonith_timeout_str); + free(priority_fencing_delay_str); + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-stack", "const char *", "enum pcmk_pacemakerd_state") +static int +cluster_stack_html(pcmk__output_t *out, va_list args) { + const char *stack_s = va_arg(args, const char *); + enum pcmk_pacemakerd_state pcmkd_state = + (enum pcmk_pacemakerd_state) va_arg(args, int); + + xmlNodePtr node = pcmk__output_create_xml_node(out, "li", NULL); + + pcmk_create_html_node(node, "span", NULL, "bold", "Stack: "); + pcmk_create_html_node(node, "span", NULL, NULL, stack_s); + + if (pcmkd_state != pcmk_pacemakerd_state_invalid) { + pcmk_create_html_node(node, "span", NULL, NULL, " ("); + pcmk_create_html_node(node, "span", NULL, NULL, + pcmk__pcmkd_state_enum2friendly(pcmkd_state)); + pcmk_create_html_node(node, "span", NULL, NULL, ")"); + } + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-stack", "const char *", "enum pcmk_pacemakerd_state") +static int +cluster_stack_text(pcmk__output_t *out, va_list args) { + const char *stack_s = va_arg(args, const char *); + enum pcmk_pacemakerd_state pcmkd_state = + (enum pcmk_pacemakerd_state) va_arg(args, int); + + if (pcmkd_state != pcmk_pacemakerd_state_invalid) { + out->list_item(out, "Stack", "%s (%s)", + stack_s, pcmk__pcmkd_state_enum2friendly(pcmkd_state)); + } else { + out->list_item(out, "Stack", "%s", stack_s); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-stack", "const char *", "enum pcmk_pacemakerd_state") +static int +cluster_stack_xml(pcmk__output_t *out, va_list args) { + const char *stack_s = va_arg(args, const char *); + enum pcmk_pacemakerd_state pcmkd_state = + (enum pcmk_pacemakerd_state) va_arg(args, int); + + const char *state_s = NULL; + + if (pcmkd_state != pcmk_pacemakerd_state_invalid) { + state_s = pcmk_pacemakerd_api_daemon_state_enum2text(pcmkd_state); + } + + pcmk__output_create_xml_node(out, "stack", + "type", stack_s, + "pacemakerd-state", state_s, + NULL); + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-times", "const char *", "const char *", + "const char *", "const char *", "const char *") +static int +cluster_times_html(pcmk__output_t *out, va_list args) { + const char *our_nodename = va_arg(args, const char *); + const char *last_written = va_arg(args, const char *); + const char *user = va_arg(args, const char *); + const char *client = va_arg(args, const char *); + const char *origin = va_arg(args, const char *); + + xmlNodePtr updated_node = pcmk__output_create_xml_node(out, "li", NULL); + xmlNodePtr changed_node = pcmk__output_create_xml_node(out, "li", NULL); + + char *time_s = pcmk__epoch2str(NULL, 0); + + pcmk_create_html_node(updated_node, "span", NULL, "bold", "Last updated: "); + pcmk_create_html_node(updated_node, "span", NULL, NULL, time_s); + + if (our_nodename != NULL) { + pcmk_create_html_node(updated_node, "span", NULL, NULL, " on "); + pcmk_create_html_node(updated_node, "span", NULL, NULL, our_nodename); + } + + free(time_s); + time_s = last_changed_string(last_written, user, client, origin); + + pcmk_create_html_node(changed_node, "span", NULL, "bold", "Last change: "); + pcmk_create_html_node(changed_node, "span", NULL, NULL, time_s); + + free(time_s); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-times", "const char *", "const char *", + "const char *", "const char *", "const char *") +static int +cluster_times_xml(pcmk__output_t *out, va_list args) { + const char *our_nodename = va_arg(args, const char *); + const char *last_written = va_arg(args, const char *); + const char *user = va_arg(args, const char *); + const char *client = va_arg(args, const char *); + const char *origin = va_arg(args, const char *); + + char *time_s = pcmk__epoch2str(NULL, 0); + + pcmk__output_create_xml_node(out, "last_update", + "time", time_s, + "origin", our_nodename, + NULL); + + pcmk__output_create_xml_node(out, "last_change", + "time", last_written ? last_written : "", + "user", user ? user : "", + "client", client ? client : "", + "origin", origin ? origin : "", + NULL); + + free(time_s); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("cluster-times", "const char *", "const char *", + "const char *", "const char *", "const char *") +static int +cluster_times_text(pcmk__output_t *out, va_list args) { + const char *our_nodename = va_arg(args, const char *); + const char *last_written = va_arg(args, const char *); + const char *user = va_arg(args, const char *); + const char *client = va_arg(args, const char *); + const char *origin = va_arg(args, const char *); + + char *time_s = pcmk__epoch2str(NULL, 0); + + out->list_item(out, "Last updated", "%s%s%s", + time_s, (our_nodename != NULL)? " on " : "", + pcmk__s(our_nodename, "")); + + free(time_s); + time_s = last_changed_string(last_written, user, client, origin); + + out->list_item(out, "Last change", " %s", time_s); + + free(time_s); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Display a failed action in less-technical natural language + * + * \param[in,out] out Output object to use for display + * \param[in] xml_op XML containing failed action + * \param[in] op_key Operation key of failed action + * \param[in] node_name Where failed action occurred + * \param[in] rc OCF exit code of failed action + * \param[in] status Execution status of failed action + * \param[in] exit_reason Exit reason given for failed action + * \param[in] exec_time String containing execution time in milliseconds + */ +static void +failed_action_friendly(pcmk__output_t *out, const xmlNode *xml_op, + const char *op_key, const char *node_name, int rc, + int status, const char *exit_reason, + const char *exec_time) +{ + char *rsc_id = NULL; + char *task = NULL; + guint interval_ms = 0; + time_t last_change_epoch = 0; + GString *str = NULL; + + if (pcmk__str_empty(op_key) + || !parse_op_key(op_key, &rsc_id, &task, &interval_ms)) { + rsc_id = strdup("unknown resource"); + task = strdup("unknown action"); + interval_ms = 0; + } + CRM_ASSERT((rsc_id != NULL) && (task != NULL)); + + str = g_string_sized_new(256); // Should be sufficient for most messages + + pcmk__g_strcat(str, rsc_id, " ", NULL); + + if (interval_ms != 0) { + pcmk__g_strcat(str, pcmk__readable_interval(interval_ms), "-interval ", + NULL); + } + pcmk__g_strcat(str, crm_action_str(task, interval_ms), " on ", node_name, + NULL); + + if (status == PCMK_EXEC_DONE) { + pcmk__g_strcat(str, " returned '", services_ocf_exitcode_str(rc), "'", + NULL); + if (!pcmk__str_empty(exit_reason)) { + pcmk__g_strcat(str, " (", exit_reason, ")", NULL); + } + + } else { + pcmk__g_strcat(str, " could not be executed (", + pcmk_exec_status_str(status), NULL); + if (!pcmk__str_empty(exit_reason)) { + pcmk__g_strcat(str, ": ", exit_reason, NULL); + } + g_string_append_c(str, ')'); + } + + + if (crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, + &last_change_epoch) == pcmk_ok) { + char *s = pcmk__epoch2str(&last_change_epoch, 0); + + pcmk__g_strcat(str, " at ", s, NULL); + free(s); + } + if (!pcmk__str_empty(exec_time)) { + int exec_time_ms = 0; + + if ((pcmk__scan_min_int(exec_time, &exec_time_ms, 0) == pcmk_rc_ok) + && (exec_time_ms > 0)) { + + pcmk__g_strcat(str, " after ", + pcmk__readable_interval(exec_time_ms), NULL); + } + } + + out->list_item(out, NULL, "%s", str->str); + g_string_free(str, TRUE); + free(rsc_id); + free(task); +} + +/*! + * \internal + * \brief Display a failed action with technical details + * + * \param[in,out] out Output object to use for display + * \param[in] xml_op XML containing failed action + * \param[in] op_key Operation key of failed action + * \param[in] node_name Where failed action occurred + * \param[in] rc OCF exit code of failed action + * \param[in] status Execution status of failed action + * \param[in] exit_reason Exit reason given for failed action + * \param[in] exec_time String containing execution time in milliseconds + */ +static void +failed_action_technical(pcmk__output_t *out, const xmlNode *xml_op, + const char *op_key, const char *node_name, int rc, + int status, const char *exit_reason, + const char *exec_time) +{ + const char *call_id = crm_element_value(xml_op, XML_LRM_ATTR_CALLID); + const char *queue_time = crm_element_value(xml_op, XML_RSC_OP_T_QUEUE); + const char *exit_status = services_ocf_exitcode_str(rc); + const char *lrm_status = pcmk_exec_status_str(status); + time_t last_change_epoch = 0; + GString *str = NULL; + + if (pcmk__str_empty(op_key)) { + op_key = "unknown operation"; + } + if (pcmk__str_empty(exit_status)) { + exit_status = "unknown exit status"; + } + if (pcmk__str_empty(call_id)) { + call_id = "unknown"; + } + + str = g_string_sized_new(256); + + g_string_append_printf(str, "%s on %s '%s' (%d): call=%s, status='%s'", + op_key, node_name, exit_status, rc, call_id, + lrm_status); + + if (!pcmk__str_empty(exit_reason)) { + pcmk__g_strcat(str, ", exitreason='", exit_reason, "'", NULL); + } + + if (crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, + &last_change_epoch) == pcmk_ok) { + char *last_change_str = pcmk__epoch2str(&last_change_epoch, 0); + + pcmk__g_strcat(str, + ", " XML_RSC_OP_LAST_CHANGE "=" + "'", last_change_str, "'", NULL); + free(last_change_str); + } + if (!pcmk__str_empty(queue_time)) { + pcmk__g_strcat(str, ", queued=", queue_time, "ms", NULL); + } + if (!pcmk__str_empty(exec_time)) { + pcmk__g_strcat(str, ", exec=", exec_time, "ms", NULL); + } + + out->list_item(out, NULL, "%s", str->str); + g_string_free(str, TRUE); +} + +PCMK__OUTPUT_ARGS("failed-action", "xmlNodePtr", "uint32_t") +static int +failed_action_default(pcmk__output_t *out, va_list args) +{ + xmlNodePtr xml_op = va_arg(args, xmlNodePtr); + uint32_t show_opts = va_arg(args, uint32_t); + + const char *op_key = pe__xe_history_key(xml_op); + const char *node_name = crm_element_value(xml_op, XML_ATTR_UNAME); + const char *exit_reason = crm_element_value(xml_op, + XML_LRM_ATTR_EXIT_REASON); + const char *exec_time = crm_element_value(xml_op, XML_RSC_OP_T_EXEC); + + int rc; + int status; + + pcmk__scan_min_int(crm_element_value(xml_op, XML_LRM_ATTR_RC), &rc, 0); + + pcmk__scan_min_int(crm_element_value(xml_op, XML_LRM_ATTR_OPSTATUS), + &status, 0); + + if (pcmk__str_empty(node_name)) { + node_name = "unknown node"; + } + + if (pcmk_is_set(show_opts, pcmk_show_failed_detail)) { + failed_action_technical(out, xml_op, op_key, node_name, rc, status, + exit_reason, exec_time); + } else { + failed_action_friendly(out, xml_op, op_key, node_name, rc, status, + exit_reason, exec_time); + } + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("failed-action", "xmlNodePtr", "uint32_t") +static int +failed_action_xml(pcmk__output_t *out, va_list args) { + xmlNodePtr xml_op = va_arg(args, xmlNodePtr); + uint32_t show_opts G_GNUC_UNUSED = va_arg(args, uint32_t); + + const char *op_key = pe__xe_history_key(xml_op); + const char *op_key_name = "op_key"; + int rc; + int status; + const char *exit_reason = crm_element_value(xml_op, XML_LRM_ATTR_EXIT_REASON); + + time_t epoch = 0; + char *rc_s = NULL; + char *reason_s = crm_xml_escape(exit_reason ? exit_reason : "none"); + xmlNodePtr node = NULL; + + pcmk__scan_min_int(crm_element_value(xml_op, XML_LRM_ATTR_RC), &rc, 0); + pcmk__scan_min_int(crm_element_value(xml_op, XML_LRM_ATTR_OPSTATUS), + &status, 0); + + rc_s = pcmk__itoa(rc); + if (crm_element_value(xml_op, XML_LRM_ATTR_TASK_KEY) == NULL) { + op_key_name = "id"; + } + node = pcmk__output_create_xml_node(out, "failure", + op_key_name, op_key, + "node", crm_element_value(xml_op, XML_ATTR_UNAME), + "exitstatus", services_ocf_exitcode_str(rc), + "exitreason", pcmk__s(reason_s, ""), + "exitcode", rc_s, + "call", crm_element_value(xml_op, XML_LRM_ATTR_CALLID), + "status", pcmk_exec_status_str(status), + NULL); + free(rc_s); + + if ((crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, + &epoch) == pcmk_ok) && (epoch > 0)) { + guint interval_ms = 0; + char *interval_ms_s = NULL; + char *rc_change = pcmk__epoch2str(&epoch, + crm_time_log_date + |crm_time_log_timeofday + |crm_time_log_with_timezone); + + crm_element_value_ms(xml_op, XML_LRM_ATTR_INTERVAL_MS, &interval_ms); + interval_ms_s = crm_strdup_printf("%u", interval_ms); + + pcmk__xe_set_props(node, XML_RSC_OP_LAST_CHANGE, rc_change, + "queued", crm_element_value(xml_op, XML_RSC_OP_T_QUEUE), + "exec", crm_element_value(xml_op, XML_RSC_OP_T_EXEC), + "interval", interval_ms_s, + "task", crm_element_value(xml_op, XML_LRM_ATTR_TASK), + NULL); + + free(interval_ms_s); + free(rc_change); + } + + free(reason_s); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("failed-action-list", "pe_working_set_t *", "GList *", + "GList *", "uint32_t", "bool") +static int +failed_action_list(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + uint32_t show_opts = va_arg(args, uint32_t); + bool print_spacer = va_arg(args, int); + + xmlNode *xml_op = NULL; + int rc = pcmk_rc_no_output; + + if (xmlChildElementCount(data_set->failed) == 0) { + return rc; + } + + for (xml_op = pcmk__xml_first_child(data_set->failed); xml_op != NULL; + xml_op = pcmk__xml_next(xml_op)) { + char *rsc = NULL; + + if (!pcmk__str_in_list(crm_element_value(xml_op, XML_ATTR_UNAME), only_node, + pcmk__str_star_matches|pcmk__str_casei)) { + continue; + } + + if (pcmk_xe_mask_probe_failure(xml_op)) { + continue; + } + + if (!parse_op_key(pe__xe_history_key(xml_op), &rsc, NULL, NULL)) { + continue; + } + + if (!pcmk__str_in_list(rsc, only_rsc, pcmk__str_star_matches)) { + free(rsc); + continue; + } + + free(rsc); + + PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Failed Resource Actions"); + out->message(out, "failed-action", xml_op, show_opts); + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +static void +status_node(pe_node_t *node, xmlNodePtr parent, uint32_t show_opts) +{ + int health = pe__node_health(node); + + // Cluster membership + if (node->details->online) { + pcmk_create_html_node(parent, "span", NULL, "online", " online"); + } else { + pcmk_create_html_node(parent, "span", NULL, "offline", " OFFLINE"); + } + + // Standby mode + if (node->details->standby_onfail && (node->details->running_rsc != NULL)) { + pcmk_create_html_node(parent, "span", NULL, "standby", + " (in standby due to on-fail," + " with active resources)"); + } else if (node->details->standby_onfail) { + pcmk_create_html_node(parent, "span", NULL, "standby", + " (in standby due to on-fail)"); + } else if (node->details->standby && (node->details->running_rsc != NULL)) { + pcmk_create_html_node(parent, "span", NULL, "standby", + " (in standby, with active resources)"); + } else if (node->details->standby) { + pcmk_create_html_node(parent, "span", NULL, "standby", " (in standby)"); + } + + // Maintenance mode + if (node->details->maintenance) { + pcmk_create_html_node(parent, "span", NULL, "maint", + " (in maintenance mode)"); + } + + // Node health + if (health < 0) { + pcmk_create_html_node(parent, "span", NULL, "health_red", + " (health is RED)"); + } else if (health == 0) { + pcmk_create_html_node(parent, "span", NULL, "health_yellow", + " (health is YELLOW)"); + } + + // Feature set + if (pcmk_is_set(show_opts, pcmk_show_feature_set)) { + const char *feature_set = get_node_feature_set(node); + if (feature_set != NULL) { + char *buf = crm_strdup_printf(", feature set %s", feature_set); + pcmk_create_html_node(parent, "span", NULL, NULL, buf); + free(buf); + } + } +} + +PCMK__OUTPUT_ARGS("node", "pe_node_t *", "uint32_t", "bool", + "GList *", "GList *") +static int +node_html(pcmk__output_t *out, va_list args) { + pe_node_t *node = va_arg(args, pe_node_t *); + uint32_t show_opts = va_arg(args, uint32_t); + bool full = va_arg(args, int); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + char *node_name = pe__node_display_name(node, pcmk_is_set(show_opts, pcmk_show_node_id)); + + if (full) { + xmlNodePtr item_node; + + if (pcmk_all_flags_set(show_opts, pcmk_show_brief | pcmk_show_rscs_by_node)) { + GList *rscs = pe__filter_rsc_list(node->details->running_rsc, only_rsc); + + out->begin_list(out, NULL, NULL, "%s:", node_name); + item_node = pcmk__output_xml_create_parent(out, "li", NULL); + pcmk_create_html_node(item_node, "span", NULL, NULL, "Status:"); + status_node(node, item_node, show_opts); + + if (rscs != NULL) { + uint32_t new_show_opts = (show_opts | pcmk_show_rsc_only) & ~pcmk_show_inactive_rscs; + out->begin_list(out, NULL, NULL, "Resources"); + pe__rscs_brief_output(out, rscs, new_show_opts); + out->end_list(out); + } + + pcmk__output_xml_pop_parent(out); + out->end_list(out); + + } else if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) { + GList *lpc2 = NULL; + int rc = pcmk_rc_no_output; + + out->begin_list(out, NULL, NULL, "%s:", node_name); + item_node = pcmk__output_xml_create_parent(out, "li", NULL); + pcmk_create_html_node(item_node, "span", NULL, NULL, "Status:"); + status_node(node, item_node, show_opts); + + for (lpc2 = node->details->running_rsc; lpc2 != NULL; lpc2 = lpc2->next) { + pe_resource_t *rsc = (pe_resource_t *) lpc2->data; + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Resources"); + + show_opts |= pcmk_show_rsc_only; + out->message(out, crm_map_element_name(rsc->xml), show_opts, + rsc, only_node, only_rsc); + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + pcmk__output_xml_pop_parent(out); + out->end_list(out); + + } else { + char *buf = crm_strdup_printf("%s:", node_name); + + item_node = pcmk__output_create_xml_node(out, "li", NULL); + pcmk_create_html_node(item_node, "span", NULL, "bold", buf); + status_node(node, item_node, show_opts); + + free(buf); + } + } else { + out->begin_list(out, NULL, NULL, "%s:", node_name); + } + + free(node_name); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Get a human-friendly textual description of a node's status + * + * \param[in] node Node to check + * + * \return String representation of node's status + */ +static const char * +node_text_status(const pe_node_t *node) +{ + if (node->details->unclean) { + if (node->details->online) { + return "UNCLEAN (online)"; + + } else if (node->details->pending) { + return "UNCLEAN (pending)"; + + } else { + return "UNCLEAN (offline)"; + } + + } else if (node->details->pending) { + return "pending"; + + } else if (node->details->standby_onfail && node->details->online) { + return "standby (on-fail)"; + + } else if (node->details->standby) { + if (node->details->online) { + if (node->details->running_rsc) { + return "standby (with active resources)"; + } else { + return "standby"; + } + } else { + return "OFFLINE (standby)"; + } + + } else if (node->details->maintenance) { + if (node->details->online) { + return "maintenance"; + } else { + return "OFFLINE (maintenance)"; + } + + } else if (node->details->online) { + return "online"; + } + + return "OFFLINE"; +} + +PCMK__OUTPUT_ARGS("node", "pe_node_t *", "uint32_t", "bool", "GList *", "GList *") +static int +node_text(pcmk__output_t *out, va_list args) { + pe_node_t *node = va_arg(args, pe_node_t *); + uint32_t show_opts = va_arg(args, uint32_t); + bool full = va_arg(args, int); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + if (full) { + char *node_name = pe__node_display_name(node, pcmk_is_set(show_opts, pcmk_show_node_id)); + GString *str = g_string_sized_new(64); + int health = pe__node_health(node); + + // Create a summary line with node type, name, and status + if (pe__is_guest_node(node)) { + g_string_append(str, "GuestNode"); + } else if (pe__is_remote_node(node)) { + g_string_append(str, "RemoteNode"); + } else { + g_string_append(str, "Node"); + } + pcmk__g_strcat(str, " ", node_name, ": ", node_text_status(node), NULL); + + if (health < 0) { + g_string_append(str, " (health is RED)"); + } else if (health == 0) { + g_string_append(str, " (health is YELLOW)"); + } + if (pcmk_is_set(show_opts, pcmk_show_feature_set)) { + const char *feature_set = get_node_feature_set(node); + if (feature_set != NULL) { + pcmk__g_strcat(str, ", feature set ", feature_set, NULL); + } + } + + /* If we're grouping by node, print its resources */ + if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) { + if (pcmk_is_set(show_opts, pcmk_show_brief)) { + GList *rscs = pe__filter_rsc_list(node->details->running_rsc, only_rsc); + + if (rscs != NULL) { + uint32_t new_show_opts = (show_opts | pcmk_show_rsc_only) & ~pcmk_show_inactive_rscs; + out->begin_list(out, NULL, NULL, "%s", str->str); + out->begin_list(out, NULL, NULL, "Resources"); + + pe__rscs_brief_output(out, rscs, new_show_opts); + + out->end_list(out); + out->end_list(out); + + g_list_free(rscs); + } + + } else { + GList *gIter2 = NULL; + + out->begin_list(out, NULL, NULL, "%s", str->str); + out->begin_list(out, NULL, NULL, "Resources"); + + for (gIter2 = node->details->running_rsc; gIter2 != NULL; gIter2 = gIter2->next) { + pe_resource_t *rsc = (pe_resource_t *) gIter2->data; + + show_opts |= pcmk_show_rsc_only; + out->message(out, crm_map_element_name(rsc->xml), show_opts, + rsc, only_node, only_rsc); + } + + out->end_list(out); + out->end_list(out); + } + } else { + out->list_item(out, NULL, "%s", str->str); + } + + g_string_free(str, TRUE); + free(node_name); + } else { + char *node_name = pe__node_display_name(node, pcmk_is_set(show_opts, pcmk_show_node_id)); + out->begin_list(out, NULL, NULL, "Node: %s", node_name); + free(node_name); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node", "pe_node_t *", "uint32_t", "bool", "GList *", "GList *") +static int +node_xml(pcmk__output_t *out, va_list args) { + pe_node_t *node = va_arg(args, pe_node_t *); + uint32_t show_opts G_GNUC_UNUSED = va_arg(args, uint32_t); + bool full = va_arg(args, int); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + if (full) { + const char *node_type = "unknown"; + char *length_s = pcmk__itoa(g_list_length(node->details->running_rsc)); + int health = pe__node_health(node); + const char *health_s = NULL; + const char *feature_set; + + switch (node->details->type) { + case node_member: + node_type = "member"; + break; + case node_remote: + node_type = "remote"; + break; + case node_ping: + node_type = "ping"; + break; + } + + if (health < 0) { + health_s = "red"; + } else if (health == 0) { + health_s = "yellow"; + } else { + health_s = "green"; + } + + feature_set = get_node_feature_set(node); + + pe__name_and_nvpairs_xml(out, true, "node", 15, + "name", node->details->uname, + "id", node->details->id, + "online", pcmk__btoa(node->details->online), + "standby", pcmk__btoa(node->details->standby), + "standby_onfail", pcmk__btoa(node->details->standby_onfail), + "maintenance", pcmk__btoa(node->details->maintenance), + "pending", pcmk__btoa(node->details->pending), + "unclean", pcmk__btoa(node->details->unclean), + "health", health_s, + "feature_set", feature_set, + "shutdown", pcmk__btoa(node->details->shutdown), + "expected_up", pcmk__btoa(node->details->expected_up), + "is_dc", pcmk__btoa(node->details->is_dc), + "resources_running", length_s, + "type", node_type); + + if (pe__is_guest_node(node)) { + xmlNodePtr xml_node = pcmk__output_xml_peek_parent(out); + crm_xml_add(xml_node, "id_as_resource", node->details->remote_rsc->container->id); + } + + if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) { + GList *lpc = NULL; + + for (lpc = node->details->running_rsc; lpc != NULL; lpc = lpc->next) { + pe_resource_t *rsc = (pe_resource_t *) lpc->data; + + show_opts |= pcmk_show_rsc_only; + out->message(out, crm_map_element_name(rsc->xml), show_opts, + rsc, only_node, only_rsc); + } + } + + free(length_s); + + out->end_list(out); + } else { + pcmk__output_xml_create_parent(out, "node", + "name", node->details->uname, + NULL); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-attribute", "const char *", "const char *", "bool", "int") +static int +node_attribute_text(pcmk__output_t *out, va_list args) { + const char *name = va_arg(args, const char *); + const char *value = va_arg(args, const char *); + bool add_extra = va_arg(args, int); + int expected_score = va_arg(args, int); + + if (add_extra) { + int v; + + if (value == NULL) { + v = 0; + } else { + pcmk__scan_min_int(value, &v, INT_MIN); + } + if (v <= 0) { + out->list_item(out, NULL, "%-32s\t: %-10s\t: Connectivity is lost", name, value); + } else if (v < expected_score) { + out->list_item(out, NULL, "%-32s\t: %-10s\t: Connectivity is degraded (Expected=%d)", name, value, expected_score); + } else { + out->list_item(out, NULL, "%-32s\t: %-10s", name, value); + } + } else { + out->list_item(out, NULL, "%-32s\t: %-10s", name, value); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-attribute", "const char *", "const char *", "bool", "int") +static int +node_attribute_html(pcmk__output_t *out, va_list args) { + const char *name = va_arg(args, const char *); + const char *value = va_arg(args, const char *); + bool add_extra = va_arg(args, int); + int expected_score = va_arg(args, int); + + if (add_extra) { + int v; + char *s = crm_strdup_printf("%s: %s", name, value); + xmlNodePtr item_node = pcmk__output_create_xml_node(out, "li", NULL); + + if (value == NULL) { + v = 0; + } else { + pcmk__scan_min_int(value, &v, INT_MIN); + } + + pcmk_create_html_node(item_node, "span", NULL, NULL, s); + free(s); + + if (v <= 0) { + pcmk_create_html_node(item_node, "span", NULL, "bold", "(connectivity is lost)"); + } else if (v < expected_score) { + char *buf = crm_strdup_printf("(connectivity is degraded -- expected %d", expected_score); + pcmk_create_html_node(item_node, "span", NULL, "bold", buf); + free(buf); + } + } else { + out->list_item(out, NULL, "%s: %s", name, value); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-and-op", "pe_working_set_t *", "xmlNodePtr") +static int +node_and_op(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + xmlNodePtr xml_op = va_arg(args, xmlNodePtr); + + pe_resource_t *rsc = NULL; + gchar *node_str = NULL; + char *last_change_str = NULL; + + const char *op_rsc = crm_element_value(xml_op, "resource"); + int status; + time_t last_change = 0; + + pcmk__scan_min_int(crm_element_value(xml_op, XML_LRM_ATTR_OPSTATUS), + &status, PCMK_EXEC_UNKNOWN); + + rsc = pe_find_resource(data_set->resources, op_rsc); + + if (rsc) { + const pe_node_t *node = pe__current_node(rsc); + const char *target_role = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE); + uint32_t show_opts = pcmk_show_rsc_only | pcmk_show_pending; + + if (node == NULL) { + node = rsc->pending_node; + } + + node_str = pcmk__native_output_string(rsc, rsc_printable_id(rsc), node, + show_opts, target_role, false); + } else { + node_str = crm_strdup_printf("Unknown resource %s", op_rsc); + } + + if (crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, + &last_change) == pcmk_ok) { + last_change_str = crm_strdup_printf(", %s='%s', exec=%sms", + XML_RSC_OP_LAST_CHANGE, + pcmk__trim(ctime(&last_change)), + crm_element_value(xml_op, XML_RSC_OP_T_EXEC)); + } + + out->list_item(out, NULL, "%s: %s (node=%s, call=%s, rc=%s%s): %s", + node_str, pe__xe_history_key(xml_op), + crm_element_value(xml_op, XML_ATTR_UNAME), + crm_element_value(xml_op, XML_LRM_ATTR_CALLID), + crm_element_value(xml_op, XML_LRM_ATTR_RC), + last_change_str ? last_change_str : "", + pcmk_exec_status_str(status)); + + g_free(node_str); + free(last_change_str); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-and-op", "pe_working_set_t *", "xmlNodePtr") +static int +node_and_op_xml(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + xmlNodePtr xml_op = va_arg(args, xmlNodePtr); + + pe_resource_t *rsc = NULL; + const char *op_rsc = crm_element_value(xml_op, "resource"); + int status; + time_t last_change = 0; + xmlNode *node = NULL; + + pcmk__scan_min_int(crm_element_value(xml_op, XML_LRM_ATTR_OPSTATUS), + &status, PCMK_EXEC_UNKNOWN); + node = pcmk__output_create_xml_node(out, "operation", + "op", pe__xe_history_key(xml_op), + "node", crm_element_value(xml_op, XML_ATTR_UNAME), + "call", crm_element_value(xml_op, XML_LRM_ATTR_CALLID), + "rc", crm_element_value(xml_op, XML_LRM_ATTR_RC), + "status", pcmk_exec_status_str(status), + NULL); + + rsc = pe_find_resource(data_set->resources, op_rsc); + + if (rsc) { + const char *class = crm_element_value(rsc->xml, XML_AGENT_ATTR_CLASS); + const char *kind = crm_element_value(rsc->xml, XML_ATTR_TYPE); + char *agent_tuple = NULL; + + agent_tuple = crm_strdup_printf("%s:%s:%s", class, + pcmk_is_set(pcmk_get_ra_caps(class), pcmk_ra_cap_provider) ? crm_element_value(rsc->xml, XML_AGENT_ATTR_PROVIDER) : "", + kind); + + pcmk__xe_set_props(node, "rsc", rsc_printable_id(rsc), + "agent", agent_tuple, + NULL); + free(agent_tuple); + } + + if (crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, + &last_change) == pcmk_ok) { + pcmk__xe_set_props(node, XML_RSC_OP_LAST_CHANGE, + pcmk__trim(ctime(&last_change)), + XML_RSC_OP_T_EXEC, crm_element_value(xml_op, XML_RSC_OP_T_EXEC), + NULL); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-attribute", "const char *", "const char *", "bool", "int") +static int +node_attribute_xml(pcmk__output_t *out, va_list args) { + const char *name = va_arg(args, const char *); + const char *value = va_arg(args, const char *); + bool add_extra = va_arg(args, int); + int expected_score = va_arg(args, int); + + xmlNodePtr node = pcmk__output_create_xml_node(out, "attribute", + "name", name, + "value", value, + NULL); + + if (add_extra) { + char *buf = pcmk__itoa(expected_score); + crm_xml_add(node, "expected", buf); + free(buf); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-attribute-list", "pe_working_set_t *", "uint32_t", + "bool", "GList *", "GList *") +static int +node_attribute_list(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + uint32_t show_opts = va_arg(args, uint32_t); + bool print_spacer = va_arg(args, int); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + + int rc = pcmk_rc_no_output; + + /* Display each node's attributes */ + for (GList *gIter = data_set->nodes; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = gIter->data; + + GList *attr_list = NULL; + GHashTableIter iter; + gpointer key; + + if (!node || !node->details || !node->details->online) { + continue; + } + + g_hash_table_iter_init(&iter, node->details->attrs); + while (g_hash_table_iter_next (&iter, &key, NULL)) { + attr_list = filter_attr_list(attr_list, key); + } + + if (attr_list == NULL) { + continue; + } + + if (!pcmk__str_in_list(node->details->uname, only_node, pcmk__str_star_matches|pcmk__str_casei)) { + g_list_free(attr_list); + continue; + } + + PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Node Attributes"); + + out->message(out, "node", node, show_opts, false, only_node, only_rsc); + + for (GList *aIter = attr_list; aIter != NULL; aIter = aIter->next) { + const char *name = aIter->data; + const char *value = NULL; + int expected_score = 0; + bool add_extra = false; + + value = pe_node_attribute_raw(node, name); + + add_extra = add_extra_info(node, node->details->running_rsc, + data_set, name, &expected_score); + + /* Print attribute name and value */ + out->message(out, "node-attribute", name, value, add_extra, + expected_score); + } + + g_list_free(attr_list); + out->end_list(out); + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +PCMK__OUTPUT_ARGS("node-capacity", "const pe_node_t *", "const char *") +static int +node_capacity(pcmk__output_t *out, va_list args) +{ + const pe_node_t *node = va_arg(args, pe_node_t *); + const char *comment = va_arg(args, const char *); + + char *dump_text = crm_strdup_printf("%s: %s capacity:", + comment, pe__node_name(node)); + + g_hash_table_foreach(node->details->utilization, append_dump_text, &dump_text); + out->list_item(out, NULL, "%s", dump_text); + free(dump_text); + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-capacity", "const pe_node_t *", "const char *") +static int +node_capacity_xml(pcmk__output_t *out, va_list args) +{ + const pe_node_t *node = va_arg(args, pe_node_t *); + const char *comment = va_arg(args, const char *); + + xmlNodePtr xml_node = pcmk__output_create_xml_node(out, "capacity", + "node", node->details->uname, + "comment", comment, + NULL); + g_hash_table_foreach(node->details->utilization, add_dump_node, xml_node); + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-history-list", "pe_working_set_t *", "pe_node_t *", "xmlNodePtr", + "GList *", "GList *", "uint32_t", "uint32_t") +static int +node_history_list(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + pe_node_t *node = va_arg(args, pe_node_t *); + xmlNode *node_state = va_arg(args, xmlNode *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + uint32_t section_opts = va_arg(args, uint32_t); + uint32_t show_opts = va_arg(args, uint32_t); + + xmlNode *lrm_rsc = NULL; + xmlNode *rsc_entry = NULL; + int rc = pcmk_rc_no_output; + + lrm_rsc = find_xml_node(node_state, XML_CIB_TAG_LRM, FALSE); + lrm_rsc = find_xml_node(lrm_rsc, XML_LRM_TAG_RESOURCES, FALSE); + + /* Print history of each of the node's resources */ + for (rsc_entry = first_named_child(lrm_rsc, XML_LRM_TAG_RESOURCE); + rsc_entry != NULL; rsc_entry = crm_next_same_xml(rsc_entry)) { + const char *rsc_id = crm_element_value(rsc_entry, XML_ATTR_ID); + pe_resource_t *rsc = pe_find_resource(data_set->resources, rsc_id); + const pe_resource_t *parent = pe__const_top_resource(rsc, false); + + /* We can't use is_filtered here to filter group resources. For is_filtered, + * we have to decide whether to check the parent or not. If we check the + * parent, all elements of a group will always be printed because that's how + * is_filtered works for groups. If we do not check the parent, sometimes + * this will filter everything out. + * + * For other resource types, is_filtered is okay. + */ + if (parent->variant == pe_group) { + if (!pcmk__str_in_list(rsc_printable_id(rsc), only_rsc, + pcmk__str_star_matches) + && !pcmk__str_in_list(rsc_printable_id(parent), only_rsc, + pcmk__str_star_matches)) { + continue; + } + } else { + if (rsc->fns->is_filtered(rsc, only_rsc, TRUE)) { + continue; + } + } + + if (!pcmk_is_set(section_opts, pcmk_section_operations)) { + time_t last_failure = 0; + int failcount = pe_get_failcount(node, rsc, &last_failure, pe_fc_default, + NULL); + + if (failcount <= 0) { + continue; + } + + if (rc == pcmk_rc_no_output) { + rc = pcmk_rc_ok; + out->message(out, "node", node, show_opts, false, only_node, + only_rsc); + } + + out->message(out, "resource-history", rsc, rsc_id, false, + failcount, last_failure, false); + } else { + GList *op_list = get_operation_list(rsc_entry); + pe_resource_t *rsc = pe_find_resource(data_set->resources, + crm_element_value(rsc_entry, XML_ATTR_ID)); + + if (op_list == NULL) { + continue; + } + + if (rc == pcmk_rc_no_output) { + rc = pcmk_rc_ok; + out->message(out, "node", node, show_opts, false, only_node, + only_rsc); + } + + out->message(out, "resource-operation-list", data_set, rsc, node, + op_list, show_opts); + } + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +PCMK__OUTPUT_ARGS("node-list", "GList *", "GList *", "GList *", "uint32_t", "bool") +static int +node_list_html(pcmk__output_t *out, va_list args) { + GList *nodes = va_arg(args, GList *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + uint32_t show_opts = va_arg(args, uint32_t); + bool print_spacer G_GNUC_UNUSED = va_arg(args, int); + + int rc = pcmk_rc_no_output; + + for (GList *gIter = nodes; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = (pe_node_t *) gIter->data; + + if (!pcmk__str_in_list(node->details->uname, only_node, + pcmk__str_star_matches|pcmk__str_casei)) { + continue; + } + + PCMK__OUTPUT_LIST_HEADER(out, false, rc, "Node List"); + + out->message(out, "node", node, show_opts, true, only_node, only_rsc); + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +PCMK__OUTPUT_ARGS("node-list", "GList *", "GList *", "GList *", "uint32_t", "bool") +static int +node_list_text(pcmk__output_t *out, va_list args) { + GList *nodes = va_arg(args, GList *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + uint32_t show_opts = va_arg(args, uint32_t); + bool print_spacer = va_arg(args, int); + + /* space-separated lists of node names */ + GString *online_nodes = NULL; + GString *online_remote_nodes = NULL; + GString *online_guest_nodes = NULL; + GString *offline_nodes = NULL; + GString *offline_remote_nodes = NULL; + + int rc = pcmk_rc_no_output; + + for (GList *gIter = nodes; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = (pe_node_t *) gIter->data; + char *node_name = pe__node_display_name(node, pcmk_is_set(show_opts, pcmk_show_node_id)); + + if (!pcmk__str_in_list(node->details->uname, only_node, + pcmk__str_star_matches|pcmk__str_casei)) { + free(node_name); + continue; + } + + PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, "Node List"); + + // Determine whether to display node individually or in a list + if (node->details->unclean || node->details->pending + || (node->details->standby_onfail && node->details->online) + || node->details->standby || node->details->maintenance + || pcmk_is_set(show_opts, pcmk_show_rscs_by_node) + || pcmk_is_set(show_opts, pcmk_show_feature_set) + || (pe__node_health(node) <= 0)) { + // Display node individually + + } else if (node->details->online) { + // Display online node in a list + if (pe__is_guest_node(node)) { + pcmk__add_word(&online_guest_nodes, 1024, node_name); + + } else if (pe__is_remote_node(node)) { + pcmk__add_word(&online_remote_nodes, 1024, node_name); + + } else { + pcmk__add_word(&online_nodes, 1024, node_name); + } + free(node_name); + continue; + + } else { + // Display offline node in a list + if (pe__is_remote_node(node)) { + pcmk__add_word(&offline_remote_nodes, 1024, node_name); + + } else if (pe__is_guest_node(node)) { + /* ignore offline guest nodes */ + + } else { + pcmk__add_word(&offline_nodes, 1024, node_name); + } + free(node_name); + continue; + } + + /* If we get here, node is in bad state, or we're grouping by node */ + out->message(out, "node", node, show_opts, true, only_node, only_rsc); + free(node_name); + } + + /* If we're not grouping by node, summarize nodes by status */ + if (online_nodes != NULL) { + out->list_item(out, "Online", "[ %s ]", + (const char *) online_nodes->str); + g_string_free(online_nodes, TRUE); + } + if (offline_nodes != NULL) { + out->list_item(out, "OFFLINE", "[ %s ]", + (const char *) offline_nodes->str); + g_string_free(offline_nodes, TRUE); + } + if (online_remote_nodes) { + out->list_item(out, "RemoteOnline", "[ %s ]", + (const char *) online_remote_nodes->str); + g_string_free(online_remote_nodes, TRUE); + } + if (offline_remote_nodes) { + out->list_item(out, "RemoteOFFLINE", "[ %s ]", + (const char *) offline_remote_nodes->str); + g_string_free(offline_remote_nodes, TRUE); + } + if (online_guest_nodes != NULL) { + out->list_item(out, "GuestOnline", "[ %s ]", + (const char *) online_guest_nodes->str); + g_string_free(online_guest_nodes, TRUE); + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +PCMK__OUTPUT_ARGS("node-list", "GList *", "GList *", "GList *", "uint32_t", "bool") +static int +node_list_xml(pcmk__output_t *out, va_list args) { + GList *nodes = va_arg(args, GList *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + uint32_t show_opts = va_arg(args, uint32_t); + bool print_spacer G_GNUC_UNUSED = va_arg(args, int); + + out->begin_list(out, NULL, NULL, "nodes"); + for (GList *gIter = nodes; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = (pe_node_t *) gIter->data; + + if (!pcmk__str_in_list(node->details->uname, only_node, + pcmk__str_star_matches|pcmk__str_casei)) { + continue; + } + + out->message(out, "node", node, show_opts, true, only_node, only_rsc); + } + out->end_list(out); + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-summary", "pe_working_set_t *", "GList *", "GList *", + "uint32_t", "uint32_t", "bool") +static int +node_summary(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + uint32_t section_opts = va_arg(args, uint32_t); + uint32_t show_opts = va_arg(args, uint32_t); + bool print_spacer = va_arg(args, int); + + xmlNode *node_state = NULL; + xmlNode *cib_status = pcmk_find_cib_element(data_set->input, + XML_CIB_TAG_STATUS); + int rc = pcmk_rc_no_output; + + if (xmlChildElementCount(cib_status) == 0) { + return rc; + } + + for (node_state = first_named_child(cib_status, XML_CIB_TAG_STATE); + node_state != NULL; node_state = crm_next_same_xml(node_state)) { + pe_node_t *node = pe_find_node_id(data_set->nodes, ID(node_state)); + + if (!node || !node->details || !node->details->online) { + continue; + } + + if (!pcmk__str_in_list(node->details->uname, only_node, + pcmk__str_star_matches|pcmk__str_casei)) { + continue; + } + + PCMK__OUTPUT_LIST_HEADER(out, print_spacer, rc, + pcmk_is_set(section_opts, pcmk_section_operations) ? "Operations" : "Migration Summary"); + + out->message(out, "node-history-list", data_set, node, node_state, + only_node, only_rsc, section_opts, show_opts); + } + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +PCMK__OUTPUT_ARGS("node-weight", "const pe_resource_t *", "const char *", + "const char *", "const char *") +static int +node_weight(pcmk__output_t *out, va_list args) +{ + const pe_resource_t *rsc = va_arg(args, const pe_resource_t *); + const char *prefix = va_arg(args, const char *); + const char *uname = va_arg(args, const char *); + const char *score = va_arg(args, const char *); + + if (rsc) { + out->list_item(out, NULL, "%s: %s allocation score on %s: %s", + prefix, rsc->id, uname, score); + } else { + out->list_item(out, NULL, "%s: %s = %s", prefix, uname, score); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("node-weight", "const pe_resource_t *", "const char *", + "const char *", "const char *") +static int +node_weight_xml(pcmk__output_t *out, va_list args) +{ + const pe_resource_t *rsc = va_arg(args, const pe_resource_t *); + const char *prefix = va_arg(args, const char *); + const char *uname = va_arg(args, const char *); + const char *score = va_arg(args, const char *); + + xmlNodePtr node = pcmk__output_create_xml_node(out, "node_weight", + "function", prefix, + "node", uname, + "score", score, + NULL); + + if (rsc) { + crm_xml_add(node, "id", rsc->id); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("op-history", "xmlNodePtr", "const char *", "const char *", "int", "uint32_t") +static int +op_history_text(pcmk__output_t *out, va_list args) { + xmlNodePtr xml_op = va_arg(args, xmlNodePtr); + const char *task = va_arg(args, const char *); + const char *interval_ms_s = va_arg(args, const char *); + int rc = va_arg(args, int); + uint32_t show_opts = va_arg(args, uint32_t); + + char *buf = op_history_string(xml_op, task, interval_ms_s, rc, + pcmk_is_set(show_opts, pcmk_show_timing)); + + out->list_item(out, NULL, "%s", buf); + + free(buf); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("op-history", "xmlNodePtr", "const char *", "const char *", "int", "uint32_t") +static int +op_history_xml(pcmk__output_t *out, va_list args) { + xmlNodePtr xml_op = va_arg(args, xmlNodePtr); + const char *task = va_arg(args, const char *); + const char *interval_ms_s = va_arg(args, const char *); + int rc = va_arg(args, int); + uint32_t show_opts = va_arg(args, uint32_t); + + char *rc_s = pcmk__itoa(rc); + xmlNodePtr node = pcmk__output_create_xml_node(out, "operation_history", + "call", crm_element_value(xml_op, XML_LRM_ATTR_CALLID), + "task", task, + "rc", rc_s, + "rc_text", services_ocf_exitcode_str(rc), + NULL); + free(rc_s); + + if (interval_ms_s && !pcmk__str_eq(interval_ms_s, "0", pcmk__str_casei)) { + char *s = crm_strdup_printf("%sms", interval_ms_s); + crm_xml_add(node, "interval", s); + free(s); + } + + if (pcmk_is_set(show_opts, pcmk_show_timing)) { + const char *value = NULL; + time_t epoch = 0; + + if ((crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, + &epoch) == pcmk_ok) && (epoch > 0)) { + char *s = pcmk__epoch2str(&epoch, 0); + crm_xml_add(node, XML_RSC_OP_LAST_CHANGE, s); + free(s); + } + + value = crm_element_value(xml_op, XML_RSC_OP_T_EXEC); + if (value) { + char *s = crm_strdup_printf("%sms", value); + crm_xml_add(node, XML_RSC_OP_T_EXEC, s); + free(s); + } + value = crm_element_value(xml_op, XML_RSC_OP_T_QUEUE); + if (value) { + char *s = crm_strdup_printf("%sms", value); + crm_xml_add(node, XML_RSC_OP_T_QUEUE, s); + free(s); + } + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("promotion-score", "pe_resource_t *", "pe_node_t *", "const char *") +static int +promotion_score(pcmk__output_t *out, va_list args) +{ + pe_resource_t *child_rsc = va_arg(args, pe_resource_t *); + pe_node_t *chosen = va_arg(args, pe_node_t *); + const char *score = va_arg(args, const char *); + + out->list_item(out, NULL, "%s promotion score on %s: %s", + child_rsc->id, + chosen? chosen->details->uname : "none", + score); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("promotion-score", "pe_resource_t *", "pe_node_t *", "const char *") +static int +promotion_score_xml(pcmk__output_t *out, va_list args) +{ + pe_resource_t *child_rsc = va_arg(args, pe_resource_t *); + pe_node_t *chosen = va_arg(args, pe_node_t *); + const char *score = va_arg(args, const char *); + + xmlNodePtr node = pcmk__output_create_xml_node(out, "promotion_score", + "id", child_rsc->id, + "score", score, + NULL); + + if (chosen) { + crm_xml_add(node, "node", chosen->details->uname); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("resource-config", "pe_resource_t *", "bool") +static int +resource_config(pcmk__output_t *out, va_list args) { + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + bool raw = va_arg(args, int); + + char *rsc_xml = formatted_xml_buf(rsc, raw); + + out->output_xml(out, "xml", rsc_xml); + + free(rsc_xml); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("resource-config", "pe_resource_t *", "bool") +static int +resource_config_text(pcmk__output_t *out, va_list args) { + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + bool raw = va_arg(args, int); + + char *rsc_xml = formatted_xml_buf(rsc, raw); + + pcmk__formatted_printf(out, "Resource XML:\n"); + out->output_xml(out, "xml", rsc_xml); + + free(rsc_xml); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("resource-history", "pe_resource_t *", "const char *", "bool", "int", "time_t", "bool") +static int +resource_history_text(pcmk__output_t *out, va_list args) { + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + const char *rsc_id = va_arg(args, const char *); + bool all = va_arg(args, int); + int failcount = va_arg(args, int); + time_t last_failure = va_arg(args, time_t); + bool as_header = va_arg(args, int); + + char *buf = resource_history_string(rsc, rsc_id, all, failcount, last_failure); + + if (as_header) { + out->begin_list(out, NULL, NULL, "%s", buf); + } else { + out->list_item(out, NULL, "%s", buf); + } + + free(buf); + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("resource-history", "pe_resource_t *", "const char *", "bool", "int", "time_t", "bool") +static int +resource_history_xml(pcmk__output_t *out, va_list args) { + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + const char *rsc_id = va_arg(args, const char *); + bool all = va_arg(args, int); + int failcount = va_arg(args, int); + time_t last_failure = va_arg(args, time_t); + bool as_header = va_arg(args, int); + + xmlNodePtr node = pcmk__output_xml_create_parent(out, "resource_history", + "id", rsc_id, + NULL); + + if (rsc == NULL) { + pcmk__xe_set_bool_attr(node, "orphan", true); + } else if (all || failcount || last_failure > 0) { + char *migration_s = pcmk__itoa(rsc->migration_threshold); + + pcmk__xe_set_props(node, "orphan", "false", + "migration-threshold", migration_s, + NULL); + free(migration_s); + + if (failcount > 0) { + char *s = pcmk__itoa(failcount); + + crm_xml_add(node, PCMK__FAIL_COUNT_PREFIX, s); + free(s); + } + + if (last_failure > 0) { + char *s = pcmk__epoch2str(&last_failure, 0); + + crm_xml_add(node, PCMK__LAST_FAILURE_PREFIX, s); + free(s); + } + } + + if (!as_header) { + pcmk__output_xml_pop_parent(out); + } + + return pcmk_rc_ok; +} + +static void +print_resource_header(pcmk__output_t *out, uint32_t show_opts) +{ + if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) { + /* Active resources have already been printed by node */ + out->begin_list(out, NULL, NULL, "Inactive Resources"); + } else if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) { + out->begin_list(out, NULL, NULL, "Full List of Resources"); + } else { + out->begin_list(out, NULL, NULL, "Active Resources"); + } +} + + +PCMK__OUTPUT_ARGS("resource-list", "pe_working_set_t *", "uint32_t", "bool", + "GList *", "GList *", "bool") +static int +resource_list(pcmk__output_t *out, va_list args) +{ + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + uint32_t show_opts = va_arg(args, uint32_t); + bool print_summary = va_arg(args, int); + GList *only_node = va_arg(args, GList *); + GList *only_rsc = va_arg(args, GList *); + bool print_spacer = va_arg(args, int); + + GList *rsc_iter; + int rc = pcmk_rc_no_output; + bool printed_header = false; + + /* If we already showed active resources by node, and + * we're not showing inactive resources, we have nothing to do + */ + if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node) && + !pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) { + return rc; + } + + /* If we haven't already printed resources grouped by node, + * and brief output was requested, print resource summary */ + if (pcmk_is_set(show_opts, pcmk_show_brief) && !pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) { + GList *rscs = pe__filter_rsc_list(data_set->resources, only_rsc); + + PCMK__OUTPUT_SPACER_IF(out, print_spacer); + print_resource_header(out, show_opts); + printed_header = true; + + rc = pe__rscs_brief_output(out, rscs, show_opts); + g_list_free(rscs); + } + + /* For each resource, display it if appropriate */ + for (rsc_iter = data_set->resources; rsc_iter != NULL; rsc_iter = rsc_iter->next) { + pe_resource_t *rsc = (pe_resource_t *) rsc_iter->data; + int x; + + /* Complex resources may have some sub-resources active and some inactive */ + gboolean is_active = rsc->fns->active(rsc, TRUE); + gboolean partially_active = rsc->fns->active(rsc, FALSE); + + /* Skip inactive orphans (deleted but still in CIB) */ + if (pcmk_is_set(rsc->flags, pe_rsc_orphan) && !is_active) { + continue; + + /* Skip active resources if we already displayed them by node */ + } else if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) { + if (is_active) { + continue; + } + + /* Skip primitives already counted in a brief summary */ + } else if (pcmk_is_set(show_opts, pcmk_show_brief) && (rsc->variant == pe_native)) { + continue; + + /* Skip resources that aren't at least partially active, + * unless we're displaying inactive resources + */ + } else if (!partially_active && !pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) { + continue; + + } else if (partially_active && !pe__rsc_running_on_any(rsc, only_node)) { + continue; + } + + if (!printed_header) { + PCMK__OUTPUT_SPACER_IF(out, print_spacer); + print_resource_header(out, show_opts); + printed_header = true; + } + + /* Print this resource */ + x = out->message(out, crm_map_element_name(rsc->xml), show_opts, rsc, + only_node, only_rsc); + if (x == pcmk_rc_ok) { + rc = pcmk_rc_ok; + } + } + + if (print_summary && rc != pcmk_rc_ok) { + if (!printed_header) { + PCMK__OUTPUT_SPACER_IF(out, print_spacer); + print_resource_header(out, show_opts); + printed_header = true; + } + + if (pcmk_is_set(show_opts, pcmk_show_rscs_by_node)) { + out->list_item(out, NULL, "No inactive resources"); + } else if (pcmk_is_set(show_opts, pcmk_show_inactive_rscs)) { + out->list_item(out, NULL, "No resources"); + } else { + out->list_item(out, NULL, "No active resources"); + } + } + + if (printed_header) { + out->end_list(out); + } + + return rc; +} + +PCMK__OUTPUT_ARGS("resource-operation-list", "pe_working_set_t *", "pe_resource_t *", + "pe_node_t *", "GList *", "uint32_t") +static int +resource_operation_list(pcmk__output_t *out, va_list args) +{ + pe_working_set_t *data_set G_GNUC_UNUSED = va_arg(args, pe_working_set_t *); + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + pe_node_t *node = va_arg(args, pe_node_t *); + GList *op_list = va_arg(args, GList *); + uint32_t show_opts = va_arg(args, uint32_t); + + GList *gIter = NULL; + int rc = pcmk_rc_no_output; + + /* Print each operation */ + for (gIter = op_list; gIter != NULL; gIter = gIter->next) { + xmlNode *xml_op = (xmlNode *) gIter->data; + const char *task = crm_element_value(xml_op, XML_LRM_ATTR_TASK); + const char *interval_ms_s = crm_element_value(xml_op, + XML_LRM_ATTR_INTERVAL_MS); + const char *op_rc = crm_element_value(xml_op, XML_LRM_ATTR_RC); + int op_rc_i; + + pcmk__scan_min_int(op_rc, &op_rc_i, 0); + + /* Display 0-interval monitors as "probe" */ + if (pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_casei) + && pcmk__str_eq(interval_ms_s, "0", pcmk__str_null_matches | pcmk__str_casei)) { + task = "probe"; + } + + /* If this is the first printed operation, print heading for resource */ + if (rc == pcmk_rc_no_output) { + time_t last_failure = 0; + int failcount = pe_get_failcount(node, rsc, &last_failure, pe_fc_default, + NULL); + + out->message(out, "resource-history", rsc, rsc_printable_id(rsc), true, + failcount, last_failure, true); + rc = pcmk_rc_ok; + } + + /* Print the operation */ + out->message(out, "op-history", xml_op, task, interval_ms_s, + op_rc_i, show_opts); + } + + /* Free the list we created (no need to free the individual items) */ + g_list_free(op_list); + + PCMK__OUTPUT_LIST_FOOTER(out, rc); + return rc; +} + +PCMK__OUTPUT_ARGS("resource-util", "pe_resource_t *", "pe_node_t *", "const char *") +static int +resource_util(pcmk__output_t *out, va_list args) +{ + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + pe_node_t *node = va_arg(args, pe_node_t *); + const char *fn = va_arg(args, const char *); + + char *dump_text = crm_strdup_printf("%s: %s utilization on %s:", + fn, rsc->id, pe__node_name(node)); + + g_hash_table_foreach(rsc->utilization, append_dump_text, &dump_text); + out->list_item(out, NULL, "%s", dump_text); + free(dump_text); + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("resource-util", "pe_resource_t *", "pe_node_t *", "const char *") +static int +resource_util_xml(pcmk__output_t *out, va_list args) +{ + pe_resource_t *rsc = va_arg(args, pe_resource_t *); + pe_node_t *node = va_arg(args, pe_node_t *); + const char *fn = va_arg(args, const char *); + + xmlNodePtr xml_node = pcmk__output_create_xml_node(out, "utilization", + "resource", rsc->id, + "node", node->details->uname, + "function", fn, + NULL); + g_hash_table_foreach(rsc->utilization, add_dump_node, xml_node); + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("ticket", "pe_ticket_t *") +static int +ticket_html(pcmk__output_t *out, va_list args) { + pe_ticket_t *ticket = va_arg(args, pe_ticket_t *); + + if (ticket->last_granted > -1) { + char *epoch_str = pcmk__epoch2str(&(ticket->last_granted), 0); + + out->list_item(out, NULL, "%s:\t%s%s %s=\"%s\"", ticket->id, + ticket->granted ? "granted" : "revoked", + ticket->standby ? " [standby]" : "", + "last-granted", pcmk__s(epoch_str, "")); + free(epoch_str); + } else { + out->list_item(out, NULL, "%s:\t%s%s", ticket->id, + ticket->granted ? "granted" : "revoked", + ticket->standby ? " [standby]" : ""); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("ticket", "pe_ticket_t *") +static int +ticket_text(pcmk__output_t *out, va_list args) { + pe_ticket_t *ticket = va_arg(args, pe_ticket_t *); + + if (ticket->last_granted > -1) { + char *epoch_str = pcmk__epoch2str(&(ticket->last_granted), 0); + + out->list_item(out, ticket->id, "%s%s %s=\"%s\"", + ticket->granted ? "granted" : "revoked", + ticket->standby ? " [standby]" : "", + "last-granted", pcmk__s(epoch_str, "")); + free(epoch_str); + } else { + out->list_item(out, ticket->id, "%s%s", + ticket->granted ? "granted" : "revoked", + ticket->standby ? " [standby]" : ""); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("ticket", "pe_ticket_t *") +static int +ticket_xml(pcmk__output_t *out, va_list args) { + pe_ticket_t *ticket = va_arg(args, pe_ticket_t *); + + xmlNodePtr node = NULL; + + node = pcmk__output_create_xml_node(out, "ticket", + "id", ticket->id, + "status", ticket->granted ? "granted" : "revoked", + "standby", pcmk__btoa(ticket->standby), + NULL); + + if (ticket->last_granted > -1) { + char *buf = pcmk__epoch2str(&ticket->last_granted, 0); + + crm_xml_add(node, "last-granted", buf); + free(buf); + } + + return pcmk_rc_ok; +} + +PCMK__OUTPUT_ARGS("ticket-list", "pe_working_set_t *", "bool") +static int +ticket_list(pcmk__output_t *out, va_list args) { + pe_working_set_t *data_set = va_arg(args, pe_working_set_t *); + bool print_spacer = va_arg(args, int); + + GHashTableIter iter; + gpointer key, value; + + if (g_hash_table_size(data_set->tickets) == 0) { + return pcmk_rc_no_output; + } + + PCMK__OUTPUT_SPACER_IF(out, print_spacer); + + /* Print section heading */ + out->begin_list(out, NULL, NULL, "Tickets"); + + /* Print each ticket */ + g_hash_table_iter_init(&iter, data_set->tickets); + while (g_hash_table_iter_next(&iter, &key, &value)) { + pe_ticket_t *ticket = (pe_ticket_t *) value; + out->message(out, "ticket", ticket); + } + + /* Close section */ + out->end_list(out); + return pcmk_rc_ok; +} + +static pcmk__message_entry_t fmt_functions[] = { + { "ban", "default", ban_text }, + { "ban", "html", ban_html }, + { "ban", "xml", ban_xml }, + { "ban-list", "default", ban_list }, + { "bundle", "default", pe__bundle_text }, + { "bundle", "xml", pe__bundle_xml }, + { "bundle", "html", pe__bundle_html }, + { "clone", "default", pe__clone_default }, + { "clone", "xml", pe__clone_xml }, + { "cluster-counts", "default", cluster_counts_text }, + { "cluster-counts", "html", cluster_counts_html }, + { "cluster-counts", "xml", cluster_counts_xml }, + { "cluster-dc", "default", cluster_dc_text }, + { "cluster-dc", "html", cluster_dc_html }, + { "cluster-dc", "xml", cluster_dc_xml }, + { "cluster-options", "default", cluster_options_text }, + { "cluster-options", "html", cluster_options_html }, + { "cluster-options", "log", cluster_options_log }, + { "cluster-options", "xml", cluster_options_xml }, + { "cluster-summary", "default", cluster_summary }, + { "cluster-summary", "html", cluster_summary_html }, + { "cluster-stack", "default", cluster_stack_text }, + { "cluster-stack", "html", cluster_stack_html }, + { "cluster-stack", "xml", cluster_stack_xml }, + { "cluster-times", "default", cluster_times_text }, + { "cluster-times", "html", cluster_times_html }, + { "cluster-times", "xml", cluster_times_xml }, + { "failed-action", "default", failed_action_default }, + { "failed-action", "xml", failed_action_xml }, + { "failed-action-list", "default", failed_action_list }, + { "group", "default", pe__group_default}, + { "group", "xml", pe__group_xml }, + { "maint-mode", "text", cluster_maint_mode_text }, + { "node", "default", node_text }, + { "node", "html", node_html }, + { "node", "xml", node_xml }, + { "node-and-op", "default", node_and_op }, + { "node-and-op", "xml", node_and_op_xml }, + { "node-capacity", "default", node_capacity }, + { "node-capacity", "xml", node_capacity_xml }, + { "node-history-list", "default", node_history_list }, + { "node-list", "default", node_list_text }, + { "node-list", "html", node_list_html }, + { "node-list", "xml", node_list_xml }, + { "node-weight", "default", node_weight }, + { "node-weight", "xml", node_weight_xml }, + { "node-attribute", "default", node_attribute_text }, + { "node-attribute", "html", node_attribute_html }, + { "node-attribute", "xml", node_attribute_xml }, + { "node-attribute-list", "default", node_attribute_list }, + { "node-summary", "default", node_summary }, + { "op-history", "default", op_history_text }, + { "op-history", "xml", op_history_xml }, + { "primitive", "default", pe__resource_text }, + { "primitive", "xml", pe__resource_xml }, + { "primitive", "html", pe__resource_html }, + { "promotion-score", "default", promotion_score }, + { "promotion-score", "xml", promotion_score_xml }, + { "resource-config", "default", resource_config }, + { "resource-config", "text", resource_config_text }, + { "resource-history", "default", resource_history_text }, + { "resource-history", "xml", resource_history_xml }, + { "resource-list", "default", resource_list }, + { "resource-operation-list", "default", resource_operation_list }, + { "resource-util", "default", resource_util }, + { "resource-util", "xml", resource_util_xml }, + { "ticket", "default", ticket_text }, + { "ticket", "html", ticket_html }, + { "ticket", "xml", ticket_xml }, + { "ticket-list", "default", ticket_list }, + + { NULL, NULL, NULL } +}; + +void +pe__register_messages(pcmk__output_t *out) { + pcmk__register_messages(out, fmt_functions); +} diff --git a/lib/pengine/pe_status_private.h b/lib/pengine/pe_status_private.h new file mode 100644 index 0000000..ae8d131 --- /dev/null +++ b/lib/pengine/pe_status_private.h @@ -0,0 +1,121 @@ +/* + * Copyright 2018-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PE_STATUS_PRIVATE__H +# define PE_STATUS_PRIVATE__H + +/* This header is for the sole use of libpe_status, so that functions can be + * declared with G_GNUC_INTERNAL for efficiency. + */ + +#if defined(PCMK__UNIT_TESTING) +#undef G_GNUC_INTERNAL +#define G_GNUC_INTERNAL +#endif + +/*! + * \internal + * \deprecated This macro will be removed in a future release + */ +# define status_print(fmt, args...) \ + if(options & pe_print_html) { \ + FILE *stream = print_data; \ + fprintf(stream, fmt, ##args); \ + } else if(options & pe_print_printf || options & pe_print_ncurses) { \ + FILE *stream = print_data; \ + fprintf(stream, fmt, ##args); \ + } else if(options & pe_print_xml) { \ + FILE *stream = print_data; \ + fprintf(stream, fmt, ##args); \ + } else if(options & pe_print_log) { \ + int log_level = *(int*)print_data; \ + do_crm_log(log_level, fmt, ##args); \ + } + +typedef struct notify_data_s { + GSList *keys; // Environment variable name/value pairs + + const char *action; + + pe_action_t *pre; + pe_action_t *post; + pe_action_t *pre_done; + pe_action_t *post_done; + + GList *active; /* notify_entry_t* */ + GList *inactive; /* notify_entry_t* */ + GList *start; /* notify_entry_t* */ + GList *stop; /* notify_entry_t* */ + GList *demote; /* notify_entry_t* */ + GList *promote; /* notify_entry_t* */ + GList *promoted; /* notify_entry_t* */ + GList *unpromoted; /* notify_entry_t* */ + GHashTable *allowed_nodes; +} notify_data_t; + +G_GNUC_INTERNAL +pe_resource_t *pe__create_clone_child(pe_resource_t *rsc, + pe_working_set_t *data_set); + +G_GNUC_INTERNAL +void pe__create_action_notifications(pe_resource_t *rsc, notify_data_t *n_data); + +G_GNUC_INTERNAL +void pe__free_action_notification_data(notify_data_t *n_data); + +G_GNUC_INTERNAL +notify_data_t *pe__action_notif_pseudo_ops(pe_resource_t *rsc, const char *task, + pe_action_t *action, + pe_action_t *complete); + +G_GNUC_INTERNAL +void pe__force_anon(const char *standard, pe_resource_t *rsc, const char *rid, + pe_working_set_t *data_set); + +G_GNUC_INTERNAL +gint pe__cmp_rsc_priority(gconstpointer a, gconstpointer b); + +G_GNUC_INTERNAL +gboolean pe__unpack_resource(xmlNode *xml_obj, pe_resource_t **rsc, + pe_resource_t *parent, pe_working_set_t *data_set); + +G_GNUC_INTERNAL +gboolean unpack_remote_nodes(xmlNode *xml_resources, pe_working_set_t *data_set); + +G_GNUC_INTERNAL +gboolean unpack_resources(const xmlNode *xml_resources, + pe_working_set_t *data_set); + +G_GNUC_INTERNAL +gboolean unpack_config(xmlNode *config, pe_working_set_t *data_set); + +G_GNUC_INTERNAL +gboolean unpack_nodes(xmlNode *xml_nodes, pe_working_set_t *data_set); + +G_GNUC_INTERNAL +gboolean unpack_tags(xmlNode *xml_tags, pe_working_set_t *data_set); + +G_GNUC_INTERNAL +gboolean unpack_status(xmlNode *status, pe_working_set_t *data_set); + +G_GNUC_INTERNAL +op_digest_cache_t *pe__compare_fencing_digest(pe_resource_t *rsc, + const char *agent, + pe_node_t *node, + pe_working_set_t *data_set); + +G_GNUC_INTERNAL +void pe__unpack_node_health_scores(pe_working_set_t *data_set); + +G_GNUC_INTERNAL +pe_node_t *pe__bundle_active_node(const pe_resource_t *rsc, + unsigned int *count_all, + unsigned int *count_clean); + +#endif // PE_STATUS_PRIVATE__H diff --git a/lib/pengine/remote.c b/lib/pengine/remote.c new file mode 100644 index 0000000..769635f --- /dev/null +++ b/lib/pengine/remote.c @@ -0,0 +1,270 @@ +/* + * Copyright 2013-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include + +bool +pe__resource_is_remote_conn(const pe_resource_t *rsc, + const pe_working_set_t *data_set) +{ + return (rsc != NULL) && rsc->is_remote_node + && pe__is_remote_node(pe_find_node(data_set->nodes, rsc->id)); +} + +bool +pe__is_remote_node(const pe_node_t *node) +{ + return (node != NULL) && (node->details->type == node_remote) + && ((node->details->remote_rsc == NULL) + || (node->details->remote_rsc->container == NULL)); +} + +bool +pe__is_guest_node(const pe_node_t *node) +{ + return (node != NULL) && (node->details->type == node_remote) + && (node->details->remote_rsc != NULL) + && (node->details->remote_rsc->container != NULL); +} + +bool +pe__is_guest_or_remote_node(const pe_node_t *node) +{ + return (node != NULL) && (node->details->type == node_remote); +} + +bool +pe__is_bundle_node(const pe_node_t *node) +{ + return pe__is_guest_node(node) + && pe_rsc_is_bundled(node->details->remote_rsc); +} + +/*! + * \internal + * \brief Check whether a resource creates a guest node + * + * If a given resource contains a filler resource that is a remote connection, + * return that filler resource (or NULL if none is found). + * + * \param[in] data_set Working set of cluster + * \param[in] rsc Resource to check + * + * \return Filler resource with remote connection, or NULL if none found + */ +pe_resource_t * +pe__resource_contains_guest_node(const pe_working_set_t *data_set, + const pe_resource_t *rsc) +{ + if ((rsc != NULL) && (data_set != NULL) + && pcmk_is_set(data_set->flags, pe_flag_have_remote_nodes)) { + + for (GList *gIter = rsc->fillers; gIter != NULL; gIter = gIter->next) { + pe_resource_t *filler = gIter->data; + + if (filler->is_remote_node) { + return filler; + } + } + } + return NULL; +} + +bool +xml_contains_remote_node(xmlNode *xml) +{ + const char *value = NULL; + + if (xml == NULL) { + return false; + } + + value = crm_element_value(xml, XML_ATTR_TYPE); + if (!pcmk__str_eq(value, "remote", pcmk__str_casei)) { + return false; + } + + value = crm_element_value(xml, XML_AGENT_ATTR_CLASS); + if (!pcmk__str_eq(value, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { + return false; + } + + value = crm_element_value(xml, XML_AGENT_ATTR_PROVIDER); + if (!pcmk__str_eq(value, "pacemaker", pcmk__str_casei)) { + return false; + } + + return true; +} + +/*! + * \internal + * \brief Execute a supplied function for each guest node running on a host + * + * \param[in] data_set Working set for cluster + * \param[in] host Host node to check + * \param[in] helper Function to call for each guest node + * \param[in,out] user_data Pointer to pass to helper function + */ +void +pe_foreach_guest_node(const pe_working_set_t *data_set, const pe_node_t *host, + void (*helper)(const pe_node_t*, void*), void *user_data) +{ + GList *iter; + + CRM_CHECK(data_set && host && host->details && helper, return); + if (!pcmk_is_set(data_set->flags, pe_flag_have_remote_nodes)) { + return; + } + for (iter = host->details->running_rsc; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (rsc->is_remote_node && (rsc->container != NULL)) { + pe_node_t *guest_node = pe_find_node(data_set->nodes, rsc->id); + + if (guest_node) { + (*helper)(guest_node, user_data); + } + } + } +} + +/*! + * \internal + * \brief Create CIB XML for an implicit remote connection + * + * \param[in,out] parent If not NULL, use as parent XML element + * \param[in] uname Name of Pacemaker Remote node + * \param[in] container If not NULL, use this as connection container + * \param[in] migrateable If not NULL, use as allow-migrate value + * \param[in] is_managed If not NULL, use as is-managed value + * \param[in] start_timeout If not NULL, use as remote connect timeout + * \param[in] server If not NULL, use as remote server value + * \param[in] port If not NULL, use as remote port value + * + * \return Newly created XML + */ +xmlNode * +pe_create_remote_xml(xmlNode *parent, const char *uname, + const char *container_id, const char *migrateable, + const char *is_managed, const char *start_timeout, + const char *server, const char *port) +{ + xmlNode *remote; + xmlNode *xml_sub; + + remote = create_xml_node(parent, XML_CIB_TAG_RESOURCE); + + // Add identity + crm_xml_add(remote, XML_ATTR_ID, uname); + crm_xml_add(remote, XML_AGENT_ATTR_CLASS, PCMK_RESOURCE_CLASS_OCF); + crm_xml_add(remote, XML_AGENT_ATTR_PROVIDER, "pacemaker"); + crm_xml_add(remote, XML_ATTR_TYPE, "remote"); + + // Add meta-attributes + xml_sub = create_xml_node(remote, XML_TAG_META_SETS); + crm_xml_set_id(xml_sub, "%s-%s", uname, XML_TAG_META_SETS); + crm_create_nvpair_xml(xml_sub, NULL, + XML_RSC_ATTR_INTERNAL_RSC, XML_BOOLEAN_TRUE); + if (container_id) { + crm_create_nvpair_xml(xml_sub, NULL, + XML_RSC_ATTR_CONTAINER, container_id); + } + if (migrateable) { + crm_create_nvpair_xml(xml_sub, NULL, + XML_OP_ATTR_ALLOW_MIGRATE, migrateable); + } + if (is_managed) { + crm_create_nvpair_xml(xml_sub, NULL, XML_RSC_ATTR_MANAGED, is_managed); + } + + // Add instance attributes + if (port || server) { + xml_sub = create_xml_node(remote, XML_TAG_ATTR_SETS); + crm_xml_set_id(xml_sub, "%s-%s", uname, XML_TAG_ATTR_SETS); + if (server) { + crm_create_nvpair_xml(xml_sub, NULL, XML_RSC_ATTR_REMOTE_RA_ADDR, + server); + } + if (port) { + crm_create_nvpair_xml(xml_sub, NULL, "port", port); + } + } + + // Add operations + xml_sub = create_xml_node(remote, "operations"); + crm_create_op_xml(xml_sub, uname, "monitor", "30s", "30s"); + if (start_timeout) { + crm_create_op_xml(xml_sub, uname, "start", "0", start_timeout); + } + return remote; +} + +// History entry to be checked for fail count clearing +struct check_op { + const xmlNode *rsc_op; // History entry XML + pe_resource_t *rsc; // Known resource corresponding to history entry + pe_node_t *node; // Known node corresponding to history entry + enum pe_check_parameters check_type; // What needs checking +}; + +void +pe__add_param_check(const xmlNode *rsc_op, pe_resource_t *rsc, + pe_node_t *node, enum pe_check_parameters flag, + pe_working_set_t *data_set) +{ + struct check_op *check_op = NULL; + + CRM_CHECK(data_set && rsc_op && rsc && node, return); + + check_op = calloc(1, sizeof(struct check_op)); + CRM_ASSERT(check_op != NULL); + + crm_trace("Deferring checks of %s until after allocation", ID(rsc_op)); + check_op->rsc_op = rsc_op; + check_op->rsc = rsc; + check_op->node = node; + check_op->check_type = flag; + data_set->param_check = g_list_prepend(data_set->param_check, check_op); +} + +/*! + * \internal + * \brief Call a function for each action to be checked for addr substitution + * + * \param[in,out] data_set Working set for cluster + * \param[in] cb Function to be called + */ +void +pe__foreach_param_check(pe_working_set_t *data_set, + void (*cb)(pe_resource_t*, pe_node_t*, const xmlNode*, + enum pe_check_parameters)) +{ + CRM_CHECK(data_set && cb, return); + + for (GList *item = data_set->param_check; item != NULL; item = item->next) { + struct check_op *check_op = item->data; + + cb(check_op->rsc, check_op->node, check_op->rsc_op, + check_op->check_type); + } +} + +void +pe__free_param_checks(pe_working_set_t *data_set) +{ + if (data_set && data_set->param_check) { + g_list_free_full(data_set->param_check, free); + data_set->param_check = NULL; + } +} diff --git a/lib/pengine/rules.c b/lib/pengine/rules.c new file mode 100644 index 0000000..7021d3c --- /dev/null +++ b/lib/pengine/rules.c @@ -0,0 +1,1316 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include + +CRM_TRACE_INIT_DATA(pe_rules); + +/*! + * \brief Evaluate any rules contained by given XML element + * + * \param[in,out] xml XML element to check for rules + * \param[in] node_hash Node attributes to use to evaluate expressions + * \param[in] now Time to use when evaluating expressions + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return TRUE if no rules, or any of rules present is in effect, else FALSE + */ +gboolean +pe_evaluate_rules(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now, + crm_time_t *next_change) +{ + pe_rule_eval_data_t rule_data = { + .node_hash = node_hash, + .role = RSC_ROLE_UNKNOWN, + .now = now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + return pe_eval_rules(ruleset, &rule_data, next_change); +} + +gboolean +pe_test_rule(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, + crm_time_t *now, crm_time_t *next_change, + pe_match_data_t *match_data) +{ + pe_rule_eval_data_t rule_data = { + .node_hash = node_hash, + .role = role, + .now = now, + .match_data = match_data, + .rsc_data = NULL, + .op_data = NULL + }; + + return pe_eval_expr(rule, &rule_data, next_change); +} + +/*! + * \brief Evaluate one rule subelement (pass/fail) + * + * A rule element may contain another rule, a node attribute expression, or a + * date expression. Given any one of those, evaluate it and return whether it + * passed. + * + * \param[in,out] expr Rule subelement XML + * \param[in] node_hash Node attributes to use when evaluating expression + * \param[in] role Resource role to use when evaluating expression + * \param[in] now Time to use when evaluating expression + * \param[out] next_change If not NULL, set to when evaluation will change + * \param[in] match_data If not NULL, resource back-references and params + * + * \return TRUE if expression is in effect under given conditions, else FALSE + */ +gboolean +pe_test_expression(xmlNode *expr, GHashTable *node_hash, enum rsc_role_e role, + crm_time_t *now, crm_time_t *next_change, + pe_match_data_t *match_data) +{ + pe_rule_eval_data_t rule_data = { + .node_hash = node_hash, + .role = role, + .now = now, + .match_data = match_data, + .rsc_data = NULL, + .op_data = NULL + }; + + return pe_eval_subexpr(expr, &rule_data, next_change); +} + +enum expression_type +find_expression_type(xmlNode * expr) +{ + const char *tag = NULL; + const char *attr = NULL; + + attr = crm_element_value(expr, XML_EXPR_ATTR_ATTRIBUTE); + tag = crm_element_name(expr); + + if (pcmk__str_eq(tag, PCMK_XE_DATE_EXPRESSION, pcmk__str_none)) { + return time_expr; + + } else if (pcmk__str_eq(tag, PCMK_XE_RSC_EXPRESSION, pcmk__str_none)) { + return rsc_expr; + + } else if (pcmk__str_eq(tag, PCMK_XE_OP_EXPRESSION, pcmk__str_none)) { + return op_expr; + + } else if (pcmk__str_eq(tag, XML_TAG_RULE, pcmk__str_none)) { + return nested_rule; + + } else if (!pcmk__str_eq(tag, XML_TAG_EXPRESSION, pcmk__str_none)) { + return not_expr; + + } else if (pcmk__str_any_of(attr, CRM_ATTR_UNAME, CRM_ATTR_KIND, CRM_ATTR_ID, NULL)) { + return loc_expr; + + } else if (pcmk__str_eq(attr, CRM_ATTR_ROLE, pcmk__str_none)) { + return role_expr; + } + + return attr_expr; +} + +/* As per the nethack rules: + * + * moon period = 29.53058 days ~= 30, year = 365.2422 days + * days moon phase advances on first day of year compared to preceding year + * = 365.2422 - 12*29.53058 ~= 11 + * years in Metonic cycle (time until same phases fall on the same days of + * the month) = 18.6 ~= 19 + * moon phase on first day of year (epact) ~= (11*(year%19) + 29) % 30 + * (29 as initial condition) + * current phase in days = first day phase + days elapsed in year + * 6 moons ~= 177 days + * 177 ~= 8 reported phases * 22 + * + 11/22 for rounding + * + * 0-7, with 0: new, 4: full + */ + +static int +phase_of_the_moon(const crm_time_t *now) +{ + uint32_t epact, diy, goldn; + uint32_t y; + + crm_time_get_ordinal(now, &y, &diy); + + goldn = (y % 19) + 1; + epact = (11 * goldn + 18) % 30; + if ((epact == 25 && goldn > 11) || epact == 24) + epact++; + + return ((((((diy + epact) * 6) + 11) % 177) / 22) & 7); +} + +static int +check_one(const xmlNode *cron_spec, const char *xml_field, uint32_t time_field) +{ + int rc = pcmk_rc_undetermined; + const char *value = crm_element_value(cron_spec, xml_field); + long long low, high; + + if (value == NULL) { + /* Return pe_date_result_undetermined if the field is missing. */ + goto bail; + } + + if (pcmk__parse_ll_range(value, &low, &high) != pcmk_rc_ok) { + goto bail; + } else if (low == high) { + /* A single number was given, not a range. */ + if (time_field < low) { + rc = pcmk_rc_before_range; + } else if (time_field > high) { + rc = pcmk_rc_after_range; + } else { + rc = pcmk_rc_within_range; + } + } else if (low != -1 && high != -1) { + /* This is a range with both bounds. */ + if (time_field < low) { + rc = pcmk_rc_before_range; + } else if (time_field > high) { + rc = pcmk_rc_after_range; + } else { + rc = pcmk_rc_within_range; + } + } else if (low == -1) { + /* This is a range with no starting value. */ + rc = time_field <= high ? pcmk_rc_within_range : pcmk_rc_after_range; + } else if (high == -1) { + /* This is a range with no ending value. */ + rc = time_field >= low ? pcmk_rc_within_range : pcmk_rc_before_range; + } + +bail: + if (rc == pcmk_rc_within_range) { + crm_debug("Condition '%s' in %s: passed", value, xml_field); + } else { + crm_debug("Condition '%s' in %s: failed", value, xml_field); + } + + return rc; +} + +static gboolean +check_passes(int rc) { + /* _within_range is obvious. _undetermined is a pass because + * this is the return value if a field is not given. In this + * case, we just want to ignore it and check other fields to + * see if they place some restriction on what can pass. + */ + return rc == pcmk_rc_within_range || rc == pcmk_rc_undetermined; +} + +#define CHECK_ONE(spec, name, var) do { \ + int subpart_rc = check_one(spec, name, var); \ + if (check_passes(subpart_rc) == FALSE) { \ + return subpart_rc; \ + } \ +} while (0) + +int +pe_cron_range_satisfied(const crm_time_t *now, const xmlNode *cron_spec) +{ + uint32_t h, m, s, y, d, w; + + CRM_CHECK(now != NULL, return pcmk_rc_op_unsatisfied); + + crm_time_get_gregorian(now, &y, &m, &d); + CHECK_ONE(cron_spec, "years", y); + CHECK_ONE(cron_spec, "months", m); + CHECK_ONE(cron_spec, "monthdays", d); + + crm_time_get_timeofday(now, &h, &m, &s); + CHECK_ONE(cron_spec, "hours", h); + CHECK_ONE(cron_spec, "minutes", m); + CHECK_ONE(cron_spec, "seconds", s); + + crm_time_get_ordinal(now, &y, &d); + CHECK_ONE(cron_spec, "yeardays", d); + + crm_time_get_isoweek(now, &y, &w, &d); + CHECK_ONE(cron_spec, "weekyears", y); + CHECK_ONE(cron_spec, "weeks", w); + CHECK_ONE(cron_spec, "weekdays", d); + + CHECK_ONE(cron_spec, "moon", phase_of_the_moon(now)); + if (crm_element_value(cron_spec, "moon") != NULL) { + pcmk__config_warn("Support for 'moon' in date_spec elements " + "(such as %s) is deprecated and will be removed " + "in a future release of Pacemaker", ID(cron_spec)); + } + + /* If we get here, either no fields were specified (which is success), or all + * the fields that were specified had their conditions met (which is also a + * success). Thus, the result is success. + */ + return pcmk_rc_ok; +} + +static void +update_field(crm_time_t *t, const xmlNode *xml, const char *attr, + void (*time_fn)(crm_time_t *, int)) +{ + long long value; + + if ((pcmk__scan_ll(crm_element_value(xml, attr), &value, 0LL) == pcmk_rc_ok) + && (value != 0LL) && (value >= INT_MIN) && (value <= INT_MAX)) { + time_fn(t, (int) value); + } +} + +static crm_time_t * +parse_xml_duration(const crm_time_t *start, const xmlNode *duration_spec) +{ + crm_time_t *end = pcmk_copy_time(start); + + update_field(end, duration_spec, "years", crm_time_add_years); + update_field(end, duration_spec, "months", crm_time_add_months); + update_field(end, duration_spec, "weeks", crm_time_add_weeks); + update_field(end, duration_spec, "days", crm_time_add_days); + update_field(end, duration_spec, "hours", crm_time_add_hours); + update_field(end, duration_spec, "minutes", crm_time_add_minutes); + update_field(end, duration_spec, "seconds", crm_time_add_seconds); + + return end; +} + +// Set next_change to t if t is earlier +static void +crm_time_set_if_earlier(crm_time_t *next_change, crm_time_t *t) +{ + if ((next_change != NULL) && (t != NULL)) { + if (!crm_time_is_defined(next_change) + || (crm_time_compare(t, next_change) < 0)) { + crm_time_set(next_change, t); + } + } +} + +// Information about a block of nvpair elements +typedef struct sorted_set_s { + int score; // This block's score for sorting + const char *name; // This block's ID + const char *special_name; // ID that should sort first + xmlNode *attr_set; // This block +} sorted_set_t; + +static gint +sort_pairs(gconstpointer a, gconstpointer b) +{ + const sorted_set_t *pair_a = a; + const sorted_set_t *pair_b = b; + + if (a == NULL && b == NULL) { + return 0; + } else if (a == NULL) { + return 1; + } else if (b == NULL) { + return -1; + } + + if (pcmk__str_eq(pair_a->name, pair_a->special_name, pcmk__str_casei)) { + return -1; + + } else if (pcmk__str_eq(pair_b->name, pair_a->special_name, pcmk__str_casei)) { + return 1; + } + + if (pair_a->score < pair_b->score) { + return 1; + } else if (pair_a->score > pair_b->score) { + return -1; + } + return 0; +} + +static void +populate_hash(xmlNode * nvpair_list, GHashTable * hash, gboolean overwrite, xmlNode * top) +{ + const char *name = NULL; + const char *value = NULL; + const char *old_value = NULL; + xmlNode *list = nvpair_list; + xmlNode *an_attr = NULL; + + name = crm_element_name(list->children); + if (pcmk__str_eq(XML_TAG_ATTRS, name, pcmk__str_casei)) { + list = list->children; + } + + for (an_attr = pcmk__xe_first_child(list); an_attr != NULL; + an_attr = pcmk__xe_next(an_attr)) { + + if (pcmk__str_eq((const char *)an_attr->name, XML_CIB_TAG_NVPAIR, pcmk__str_none)) { + xmlNode *ref_nvpair = expand_idref(an_attr, top); + + name = crm_element_value(an_attr, XML_NVPAIR_ATTR_NAME); + if (name == NULL) { + name = crm_element_value(ref_nvpair, XML_NVPAIR_ATTR_NAME); + } + + value = crm_element_value(an_attr, XML_NVPAIR_ATTR_VALUE); + if (value == NULL) { + value = crm_element_value(ref_nvpair, XML_NVPAIR_ATTR_VALUE); + } + + if (name == NULL || value == NULL) { + continue; + } + + old_value = g_hash_table_lookup(hash, name); + + if (pcmk__str_eq(value, "#default", pcmk__str_casei)) { + if (old_value) { + crm_trace("Letting %s default (removing explicit value \"%s\")", + name, value); + g_hash_table_remove(hash, name); + } + continue; + + } else if (old_value == NULL) { + crm_trace("Setting %s=\"%s\"", name, value); + g_hash_table_insert(hash, strdup(name), strdup(value)); + + } else if (overwrite) { + crm_trace("Setting %s=\"%s\" (overwriting old value \"%s\")", + name, value, old_value); + g_hash_table_replace(hash, strdup(name), strdup(value)); + } + } + } +} + +typedef struct unpack_data_s { + gboolean overwrite; + void *hash; + crm_time_t *next_change; + const pe_rule_eval_data_t *rule_data; + xmlNode *top; +} unpack_data_t; + +static void +unpack_attr_set(gpointer data, gpointer user_data) +{ + sorted_set_t *pair = data; + unpack_data_t *unpack_data = user_data; + + if (!pe_eval_rules(pair->attr_set, unpack_data->rule_data, + unpack_data->next_change)) { + return; + } + + crm_trace("Adding attributes from %s (score %d) %s overwrite", + pair->name, pair->score, + (unpack_data->overwrite? "with" : "without")); + populate_hash(pair->attr_set, unpack_data->hash, unpack_data->overwrite, unpack_data->top); +} + +/*! + * \internal + * \brief Create a sorted list of nvpair blocks + * + * \param[in,out] top XML document root (used to expand id-ref's) + * \param[in] xml_obj XML element containing blocks of nvpair elements + * \param[in] set_name If not NULL, only get blocks of this element + * \param[in] always_first If not NULL, sort block with this ID as first + * + * \return List of sorted_set_t entries for nvpair blocks + */ +static GList * +make_pairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, + const char *always_first) +{ + GList *unsorted = NULL; + + if (xml_obj == NULL) { + return NULL; + } + for (xmlNode *attr_set = pcmk__xe_first_child(xml_obj); attr_set != NULL; + attr_set = pcmk__xe_next(attr_set)) { + + if (pcmk__str_eq(set_name, (const char *) attr_set->name, + pcmk__str_null_matches)) { + const char *score = NULL; + sorted_set_t *pair = NULL; + xmlNode *expanded_attr_set = expand_idref(attr_set, top); + + if (expanded_attr_set == NULL) { + // Schema (if not "none") prevents this + continue; + } + + pair = calloc(1, sizeof(sorted_set_t)); + pair->name = ID(expanded_attr_set); + pair->special_name = always_first; + pair->attr_set = expanded_attr_set; + + score = crm_element_value(expanded_attr_set, XML_RULE_ATTR_SCORE); + pair->score = char2score(score); + + unsorted = g_list_prepend(unsorted, pair); + } + } + return g_list_sort(unsorted, sort_pairs); +} + +/*! + * \brief Extract nvpair blocks contained by an XML element into a hash table + * + * \param[in,out] top XML document root (used to expand id-ref's) + * \param[in] xml_obj XML element containing blocks of nvpair elements + * \param[in] set_name If not NULL, only use blocks of this element + * \param[in] rule_data Matching parameters to use when unpacking + * \param[out] hash Where to store extracted name/value pairs + * \param[in] always_first If not NULL, process block with this ID first + * \param[in] overwrite Whether to replace existing values with same name + * \param[out] next_change If not NULL, set to when evaluation will change + */ +void +pe_eval_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, + const pe_rule_eval_data_t *rule_data, GHashTable *hash, + const char *always_first, gboolean overwrite, + crm_time_t *next_change) +{ + GList *pairs = make_pairs(top, xml_obj, set_name, always_first); + + if (pairs) { + unpack_data_t data = { + .hash = hash, + .overwrite = overwrite, + .next_change = next_change, + .top = top, + .rule_data = rule_data + }; + + g_list_foreach(pairs, unpack_attr_set, &data); + g_list_free_full(pairs, free); + } +} + +/*! + * \brief Extract nvpair blocks contained by an XML element into a hash table + * + * \param[in,out] top XML document root (used to expand id-ref's) + * \param[in] xml_obj XML element containing blocks of nvpair elements + * \param[in] set_name Element name to identify nvpair blocks + * \param[in] node_hash Node attributes to use when evaluating rules + * \param[out] hash Where to store extracted name/value pairs + * \param[in] always_first If not NULL, process block with this ID first + * \param[in] overwrite Whether to replace existing values with same name + * \param[in] now Time to use when evaluating rules + * \param[out] next_change If not NULL, set to when evaluation will change + */ +void +pe_unpack_nvpairs(xmlNode *top, const xmlNode *xml_obj, const char *set_name, + GHashTable *node_hash, GHashTable *hash, + const char *always_first, gboolean overwrite, + crm_time_t *now, crm_time_t *next_change) +{ + pe_rule_eval_data_t rule_data = { + .node_hash = node_hash, + .role = RSC_ROLE_UNKNOWN, + .now = now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash, + always_first, overwrite, next_change); +} + +/*! + * \brief Expand any regular expression submatches (%0-%9) in a string + * + * \param[in] string String possibly containing submatch variables + * \param[in] match_data If not NULL, regular expression matches + * + * \return Newly allocated string identical to \p string with submatches + * expanded, or NULL if there were no matches + */ +char * +pe_expand_re_matches(const char *string, const pe_re_match_data_t *match_data) +{ + size_t len = 0; + int i; + const char *p, *last_match_index; + char *p_dst, *result = NULL; + + if (pcmk__str_empty(string) || !match_data) { + return NULL; + } + + p = last_match_index = string; + + while (*p) { + if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) { + i = *(p + 1) - '0'; + if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 && + match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) { + len += p - last_match_index + (match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so); + last_match_index = p + 2; + } + p++; + } + p++; + } + len += p - last_match_index + 1; + + /* FIXME: Excessive? */ + if (len - 1 <= 0) { + return NULL; + } + + p_dst = result = calloc(1, len); + p = string; + + while (*p) { + if (*p == '%' && *(p + 1) && isdigit(*(p + 1))) { + i = *(p + 1) - '0'; + if (match_data->nregs >= i && match_data->pmatch[i].rm_so != -1 && + match_data->pmatch[i].rm_eo > match_data->pmatch[i].rm_so) { + /* rm_eo can be equal to rm_so, but then there is nothing to do */ + int match_len = match_data->pmatch[i].rm_eo - match_data->pmatch[i].rm_so; + memcpy(p_dst, match_data->string + match_data->pmatch[i].rm_so, match_len); + p_dst += match_len; + } + p++; + } else { + *(p_dst) = *(p); + p_dst++; + } + p++; + } + + return result; +} + +/*! + * \brief Evaluate rules + * + * \param[in,out] ruleset XML possibly containing rule sub-elements + * \param[in] rule_data + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return TRUE if there are no rules or + */ +gboolean +pe_eval_rules(xmlNode *ruleset, const pe_rule_eval_data_t *rule_data, + crm_time_t *next_change) +{ + // If there are no rules, pass by default + gboolean ruleset_default = TRUE; + + for (xmlNode *rule = first_named_child(ruleset, XML_TAG_RULE); + rule != NULL; rule = crm_next_same_xml(rule)) { + + ruleset_default = FALSE; + if (pe_eval_expr(rule, rule_data, next_change)) { + /* Only the deprecated "lifetime" element of location constraints + * may contain more than one rule at the top level -- the schema + * limits a block of nvpairs to a single top-level rule. So, this + * effectively means that a lifetime is active if any rule it + * contains is active. + */ + return TRUE; + } + } + + return ruleset_default; +} + +/*! + * \brief Evaluate all of a rule's expressions + * + * \param[in,out] rule XML containing a rule definition or its id-ref + * \param[in] rule_data Matching parameters to check against rule + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return TRUE if \p rule_data passes \p rule, otherwise FALSE + */ +gboolean +pe_eval_expr(xmlNode *rule, const pe_rule_eval_data_t *rule_data, + crm_time_t *next_change) +{ + xmlNode *expr = NULL; + gboolean test = TRUE; + gboolean empty = TRUE; + gboolean passed = TRUE; + gboolean do_and = TRUE; + const char *value = NULL; + + rule = expand_idref(rule, NULL); + value = crm_element_value(rule, XML_RULE_ATTR_BOOLEAN_OP); + if (pcmk__str_eq(value, "or", pcmk__str_casei)) { + do_and = FALSE; + passed = FALSE; + } + + crm_trace("Testing rule %s", ID(rule)); + for (expr = pcmk__xe_first_child(rule); expr != NULL; + expr = pcmk__xe_next(expr)) { + + test = pe_eval_subexpr(expr, rule_data, next_change); + empty = FALSE; + + if (test && do_and == FALSE) { + crm_trace("Expression %s/%s passed", ID(rule), ID(expr)); + return TRUE; + + } else if (test == FALSE && do_and) { + crm_trace("Expression %s/%s failed", ID(rule), ID(expr)); + return FALSE; + } + } + + if (empty) { + crm_err("Invalid Rule %s: rules must contain at least one expression", ID(rule)); + } + + crm_trace("Rule %s %s", ID(rule), passed ? "passed" : "failed"); + return passed; +} + +/*! + * \brief Evaluate a single rule expression, including any subexpressions + * + * \param[in,out] expr XML containing a rule expression + * \param[in] rule_data Matching parameters to check against expression + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return TRUE if \p rule_data passes \p expr, otherwise FALSE + */ +gboolean +pe_eval_subexpr(xmlNode *expr, const pe_rule_eval_data_t *rule_data, + crm_time_t *next_change) +{ + gboolean accept = FALSE; + const char *uname = NULL; + + switch (find_expression_type(expr)) { + case nested_rule: + accept = pe_eval_expr(expr, rule_data, next_change); + break; + case attr_expr: + case loc_expr: + /* these expressions can never succeed if there is + * no node to compare with + */ + if (rule_data->node_hash != NULL) { + accept = pe__eval_attr_expr(expr, rule_data); + } + break; + + case time_expr: + switch (pe__eval_date_expr(expr, rule_data, next_change)) { + case pcmk_rc_within_range: + case pcmk_rc_ok: + accept = TRUE; + break; + + default: + accept = FALSE; + break; + } + break; + + case role_expr: + accept = pe__eval_role_expr(expr, rule_data); + break; + + case rsc_expr: + accept = pe__eval_rsc_expr(expr, rule_data); + break; + + case op_expr: + accept = pe__eval_op_expr(expr, rule_data); + break; + + default: + CRM_CHECK(FALSE /* bad type */ , return FALSE); + accept = FALSE; + } + if (rule_data->node_hash) { + uname = g_hash_table_lookup(rule_data->node_hash, CRM_ATTR_UNAME); + } + + crm_trace("Expression %s %s on %s", + ID(expr), accept ? "passed" : "failed", uname ? uname : "all nodes"); + return accept; +} + +/*! + * \internal + * \brief Compare two values in a rule's node attribute expression + * + * \param[in] l_val Value on left-hand side of comparison + * \param[in] r_val Value on right-hand side of comparison + * \param[in] type How to interpret the values (allowed values: + * \c "string", \c "integer", \c "number", + * \c "version", \c NULL) + * \param[in] op Type of comparison + * + * \return -1 if (l_val < r_val), + * 0 if (l_val == r_val), + * 1 if (l_val > r_val) + */ +static int +compare_attr_expr_vals(const char *l_val, const char *r_val, const char *type, + const char *op) +{ + int cmp = 0; + + if (l_val != NULL && r_val != NULL) { + if (type == NULL) { + if (pcmk__strcase_any_of(op, "lt", "lte", "gt", "gte", NULL)) { + if (pcmk__char_in_any_str('.', l_val, r_val, NULL)) { + type = "number"; + } else { + type = "integer"; + } + + } else { + type = "string"; + } + crm_trace("Defaulting to %s based comparison for '%s' op", type, op); + } + + if (pcmk__str_eq(type, "string", pcmk__str_casei)) { + cmp = strcasecmp(l_val, r_val); + + } else if (pcmk__str_eq(type, "integer", pcmk__str_casei)) { + long long l_val_num; + int rc1 = pcmk__scan_ll(l_val, &l_val_num, 0LL); + + long long r_val_num; + int rc2 = pcmk__scan_ll(r_val, &r_val_num, 0LL); + + if ((rc1 == pcmk_rc_ok) && (rc2 == pcmk_rc_ok)) { + if (l_val_num < r_val_num) { + cmp = -1; + } else if (l_val_num > r_val_num) { + cmp = 1; + } else { + cmp = 0; + } + + } else { + crm_debug("Integer parse error. Comparing %s and %s as strings", + l_val, r_val); + cmp = compare_attr_expr_vals(l_val, r_val, "string", op); + } + + } else if (pcmk__str_eq(type, "number", pcmk__str_casei)) { + double l_val_num; + double r_val_num; + + int rc1 = pcmk__scan_double(l_val, &l_val_num, NULL, NULL); + int rc2 = pcmk__scan_double(r_val, &r_val_num, NULL, NULL); + + if (rc1 == pcmk_rc_ok && rc2 == pcmk_rc_ok) { + if (l_val_num < r_val_num) { + cmp = -1; + } else if (l_val_num > r_val_num) { + cmp = 1; + } else { + cmp = 0; + } + + } else { + crm_debug("Floating-point parse error. Comparing %s and %s as " + "strings", l_val, r_val); + cmp = compare_attr_expr_vals(l_val, r_val, "string", op); + } + + } else if (pcmk__str_eq(type, "version", pcmk__str_casei)) { + cmp = compare_version(l_val, r_val); + + } + + } else if (l_val == NULL && r_val == NULL) { + cmp = 0; + } else if (r_val == NULL) { + cmp = 1; + } else { // l_val == NULL && r_val != NULL + cmp = -1; + } + + return cmp; +} + +/*! + * \internal + * \brief Check whether an attribute expression evaluates to \c true + * + * \param[in] l_val Value on left-hand side of comparison + * \param[in] r_val Value on right-hand side of comparison + * \param[in] type How to interpret the values (allowed values: + * \c "string", \c "integer", \c "number", + * \c "version", \c NULL) + * \param[in] op Type of comparison. + * + * \return \c true if expression evaluates to \c true, \c false + * otherwise + */ +static bool +accept_attr_expr(const char *l_val, const char *r_val, const char *type, + const char *op) +{ + int cmp; + + if (pcmk__str_eq(op, "defined", pcmk__str_casei)) { + return (l_val != NULL); + + } else if (pcmk__str_eq(op, "not_defined", pcmk__str_casei)) { + return (l_val == NULL); + + } + + cmp = compare_attr_expr_vals(l_val, r_val, type, op); + + if (pcmk__str_eq(op, "eq", pcmk__str_casei)) { + return (cmp == 0); + + } else if (pcmk__str_eq(op, "ne", pcmk__str_casei)) { + return (cmp != 0); + + } else if (l_val == NULL || r_val == NULL) { + // The comparison is meaningless from this point on + return false; + + } else if (pcmk__str_eq(op, "lt", pcmk__str_casei)) { + return (cmp < 0); + + } else if (pcmk__str_eq(op, "lte", pcmk__str_casei)) { + return (cmp <= 0); + + } else if (pcmk__str_eq(op, "gt", pcmk__str_casei)) { + return (cmp > 0); + + } else if (pcmk__str_eq(op, "gte", pcmk__str_casei)) { + return (cmp >= 0); + } + + return false; // Should never reach this point +} + +/*! + * \internal + * \brief Get correct value according to value-source + * + * \param[in] value value given in rule expression + * \param[in] value_source value-source given in rule expressions + * \param[in] match_data If not NULL, resource back-references and params + */ +static const char * +expand_value_source(const char *value, const char *value_source, + const pe_match_data_t *match_data) +{ + GHashTable *table = NULL; + + if (pcmk__str_empty(value)) { + return NULL; // value_source is irrelevant + + } else if (pcmk__str_eq(value_source, "param", pcmk__str_casei)) { + table = match_data->params; + + } else if (pcmk__str_eq(value_source, "meta", pcmk__str_casei)) { + table = match_data->meta; + + } else { // literal + return value; + } + + if (table == NULL) { + return NULL; + } + return (const char *) g_hash_table_lookup(table, value); +} + +/*! + * \internal + * \brief Evaluate a node attribute expression based on #uname, #id, #kind, + * or a generic node attribute + * + * \param[in] expr XML of rule expression + * \param[in] rule_data The match_data and node_hash members are used + * + * \return TRUE if rule_data satisfies the expression, FALSE otherwise + */ +gboolean +pe__eval_attr_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) +{ + gboolean attr_allocated = FALSE; + const char *h_val = NULL; + + const char *op = NULL; + const char *type = NULL; + const char *attr = NULL; + const char *value = NULL; + const char *value_source = NULL; + + attr = crm_element_value(expr, XML_EXPR_ATTR_ATTRIBUTE); + op = crm_element_value(expr, XML_EXPR_ATTR_OPERATION); + value = crm_element_value(expr, XML_EXPR_ATTR_VALUE); + type = crm_element_value(expr, XML_EXPR_ATTR_TYPE); + value_source = crm_element_value(expr, XML_EXPR_ATTR_VALUE_SOURCE); + + if (attr == NULL) { + pe_err("Expression %s invalid: " XML_EXPR_ATTR_ATTRIBUTE + " not specified", pcmk__s(ID(expr), "without ID")); + return FALSE; + } else if (op == NULL) { + pe_err("Expression %s invalid: " XML_EXPR_ATTR_OPERATION + " not specified", pcmk__s(ID(expr), "without ID")); + } + + if (rule_data->match_data != NULL) { + // Expand any regular expression submatches (%0-%9) in attribute name + if (rule_data->match_data->re != NULL) { + char *resolved_attr = pe_expand_re_matches(attr, rule_data->match_data->re); + + if (resolved_attr != NULL) { + attr = (const char *) resolved_attr; + attr_allocated = TRUE; + } + } + + // Get value appropriate to value-source + value = expand_value_source(value, value_source, rule_data->match_data); + } + + if (rule_data->node_hash != NULL) { + h_val = (const char *)g_hash_table_lookup(rule_data->node_hash, attr); + } + + if (attr_allocated) { + free((char *)attr); + attr = NULL; + } + + return accept_attr_expr(h_val, value, type, op); +} + +/*! + * \internal + * \brief Evaluate a date_expression + * + * \param[in] expr XML of rule expression + * \param[in] rule_data Only the now member is used + * \param[out] next_change If not NULL, set to when evaluation will change + * + * \return Standard Pacemaker return code + */ +int +pe__eval_date_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data, + crm_time_t *next_change) +{ + crm_time_t *start = NULL; + crm_time_t *end = NULL; + const char *value = NULL; + const char *op = crm_element_value(expr, "operation"); + + xmlNode *duration_spec = NULL; + xmlNode *date_spec = NULL; + + // "undetermined" will also be returned for parsing errors + int rc = pcmk_rc_undetermined; + + crm_trace("Testing expression: %s", ID(expr)); + + duration_spec = first_named_child(expr, "duration"); + date_spec = first_named_child(expr, "date_spec"); + + value = crm_element_value(expr, "start"); + if (value != NULL) { + start = crm_time_new(value); + } + value = crm_element_value(expr, "end"); + if (value != NULL) { + end = crm_time_new(value); + } + + if (start != NULL && end == NULL && duration_spec != NULL) { + end = parse_xml_duration(start, duration_spec); + } + + if (pcmk__str_eq(op, "in_range", pcmk__str_null_matches | pcmk__str_casei)) { + if ((start == NULL) && (end == NULL)) { + // in_range requires at least one of start or end + } else if ((start != NULL) && (crm_time_compare(rule_data->now, start) < 0)) { + rc = pcmk_rc_before_range; + crm_time_set_if_earlier(next_change, start); + } else if ((end != NULL) && (crm_time_compare(rule_data->now, end) > 0)) { + rc = pcmk_rc_after_range; + } else { + rc = pcmk_rc_within_range; + if (end && next_change) { + // Evaluation doesn't change until second after end + crm_time_add_seconds(end, 1); + crm_time_set_if_earlier(next_change, end); + } + } + + } else if (pcmk__str_eq(op, "date_spec", pcmk__str_casei)) { + rc = pe_cron_range_satisfied(rule_data->now, date_spec); + // @TODO set next_change appropriately + + } else if (pcmk__str_eq(op, "gt", pcmk__str_casei)) { + if (start == NULL) { + // gt requires start + } else if (crm_time_compare(rule_data->now, start) > 0) { + rc = pcmk_rc_within_range; + } else { + rc = pcmk_rc_before_range; + + // Evaluation doesn't change until second after start + crm_time_add_seconds(start, 1); + crm_time_set_if_earlier(next_change, start); + } + + } else if (pcmk__str_eq(op, "lt", pcmk__str_casei)) { + if (end == NULL) { + // lt requires end + } else if (crm_time_compare(rule_data->now, end) < 0) { + rc = pcmk_rc_within_range; + crm_time_set_if_earlier(next_change, end); + } else { + rc = pcmk_rc_after_range; + } + } + + crm_time_free(start); + crm_time_free(end); + return rc; +} + +gboolean +pe__eval_op_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) +{ + const char *name = crm_element_value(expr, XML_NVPAIR_ATTR_NAME); + const char *interval_s = crm_element_value(expr, XML_LRM_ATTR_INTERVAL); + guint interval; + + crm_trace("Testing op_defaults expression: %s", ID(expr)); + + if (rule_data->op_data == NULL) { + crm_trace("No operations data provided"); + return FALSE; + } + + interval = crm_parse_interval_spec(interval_s); + if (interval == 0 && errno != 0) { + crm_trace("Could not parse interval: %s", interval_s); + return FALSE; + } + + if (interval_s != NULL && interval != rule_data->op_data->interval) { + crm_trace("Interval doesn't match: %d != %d", interval, rule_data->op_data->interval); + return FALSE; + } + + if (!pcmk__str_eq(name, rule_data->op_data->op_name, pcmk__str_none)) { + crm_trace("Name doesn't match: %s != %s", name, rule_data->op_data->op_name); + return FALSE; + } + + return TRUE; +} + +/*! + * \internal + * \brief Evaluate a node attribute expression based on #role + * + * \param[in] expr XML of rule expression + * \param[in] rule_data Only the role member is used + * + * \return TRUE if rule_data->role satisfies the expression, FALSE otherwise + */ +gboolean +pe__eval_role_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) +{ + gboolean accept = FALSE; + const char *op = NULL; + const char *value = NULL; + + if (rule_data->role == RSC_ROLE_UNKNOWN) { + return accept; + } + + value = crm_element_value(expr, XML_EXPR_ATTR_VALUE); + op = crm_element_value(expr, XML_EXPR_ATTR_OPERATION); + + if (pcmk__str_eq(op, "defined", pcmk__str_casei)) { + if (rule_data->role > RSC_ROLE_STARTED) { + accept = TRUE; + } + + } else if (pcmk__str_eq(op, "not_defined", pcmk__str_casei)) { + if ((rule_data->role > RSC_ROLE_UNKNOWN) + && (rule_data->role < RSC_ROLE_UNPROMOTED)) { + accept = TRUE; + } + + } else if (pcmk__str_eq(op, "eq", pcmk__str_casei)) { + if (text2role(value) == rule_data->role) { + accept = TRUE; + } + + } else if (pcmk__str_eq(op, "ne", pcmk__str_casei)) { + // Test "ne" only with promotable clone roles + if ((rule_data->role > RSC_ROLE_UNKNOWN) + && (rule_data->role < RSC_ROLE_UNPROMOTED)) { + accept = FALSE; + + } else if (text2role(value) != rule_data->role) { + accept = TRUE; + } + } + return accept; +} + +gboolean +pe__eval_rsc_expr(const xmlNode *expr, const pe_rule_eval_data_t *rule_data) +{ + const char *class = crm_element_value(expr, XML_AGENT_ATTR_CLASS); + const char *provider = crm_element_value(expr, XML_AGENT_ATTR_PROVIDER); + const char *type = crm_element_value(expr, XML_EXPR_ATTR_TYPE); + + crm_trace("Testing rsc_defaults expression: %s", ID(expr)); + + if (rule_data->rsc_data == NULL) { + crm_trace("No resource data provided"); + return FALSE; + } + + if (class != NULL && + !pcmk__str_eq(class, rule_data->rsc_data->standard, pcmk__str_none)) { + crm_trace("Class doesn't match: %s != %s", class, rule_data->rsc_data->standard); + return FALSE; + } + + if ((provider == NULL && rule_data->rsc_data->provider != NULL) || + (provider != NULL && rule_data->rsc_data->provider == NULL) || + !pcmk__str_eq(provider, rule_data->rsc_data->provider, pcmk__str_none)) { + crm_trace("Provider doesn't match: %s != %s", provider, rule_data->rsc_data->provider); + return FALSE; + } + + if (type != NULL && + !pcmk__str_eq(type, rule_data->rsc_data->agent, pcmk__str_none)) { + crm_trace("Agent doesn't match: %s != %s", type, rule_data->rsc_data->agent); + return FALSE; + } + + return TRUE; +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +gboolean +test_ruleset(xmlNode *ruleset, GHashTable *node_hash, crm_time_t *now) +{ + return pe_evaluate_rules(ruleset, node_hash, now, NULL); +} + +gboolean +test_rule(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) +{ + return pe_test_rule(rule, node_hash, role, now, NULL, NULL); +} + +gboolean +pe_test_rule_re(xmlNode * rule, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data) +{ + pe_match_data_t match_data = { + .re = re_match_data, + .params = NULL, + .meta = NULL, + }; + return pe_test_rule(rule, node_hash, role, now, NULL, &match_data); +} + +gboolean +pe_test_rule_full(xmlNode *rule, GHashTable *node_hash, enum rsc_role_e role, + crm_time_t *now, pe_match_data_t *match_data) +{ + return pe_test_rule(rule, node_hash, role, now, NULL, match_data); +} + +gboolean +test_expression(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now) +{ + return pe_test_expression(expr, node_hash, role, now, NULL, NULL); +} + +gboolean +pe_test_expression_re(xmlNode * expr, GHashTable * node_hash, enum rsc_role_e role, crm_time_t * now, pe_re_match_data_t * re_match_data) +{ + pe_match_data_t match_data = { + .re = re_match_data, + .params = NULL, + .meta = NULL, + }; + return pe_test_expression(expr, node_hash, role, now, NULL, &match_data); +} + +gboolean +pe_test_expression_full(xmlNode *expr, GHashTable *node_hash, + enum rsc_role_e role, crm_time_t *now, + pe_match_data_t *match_data) +{ + return pe_test_expression(expr, node_hash, role, now, NULL, match_data); +} + +void +unpack_instance_attributes(xmlNode *top, xmlNode *xml_obj, const char *set_name, + GHashTable *node_hash, GHashTable *hash, + const char *always_first, gboolean overwrite, + crm_time_t *now) +{ + pe_rule_eval_data_t rule_data = { + .node_hash = node_hash, + .role = RSC_ROLE_UNKNOWN, + .now = now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + pe_eval_nvpairs(top, xml_obj, set_name, &rule_data, hash, always_first, + overwrite, NULL); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/pengine/rules_alerts.c b/lib/pengine/rules_alerts.c new file mode 100644 index 0000000..073b0c1 --- /dev/null +++ b/lib/pengine/rules_alerts.c @@ -0,0 +1,299 @@ +/* + * Copyright 2015-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include +#include +#include + +/*! + * \internal + * \brief Unpack an alert's or alert recipient's meta attributes + * + * \param[in,out] basenode Alert or recipient XML + * \param[in,out] entry Where to store unpacked values + * \param[in,out] max_timeout Max timeout of all alerts and recipients thus far + * + * \return Standard Pacemaker return code + */ +static int +get_meta_attrs_from_cib(xmlNode *basenode, pcmk__alert_t *entry, + guint *max_timeout) +{ + GHashTable *config_hash = pcmk__strkey_table(free, free); + crm_time_t *now = crm_time_new(NULL); + const char *value = NULL; + int rc = pcmk_rc_ok; + + pe_unpack_nvpairs(basenode, basenode, XML_TAG_META_SETS, NULL, config_hash, + NULL, FALSE, now, NULL); + crm_time_free(now); + + value = g_hash_table_lookup(config_hash, PCMK_META_ENABLED); + if ((value != NULL) && !crm_is_true(value)) { + // No need to continue unpacking + rc = pcmk_rc_disabled; + goto done; + } + + value = g_hash_table_lookup(config_hash, XML_ALERT_ATTR_TIMEOUT); + if (value) { + entry->timeout = crm_get_msec(value); + if (entry->timeout <= 0) { + if (entry->timeout == 0) { + crm_trace("Alert %s uses default timeout of %dmsec", + entry->id, PCMK__ALERT_DEFAULT_TIMEOUT_MS); + } else { + crm_warn("Alert %s has invalid timeout value '%s', using default %dmsec", + entry->id, (char*)value, PCMK__ALERT_DEFAULT_TIMEOUT_MS); + } + entry->timeout = PCMK__ALERT_DEFAULT_TIMEOUT_MS; + } else { + crm_trace("Alert %s uses timeout of %dmsec", + entry->id, entry->timeout); + } + if (entry->timeout > *max_timeout) { + *max_timeout = entry->timeout; + } + } + value = g_hash_table_lookup(config_hash, XML_ALERT_ATTR_TSTAMP_FORMAT); + if (value) { + /* hard to do any checks here as merely anything can + * can be a valid time-format-string + */ + entry->tstamp_format = strdup(value); + crm_trace("Alert %s uses timestamp format '%s'", + entry->id, entry->tstamp_format); + } + +done: + g_hash_table_destroy(config_hash); + return rc; +} + +static void +get_envvars_from_cib(xmlNode *basenode, pcmk__alert_t *entry) +{ + xmlNode *child; + + if ((basenode == NULL) || (entry == NULL)) { + return; + } + + child = first_named_child(basenode, XML_TAG_ATTR_SETS); + if (child == NULL) { + return; + } + + if (entry->envvars == NULL) { + entry->envvars = pcmk__strkey_table(free, free); + } + + for (child = first_named_child(child, XML_CIB_TAG_NVPAIR); child != NULL; + child = crm_next_same_xml(child)) { + + const char *name = crm_element_value(child, XML_NVPAIR_ATTR_NAME); + const char *value = crm_element_value(child, XML_NVPAIR_ATTR_VALUE); + + if (value == NULL) { + value = ""; + } + g_hash_table_insert(entry->envvars, strdup(name), strdup(value)); + crm_trace("Alert %s: added environment variable %s='%s'", + entry->id, name, value); + } +} + +static void +unpack_alert_filter(xmlNode *basenode, pcmk__alert_t *entry) +{ + xmlNode *select = first_named_child(basenode, XML_CIB_TAG_ALERT_SELECT); + xmlNode *event_type = NULL; + uint32_t flags = pcmk__alert_none; + + for (event_type = pcmk__xe_first_child(select); event_type != NULL; + event_type = pcmk__xe_next(event_type)) { + + const char *tagname = crm_element_name(event_type); + + if (tagname == NULL) { + continue; + + } else if (!strcmp(tagname, XML_CIB_TAG_ALERT_FENCING)) { + flags |= pcmk__alert_fencing; + + } else if (!strcmp(tagname, XML_CIB_TAG_ALERT_NODES)) { + flags |= pcmk__alert_node; + + } else if (!strcmp(tagname, XML_CIB_TAG_ALERT_RESOURCES)) { + flags |= pcmk__alert_resource; + + } else if (!strcmp(tagname, XML_CIB_TAG_ALERT_ATTRIBUTES)) { + xmlNode *attr; + const char *attr_name; + int nattrs = 0; + + flags |= pcmk__alert_attribute; + for (attr = first_named_child(event_type, XML_CIB_TAG_ALERT_ATTR); + attr != NULL; + attr = crm_next_same_xml(attr)) { + + attr_name = crm_element_value(attr, XML_NVPAIR_ATTR_NAME); + if (attr_name) { + if (nattrs == 0) { + g_strfreev(entry->select_attribute_name); + entry->select_attribute_name = NULL; + } + ++nattrs; + entry->select_attribute_name = pcmk__realloc(entry->select_attribute_name, + (nattrs + 1) * sizeof(char*)); + entry->select_attribute_name[nattrs - 1] = strdup(attr_name); + entry->select_attribute_name[nattrs] = NULL; + } + } + } + } + + if (flags != pcmk__alert_none) { + entry->flags = flags; + crm_debug("Alert %s receives events: attributes:%s%s%s%s", + entry->id, + (pcmk_is_set(flags, pcmk__alert_attribute)? + (entry->select_attribute_name? "some" : "all") : "none"), + (pcmk_is_set(flags, pcmk__alert_fencing)? " fencing" : ""), + (pcmk_is_set(flags, pcmk__alert_node)? " nodes" : ""), + (pcmk_is_set(flags, pcmk__alert_resource)? " resources" : "")); + } +} + +/*! + * \internal + * \brief Unpack an alert or an alert recipient + * + * \param[in,out] alert Alert or recipient XML + * \param[in,out] entry Where to store unpacked values + * \param[in,out] max_timeout Max timeout of all alerts and recipients thus far + * + * \return Standard Pacemaker return code + */ +static int +unpack_alert(xmlNode *alert, pcmk__alert_t *entry, guint *max_timeout) +{ + int rc = pcmk_rc_ok; + + get_envvars_from_cib(alert, entry); + rc = get_meta_attrs_from_cib(alert, entry, max_timeout); + if (rc == pcmk_rc_ok) { + unpack_alert_filter(alert, entry); + } + return rc; +} + +/*! + * \internal + * \brief Unpack a CIB alerts section + * + * \param[in] alerts XML of alerts section + * + * \return List of unpacked alert entries + * + * \note Unlike most unpack functions, this is not used by the scheduler itself, + * but is supplied for use by daemons that need to send alerts. + */ +GList * +pe_unpack_alerts(const xmlNode *alerts) +{ + xmlNode *alert; + pcmk__alert_t *entry; + guint max_timeout = 0; + GList *alert_list = NULL; + + if (alerts == NULL) { + return alert_list; + } + + for (alert = first_named_child(alerts, XML_CIB_TAG_ALERT); + alert != NULL; alert = crm_next_same_xml(alert)) { + + xmlNode *recipient; + int recipients = 0; + const char *alert_id = ID(alert); + const char *alert_path = crm_element_value(alert, XML_ALERT_ATTR_PATH); + + /* The schema should enforce this, but to be safe ... */ + if ((alert_id == NULL) || (alert_path == NULL)) { + crm_warn("Ignoring invalid alert without id and path"); + continue; + } + + entry = pcmk__alert_new(alert_id, alert_path); + + if (unpack_alert(alert, entry, &max_timeout) != pcmk_rc_ok) { + // Don't allow recipients to override if entire alert is disabled + crm_debug("Alert %s is disabled", entry->id); + pcmk__free_alert(entry); + continue; + } + + if (entry->tstamp_format == NULL) { + entry->tstamp_format = strdup(PCMK__ALERT_DEFAULT_TSTAMP_FORMAT); + } + + crm_debug("Alert %s: path=%s timeout=%dms tstamp-format='%s' %u vars", + entry->id, entry->path, entry->timeout, entry->tstamp_format, + (entry->envvars? g_hash_table_size(entry->envvars) : 0)); + + for (recipient = first_named_child(alert, XML_CIB_TAG_ALERT_RECIPIENT); + recipient != NULL; recipient = crm_next_same_xml(recipient)) { + + pcmk__alert_t *recipient_entry = pcmk__dup_alert(entry); + + recipients++; + recipient_entry->recipient = strdup(crm_element_value(recipient, + XML_ALERT_ATTR_REC_VALUE)); + + if (unpack_alert(recipient, recipient_entry, + &max_timeout) != pcmk_rc_ok) { + crm_debug("Alert %s: recipient %s is disabled", + entry->id, recipient_entry->id); + pcmk__free_alert(recipient_entry); + continue; + } + alert_list = g_list_prepend(alert_list, recipient_entry); + crm_debug("Alert %s has recipient %s with value %s and %d envvars", + entry->id, ID(recipient), recipient_entry->recipient, + (recipient_entry->envvars? + g_hash_table_size(recipient_entry->envvars) : 0)); + } + + if (recipients == 0) { + alert_list = g_list_prepend(alert_list, entry); + } else { + pcmk__free_alert(entry); + } + } + return alert_list; +} + +/*! + * \internal + * \brief Free an alert list generated by pe_unpack_alerts() + * + * \param[in,out] alert_list Alert list to free + */ +void +pe_free_alert_list(GList *alert_list) +{ + if (alert_list) { + g_list_free_full(alert_list, (GDestroyNotify) pcmk__free_alert); + } +} diff --git a/lib/pengine/status.c b/lib/pengine/status.c new file mode 100644 index 0000000..b1144eb --- /dev/null +++ b/lib/pengine/status.c @@ -0,0 +1,483 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include +#include + +#include + +#include +#include + +/*! + * \brief Create a new working set + * + * \return New, initialized working set on success, else NULL (and set errno) + * \note Only pe_working_set_t objects created with this function (as opposed + * to statically declared or directly allocated) should be used with the + * functions in this library, to allow for future extensions to the + * data type. The caller is responsible for freeing the memory with + * pe_free_working_set() when the instance is no longer needed. + */ +pe_working_set_t * +pe_new_working_set(void) +{ + pe_working_set_t *data_set = calloc(1, sizeof(pe_working_set_t)); + + if (data_set != NULL) { + set_working_set_defaults(data_set); + } + return data_set; +} + +/*! + * \brief Free a working set + * + * \param[in,out] data_set Working set to free + */ +void +pe_free_working_set(pe_working_set_t *data_set) +{ + if (data_set != NULL) { + pe_reset_working_set(data_set); + data_set->priv = NULL; + free(data_set); + } +} + +/* + * Unpack everything + * At the end you'll have: + * - A list of nodes + * - A list of resources (each with any dependencies on other resources) + * - A list of constraints between resources and nodes + * - A list of constraints between start/stop actions + * - A list of nodes that need to be stonith'd + * - A list of nodes that need to be shutdown + * - A list of the possible stop/start actions (without dependencies) + */ +gboolean +cluster_status(pe_working_set_t * data_set) +{ + xmlNode *section = NULL; + + if ((data_set == NULL) || (data_set->input == NULL)) { + return FALSE; + } + + crm_trace("Beginning unpack"); + + if (data_set->failed != NULL) { + free_xml(data_set->failed); + } + data_set->failed = create_xml_node(NULL, "failed-ops"); + + if (data_set->now == NULL) { + data_set->now = crm_time_new(NULL); + } + + if (data_set->dc_uuid == NULL) { + data_set->dc_uuid = crm_element_value_copy(data_set->input, + XML_ATTR_DC_UUID); + } + + if (pcmk__xe_attr_is_true(data_set->input, XML_ATTR_HAVE_QUORUM)) { + pe__set_working_set_flags(data_set, pe_flag_have_quorum); + } else { + pe__clear_working_set_flags(data_set, pe_flag_have_quorum); + } + + data_set->op_defaults = get_xpath_object("//" XML_CIB_TAG_OPCONFIG, + data_set->input, LOG_NEVER); + data_set->rsc_defaults = get_xpath_object("//" XML_CIB_TAG_RSCCONFIG, + data_set->input, LOG_NEVER); + + section = get_xpath_object("//" XML_CIB_TAG_CRMCONFIG, data_set->input, + LOG_TRACE); + unpack_config(section, data_set); + + if (!pcmk_any_flags_set(data_set->flags, + pe_flag_quick_location|pe_flag_have_quorum) + && (data_set->no_quorum_policy != no_quorum_ignore)) { + crm_warn("Fencing and resource management disabled due to lack of quorum"); + } + + section = get_xpath_object("//" XML_CIB_TAG_NODES, data_set->input, + LOG_TRACE); + unpack_nodes(section, data_set); + + section = get_xpath_object("//" XML_CIB_TAG_RESOURCES, data_set->input, + LOG_TRACE); + if (!pcmk_is_set(data_set->flags, pe_flag_quick_location)) { + unpack_remote_nodes(section, data_set); + } + unpack_resources(section, data_set); + + section = get_xpath_object("//" XML_CIB_TAG_TAGS, data_set->input, + LOG_NEVER); + unpack_tags(section, data_set); + + if (!pcmk_is_set(data_set->flags, pe_flag_quick_location)) { + section = get_xpath_object("//"XML_CIB_TAG_STATUS, data_set->input, + LOG_TRACE); + unpack_status(section, data_set); + } + + if (!pcmk_is_set(data_set->flags, pe_flag_no_counts)) { + for (GList *item = data_set->resources; item != NULL; + item = item->next) { + ((pe_resource_t *) (item->data))->fns->count(item->data); + } + crm_trace("Cluster resource count: %d (%d disabled, %d blocked)", + data_set->ninstances, data_set->disabled_resources, + data_set->blocked_resources); + } + + pe__set_working_set_flags(data_set, pe_flag_have_status); + return TRUE; +} + +/*! + * \internal + * \brief Free a list of pe_resource_t + * + * \param[in,out] resources List to free + * + * \note When a working set's resource list is freed, that includes the original + * storage for the uname and id of any Pacemaker Remote nodes in the + * working set's node list, so take care not to use those afterward. + * \todo Refactor pe_node_t to strdup() the node name. + */ +static void +pe_free_resources(GList *resources) +{ + pe_resource_t *rsc = NULL; + GList *iterator = resources; + + while (iterator != NULL) { + rsc = (pe_resource_t *) iterator->data; + iterator = iterator->next; + rsc->fns->free(rsc); + } + if (resources != NULL) { + g_list_free(resources); + } +} + +static void +pe_free_actions(GList *actions) +{ + GList *iterator = actions; + + while (iterator != NULL) { + pe_free_action(iterator->data); + iterator = iterator->next; + } + if (actions != NULL) { + g_list_free(actions); + } +} + +static void +pe_free_nodes(GList *nodes) +{ + for (GList *iterator = nodes; iterator != NULL; iterator = iterator->next) { + pe_node_t *node = (pe_node_t *) iterator->data; + + // Shouldn't be possible, but to be safe ... + if (node == NULL) { + continue; + } + if (node->details == NULL) { + free(node); + continue; + } + + /* This is called after pe_free_resources(), which means that we can't + * use node->details->uname for Pacemaker Remote nodes. + */ + crm_trace("Freeing node %s", (pe__is_guest_or_remote_node(node)? + "(guest or remote)" : pe__node_name(node))); + + if (node->details->attrs != NULL) { + g_hash_table_destroy(node->details->attrs); + } + if (node->details->utilization != NULL) { + g_hash_table_destroy(node->details->utilization); + } + if (node->details->digest_cache != NULL) { + g_hash_table_destroy(node->details->digest_cache); + } + g_list_free(node->details->running_rsc); + g_list_free(node->details->allocated_rsc); + free(node->details); + free(node); + } + if (nodes != NULL) { + g_list_free(nodes); + } +} + +static void +pe__free_ordering(GList *constraints) +{ + GList *iterator = constraints; + + while (iterator != NULL) { + pe__ordering_t *order = iterator->data; + + iterator = iterator->next; + + free(order->lh_action_task); + free(order->rh_action_task); + free(order); + } + if (constraints != NULL) { + g_list_free(constraints); + } +} + +static void +pe__free_location(GList *constraints) +{ + GList *iterator = constraints; + + while (iterator != NULL) { + pe__location_t *cons = iterator->data; + + iterator = iterator->next; + + g_list_free_full(cons->node_list_rh, free); + free(cons->id); + free(cons); + } + if (constraints != NULL) { + g_list_free(constraints); + } +} + +/*! + * \brief Reset working set to default state without freeing it or constraints + * + * \param[in,out] data_set Working set to reset + * + * \deprecated This function is deprecated as part of the API; + * pe_reset_working_set() should be used instead. + */ +void +cleanup_calculations(pe_working_set_t * data_set) +{ + if (data_set == NULL) { + return; + } + + pe__clear_working_set_flags(data_set, pe_flag_have_status); + if (data_set->config_hash != NULL) { + g_hash_table_destroy(data_set->config_hash); + } + + if (data_set->singletons != NULL) { + g_hash_table_destroy(data_set->singletons); + } + + if (data_set->tickets) { + g_hash_table_destroy(data_set->tickets); + } + + if (data_set->template_rsc_sets) { + g_hash_table_destroy(data_set->template_rsc_sets); + } + + if (data_set->tags) { + g_hash_table_destroy(data_set->tags); + } + + free(data_set->dc_uuid); + + crm_trace("deleting resources"); + pe_free_resources(data_set->resources); + + crm_trace("deleting actions"); + pe_free_actions(data_set->actions); + + crm_trace("deleting nodes"); + pe_free_nodes(data_set->nodes); + + pe__free_param_checks(data_set); + g_list_free(data_set->stop_needed); + free_xml(data_set->graph); + crm_time_free(data_set->now); + free_xml(data_set->input); + free_xml(data_set->failed); + + set_working_set_defaults(data_set); + + CRM_CHECK(data_set->ordering_constraints == NULL,; + ); + CRM_CHECK(data_set->placement_constraints == NULL,; + ); +} + +/*! + * \brief Reset a working set to default state without freeing it + * + * \param[in,out] data_set Working set to reset + */ +void +pe_reset_working_set(pe_working_set_t *data_set) +{ + if (data_set == NULL) { + return; + } + + crm_trace("Deleting %d ordering constraints", + g_list_length(data_set->ordering_constraints)); + pe__free_ordering(data_set->ordering_constraints); + data_set->ordering_constraints = NULL; + + crm_trace("Deleting %d location constraints", + g_list_length(data_set->placement_constraints)); + pe__free_location(data_set->placement_constraints); + data_set->placement_constraints = NULL; + + crm_trace("Deleting %d colocation constraints", + g_list_length(data_set->colocation_constraints)); + g_list_free_full(data_set->colocation_constraints, free); + data_set->colocation_constraints = NULL; + + crm_trace("Deleting %d ticket constraints", + g_list_length(data_set->ticket_constraints)); + g_list_free_full(data_set->ticket_constraints, free); + data_set->ticket_constraints = NULL; + + cleanup_calculations(data_set); +} + +void +set_working_set_defaults(pe_working_set_t * data_set) +{ + void *priv = data_set->priv; + + memset(data_set, 0, sizeof(pe_working_set_t)); + + data_set->priv = priv; + data_set->order_id = 1; + data_set->action_id = 1; + data_set->no_quorum_policy = no_quorum_stop; + + data_set->flags = 0x0ULL; + + pe__set_working_set_flags(data_set, + pe_flag_stop_rsc_orphans + |pe_flag_symmetric_cluster + |pe_flag_stop_action_orphans); + if (!strcmp(PCMK__CONCURRENT_FENCING_DEFAULT, "true")) { + pe__set_working_set_flags(data_set, pe_flag_concurrent_fencing); + } +} + +pe_resource_t * +pe_find_resource(GList *rsc_list, const char *id) +{ + return pe_find_resource_with_flags(rsc_list, id, pe_find_renamed); +} + +pe_resource_t * +pe_find_resource_with_flags(GList *rsc_list, const char *id, enum pe_find flags) +{ + GList *rIter = NULL; + + for (rIter = rsc_list; id && rIter; rIter = rIter->next) { + pe_resource_t *parent = rIter->data; + + pe_resource_t *match = + parent->fns->find_rsc(parent, id, NULL, flags); + if (match != NULL) { + return match; + } + } + crm_trace("No match for %s", id); + return NULL; +} + +/*! + * \brief Find a node by name or ID in a list of nodes + * + * \param[in] nodes List of nodes (as pe_node_t*) + * \param[in] id If not NULL, ID of node to find + * \param[in] node_name If not NULL, name of node to find + * + * \return Node from \p nodes that matches \p id if any, + * otherwise node from \p nodes that matches \p uname if any, + * otherwise NULL + */ +pe_node_t * +pe_find_node_any(const GList *nodes, const char *id, const char *uname) +{ + pe_node_t *match = NULL; + + if (id != NULL) { + match = pe_find_node_id(nodes, id); + } + if ((match == NULL) && (uname != NULL)) { + match = pe_find_node(nodes, uname); + } + return match; +} + +/*! + * \brief Find a node by ID in a list of nodes + * + * \param[in] nodes List of nodes (as pe_node_t*) + * \param[in] id ID of node to find + * + * \return Node from \p nodes that matches \p id if any, otherwise NULL + */ +pe_node_t * +pe_find_node_id(const GList *nodes, const char *id) +{ + for (const GList *iter = nodes; iter != NULL; iter = iter->next) { + pe_node_t *node = (pe_node_t *) iter->data; + + /* @TODO Whether node IDs should be considered case-sensitive should + * probably depend on the node type, so functionizing the comparison + * would be worthwhile + */ + if (pcmk__str_eq(node->details->id, id, pcmk__str_casei)) { + return node; + } + } + return NULL; +} + +/*! + * \brief Find a node by name in a list of nodes + * + * \param[in] nodes List of nodes (as pe_node_t*) + * \param[in] node_name Name of node to find + * + * \return Node from \p nodes that matches \p node_name if any, otherwise NULL + */ +pe_node_t * +pe_find_node(const GList *nodes, const char *node_name) +{ + for (const GList *iter = nodes; iter != NULL; iter = iter->next) { + pe_node_t *node = (pe_node_t *) iter->data; + + if (pcmk__str_eq(node->details->uname, node_name, pcmk__str_casei)) { + return node; + } + } + return NULL; +} diff --git a/lib/pengine/tags.c b/lib/pengine/tags.c new file mode 100644 index 0000000..81c27e4 --- /dev/null +++ b/lib/pengine/tags.c @@ -0,0 +1,111 @@ +/* + * Copyright 2020-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include +#include + +GList * +pe__rscs_with_tag(pe_working_set_t *data_set, const char *tag_name) +{ + gpointer value; + GList *retval = NULL; + + if (data_set->tags == NULL) { + return retval; + } + + value = g_hash_table_lookup(data_set->tags, tag_name); + + if (value == NULL) { + return retval; + } + + for (GList *refs = ((pe_tag_t *) value)->refs; refs; refs = refs->next) { + const char *id = (const char *) refs->data; + pe_resource_t *rsc = pe_find_resource_with_flags(data_set->resources, id, + pe_find_renamed|pe_find_any); + + if (!rsc) { + continue; + } + + retval = g_list_append(retval, strdup(rsc_printable_id(rsc))); + } + + return retval; +} + +GList * +pe__unames_with_tag(pe_working_set_t *data_set, const char *tag_name) +{ + gpointer value; + GList *retval = NULL; + + if (data_set->tags == NULL) { + return retval; + } + + value = g_hash_table_lookup(data_set->tags, tag_name); + + if (value == NULL) { + return retval; + } + + /* Iterate over the list of node IDs. */ + for (GList *refs = ((pe_tag_t *) value)->refs; refs; refs = refs->next) { + /* Find the node that has this ID. */ + const char *id = (const char *) refs->data; + pe_node_t *node = pe_find_node_id(data_set->nodes, id); + + if (!node) { + continue; + } + + /* Get the uname for the node and add it to the return list. */ + retval = g_list_append(retval, strdup(node->details->uname)); + } + + return retval; +} + +bool +pe__rsc_has_tag(pe_working_set_t *data_set, const char *rsc_name, const char *tag_name) +{ + GList *rscs = pe__rscs_with_tag(data_set, tag_name); + bool retval = false; + + if (rscs == NULL) { + return retval; + } + + retval = g_list_find_custom(rscs, rsc_name, (GCompareFunc) strcmp) != NULL; + g_list_free_full(rscs, free); + return retval; +} + +bool +pe__uname_has_tag(pe_working_set_t *data_set, const char *node_name, const char *tag_name) +{ + GList *unames = pe__unames_with_tag(data_set, tag_name); + bool retval = false; + + if (unames == NULL) { + return retval; + } + + retval = g_list_find_custom(unames, node_name, (GCompareFunc) strcmp) != NULL; + g_list_free_full(unames, free); + return retval; +} diff --git a/lib/pengine/tests/Makefile.am b/lib/pengine/tests/Makefile.am new file mode 100644 index 0000000..4986ef2 --- /dev/null +++ b/lib/pengine/tests/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = rules native status unpack utils diff --git a/lib/pengine/tests/native/Makefile.am b/lib/pengine/tests/native/Makefile.am new file mode 100644 index 0000000..5046ff1 --- /dev/null +++ b/lib/pengine/tests/native/Makefile.am @@ -0,0 +1,22 @@ +# +# Copyright 2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +AM_CPPFLAGS += -I$(top_srcdir) +LDADD += $(top_builddir)/lib/pengine/libpe_status_test.la + +AM_TESTS_ENVIRONMENT += PCMK_CTS_CLI_DIR=$(top_srcdir)/cts/cli + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = native_find_rsc_test \ + pe_base_name_eq_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/pengine/tests/native/native_find_rsc_test.c b/lib/pengine/tests/native/native_find_rsc_test.c new file mode 100644 index 0000000..22aaf41 --- /dev/null +++ b/lib/pengine/tests/native/native_find_rsc_test.c @@ -0,0 +1,677 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include +#include + +/* Needed to access replicas inside a bundle. */ +#define PE__VARIANT_BUNDLE 1 +#include + +xmlNode *input = NULL; +pe_working_set_t *data_set = NULL; + +pe_node_t *cluster01, *cluster02, *httpd_bundle_0; +pe_resource_t *exim_group, *inactive_group, *promotable_clone, *inactive_clone; +pe_resource_t *httpd_bundle, *mysql_clone_group; + +static int +setup(void **state) { + char *path = NULL; + + crm_xml_init(); + + path = crm_strdup_printf("%s/crm_mon.xml", getenv("PCMK_CTS_CLI_DIR")); + input = filename2xml(path); + free(path); + + if (input == NULL) { + return 1; + } + + data_set = pe_new_working_set(); + + if (data_set == NULL) { + return 1; + } + + pe__set_working_set_flags(data_set, pe_flag_no_counts|pe_flag_no_compat); + data_set->input = input; + + cluster_status(data_set); + + /* Get references to the cluster nodes so we don't have to find them repeatedly. */ + cluster01 = pe_find_node(data_set->nodes, "cluster01"); + cluster02 = pe_find_node(data_set->nodes, "cluster02"); + httpd_bundle_0 = pe_find_node(data_set->nodes, "httpd-bundle-0"); + + /* Get references to several resources we use frequently. */ + for (GList *iter = data_set->resources; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "exim-group") == 0) { + exim_group = rsc; + } else if (strcmp(rsc->id, "httpd-bundle") == 0) { + httpd_bundle = rsc; + } else if (strcmp(rsc->id, "inactive-clone") == 0) { + inactive_clone = rsc; + } else if (strcmp(rsc->id, "inactive-group") == 0) { + inactive_group = rsc; + } else if (strcmp(rsc->id, "mysql-clone-group") == 0) { + mysql_clone_group = rsc; + } else if (strcmp(rsc->id, "promotable-clone") == 0) { + promotable_clone = rsc; + } + } + + return 0; +} + +static int +teardown(void **state) { + pe_free_working_set(data_set); + + return 0; +} + +static void +bad_args(void **state) { + pe_resource_t *rsc = (pe_resource_t *) g_list_first(data_set->resources)->data; + char *id = rsc->id; + char *name = NULL; + + assert_non_null(rsc); + + assert_null(native_find_rsc(NULL, "dummy", NULL, 0)); + assert_null(native_find_rsc(rsc, NULL, NULL, 0)); + + /* No resources exist with these names. */ + name = crm_strdup_printf("%sX", rsc->id); + assert_null(native_find_rsc(rsc, name, NULL, 0)); + free(name); + + name = crm_strdup_printf("x%s", rsc->id); + assert_null(native_find_rsc(rsc, name, NULL, 0)); + free(name); + + name = g_ascii_strup(rsc->id, -1); + assert_null(native_find_rsc(rsc, name, NULL, 0)); + g_free(name); + + /* Fails because resource ID is NULL. */ + rsc->id = NULL; + assert_null(native_find_rsc(rsc, id, NULL, 0)); + rsc->id = id; +} + +static void +primitive_rsc(void **state) { + pe_resource_t *dummy = NULL; + + /* Find the "dummy" resource, which is the only one with that ID in the set. */ + for (GList *iter = data_set->resources; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "dummy") == 0) { + dummy = rsc; + break; + } + } + + assert_non_null(dummy); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(dummy, native_find_rsc(dummy, "dummy", NULL, 0)); + assert_ptr_equal(dummy, native_find_rsc(dummy, "dummy", NULL, pe_find_current)); + + /* Fails because resource is not a clone (nor cloned). */ + assert_null(native_find_rsc(dummy, "dummy", NULL, pe_find_clone)); + assert_null(native_find_rsc(dummy, "dummy", cluster02, pe_find_clone)); + + /* Fails because dummy is not running on cluster01, even with the right flags. */ + assert_null(native_find_rsc(dummy, "dummy", cluster01, pe_find_current)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(dummy, "dummy", cluster02, 0)); + + /* Passes because dummy is running on cluster02. */ + assert_ptr_equal(dummy, native_find_rsc(dummy, "dummy", cluster02, pe_find_current)); +} + +static void +group_rsc(void **state) { + assert_non_null(exim_group); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(exim_group, native_find_rsc(exim_group, "exim-group", NULL, 0)); + assert_ptr_equal(exim_group, native_find_rsc(exim_group, "exim-group", NULL, pe_find_current)); + + /* Fails because resource is not a clone (nor cloned). */ + assert_null(native_find_rsc(exim_group, "exim-group", NULL, pe_find_clone)); + assert_null(native_find_rsc(exim_group, "exim-group", cluster01, pe_find_clone)); + + /* Fails because none of exim-group's children are running on cluster01, even with the right flags. */ + assert_null(native_find_rsc(exim_group, "exim-group", cluster01, pe_find_current)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(exim_group, "exim-group", cluster01, 0)); + + /* Passes because one of exim-group's children is running on cluster02. */ + assert_ptr_equal(exim_group, native_find_rsc(exim_group, "exim-group", cluster02, pe_find_current)); +} + +static void +inactive_group_rsc(void **state) { + assert_non_null(inactive_group); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(inactive_group, native_find_rsc(inactive_group, "inactive-group", NULL, 0)); + assert_ptr_equal(inactive_group, native_find_rsc(inactive_group, "inactive-group", NULL, pe_find_current)); + assert_ptr_equal(inactive_group, native_find_rsc(inactive_group, "inactive-group", NULL, pe_find_inactive)); + + /* Fails because resource is not a clone (nor cloned). */ + assert_null(native_find_rsc(inactive_group, "inactive-group", NULL, pe_find_clone)); + assert_null(native_find_rsc(inactive_group, "inactive-group", cluster01, pe_find_clone)); + + /* Fails because none of inactive-group's children are running. */ + assert_null(native_find_rsc(inactive_group, "inactive-group", cluster01, pe_find_current)); + assert_null(native_find_rsc(inactive_group, "inactive-group", cluster02, pe_find_current)); + + /* Passes because of flags. */ + assert_ptr_equal(inactive_group, native_find_rsc(inactive_group, "inactive-group", cluster01, pe_find_inactive)); + /* Passes because of flags. */ + assert_ptr_equal(inactive_group, native_find_rsc(inactive_group, "inactive-group", cluster02, pe_find_inactive)); +} + +static void +group_member_rsc(void **state) { + pe_resource_t *public_ip = NULL; + + /* Find the "Public-IP" resource, a member of "exim-group". */ + for (GList *iter = exim_group->children; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "Public-IP") == 0) { + public_ip = rsc; + break; + } + } + + assert_non_null(public_ip); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(public_ip, native_find_rsc(public_ip, "Public-IP", NULL, 0)); + assert_ptr_equal(public_ip, native_find_rsc(public_ip, "Public-IP", NULL, pe_find_current)); + + /* Fails because resource is not a clone (nor cloned). */ + assert_null(native_find_rsc(public_ip, "Public-IP", NULL, pe_find_clone)); + assert_null(native_find_rsc(public_ip, "Public-IP", cluster02, pe_find_clone)); + + /* Fails because Public-IP is not running on cluster01, even with the right flags. */ + assert_null(native_find_rsc(public_ip, "Public-IP", cluster01, pe_find_current)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(public_ip, "Public-IP", cluster02, 0)); + + /* Passes because Public-IP is running on cluster02. */ + assert_ptr_equal(public_ip, native_find_rsc(public_ip, "Public-IP", cluster02, pe_find_current)); +} + +static void +inactive_group_member_rsc(void **state) { + pe_resource_t *inactive_dummy_1 = NULL; + + /* Find the "inactive-dummy-1" resource, a member of "inactive-group". */ + for (GList *iter = inactive_group->children; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "inactive-dummy-1") == 0) { + inactive_dummy_1 = rsc; + break; + } + } + + assert_non_null(inactive_dummy_1); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(inactive_dummy_1, native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL, 0)); + assert_ptr_equal(inactive_dummy_1, native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL, pe_find_current)); + + /* Fails because resource is not a clone (nor cloned). */ + assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", NULL, pe_find_clone)); + assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster01, pe_find_clone)); + + /* Fails because inactive-dummy-1 is not running. */ + assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster01, pe_find_current)); + assert_null(native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster02, pe_find_current)); + + /* Passes because of flags. */ + assert_ptr_equal(inactive_dummy_1, native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster01, pe_find_inactive)); + /* Passes because of flags. */ + assert_ptr_equal(inactive_dummy_1, native_find_rsc(inactive_dummy_1, "inactive-dummy-1", cluster02, pe_find_inactive)); +} + +static void +clone_rsc(void **state) { + assert_non_null(promotable_clone); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", NULL, 0)); + assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", NULL, pe_find_current)); + assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", NULL, pe_find_clone)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(promotable_clone, "promotable-clone", cluster01, 0)); + + /* Passes because one of ping-clone's children is running on cluster01. */ + assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", cluster01, pe_find_current)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(promotable_clone, "promotable-clone", cluster02, 0)); + + /* Passes because one of ping_clone's children is running on cluster02. */ + assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", cluster02, pe_find_current)); + + /* Passes for previous reasons, plus includes pe_find_clone check. */ + assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", cluster01, pe_find_clone|pe_find_current)); + assert_ptr_equal(promotable_clone, native_find_rsc(promotable_clone, "promotable-clone", cluster02, pe_find_clone|pe_find_current)); +} + +static void +inactive_clone_rsc(void **state) { + assert_non_null(inactive_clone); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", NULL, 0)); + assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", NULL, pe_find_current)); + assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", NULL, pe_find_clone)); + assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", NULL, pe_find_inactive)); + + /* Fails because none of inactive-clone's children are running. */ + assert_null(native_find_rsc(inactive_clone, "inactive-clone", cluster01, pe_find_current|pe_find_clone)); + assert_null(native_find_rsc(inactive_clone, "inactive-clone", cluster02, pe_find_current|pe_find_clone)); + + /* Passes because of flags. */ + assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", cluster01, pe_find_inactive)); + /* Passes because of flags. */ + assert_ptr_equal(inactive_clone, native_find_rsc(inactive_clone, "inactive-clone", cluster02, pe_find_inactive)); +} + +static void +clone_instance_rsc(void **state) { + pe_resource_t *promotable_0 = NULL; + pe_resource_t *promotable_1 = NULL; + + /* Find the "promotable-rsc:0" and "promotable-rsc:1" resources, members of "promotable-clone". */ + for (GList *iter = promotable_clone->children; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "promotable-rsc:0") == 0) { + promotable_0 = rsc; + } else if (strcmp(rsc->id, "promotable-rsc:1") == 0) { + promotable_1 = rsc; + } + } + + assert_non_null(promotable_0); + assert_non_null(promotable_1); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc:0", NULL, 0)); + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc:0", NULL, pe_find_current)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc:1", NULL, 0)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc:1", NULL, pe_find_current)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(promotable_0, "promotable-rsc:0", cluster02, 0)); + assert_null(native_find_rsc(promotable_1, "promotable-rsc:1", cluster01, 0)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc:0", cluster02, pe_find_current)); + assert_null(native_find_rsc(promotable_0, "promotable-rsc:0", cluster01, pe_find_current)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc:1", cluster01, pe_find_current)); + assert_null(native_find_rsc(promotable_1, "promotable-rsc:1", cluster02, pe_find_current)); + + /* Passes because NULL was passed for node and primitive name was given, with correct flags. */ + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", NULL, pe_find_clone)); + + /* Passes because pe_find_any matches any instance's base name. */ + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", NULL, pe_find_any)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", NULL, pe_find_any)); + + /* Passes because pe_find_anon matches. */ + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", NULL, pe_find_anon)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", NULL, pe_find_anon)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", cluster02, pe_find_any|pe_find_current)); + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", cluster02, pe_find_anon|pe_find_current)); + assert_null(native_find_rsc(promotable_0, "promotable-rsc", cluster01, pe_find_any|pe_find_current)); + assert_null(native_find_rsc(promotable_0, "promotable-rsc", cluster01, pe_find_anon|pe_find_current)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", cluster01, pe_find_any|pe_find_current)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", cluster01, pe_find_anon|pe_find_current)); + assert_null(native_find_rsc(promotable_1, "promotable-rsc", cluster02, pe_find_any|pe_find_current)); + assert_null(native_find_rsc(promotable_1, "promotable-rsc", cluster02, pe_find_anon|pe_find_current)); + + /* Fails because incorrect flags were given along with primitive name. */ + assert_null(native_find_rsc(promotable_0, "promotable-rsc", NULL, pe_find_current)); + assert_null(native_find_rsc(promotable_1, "promotable-rsc", NULL, pe_find_current)); + + /* And then we check failure possibilities again, except passing promotable_clone + * instead of promotable_X as the first argument to native_find_rsc. + */ + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(promotable_clone, "promotable-rsc:0", cluster02, 0)); + assert_null(native_find_rsc(promotable_clone, "promotable-rsc:1", cluster01, 0)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(promotable_0, native_find_rsc(promotable_clone, "promotable-rsc:0", cluster02, pe_find_current)); + assert_ptr_equal(promotable_0, native_find_rsc(promotable_clone, "promotable-rsc", cluster02, pe_find_any|pe_find_current)); + assert_ptr_equal(promotable_0, native_find_rsc(promotable_clone, "promotable-rsc", cluster02, pe_find_anon|pe_find_current)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_clone, "promotable-rsc:1", cluster01, pe_find_current)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_clone, "promotable-rsc", cluster01, pe_find_any|pe_find_current)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_clone, "promotable-rsc", cluster01, pe_find_anon|pe_find_current)); +} + +static void +renamed_rsc(void **state) { + pe_resource_t *promotable_0 = NULL; + pe_resource_t *promotable_1 = NULL; + + /* Find the "promotable-rsc:0" and "promotable-rsc:1" resources, members of "promotable-clone". */ + for (GList *iter = promotable_clone->children; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "promotable-rsc:0") == 0) { + promotable_0 = rsc; + } else if (strcmp(rsc->id, "promotable-rsc:1") == 0) { + promotable_1 = rsc; + } + } + + assert_non_null(promotable_0); + assert_non_null(promotable_1); + + /* Passes because pe_find_renamed means the base name matches clone_name. */ + assert_ptr_equal(promotable_0, native_find_rsc(promotable_0, "promotable-rsc", NULL, pe_find_renamed)); + assert_ptr_equal(promotable_1, native_find_rsc(promotable_1, "promotable-rsc", NULL, pe_find_renamed)); +} + +static void +bundle_rsc(void **state) { + assert_non_null(httpd_bundle); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(httpd_bundle, native_find_rsc(httpd_bundle, "httpd-bundle", NULL, 0)); + assert_ptr_equal(httpd_bundle, native_find_rsc(httpd_bundle, "httpd-bundle", NULL, pe_find_current)); + + /* Fails because resource is not a clone (nor cloned). */ + assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", NULL, pe_find_clone)); + assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", cluster01, pe_find_clone)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(httpd_bundle, "httpd-bundle", cluster01, 0)); + + /* Passes because one of httpd_bundle's children is running on cluster01. */ + assert_ptr_equal(httpd_bundle, native_find_rsc(httpd_bundle, "httpd-bundle", cluster01, pe_find_current)); +} + +static void +bundle_replica_rsc(void **state) { + pe__bundle_variant_data_t *bundle_data = NULL; + pe__bundle_replica_t *replica_0 = NULL; + + pe_resource_t *ip_0 = NULL; + pe_resource_t *child_0 = NULL; + pe_resource_t *container_0 = NULL; + pe_resource_t *remote_0 = NULL; + + get_bundle_variant_data(bundle_data, httpd_bundle); + replica_0 = (pe__bundle_replica_t *) bundle_data->replicas->data; + + ip_0 = replica_0->ip; + child_0 = replica_0->child; + container_0 = replica_0->container; + remote_0 = replica_0->remote; + + assert_non_null(ip_0); + assert_non_null(child_0); + assert_non_null(container_0); + assert_non_null(remote_0); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(ip_0, native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", NULL, 0)); + assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd:0", NULL, 0)); + assert_ptr_equal(container_0, native_find_rsc(container_0, "httpd-bundle-docker-0", NULL, 0)); + assert_ptr_equal(remote_0, native_find_rsc(remote_0, "httpd-bundle-0", NULL, 0)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", cluster01, 0)); + assert_null(native_find_rsc(child_0, "httpd:0", httpd_bundle_0, 0)); + assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0", cluster01, 0)); + assert_null(native_find_rsc(remote_0, "httpd-bundle-0", cluster01, 0)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(ip_0, native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", cluster01, pe_find_current)); + assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", cluster02, pe_find_current)); + assert_null(native_find_rsc(ip_0, "httpd-bundle-ip-192.168.122.131", httpd_bundle_0, pe_find_current)); + assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd:0", httpd_bundle_0, pe_find_current)); + assert_null(native_find_rsc(child_0, "httpd:0", cluster01, pe_find_current)); + assert_null(native_find_rsc(child_0, "httpd:0", cluster02, pe_find_current)); + assert_ptr_equal(container_0, native_find_rsc(container_0, "httpd-bundle-docker-0", cluster01, pe_find_current)); + assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0", cluster02, pe_find_current)); + assert_null(native_find_rsc(container_0, "httpd-bundle-docker-0", httpd_bundle_0, pe_find_current)); + assert_ptr_equal(remote_0, native_find_rsc(remote_0, "httpd-bundle-0", cluster01, pe_find_current)); + assert_null(native_find_rsc(remote_0, "httpd-bundle-0", cluster02, pe_find_current)); + assert_null(native_find_rsc(remote_0, "httpd-bundle-0", httpd_bundle_0, pe_find_current)); + + /* Passes because pe_find_any matches any replica's base name. */ + assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd", NULL, pe_find_any)); + + /* Passes because pe_find_anon matches. */ + assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd", NULL, pe_find_anon)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd", httpd_bundle_0, pe_find_any|pe_find_current)); + assert_ptr_equal(child_0, native_find_rsc(child_0, "httpd", httpd_bundle_0, pe_find_anon|pe_find_current)); + assert_null(native_find_rsc(child_0, "httpd", cluster01, pe_find_any|pe_find_current)); + assert_null(native_find_rsc(child_0, "httpd", cluster01, pe_find_anon|pe_find_current)); + assert_null(native_find_rsc(child_0, "httpd", cluster02, pe_find_any|pe_find_current)); + assert_null(native_find_rsc(child_0, "httpd", cluster02, pe_find_anon|pe_find_current)); + + /* Fails because incorrect flags were given along with base name. */ + assert_null(native_find_rsc(child_0, "httpd", NULL, pe_find_current)); + + /* And then we check failure possibilities again, except passing httpd-bundle + * instead of X_0 as the first argument to native_find_rsc. + */ + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-ip-192.168.122.131", cluster01, 0)); + assert_null(native_find_rsc(httpd_bundle, "httpd:0", httpd_bundle_0, 0)); + assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-docker-0", cluster01, 0)); + assert_null(native_find_rsc(httpd_bundle, "httpd-bundle-0", cluster01, 0)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(ip_0, native_find_rsc(httpd_bundle, "httpd-bundle-ip-192.168.122.131", cluster01, pe_find_current)); + assert_ptr_equal(child_0, native_find_rsc(httpd_bundle, "httpd:0", httpd_bundle_0, pe_find_current)); + assert_ptr_equal(container_0, native_find_rsc(httpd_bundle, "httpd-bundle-docker-0", cluster01, pe_find_current)); + assert_ptr_equal(remote_0, native_find_rsc(httpd_bundle, "httpd-bundle-0", cluster01, pe_find_current)); +} + +static void +clone_group_rsc(void **rsc) { + assert_non_null(mysql_clone_group); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", NULL, 0)); + assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", NULL, pe_find_current)); + assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", NULL, pe_find_clone)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster01, 0)); + + /* Passes because one of mysql-clone-group's children is running on cluster01. */ + assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster01, pe_find_current)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster02, 0)); + + /* Passes because one of mysql-clone-group's children is running on cluster02. */ + assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster02, pe_find_current)); + + /* Passes for previous reasons, plus includes pe_find_clone check. */ + assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster01, pe_find_clone|pe_find_current)); + assert_ptr_equal(mysql_clone_group, native_find_rsc(mysql_clone_group, "mysql-clone-group", cluster02, pe_find_clone|pe_find_current)); +} + +static void +clone_group_instance_rsc(void **rsc) { + pe_resource_t *mysql_group_0 = NULL; + pe_resource_t *mysql_group_1 = NULL; + + /* Find the "mysql-group:0" and "mysql-group:1" resources, members of "mysql-clone-group". */ + for (GList *iter = mysql_clone_group->children; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "mysql-group:0") == 0) { + mysql_group_0 = rsc; + } else if (strcmp(rsc->id, "mysql-group:1") == 0) { + mysql_group_1 = rsc; + } + } + + assert_non_null(mysql_group_0); + assert_non_null(mysql_group_1); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group:0", NULL, 0)); + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group:0", NULL, pe_find_current)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group:1", NULL, 0)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group:1", NULL, pe_find_current)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(mysql_group_0, "mysql-group:0", cluster02, 0)); + assert_null(native_find_rsc(mysql_group_1, "mysql-group:1", cluster01, 0)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group:0", cluster02, pe_find_current)); + assert_null(native_find_rsc(mysql_group_0, "mysql-group:0", cluster01, pe_find_current)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group:1", cluster01, pe_find_current)); + assert_null(native_find_rsc(mysql_group_1, "mysql-group:1", cluster02, pe_find_current)); + + /* Passes because NULL was passed for node and base name was given, with correct flags. */ + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group" , NULL, pe_find_clone)); + + /* Passes because pe_find_any matches any base name. */ + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group" , NULL, pe_find_any)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group" , NULL, pe_find_any)); + + /* Passes because pe_find_anon matches. */ + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group" , NULL, pe_find_anon)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group" , NULL, pe_find_anon)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group", cluster02, pe_find_any|pe_find_current)); + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_group_0, "mysql-group", cluster02, pe_find_anon|pe_find_current)); + assert_null(native_find_rsc(mysql_group_0, "mysql-group", cluster01, pe_find_any|pe_find_current)); + assert_null(native_find_rsc(mysql_group_0, "mysql-group", cluster01, pe_find_anon|pe_find_current)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group", cluster01, pe_find_any|pe_find_current)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_group_1, "mysql-group", cluster01, pe_find_anon|pe_find_current)); + assert_null(native_find_rsc(mysql_group_1, "mysql-group", cluster02, pe_find_any|pe_find_current)); + assert_null(native_find_rsc(mysql_group_1, "mysql-group", cluster02, pe_find_anon|pe_find_current)); + + /* Fails because incorrect flags were given along with base name. */ + assert_null(native_find_rsc(mysql_group_0, "mysql-group", NULL, pe_find_current)); + assert_null(native_find_rsc(mysql_group_1, "mysql-group", NULL, pe_find_current)); + + /* And then we check failure possibilities again, except passing mysql_clone_group + * instead of mysql_group_X as the first argument to native_find_rsc. + */ + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(mysql_clone_group, "mysql-group:0", cluster02, 0)); + assert_null(native_find_rsc(mysql_clone_group, "mysql-group:1", cluster01, 0)); + + /* Check that the resource is running on the node we expect. */ + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_clone_group, "mysql-group:0", cluster02, pe_find_current)); + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_clone_group, "mysql-group", cluster02, pe_find_any|pe_find_current)); + assert_ptr_equal(mysql_group_0, native_find_rsc(mysql_clone_group, "mysql-group", cluster02, pe_find_anon|pe_find_current)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_clone_group, "mysql-group:1", cluster01, pe_find_current)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_clone_group, "mysql-group", cluster01, pe_find_any|pe_find_current)); + assert_ptr_equal(mysql_group_1, native_find_rsc(mysql_clone_group, "mysql-group", cluster01, pe_find_anon|pe_find_current)); +} + +static void +clone_group_member_rsc(void **state) { + pe_resource_t *mysql_proxy = NULL; + + /* Find the "mysql-proxy" resource, a member of "mysql-group". */ + for (GList *iter = mysql_clone_group->children; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "mysql-group:0") == 0) { + for (GList *iter2 = rsc->children; iter2 != NULL; iter2 = iter2->next) { + pe_resource_t *child = (pe_resource_t *) iter2->data; + + if (strcmp(child->id, "mysql-proxy:0") == 0) { + mysql_proxy = child; + break; + } + } + + break; + } + } + + assert_non_null(mysql_proxy); + + /* Passes because NULL was passed for node, regardless of flags. */ + assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL, 0)); + assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL, pe_find_current)); + + /* Passes because resource's parent is a clone. */ + assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", NULL, pe_find_clone)); + assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02, pe_find_clone|pe_find_current)); + + /* Fails because mysql-proxy:0 is not running on cluster01, even with the right flags. */ + assert_null(native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster01, pe_find_current)); + + /* Fails because pe_find_current is required if a node is given. */ + assert_null(native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02, 0)); + + /* Passes because mysql-proxy:0 is running on cluster02. */ + assert_ptr_equal(mysql_proxy, native_find_rsc(mysql_proxy, "mysql-proxy:0", cluster02, pe_find_current)); +} + +/* TODO: Add tests for finding on allocated node (passing a node without + * pe_find_current, after scheduling, for a resource that is starting/stopping/moving. + */ +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(bad_args), + cmocka_unit_test(primitive_rsc), + cmocka_unit_test(group_rsc), + cmocka_unit_test(inactive_group_rsc), + cmocka_unit_test(group_member_rsc), + cmocka_unit_test(inactive_group_member_rsc), + cmocka_unit_test(clone_rsc), + cmocka_unit_test(inactive_clone_rsc), + cmocka_unit_test(clone_instance_rsc), + cmocka_unit_test(renamed_rsc), + cmocka_unit_test(bundle_rsc), + cmocka_unit_test(bundle_replica_rsc), + cmocka_unit_test(clone_group_rsc), + cmocka_unit_test(clone_group_instance_rsc), + cmocka_unit_test(clone_group_member_rsc)) diff --git a/lib/pengine/tests/native/pe_base_name_eq_test.c b/lib/pengine/tests/native/pe_base_name_eq_test.c new file mode 100644 index 0000000..67a62f8 --- /dev/null +++ b/lib/pengine/tests/native/pe_base_name_eq_test.c @@ -0,0 +1,149 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include +#include +#include + +xmlNode *input = NULL; +pe_working_set_t *data_set = NULL; + +pe_resource_t *exim_group, *promotable_0, *promotable_1, *dummy; +pe_resource_t *httpd_bundle, *mysql_group_0, *mysql_group_1; + +static int +setup(void **state) { + char *path = NULL; + + crm_xml_init(); + + path = crm_strdup_printf("%s/crm_mon.xml", getenv("PCMK_CTS_CLI_DIR")); + input = filename2xml(path); + free(path); + + if (input == NULL) { + return 1; + } + + data_set = pe_new_working_set(); + + if (data_set == NULL) { + return 1; + } + + pe__set_working_set_flags(data_set, pe_flag_no_counts|pe_flag_no_compat); + data_set->input = input; + + cluster_status(data_set); + + /* Get references to several resources we use frequently. */ + for (GList *iter = data_set->resources; iter != NULL; iter = iter->next) { + pe_resource_t *rsc = (pe_resource_t *) iter->data; + + if (strcmp(rsc->id, "dummy") == 0) { + dummy = rsc; + } else if (strcmp(rsc->id, "exim-group") == 0) { + exim_group = rsc; + } else if (strcmp(rsc->id, "httpd-bundle") == 0) { + httpd_bundle = rsc; + } else if (strcmp(rsc->id, "mysql-clone-group") == 0) { + for (GList *iter = rsc->children; iter != NULL; iter = iter->next) { + pe_resource_t *child = (pe_resource_t *) iter->data; + + if (strcmp(child->id, "mysql-group:0") == 0) { + mysql_group_0 = child; + } else if (strcmp(child->id, "mysql-group:1") == 0) { + mysql_group_1 = child; + } + } + } else if (strcmp(rsc->id, "promotable-clone") == 0) { + for (GList *iter = rsc->children; iter != NULL; iter = iter->next) { + pe_resource_t *child = (pe_resource_t *) iter->data; + + if (strcmp(child->id, "promotable-rsc:0") == 0) { + promotable_0 = child; + } else if (strcmp(child->id, "promotable-rsc:1") == 0) { + promotable_1 = child; + } + } + } + } + + return 0; +} + +static int +teardown(void **state) { + pe_free_working_set(data_set); + + return 0; +} + +static void +bad_args(void **state) { + char *id = dummy->id; + + assert_false(pe_base_name_eq(NULL, "dummy")); + assert_false(pe_base_name_eq(dummy, NULL)); + + dummy->id = NULL; + assert_false(pe_base_name_eq(dummy, "dummy")); + dummy->id = id; +} + +static void +primitive_rsc(void **state) { + assert_true(pe_base_name_eq(dummy, "dummy")); + assert_false(pe_base_name_eq(dummy, "DUMMY")); + assert_false(pe_base_name_eq(dummy, "dUmMy")); + assert_false(pe_base_name_eq(dummy, "dummy0")); + assert_false(pe_base_name_eq(dummy, "dummy:0")); +} + +static void +group_rsc(void **state) { + assert_true(pe_base_name_eq(exim_group, "exim-group")); + assert_false(pe_base_name_eq(exim_group, "EXIM-GROUP")); + assert_false(pe_base_name_eq(exim_group, "exim-group0")); + assert_false(pe_base_name_eq(exim_group, "exim-group:0")); + assert_false(pe_base_name_eq(exim_group, "Public-IP")); +} + +static void +clone_rsc(void **state) { + assert_true(pe_base_name_eq(promotable_0, "promotable-rsc")); + assert_true(pe_base_name_eq(promotable_1, "promotable-rsc")); + + assert_false(pe_base_name_eq(promotable_0, "promotable-rsc:0")); + assert_false(pe_base_name_eq(promotable_1, "promotable-rsc:1")); + assert_false(pe_base_name_eq(promotable_0, "PROMOTABLE-RSC")); + assert_false(pe_base_name_eq(promotable_1, "PROMOTABLE-RSC")); + assert_false(pe_base_name_eq(promotable_0, "Promotable-rsc")); + assert_false(pe_base_name_eq(promotable_1, "Promotable-rsc")); +} + +static void +bundle_rsc(void **state) { + assert_true(pe_base_name_eq(httpd_bundle, "httpd-bundle")); + assert_false(pe_base_name_eq(httpd_bundle, "HTTPD-BUNDLE")); + assert_false(pe_base_name_eq(httpd_bundle, "httpd")); + assert_false(pe_base_name_eq(httpd_bundle, "httpd-docker-0")); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(bad_args), + cmocka_unit_test(primitive_rsc), + cmocka_unit_test(group_rsc), + cmocka_unit_test(clone_rsc), + cmocka_unit_test(bundle_rsc)) diff --git a/lib/pengine/tests/rules/Makefile.am b/lib/pengine/tests/rules/Makefile.am new file mode 100644 index 0000000..261ec16 --- /dev/null +++ b/lib/pengine/tests/rules/Makefile.am @@ -0,0 +1,18 @@ +# +# Copyright 2020-2021 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +LDADD += $(top_builddir)/lib/pengine/libpe_rules_test.la + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = pe_cron_range_satisfied_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/pengine/tests/rules/pe_cron_range_satisfied_test.c b/lib/pengine/tests/rules/pe_cron_range_satisfied_test.c new file mode 100644 index 0000000..a8ba6cf --- /dev/null +++ b/lib/pengine/tests/rules/pe_cron_range_satisfied_test.c @@ -0,0 +1,165 @@ +/* + * Copyright 2020-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include +#include +#include + +static void +run_one_test(const char *t, const char *x, int expected) { + crm_time_t *tm = crm_time_new(t); + xmlNodePtr xml = string2xml(x); + + assert_int_equal(pe_cron_range_satisfied(tm, xml), expected); + + crm_time_free(tm); + free_xml(xml); +} + +static void +no_time_given(void **state) { + assert_int_equal(pe_cron_range_satisfied(NULL, NULL), pcmk_rc_op_unsatisfied); +} + +static void +any_time_satisfies_empty_spec(void **state) { + crm_time_t *tm = crm_time_new(NULL); + + assert_int_equal(pe_cron_range_satisfied(tm, NULL), pcmk_rc_ok); + + crm_time_free(tm); +} + +static void +time_satisfies_year_spec(void **state) { + run_one_test("2020-01-01", + "", + pcmk_rc_ok); +} + +static void +time_after_year_spec(void **state) { + run_one_test("2020-01-01", + "", + pcmk_rc_after_range); +} + +static void +time_satisfies_year_range(void **state) { + run_one_test("2020-01-01", + "", + pcmk_rc_ok); +} + +static void +time_before_year_range(void **state) { + run_one_test("2000-01-01", + "", + pcmk_rc_before_range); +} + +static void +time_after_year_range(void **state) { + run_one_test("2020-01-01", + "", + pcmk_rc_after_range); +} + +static void +range_without_start_year_passes(void **state) { + run_one_test("2010-01-01", + "", + pcmk_rc_ok); +} + +static void +range_without_end_year_passes(void **state) { + run_one_test("2010-01-01", + "", + pcmk_rc_ok); + run_one_test("2000-10-01", + "", + pcmk_rc_ok); +} + +static void +yeardays_satisfies(void **state) { + run_one_test("2020-01-30", + "", + pcmk_rc_ok); +} + +static void +time_after_yeardays_spec(void **state) { + run_one_test("2020-02-15", + "", + pcmk_rc_after_range); +} + +static void +yeardays_feb_29_satisfies(void **state) { + run_one_test("2016-02-29", + "", + pcmk_rc_ok); +} + +static void +exact_ymd_satisfies(void **state) { + run_one_test("2001-12-31", + "", + pcmk_rc_ok); +} + +static void +range_in_month_satisfies(void **state) { + run_one_test("2001-06-10", + "", + pcmk_rc_ok); +} + +static void +exact_ymd_after_range(void **state) { + run_one_test("2001-12-31", + "", + pcmk_rc_after_range); +} + +static void +time_after_monthdays_range(void **state) { + run_one_test("2001-06-10", + "", + pcmk_rc_before_range); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(no_time_given), + cmocka_unit_test(any_time_satisfies_empty_spec), + cmocka_unit_test(time_satisfies_year_spec), + cmocka_unit_test(time_after_year_spec), + cmocka_unit_test(time_satisfies_year_range), + cmocka_unit_test(time_before_year_range), + cmocka_unit_test(time_after_year_range), + cmocka_unit_test(range_without_start_year_passes), + cmocka_unit_test(range_without_end_year_passes), + cmocka_unit_test(yeardays_satisfies), + cmocka_unit_test(time_after_yeardays_spec), + cmocka_unit_test(yeardays_feb_29_satisfies), + cmocka_unit_test(exact_ymd_satisfies), + cmocka_unit_test(range_in_month_satisfies), + cmocka_unit_test(exact_ymd_after_range), + cmocka_unit_test(time_after_monthdays_range)) diff --git a/lib/pengine/tests/status/Makefile.am b/lib/pengine/tests/status/Makefile.am new file mode 100644 index 0000000..3f95496 --- /dev/null +++ b/lib/pengine/tests/status/Makefile.am @@ -0,0 +1,22 @@ +# +# Copyright 2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +LDADD += $(top_builddir)/lib/pengine/libpe_status_test.la + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = pe_find_node_any_test \ + pe_find_node_id_test \ + pe_find_node_test \ + pe_new_working_set_test \ + set_working_set_defaults_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/pengine/tests/status/pe_find_node_any_test.c b/lib/pengine/tests/status/pe_find_node_any_test.c new file mode 100644 index 0000000..b911424 --- /dev/null +++ b/lib/pengine/tests/status/pe_find_node_any_test.c @@ -0,0 +1,62 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +empty_list(void **state) { + assert_null(pe_find_node_any(NULL, NULL, NULL)); + assert_null(pe_find_node_any(NULL, NULL, "cluster1")); + assert_null(pe_find_node_any(NULL, "id1", NULL)); + assert_null(pe_find_node_any(NULL, "id1", "cluster1")); +} + +static void +non_null_list(void **state) { + GList *nodes = NULL; + + pe_node_t *a = calloc(1, sizeof(pe_node_t)); + pe_node_t *b = calloc(1, sizeof(pe_node_t)); + + a->details = calloc(1, sizeof(struct pe_node_shared_s)); + a->details->uname = "cluster1"; + a->details->id = "id1"; + b->details = calloc(1, sizeof(struct pe_node_shared_s)); + b->details->uname = "cluster2"; + b->details->id = "id2"; + + nodes = g_list_append(nodes, a); + nodes = g_list_append(nodes, b); + + assert_ptr_equal(b, pe_find_node_any(nodes, "id2", NULL)); + assert_ptr_equal(b, pe_find_node_any(nodes, "ID2", NULL)); + + assert_ptr_equal(a, pe_find_node_any(nodes, "xyz", "cluster1")); + assert_ptr_equal(a, pe_find_node_any(nodes, NULL, "cluster1")); + + assert_null(pe_find_node_any(nodes, "id10", NULL)); + assert_null(pe_find_node_any(nodes, "nodeid1", NULL)); + assert_null(pe_find_node_any(nodes, NULL, "cluster10")); + assert_null(pe_find_node_any(nodes, NULL, "nodecluster1")); + assert_null(pe_find_node_any(nodes, "id3", "cluster3")); + assert_null(pe_find_node_any(nodes, NULL, NULL)); + + free(a->details); + free(a); + free(b->details); + free(b); + g_list_free(nodes); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_list), + cmocka_unit_test(non_null_list)) diff --git a/lib/pengine/tests/status/pe_find_node_id_test.c b/lib/pengine/tests/status/pe_find_node_id_test.c new file mode 100644 index 0000000..832a40a --- /dev/null +++ b/lib/pengine/tests/status/pe_find_node_id_test.c @@ -0,0 +1,51 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +empty_list(void **state) { + assert_null(pe_find_node_id(NULL, NULL)); + assert_null(pe_find_node_id(NULL, "id1")); +} + +static void +non_null_list(void **state) { + GList *nodes = NULL; + + pe_node_t *a = calloc(1, sizeof(pe_node_t)); + pe_node_t *b = calloc(1, sizeof(pe_node_t)); + + a->details = calloc(1, sizeof(struct pe_node_shared_s)); + a->details->id = "id1"; + b->details = calloc(1, sizeof(struct pe_node_shared_s)); + b->details->id = "id2"; + + nodes = g_list_append(nodes, a); + nodes = g_list_append(nodes, b); + + assert_ptr_equal(a, pe_find_node_id(nodes, "id1")); + assert_null(pe_find_node_id(nodes, "id10")); + assert_null(pe_find_node_id(nodes, "nodeid1")); + assert_ptr_equal(b, pe_find_node_id(nodes, "ID2")); + assert_null(pe_find_node_id(nodes, "xyz")); + + free(a->details); + free(a); + free(b->details); + free(b); + g_list_free(nodes); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_list), + cmocka_unit_test(non_null_list)) diff --git a/lib/pengine/tests/status/pe_find_node_test.c b/lib/pengine/tests/status/pe_find_node_test.c new file mode 100644 index 0000000..7c7ea30 --- /dev/null +++ b/lib/pengine/tests/status/pe_find_node_test.c @@ -0,0 +1,51 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +empty_list(void **state) { + assert_null(pe_find_node(NULL, NULL)); + assert_null(pe_find_node(NULL, "cluster1")); +} + +static void +non_null_list(void **state) { + GList *nodes = NULL; + + pe_node_t *a = calloc(1, sizeof(pe_node_t)); + pe_node_t *b = calloc(1, sizeof(pe_node_t)); + + a->details = calloc(1, sizeof(struct pe_node_shared_s)); + a->details->uname = "cluster1"; + b->details = calloc(1, sizeof(struct pe_node_shared_s)); + b->details->uname = "cluster2"; + + nodes = g_list_append(nodes, a); + nodes = g_list_append(nodes, b); + + assert_ptr_equal(a, pe_find_node(nodes, "cluster1")); + assert_null(pe_find_node(nodes, "cluster10")); + assert_null(pe_find_node(nodes, "nodecluster1")); + assert_ptr_equal(b, pe_find_node(nodes, "CLUSTER2")); + assert_null(pe_find_node(nodes, "xyz")); + + free(a->details); + free(a); + free(b->details); + free(b); + g_list_free(nodes); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(empty_list), + cmocka_unit_test(non_null_list)) diff --git a/lib/pengine/tests/status/pe_new_working_set_test.c b/lib/pengine/tests/status/pe_new_working_set_test.c new file mode 100644 index 0000000..cf2df4f --- /dev/null +++ b/lib/pengine/tests/status/pe_new_working_set_test.c @@ -0,0 +1,46 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include "mock_private.h" + +static void +calloc_fails(void **state) { + pcmk__mock_calloc = true; // calloc() will return NULL + + expect_value(__wrap_calloc, nmemb, 1); + expect_value(__wrap_calloc, size, sizeof(pe_working_set_t)); + assert_null(pe_new_working_set()); + + pcmk__mock_calloc = false; // Use real calloc() +} + +static void +calloc_succeeds(void **state) { + pe_working_set_t *data_set = pe_new_working_set(); + + /* Nothing else to test about this function, as all it does is call + * set_working_set_defaults which is also a public function and should + * get its own unit test. + */ + assert_non_null(data_set); + + /* Avoid calling pe_free_working_set here so we don't artificially + * inflate the coverage numbers. + */ + free(data_set); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(calloc_fails), + cmocka_unit_test(calloc_succeeds)) diff --git a/lib/pengine/tests/status/set_working_set_defaults_test.c b/lib/pengine/tests/status/set_working_set_defaults_test.c new file mode 100644 index 0000000..c822278 --- /dev/null +++ b/lib/pengine/tests/status/set_working_set_defaults_test.c @@ -0,0 +1,46 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include + +#include "mock_private.h" + +static void +check_defaults(void **state) { + uint32_t flags; + pe_working_set_t *data_set = calloc(1, sizeof(pe_working_set_t)); + + set_working_set_defaults(data_set); + + flags = pe_flag_stop_rsc_orphans|pe_flag_symmetric_cluster|pe_flag_stop_action_orphans; + + if (!strcmp(PCMK__CONCURRENT_FENCING_DEFAULT, "true")) { + flags |= pe_flag_concurrent_fencing; + } + + + assert_null(data_set->priv); + assert_int_equal(data_set->order_id, 1); + assert_int_equal(data_set->action_id, 1); + assert_int_equal(data_set->no_quorum_policy, no_quorum_stop); + assert_int_equal(data_set->flags, flags); + + /* Avoid calling pe_free_working_set here so we don't artificially + * inflate the coverage numbers. + */ + free(data_set); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(check_defaults)) diff --git a/lib/pengine/tests/unpack/Makefile.am b/lib/pengine/tests/unpack/Makefile.am new file mode 100644 index 0000000..baa8633 --- /dev/null +++ b/lib/pengine/tests/unpack/Makefile.am @@ -0,0 +1,18 @@ +# +# Copyright 2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +LDADD += $(top_builddir)/lib/pengine/libpe_status_test.la + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = pe_base_name_end_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/pengine/tests/unpack/pe_base_name_end_test.c b/lib/pengine/tests/unpack/pe_base_name_end_test.c new file mode 100644 index 0000000..6f1c165 --- /dev/null +++ b/lib/pengine/tests/unpack/pe_base_name_end_test.c @@ -0,0 +1,36 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +static void +bad_args(void **state) { + assert_null(pe_base_name_end(NULL)); + assert_null(pe_base_name_end("")); +} + +static void +no_suffix(void **state) { + assert_string_equal(pe_base_name_end("rsc"), "c"); + assert_string_equal(pe_base_name_end("rsc0"), "0"); +} + +static void +has_suffix(void **state) { + assert_string_equal(pe_base_name_end("rsc:0"), "c:0"); + assert_string_equal(pe_base_name_end("rsc:100"), "c:100"); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(bad_args), + cmocka_unit_test(no_suffix), + cmocka_unit_test(has_suffix)) diff --git a/lib/pengine/tests/utils/Makefile.am b/lib/pengine/tests/utils/Makefile.am new file mode 100644 index 0000000..4a3e8a2 --- /dev/null +++ b/lib/pengine/tests/utils/Makefile.am @@ -0,0 +1,21 @@ +# +# Copyright 2022 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU General Public License version 2 +# or later (GPLv2+) WITHOUT ANY WARRANTY. +# + +include $(top_srcdir)/mk/tap.mk +include $(top_srcdir)/mk/unittest.mk + +AM_CPPFLAGS += -I$(top_srcdir)/lib/pengine +LDADD += $(top_builddir)/lib/pengine/libpe_status_test.la + +# Add "_test" to the end of all test program names to simplify .gitignore. +check_PROGRAMS = \ + pe__cmp_node_name_test \ + pe__cmp_rsc_priority_test + +TESTS = $(check_PROGRAMS) diff --git a/lib/pengine/tests/utils/pe__cmp_node_name_test.c b/lib/pengine/tests/utils/pe__cmp_node_name_test.c new file mode 100644 index 0000000..45d87ee --- /dev/null +++ b/lib/pengine/tests/utils/pe__cmp_node_name_test.c @@ -0,0 +1,55 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +struct pe_node_shared_s node1_details; +struct pe_node_shared_s node2_details; + +pe_node_t node1 = {.details = &node1_details}; +pe_node_t node2 = {.details = &node2_details}; + +static void +nodes_equal(void **state) +{ + assert_int_equal(pe__cmp_node_name(NULL, NULL), 0); + + node1.details->uname = "node10"; + node2.details->uname = "node10"; + assert_int_equal(pe__cmp_node_name(&node1, &node2), 0); +} + +static void +node1_first(void **state) +{ + assert_int_equal(pe__cmp_node_name(NULL, &node2), -1); + + // The heavy testing is done in pcmk__numeric_strcasecmp()'s unit tests + node1.details->uname = "node9"; + node2.details->uname = "node10"; + assert_int_equal(pe__cmp_node_name(&node1, &node2), -1); +} + +static void +node2_first(void **state) +{ + assert_int_equal(pe__cmp_node_name(&node1, NULL), 1); + + node1.details->uname = "node10"; + node2.details->uname = "node9"; + assert_int_equal(pe__cmp_node_name(&node1, &node2), 1); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(nodes_equal), + cmocka_unit_test(node1_first), + cmocka_unit_test(node2_first)) diff --git a/lib/pengine/tests/utils/pe__cmp_rsc_priority_test.c b/lib/pengine/tests/utils/pe__cmp_rsc_priority_test.c new file mode 100644 index 0000000..669e7a9 --- /dev/null +++ b/lib/pengine/tests/utils/pe__cmp_rsc_priority_test.c @@ -0,0 +1,50 @@ +/* + * Copyright 2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU General Public License version 2 + * or later (GPLv2+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include "pe_status_private.h" + +pe_resource_t rsc1; +pe_resource_t rsc2; + +static void +rscs_equal(void **state) +{ + rsc1.priority = 0; + rsc2.priority = 0; + assert_int_equal(pe__cmp_rsc_priority(NULL, NULL), 0); + assert_int_equal(pe__cmp_rsc_priority(&rsc1, &rsc2), 0); +} + +static void +rsc1_first(void **state) +{ + rsc1.priority = 1; + rsc2.priority = 0; + assert_int_equal(pe__cmp_rsc_priority(&rsc1, NULL), -1); + assert_int_equal(pe__cmp_rsc_priority(&rsc1, &rsc2), -1); +} + +static void +rsc2_first(void **state) +{ + rsc1.priority = 0; + rsc2.priority = 1; + assert_int_equal(pe__cmp_rsc_priority(NULL, &rsc2), 1); + assert_int_equal(pe__cmp_rsc_priority(&rsc1, &rsc2), 1); +} + +PCMK__UNIT_TEST(NULL, NULL, + cmocka_unit_test(rscs_equal), + cmocka_unit_test(rsc1_first), + cmocka_unit_test(rsc2_first)) diff --git a/lib/pengine/unpack.c b/lib/pengine/unpack.c new file mode 100644 index 0000000..2bd6707 --- /dev/null +++ b/lib/pengine/unpack.c @@ -0,0 +1,4829 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +CRM_TRACE_INIT_DATA(pe_status); + +// A (parsed) resource action history entry +struct action_history { + pe_resource_t *rsc; // Resource that history is for + pe_node_t *node; // Node that history is for + xmlNode *xml; // History entry XML + + // Parsed from entry XML + const char *id; // XML ID of history entry + const char *key; // Operation key of action + const char *task; // Action name + const char *exit_reason; // Exit reason given for result + guint interval_ms; // Action interval + int call_id; // Call ID of action + int expected_exit_status; // Expected exit status of action + int exit_status; // Actual exit status of action + int execution_status; // Execution status of action +}; + +/* This uses pcmk__set_flags_as()/pcmk__clear_flags_as() directly rather than + * use pe__set_working_set_flags()/pe__clear_working_set_flags() so that the + * flag is stringified more readably in log messages. + */ +#define set_config_flag(data_set, option, flag) do { \ + const char *scf_value = pe_pref((data_set)->config_hash, (option)); \ + if (scf_value != NULL) { \ + if (crm_is_true(scf_value)) { \ + (data_set)->flags = pcmk__set_flags_as(__func__, __LINE__, \ + LOG_TRACE, "Working set", \ + crm_system_name, (data_set)->flags, \ + (flag), #flag); \ + } else { \ + (data_set)->flags = pcmk__clear_flags_as(__func__, __LINE__,\ + LOG_TRACE, "Working set", \ + crm_system_name, (data_set)->flags, \ + (flag), #flag); \ + } \ + } \ + } while(0) + +static void unpack_rsc_op(pe_resource_t *rsc, pe_node_t *node, xmlNode *xml_op, + xmlNode **last_failure, + enum action_fail_response *failed); +static void determine_remote_online_status(pe_working_set_t *data_set, + pe_node_t *this_node); +static void add_node_attrs(const xmlNode *xml_obj, pe_node_t *node, + bool overwrite, pe_working_set_t *data_set); +static void determine_online_status(const xmlNode *node_state, + pe_node_t *this_node, + pe_working_set_t *data_set); + +static void unpack_node_lrm(pe_node_t *node, const xmlNode *xml, + pe_working_set_t *data_set); + + +// Bitmask for warnings we only want to print once +uint32_t pe_wo = 0; + +static gboolean +is_dangling_guest_node(pe_node_t *node) +{ + /* we are looking for a remote-node that was supposed to be mapped to a + * container resource, but all traces of that container have disappeared + * from both the config and the status section. */ + if (pe__is_guest_or_remote_node(node) && + node->details->remote_rsc && + node->details->remote_rsc->container == NULL && + pcmk_is_set(node->details->remote_rsc->flags, + pe_rsc_orphan_container_filler)) { + return TRUE; + } + + return FALSE; +} + +/*! + * \brief Schedule a fence action for a node + * + * \param[in,out] data_set Current working set of cluster + * \param[in,out] node Node to fence + * \param[in] reason Text description of why fencing is needed + * \param[in] priority_delay Whether to consider `priority-fencing-delay` + */ +void +pe_fence_node(pe_working_set_t * data_set, pe_node_t * node, + const char *reason, bool priority_delay) +{ + CRM_CHECK(node, return); + + /* A guest node is fenced by marking its container as failed */ + if (pe__is_guest_node(node)) { + pe_resource_t *rsc = node->details->remote_rsc->container; + + if (!pcmk_is_set(rsc->flags, pe_rsc_failed)) { + if (!pcmk_is_set(rsc->flags, pe_rsc_managed)) { + crm_notice("Not fencing guest node %s " + "(otherwise would because %s): " + "its guest resource %s is unmanaged", + pe__node_name(node), reason, rsc->id); + } else { + crm_warn("Guest node %s will be fenced " + "(by recovering its guest resource %s): %s", + pe__node_name(node), rsc->id, reason); + + /* We don't mark the node as unclean because that would prevent the + * node from running resources. We want to allow it to run resources + * in this transition if the recovery succeeds. + */ + node->details->remote_requires_reset = TRUE; + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + } + } + + } else if (is_dangling_guest_node(node)) { + crm_info("Cleaning up dangling connection for guest node %s: " + "fencing was already done because %s, " + "and guest resource no longer exists", + pe__node_name(node), reason); + pe__set_resource_flags(node->details->remote_rsc, + pe_rsc_failed|pe_rsc_stop); + + } else if (pe__is_remote_node(node)) { + pe_resource_t *rsc = node->details->remote_rsc; + + if ((rsc != NULL) && !pcmk_is_set(rsc->flags, pe_rsc_managed)) { + crm_notice("Not fencing remote node %s " + "(otherwise would because %s): connection is unmanaged", + pe__node_name(node), reason); + } else if(node->details->remote_requires_reset == FALSE) { + node->details->remote_requires_reset = TRUE; + crm_warn("Remote node %s %s: %s", + pe__node_name(node), + pe_can_fence(data_set, node)? "will be fenced" : "is unclean", + reason); + } + node->details->unclean = TRUE; + // No need to apply `priority-fencing-delay` for remote nodes + pe_fence_op(node, NULL, TRUE, reason, FALSE, data_set); + + } else if (node->details->unclean) { + crm_trace("Cluster node %s %s because %s", + pe__node_name(node), + pe_can_fence(data_set, node)? "would also be fenced" : "also is unclean", + reason); + + } else { + crm_warn("Cluster node %s %s: %s", + pe__node_name(node), + pe_can_fence(data_set, node)? "will be fenced" : "is unclean", + reason); + node->details->unclean = TRUE; + pe_fence_op(node, NULL, TRUE, reason, priority_delay, data_set); + } +} + +// @TODO xpaths can't handle templates, rules, or id-refs + +// nvpair with provides or requires set to unfencing +#define XPATH_UNFENCING_NVPAIR XML_CIB_TAG_NVPAIR \ + "[(@" XML_NVPAIR_ATTR_NAME "='" PCMK_STONITH_PROVIDES "'" \ + "or @" XML_NVPAIR_ATTR_NAME "='" XML_RSC_ATTR_REQUIRES "') " \ + "and @" XML_NVPAIR_ATTR_VALUE "='" PCMK__VALUE_UNFENCING "']" + +// unfencing in rsc_defaults or any resource +#define XPATH_ENABLE_UNFENCING \ + "/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RESOURCES \ + "//" XML_TAG_META_SETS "/" XPATH_UNFENCING_NVPAIR \ + "|/" XML_TAG_CIB "/" XML_CIB_TAG_CONFIGURATION "/" XML_CIB_TAG_RSCCONFIG \ + "/" XML_TAG_META_SETS "/" XPATH_UNFENCING_NVPAIR + +static void +set_if_xpath(uint64_t flag, const char *xpath, pe_working_set_t *data_set) +{ + xmlXPathObjectPtr result = NULL; + + if (!pcmk_is_set(data_set->flags, flag)) { + result = xpath_search(data_set->input, xpath); + if (result && (numXpathResults(result) > 0)) { + pe__set_working_set_flags(data_set, flag); + } + freeXpathObject(result); + } +} + +gboolean +unpack_config(xmlNode * config, pe_working_set_t * data_set) +{ + const char *value = NULL; + GHashTable *config_hash = pcmk__strkey_table(free, free); + + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + data_set->config_hash = config_hash; + + pe__unpack_dataset_nvpairs(config, XML_CIB_TAG_PROPSET, &rule_data, config_hash, + CIB_OPTIONS_FIRST, FALSE, data_set); + + verify_pe_options(data_set->config_hash); + + set_config_flag(data_set, "enable-startup-probes", pe_flag_startup_probes); + if (!pcmk_is_set(data_set->flags, pe_flag_startup_probes)) { + crm_info("Startup probes: disabled (dangerous)"); + } + + value = pe_pref(data_set->config_hash, XML_ATTR_HAVE_WATCHDOG); + if (value && crm_is_true(value)) { + crm_info("Watchdog-based self-fencing will be performed via SBD if " + "fencing is required and stonith-watchdog-timeout is nonzero"); + pe__set_working_set_flags(data_set, pe_flag_have_stonith_resource); + } + + /* Set certain flags via xpath here, so they can be used before the relevant + * configuration sections are unpacked. + */ + set_if_xpath(pe_flag_enable_unfencing, XPATH_ENABLE_UNFENCING, data_set); + + value = pe_pref(data_set->config_hash, "stonith-timeout"); + data_set->stonith_timeout = (int) crm_parse_interval_spec(value); + crm_debug("STONITH timeout: %d", data_set->stonith_timeout); + + set_config_flag(data_set, "stonith-enabled", pe_flag_stonith_enabled); + crm_debug("STONITH of failed nodes is %s", + pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)? "enabled" : "disabled"); + + data_set->stonith_action = pe_pref(data_set->config_hash, "stonith-action"); + if (!strcmp(data_set->stonith_action, "poweroff")) { + pe_warn_once(pe_wo_poweroff, + "Support for stonith-action of 'poweroff' is deprecated " + "and will be removed in a future release (use 'off' instead)"); + data_set->stonith_action = "off"; + } + crm_trace("STONITH will %s nodes", data_set->stonith_action); + + set_config_flag(data_set, "concurrent-fencing", pe_flag_concurrent_fencing); + crm_debug("Concurrent fencing is %s", + pcmk_is_set(data_set->flags, pe_flag_concurrent_fencing)? "enabled" : "disabled"); + + value = pe_pref(data_set->config_hash, + XML_CONFIG_ATTR_PRIORITY_FENCING_DELAY); + if (value) { + data_set->priority_fencing_delay = crm_parse_interval_spec(value) / 1000; + crm_trace("Priority fencing delay is %ds", data_set->priority_fencing_delay); + } + + set_config_flag(data_set, "stop-all-resources", pe_flag_stop_everything); + crm_debug("Stop all active resources: %s", + pcmk__btoa(pcmk_is_set(data_set->flags, pe_flag_stop_everything))); + + set_config_flag(data_set, "symmetric-cluster", pe_flag_symmetric_cluster); + if (pcmk_is_set(data_set->flags, pe_flag_symmetric_cluster)) { + crm_debug("Cluster is symmetric" " - resources can run anywhere by default"); + } + + value = pe_pref(data_set->config_hash, "no-quorum-policy"); + + if (pcmk__str_eq(value, "ignore", pcmk__str_casei)) { + data_set->no_quorum_policy = no_quorum_ignore; + + } else if (pcmk__str_eq(value, "freeze", pcmk__str_casei)) { + data_set->no_quorum_policy = no_quorum_freeze; + + } else if (pcmk__str_eq(value, "demote", pcmk__str_casei)) { + data_set->no_quorum_policy = no_quorum_demote; + + } else if (pcmk__str_eq(value, "suicide", pcmk__str_casei)) { + if (pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + int do_panic = 0; + + crm_element_value_int(data_set->input, XML_ATTR_QUORUM_PANIC, + &do_panic); + if (do_panic || pcmk_is_set(data_set->flags, pe_flag_have_quorum)) { + data_set->no_quorum_policy = no_quorum_suicide; + } else { + crm_notice("Resetting no-quorum-policy to 'stop': cluster has never had quorum"); + data_set->no_quorum_policy = no_quorum_stop; + } + } else { + pcmk__config_err("Resetting no-quorum-policy to 'stop' because " + "fencing is disabled"); + data_set->no_quorum_policy = no_quorum_stop; + } + + } else { + data_set->no_quorum_policy = no_quorum_stop; + } + + switch (data_set->no_quorum_policy) { + case no_quorum_freeze: + crm_debug("On loss of quorum: Freeze resources"); + break; + case no_quorum_stop: + crm_debug("On loss of quorum: Stop ALL resources"); + break; + case no_quorum_demote: + crm_debug("On loss of quorum: " + "Demote promotable resources and stop other resources"); + break; + case no_quorum_suicide: + crm_notice("On loss of quorum: Fence all remaining nodes"); + break; + case no_quorum_ignore: + crm_notice("On loss of quorum: Ignore"); + break; + } + + set_config_flag(data_set, "stop-orphan-resources", pe_flag_stop_rsc_orphans); + crm_trace("Orphan resources are %s", + pcmk_is_set(data_set->flags, pe_flag_stop_rsc_orphans)? "stopped" : "ignored"); + + set_config_flag(data_set, "stop-orphan-actions", pe_flag_stop_action_orphans); + crm_trace("Orphan resource actions are %s", + pcmk_is_set(data_set->flags, pe_flag_stop_action_orphans)? "stopped" : "ignored"); + + value = pe_pref(data_set->config_hash, "remove-after-stop"); + if (value != NULL) { + if (crm_is_true(value)) { + pe__set_working_set_flags(data_set, pe_flag_remove_after_stop); +#ifndef PCMK__COMPAT_2_0 + pe_warn_once(pe_wo_remove_after, + "Support for the remove-after-stop cluster property is" + " deprecated and will be removed in a future release"); +#endif + } else { + pe__clear_working_set_flags(data_set, pe_flag_remove_after_stop); + } + } + + set_config_flag(data_set, "maintenance-mode", pe_flag_maintenance_mode); + crm_trace("Maintenance mode: %s", + pcmk__btoa(pcmk_is_set(data_set->flags, pe_flag_maintenance_mode))); + + set_config_flag(data_set, "start-failure-is-fatal", pe_flag_start_failure_fatal); + crm_trace("Start failures are %s", + pcmk_is_set(data_set->flags, pe_flag_start_failure_fatal)? "always fatal" : "handled by failcount"); + + if (pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + set_config_flag(data_set, "startup-fencing", pe_flag_startup_fencing); + } + if (pcmk_is_set(data_set->flags, pe_flag_startup_fencing)) { + crm_trace("Unseen nodes will be fenced"); + } else { + pe_warn_once(pe_wo_blind, "Blind faith: not fencing unseen nodes"); + } + + pe__unpack_node_health_scores(data_set); + + data_set->placement_strategy = pe_pref(data_set->config_hash, "placement-strategy"); + crm_trace("Placement strategy: %s", data_set->placement_strategy); + + set_config_flag(data_set, "shutdown-lock", pe_flag_shutdown_lock); + crm_trace("Resources will%s be locked to cleanly shut down nodes", + (pcmk_is_set(data_set->flags, pe_flag_shutdown_lock)? "" : " not")); + if (pcmk_is_set(data_set->flags, pe_flag_shutdown_lock)) { + value = pe_pref(data_set->config_hash, + XML_CONFIG_ATTR_SHUTDOWN_LOCK_LIMIT); + data_set->shutdown_lock = crm_parse_interval_spec(value) / 1000; + crm_trace("Shutdown locks expire after %us", data_set->shutdown_lock); + } + + return TRUE; +} + +pe_node_t * +pe_create_node(const char *id, const char *uname, const char *type, + const char *score, pe_working_set_t * data_set) +{ + pe_node_t *new_node = NULL; + + if (pe_find_node(data_set->nodes, uname) != NULL) { + pcmk__config_warn("More than one node entry has name '%s'", uname); + } + + new_node = calloc(1, sizeof(pe_node_t)); + if (new_node == NULL) { + return NULL; + } + + new_node->weight = char2score(score); + new_node->details = calloc(1, sizeof(struct pe_node_shared_s)); + + if (new_node->details == NULL) { + free(new_node); + return NULL; + } + + crm_trace("Creating node for entry %s/%s", uname, id); + new_node->details->id = id; + new_node->details->uname = uname; + new_node->details->online = FALSE; + new_node->details->shutdown = FALSE; + new_node->details->rsc_discovery_enabled = TRUE; + new_node->details->running_rsc = NULL; + new_node->details->data_set = data_set; + + if (pcmk__str_eq(type, "member", pcmk__str_null_matches | pcmk__str_casei)) { + new_node->details->type = node_member; + + } else if (pcmk__str_eq(type, "remote", pcmk__str_casei)) { + new_node->details->type = node_remote; + pe__set_working_set_flags(data_set, pe_flag_have_remote_nodes); + + } else { + /* @COMPAT 'ping' is the default for backward compatibility, but it + * should be changed to 'member' at a compatibility break + */ + if (!pcmk__str_eq(type, "ping", pcmk__str_casei)) { + pcmk__config_warn("Node %s has unrecognized type '%s', " + "assuming 'ping'", pcmk__s(uname, "without name"), + type); + } + pe_warn_once(pe_wo_ping_node, + "Support for nodes of type 'ping' (such as %s) is " + "deprecated and will be removed in a future release", + pcmk__s(uname, "unnamed node")); + new_node->details->type = node_ping; + } + + new_node->details->attrs = pcmk__strkey_table(free, free); + + if (pe__is_guest_or_remote_node(new_node)) { + g_hash_table_insert(new_node->details->attrs, strdup(CRM_ATTR_KIND), + strdup("remote")); + } else { + g_hash_table_insert(new_node->details->attrs, strdup(CRM_ATTR_KIND), + strdup("cluster")); + } + + new_node->details->utilization = pcmk__strkey_table(free, free); + new_node->details->digest_cache = pcmk__strkey_table(free, + pe__free_digests); + + data_set->nodes = g_list_insert_sorted(data_set->nodes, new_node, + pe__cmp_node_name); + return new_node; +} + +static const char * +expand_remote_rsc_meta(xmlNode *xml_obj, xmlNode *parent, pe_working_set_t *data) +{ + xmlNode *attr_set = NULL; + xmlNode *attr = NULL; + + const char *container_id = ID(xml_obj); + const char *remote_name = NULL; + const char *remote_server = NULL; + const char *remote_port = NULL; + const char *connect_timeout = "60s"; + const char *remote_allow_migrate=NULL; + const char *is_managed = NULL; + + for (attr_set = pcmk__xe_first_child(xml_obj); attr_set != NULL; + attr_set = pcmk__xe_next(attr_set)) { + + if (!pcmk__str_eq((const char *)attr_set->name, XML_TAG_META_SETS, + pcmk__str_casei)) { + continue; + } + + for (attr = pcmk__xe_first_child(attr_set); attr != NULL; + attr = pcmk__xe_next(attr)) { + const char *value = crm_element_value(attr, XML_NVPAIR_ATTR_VALUE); + const char *name = crm_element_value(attr, XML_NVPAIR_ATTR_NAME); + + if (pcmk__str_eq(name, XML_RSC_ATTR_REMOTE_NODE, pcmk__str_casei)) { + remote_name = value; + } else if (pcmk__str_eq(name, "remote-addr", pcmk__str_casei)) { + remote_server = value; + } else if (pcmk__str_eq(name, "remote-port", pcmk__str_casei)) { + remote_port = value; + } else if (pcmk__str_eq(name, "remote-connect-timeout", pcmk__str_casei)) { + connect_timeout = value; + } else if (pcmk__str_eq(name, "remote-allow-migrate", pcmk__str_casei)) { + remote_allow_migrate=value; + } else if (pcmk__str_eq(name, XML_RSC_ATTR_MANAGED, pcmk__str_casei)) { + is_managed = value; + } + } + } + + if (remote_name == NULL) { + return NULL; + } + + if (pe_find_resource(data->resources, remote_name) != NULL) { + return NULL; + } + + pe_create_remote_xml(parent, remote_name, container_id, + remote_allow_migrate, is_managed, + connect_timeout, remote_server, remote_port); + return remote_name; +} + +static void +handle_startup_fencing(pe_working_set_t *data_set, pe_node_t *new_node) +{ + if ((new_node->details->type == node_remote) && (new_node->details->remote_rsc == NULL)) { + /* Ignore fencing for remote nodes that don't have a connection resource + * associated with them. This happens when remote node entries get left + * in the nodes section after the connection resource is removed. + */ + return; + } + + if (pcmk_is_set(data_set->flags, pe_flag_startup_fencing)) { + // All nodes are unclean until we've seen their status entry + new_node->details->unclean = TRUE; + + } else { + // Blind faith ... + new_node->details->unclean = FALSE; + } + + /* We need to be able to determine if a node's status section + * exists or not separate from whether the node is unclean. */ + new_node->details->unseen = TRUE; +} + +gboolean +unpack_nodes(xmlNode * xml_nodes, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = NULL; + pe_node_t *new_node = NULL; + const char *id = NULL; + const char *uname = NULL; + const char *type = NULL; + const char *score = NULL; + + for (xml_obj = pcmk__xe_first_child(xml_nodes); xml_obj != NULL; + xml_obj = pcmk__xe_next(xml_obj)) { + + if (pcmk__str_eq((const char *)xml_obj->name, XML_CIB_TAG_NODE, pcmk__str_none)) { + new_node = NULL; + + id = crm_element_value(xml_obj, XML_ATTR_ID); + uname = crm_element_value(xml_obj, XML_ATTR_UNAME); + type = crm_element_value(xml_obj, XML_ATTR_TYPE); + score = crm_element_value(xml_obj, XML_RULE_ATTR_SCORE); + crm_trace("Processing node %s/%s", uname, id); + + if (id == NULL) { + pcmk__config_err("Ignoring <" XML_CIB_TAG_NODE + "> entry in configuration without id"); + continue; + } + new_node = pe_create_node(id, uname, type, score, data_set); + + if (new_node == NULL) { + return FALSE; + } + + handle_startup_fencing(data_set, new_node); + + add_node_attrs(xml_obj, new_node, FALSE, data_set); + + crm_trace("Done with node %s", crm_element_value(xml_obj, XML_ATTR_UNAME)); + } + } + + if (data_set->localhost && pe_find_node(data_set->nodes, data_set->localhost) == NULL) { + crm_info("Creating a fake local node"); + pe_create_node(data_set->localhost, data_set->localhost, NULL, 0, + data_set); + } + + return TRUE; +} + +static void +setup_container(pe_resource_t * rsc, pe_working_set_t * data_set) +{ + const char *container_id = NULL; + + if (rsc->children) { + g_list_foreach(rsc->children, (GFunc) setup_container, data_set); + return; + } + + container_id = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_CONTAINER); + if (container_id && !pcmk__str_eq(container_id, rsc->id, pcmk__str_casei)) { + pe_resource_t *container = pe_find_resource(data_set->resources, container_id); + + if (container) { + rsc->container = container; + pe__set_resource_flags(container, pe_rsc_is_container); + container->fillers = g_list_append(container->fillers, rsc); + pe_rsc_trace(rsc, "Resource %s's container is %s", rsc->id, container_id); + } else { + pe_err("Resource %s: Unknown resource container (%s)", rsc->id, container_id); + } + } +} + +gboolean +unpack_remote_nodes(xmlNode * xml_resources, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = NULL; + + /* Create remote nodes and guest nodes from the resource configuration + * before unpacking resources. + */ + for (xml_obj = pcmk__xe_first_child(xml_resources); xml_obj != NULL; + xml_obj = pcmk__xe_next(xml_obj)) { + + const char *new_node_id = NULL; + + /* Check for remote nodes, which are defined by ocf:pacemaker:remote + * primitives. + */ + if (xml_contains_remote_node(xml_obj)) { + new_node_id = ID(xml_obj); + /* The "pe_find_node" check is here to make sure we don't iterate over + * an expanded node that has already been added to the node list. */ + if (new_node_id && pe_find_node(data_set->nodes, new_node_id) == NULL) { + crm_trace("Found remote node %s defined by resource %s", + new_node_id, ID(xml_obj)); + pe_create_node(new_node_id, new_node_id, "remote", NULL, + data_set); + } + continue; + } + + /* Check for guest nodes, which are defined by special meta-attributes + * of a primitive of any type (for example, VirtualDomain or Xen). + */ + if (pcmk__str_eq((const char *)xml_obj->name, XML_CIB_TAG_RESOURCE, pcmk__str_none)) { + /* This will add an ocf:pacemaker:remote primitive to the + * configuration for the guest node's connection, to be unpacked + * later. + */ + new_node_id = expand_remote_rsc_meta(xml_obj, xml_resources, data_set); + if (new_node_id && pe_find_node(data_set->nodes, new_node_id) == NULL) { + crm_trace("Found guest node %s in resource %s", + new_node_id, ID(xml_obj)); + pe_create_node(new_node_id, new_node_id, "remote", NULL, + data_set); + } + continue; + } + + /* Check for guest nodes inside a group. Clones are currently not + * supported as guest nodes. + */ + if (pcmk__str_eq((const char *)xml_obj->name, XML_CIB_TAG_GROUP, pcmk__str_none)) { + xmlNode *xml_obj2 = NULL; + for (xml_obj2 = pcmk__xe_first_child(xml_obj); xml_obj2 != NULL; + xml_obj2 = pcmk__xe_next(xml_obj2)) { + + new_node_id = expand_remote_rsc_meta(xml_obj2, xml_resources, data_set); + + if (new_node_id && pe_find_node(data_set->nodes, new_node_id) == NULL) { + crm_trace("Found guest node %s in resource %s inside group %s", + new_node_id, ID(xml_obj2), ID(xml_obj)); + pe_create_node(new_node_id, new_node_id, "remote", NULL, + data_set); + } + } + } + } + return TRUE; +} + +/* Call this after all the nodes and resources have been + * unpacked, but before the status section is read. + * + * A remote node's online status is reflected by the state + * of the remote node's connection resource. We need to link + * the remote node to this connection resource so we can have + * easy access to the connection resource during the scheduler calculations. + */ +static void +link_rsc2remotenode(pe_working_set_t *data_set, pe_resource_t *new_rsc) +{ + pe_node_t *remote_node = NULL; + + if (new_rsc->is_remote_node == FALSE) { + return; + } + + if (pcmk_is_set(data_set->flags, pe_flag_quick_location)) { + /* remote_nodes and remote_resources are not linked in quick location calculations */ + return; + } + + remote_node = pe_find_node(data_set->nodes, new_rsc->id); + CRM_CHECK(remote_node != NULL, return); + + pe_rsc_trace(new_rsc, "Linking remote connection resource %s to %s", + new_rsc->id, pe__node_name(remote_node)); + remote_node->details->remote_rsc = new_rsc; + + if (new_rsc->container == NULL) { + /* Handle start-up fencing for remote nodes (as opposed to guest nodes) + * the same as is done for cluster nodes. + */ + handle_startup_fencing(data_set, remote_node); + + } else { + /* pe_create_node() marks the new node as "remote" or "cluster"; now + * that we know the node is a guest node, update it correctly. + */ + g_hash_table_replace(remote_node->details->attrs, strdup(CRM_ATTR_KIND), + strdup("container")); + } +} + +static void +destroy_tag(gpointer data) +{ + pe_tag_t *tag = data; + + if (tag) { + free(tag->id); + g_list_free_full(tag->refs, free); + free(tag); + } +} + +/*! + * \internal + * \brief Parse configuration XML for resource information + * + * \param[in] xml_resources Top of resource configuration XML + * \param[in,out] data_set Where to put resource information + * + * \return TRUE + * + * \note unpack_remote_nodes() MUST be called before this, so that the nodes can + * be used when pe__unpack_resource() calls resource_location() + */ +gboolean +unpack_resources(const xmlNode *xml_resources, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = NULL; + GList *gIter = NULL; + + data_set->template_rsc_sets = pcmk__strkey_table(free, destroy_tag); + + for (xml_obj = pcmk__xe_first_child(xml_resources); xml_obj != NULL; + xml_obj = pcmk__xe_next(xml_obj)) { + + pe_resource_t *new_rsc = NULL; + const char *id = ID(xml_obj); + + if (pcmk__str_empty(id)) { + pcmk__config_err("Ignoring <%s> resource without ID", + crm_element_name(xml_obj)); + continue; + } + + if (pcmk__str_eq((const char *) xml_obj->name, XML_CIB_TAG_RSC_TEMPLATE, + pcmk__str_none)) { + if (g_hash_table_lookup_extended(data_set->template_rsc_sets, id, + NULL, NULL) == FALSE) { + /* Record the template's ID for the knowledge of its existence anyway. */ + g_hash_table_insert(data_set->template_rsc_sets, strdup(id), NULL); + } + continue; + } + + crm_trace("Unpacking <%s " XML_ATTR_ID "='%s'>", + crm_element_name(xml_obj), id); + if (pe__unpack_resource(xml_obj, &new_rsc, NULL, + data_set) == pcmk_rc_ok) { + data_set->resources = g_list_append(data_set->resources, new_rsc); + pe_rsc_trace(new_rsc, "Added resource %s", new_rsc->id); + + } else { + pcmk__config_err("Ignoring <%s> resource '%s' " + "because configuration is invalid", + crm_element_name(xml_obj), id); + } + } + + for (gIter = data_set->resources; gIter != NULL; gIter = gIter->next) { + pe_resource_t *rsc = (pe_resource_t *) gIter->data; + + setup_container(rsc, data_set); + link_rsc2remotenode(data_set, rsc); + } + + data_set->resources = g_list_sort(data_set->resources, + pe__cmp_rsc_priority); + if (pcmk_is_set(data_set->flags, pe_flag_quick_location)) { + /* Ignore */ + + } else if (pcmk_is_set(data_set->flags, pe_flag_stonith_enabled) + && !pcmk_is_set(data_set->flags, pe_flag_have_stonith_resource)) { + + pcmk__config_err("Resource start-up disabled since no STONITH resources have been defined"); + pcmk__config_err("Either configure some or disable STONITH with the stonith-enabled option"); + pcmk__config_err("NOTE: Clusters with shared data need STONITH to ensure data integrity"); + } + + return TRUE; +} + +gboolean +unpack_tags(xmlNode * xml_tags, pe_working_set_t * data_set) +{ + xmlNode *xml_tag = NULL; + + data_set->tags = pcmk__strkey_table(free, destroy_tag); + + for (xml_tag = pcmk__xe_first_child(xml_tags); xml_tag != NULL; + xml_tag = pcmk__xe_next(xml_tag)) { + + xmlNode *xml_obj_ref = NULL; + const char *tag_id = ID(xml_tag); + + if (!pcmk__str_eq((const char *)xml_tag->name, XML_CIB_TAG_TAG, pcmk__str_none)) { + continue; + } + + if (tag_id == NULL) { + pcmk__config_err("Ignoring <%s> without " XML_ATTR_ID, + crm_element_name(xml_tag)); + continue; + } + + for (xml_obj_ref = pcmk__xe_first_child(xml_tag); xml_obj_ref != NULL; + xml_obj_ref = pcmk__xe_next(xml_obj_ref)) { + + const char *obj_ref = ID(xml_obj_ref); + + if (!pcmk__str_eq((const char *)xml_obj_ref->name, XML_CIB_TAG_OBJ_REF, pcmk__str_none)) { + continue; + } + + if (obj_ref == NULL) { + pcmk__config_err("Ignoring <%s> for tag '%s' without " XML_ATTR_ID, + crm_element_name(xml_obj_ref), tag_id); + continue; + } + + if (add_tag_ref(data_set->tags, tag_id, obj_ref) == FALSE) { + return FALSE; + } + } + } + + return TRUE; +} + +/* The ticket state section: + * "/cib/status/tickets/ticket_state" */ +static gboolean +unpack_ticket_state(xmlNode * xml_ticket, pe_working_set_t * data_set) +{ + const char *ticket_id = NULL; + const char *granted = NULL; + const char *last_granted = NULL; + const char *standby = NULL; + xmlAttrPtr xIter = NULL; + + pe_ticket_t *ticket = NULL; + + ticket_id = ID(xml_ticket); + if (pcmk__str_empty(ticket_id)) { + return FALSE; + } + + crm_trace("Processing ticket state for %s", ticket_id); + + ticket = g_hash_table_lookup(data_set->tickets, ticket_id); + if (ticket == NULL) { + ticket = ticket_new(ticket_id, data_set); + if (ticket == NULL) { + return FALSE; + } + } + + for (xIter = xml_ticket->properties; xIter; xIter = xIter->next) { + const char *prop_name = (const char *)xIter->name; + const char *prop_value = crm_element_value(xml_ticket, prop_name); + + if (pcmk__str_eq(prop_name, XML_ATTR_ID, pcmk__str_none)) { + continue; + } + g_hash_table_replace(ticket->state, strdup(prop_name), strdup(prop_value)); + } + + granted = g_hash_table_lookup(ticket->state, "granted"); + if (granted && crm_is_true(granted)) { + ticket->granted = TRUE; + crm_info("We have ticket '%s'", ticket->id); + } else { + ticket->granted = FALSE; + crm_info("We do not have ticket '%s'", ticket->id); + } + + last_granted = g_hash_table_lookup(ticket->state, "last-granted"); + if (last_granted) { + long long last_granted_ll; + + pcmk__scan_ll(last_granted, &last_granted_ll, 0LL); + ticket->last_granted = (time_t) last_granted_ll; + } + + standby = g_hash_table_lookup(ticket->state, "standby"); + if (standby && crm_is_true(standby)) { + ticket->standby = TRUE; + if (ticket->granted) { + crm_info("Granted ticket '%s' is in standby-mode", ticket->id); + } + } else { + ticket->standby = FALSE; + } + + crm_trace("Done with ticket state for %s", ticket_id); + + return TRUE; +} + +static gboolean +unpack_tickets_state(xmlNode * xml_tickets, pe_working_set_t * data_set) +{ + xmlNode *xml_obj = NULL; + + for (xml_obj = pcmk__xe_first_child(xml_tickets); xml_obj != NULL; + xml_obj = pcmk__xe_next(xml_obj)) { + + if (!pcmk__str_eq((const char *)xml_obj->name, XML_CIB_TAG_TICKET_STATE, pcmk__str_none)) { + continue; + } + unpack_ticket_state(xml_obj, data_set); + } + + return TRUE; +} + +static void +unpack_handle_remote_attrs(pe_node_t *this_node, const xmlNode *state, + pe_working_set_t *data_set) +{ + const char *resource_discovery_enabled = NULL; + const xmlNode *attrs = NULL; + pe_resource_t *rsc = NULL; + + if (!pcmk__str_eq((const char *)state->name, XML_CIB_TAG_STATE, pcmk__str_none)) { + return; + } + + if ((this_node == NULL) || !pe__is_guest_or_remote_node(this_node)) { + return; + } + crm_trace("Processing Pacemaker Remote node %s", pe__node_name(this_node)); + + pcmk__scan_min_int(crm_element_value(state, XML_NODE_IS_MAINTENANCE), + &(this_node->details->remote_maintenance), 0); + + rsc = this_node->details->remote_rsc; + if (this_node->details->remote_requires_reset == FALSE) { + this_node->details->unclean = FALSE; + this_node->details->unseen = FALSE; + } + attrs = find_xml_node(state, XML_TAG_TRANSIENT_NODEATTRS, FALSE); + add_node_attrs(attrs, this_node, TRUE, data_set); + + if (pe__shutdown_requested(this_node)) { + crm_info("%s is shutting down", pe__node_name(this_node)); + this_node->details->shutdown = TRUE; + } + + if (crm_is_true(pe_node_attribute_raw(this_node, "standby"))) { + crm_info("%s is in standby mode", pe__node_name(this_node)); + this_node->details->standby = TRUE; + } + + if (crm_is_true(pe_node_attribute_raw(this_node, "maintenance")) || + ((rsc != NULL) && !pcmk_is_set(rsc->flags, pe_rsc_managed))) { + crm_info("%s is in maintenance mode", pe__node_name(this_node)); + this_node->details->maintenance = TRUE; + } + + resource_discovery_enabled = pe_node_attribute_raw(this_node, XML_NODE_ATTR_RSC_DISCOVERY); + if (resource_discovery_enabled && !crm_is_true(resource_discovery_enabled)) { + if (pe__is_remote_node(this_node) + && !pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + crm_warn("Ignoring " XML_NODE_ATTR_RSC_DISCOVERY + " attribute on Pacemaker Remote node %s" + " because fencing is disabled", + pe__node_name(this_node)); + } else { + /* This is either a remote node with fencing enabled, or a guest + * node. We don't care whether fencing is enabled when fencing guest + * nodes, because they are "fenced" by recovering their containing + * resource. + */ + crm_info("%s has resource discovery disabled", + pe__node_name(this_node)); + this_node->details->rsc_discovery_enabled = FALSE; + } + } +} + +/*! + * \internal + * \brief Unpack a cluster node's transient attributes + * + * \param[in] state CIB node state XML + * \param[in,out] node Cluster node whose attributes are being unpacked + * \param[in,out] data_set Cluster working set + */ +static void +unpack_transient_attributes(const xmlNode *state, pe_node_t *node, + pe_working_set_t *data_set) +{ + const char *discovery = NULL; + const xmlNode *attrs = find_xml_node(state, XML_TAG_TRANSIENT_NODEATTRS, + FALSE); + + add_node_attrs(attrs, node, TRUE, data_set); + + if (crm_is_true(pe_node_attribute_raw(node, "standby"))) { + crm_info("%s is in standby mode", pe__node_name(node)); + node->details->standby = TRUE; + } + + if (crm_is_true(pe_node_attribute_raw(node, "maintenance"))) { + crm_info("%s is in maintenance mode", pe__node_name(node)); + node->details->maintenance = TRUE; + } + + discovery = pe_node_attribute_raw(node, XML_NODE_ATTR_RSC_DISCOVERY); + if ((discovery != NULL) && !crm_is_true(discovery)) { + crm_warn("Ignoring " XML_NODE_ATTR_RSC_DISCOVERY + " attribute for %s because disabling resource discovery " + "is not allowed for cluster nodes", pe__node_name(node)); + } +} + +/*! + * \internal + * \brief Unpack a node state entry (first pass) + * + * Unpack one node state entry from status. This unpacks information from the + * node_state element itself and node attributes inside it, but not the + * resource history inside it. Multiple passes through the status are needed to + * fully unpack everything. + * + * \param[in] state CIB node state XML + * \param[in,out] data_set Cluster working set + */ +static void +unpack_node_state(const xmlNode *state, pe_working_set_t *data_set) +{ + const char *id = NULL; + const char *uname = NULL; + pe_node_t *this_node = NULL; + + id = crm_element_value(state, XML_ATTR_ID); + if (id == NULL) { + crm_warn("Ignoring malformed " XML_CIB_TAG_STATE " entry without " + XML_ATTR_ID); + return; + } + + uname = crm_element_value(state, XML_ATTR_UNAME); + if (uname == NULL) { + crm_warn("Ignoring malformed " XML_CIB_TAG_STATE " entry without " + XML_ATTR_UNAME); + return; + } + + this_node = pe_find_node_any(data_set->nodes, id, uname); + if (this_node == NULL) { + pcmk__config_warn("Ignoring recorded node state for '%s' because " + "it is no longer in the configuration", uname); + return; + } + + if (pe__is_guest_or_remote_node(this_node)) { + /* We can't determine the online status of Pacemaker Remote nodes until + * after all resource history has been unpacked. In this first pass, we + * do need to mark whether the node has been fenced, as this plays a + * role during unpacking cluster node resource state. + */ + pcmk__scan_min_int(crm_element_value(state, XML_NODE_IS_FENCED), + &(this_node->details->remote_was_fenced), 0); + return; + } + + unpack_transient_attributes(state, this_node, data_set); + + /* Provisionally mark this cluster node as clean. We have at least seen it + * in the current cluster's lifetime. + */ + this_node->details->unclean = FALSE; + this_node->details->unseen = FALSE; + + crm_trace("Determining online status of cluster node %s (id %s)", + pe__node_name(this_node), id); + determine_online_status(state, this_node, data_set); + + if (!pcmk_is_set(data_set->flags, pe_flag_have_quorum) + && this_node->details->online + && (data_set->no_quorum_policy == no_quorum_suicide)) { + /* Everything else should flow from this automatically + * (at least until the scheduler becomes able to migrate off + * healthy resources) + */ + pe_fence_node(data_set, this_node, "cluster does not have quorum", + FALSE); + } +} + +/*! + * \internal + * \brief Unpack nodes' resource history as much as possible + * + * Unpack as many nodes' resource history as possible in one pass through the + * status. We need to process Pacemaker Remote nodes' connections/containers + * before unpacking their history; the connection/container history will be + * in another node's history, so it might take multiple passes to unpack + * everything. + * + * \param[in] status CIB XML status section + * \param[in] fence If true, treat any not-yet-unpacked nodes as unseen + * \param[in,out] data_set Cluster working set + * + * \return Standard Pacemaker return code (specifically pcmk_rc_ok if done, + * or EAGAIN if more unpacking remains to be done) + */ +static int +unpack_node_history(const xmlNode *status, bool fence, + pe_working_set_t *data_set) +{ + int rc = pcmk_rc_ok; + + // Loop through all node_state entries in CIB status + for (const xmlNode *state = first_named_child(status, XML_CIB_TAG_STATE); + state != NULL; state = crm_next_same_xml(state)) { + + const char *id = ID(state); + const char *uname = crm_element_value(state, XML_ATTR_UNAME); + pe_node_t *this_node = NULL; + + if ((id == NULL) || (uname == NULL)) { + // Warning already logged in first pass through status section + crm_trace("Not unpacking resource history from malformed " + XML_CIB_TAG_STATE " without id and/or uname"); + continue; + } + + this_node = pe_find_node_any(data_set->nodes, id, uname); + if (this_node == NULL) { + // Warning already logged in first pass through status section + crm_trace("Not unpacking resource history for node %s because " + "no longer in configuration", id); + continue; + } + + if (this_node->details->unpacked) { + crm_trace("Not unpacking resource history for node %s because " + "already unpacked", id); + continue; + } + + if (fence) { + // We're processing all remaining nodes + + } else if (pe__is_guest_node(this_node)) { + /* We can unpack a guest node's history only after we've unpacked + * other resource history to the point that we know that the node's + * connection and containing resource are both up. + */ + pe_resource_t *rsc = this_node->details->remote_rsc; + + if ((rsc == NULL) || (rsc->role != RSC_ROLE_STARTED) + || (rsc->container->role != RSC_ROLE_STARTED)) { + crm_trace("Not unpacking resource history for guest node %s " + "because container and connection are not known to " + "be up", id); + continue; + } + + } else if (pe__is_remote_node(this_node)) { + /* We can unpack a remote node's history only after we've unpacked + * other resource history to the point that we know that the node's + * connection is up, with the exception of when shutdown locks are + * in use. + */ + pe_resource_t *rsc = this_node->details->remote_rsc; + + if ((rsc == NULL) + || (!pcmk_is_set(data_set->flags, pe_flag_shutdown_lock) + && (rsc->role != RSC_ROLE_STARTED))) { + crm_trace("Not unpacking resource history for remote node %s " + "because connection is not known to be up", id); + continue; + } + + /* If fencing and shutdown locks are disabled and we're not processing + * unseen nodes, then we don't want to unpack offline nodes until online + * nodes have been unpacked. This allows us to number active clone + * instances first. + */ + } else if (!pcmk_any_flags_set(data_set->flags, pe_flag_stonith_enabled + |pe_flag_shutdown_lock) + && !this_node->details->online) { + crm_trace("Not unpacking resource history for offline " + "cluster node %s", id); + continue; + } + + if (pe__is_guest_or_remote_node(this_node)) { + determine_remote_online_status(data_set, this_node); + unpack_handle_remote_attrs(this_node, state, data_set); + } + + crm_trace("Unpacking resource history for %snode %s", + (fence? "unseen " : ""), id); + + this_node->details->unpacked = TRUE; + unpack_node_lrm(this_node, state, data_set); + + rc = EAGAIN; // Other node histories might depend on this one + } + return rc; +} + +/* remove nodes that are down, stopping */ +/* create positive rsc_to_node constraints between resources and the nodes they are running on */ +/* anything else? */ +gboolean +unpack_status(xmlNode * status, pe_working_set_t * data_set) +{ + xmlNode *state = NULL; + + crm_trace("Beginning unpack"); + + if (data_set->tickets == NULL) { + data_set->tickets = pcmk__strkey_table(free, destroy_ticket); + } + + for (state = pcmk__xe_first_child(status); state != NULL; + state = pcmk__xe_next(state)) { + + if (pcmk__str_eq((const char *)state->name, XML_CIB_TAG_TICKETS, pcmk__str_none)) { + unpack_tickets_state((xmlNode *) state, data_set); + + } else if (pcmk__str_eq((const char *)state->name, XML_CIB_TAG_STATE, pcmk__str_none)) { + unpack_node_state(state, data_set); + } + } + + while (unpack_node_history(status, FALSE, data_set) == EAGAIN) { + crm_trace("Another pass through node resource histories is needed"); + } + + // Now catch any nodes we didn't see + unpack_node_history(status, + pcmk_is_set(data_set->flags, pe_flag_stonith_enabled), + data_set); + + /* Now that we know where resources are, we can schedule stops of containers + * with failed bundle connections + */ + if (data_set->stop_needed != NULL) { + for (GList *item = data_set->stop_needed; item; item = item->next) { + pe_resource_t *container = item->data; + pe_node_t *node = pe__current_node(container); + + if (node) { + stop_action(container, node, FALSE); + } + } + g_list_free(data_set->stop_needed); + data_set->stop_needed = NULL; + } + + /* Now that we know status of all Pacemaker Remote connections and nodes, + * we can stop connections for node shutdowns, and check the online status + * of remote/guest nodes that didn't have any node history to unpack. + */ + for (GList *gIter = data_set->nodes; gIter != NULL; gIter = gIter->next) { + pe_node_t *this_node = gIter->data; + + if (!pe__is_guest_or_remote_node(this_node)) { + continue; + } + if (this_node->details->shutdown + && (this_node->details->remote_rsc != NULL)) { + pe__set_next_role(this_node->details->remote_rsc, RSC_ROLE_STOPPED, + "remote shutdown"); + } + if (!this_node->details->unpacked) { + determine_remote_online_status(data_set, this_node); + } + } + + return TRUE; +} + +static gboolean +determine_online_status_no_fencing(pe_working_set_t *data_set, + const xmlNode *node_state, + pe_node_t *this_node) +{ + gboolean online = FALSE; + const char *join = crm_element_value(node_state, XML_NODE_JOIN_STATE); + const char *is_peer = crm_element_value(node_state, XML_NODE_IS_PEER); + const char *in_cluster = crm_element_value(node_state, XML_NODE_IN_CLUSTER); + const char *exp_state = crm_element_value(node_state, XML_NODE_EXPECTED); + + if (!crm_is_true(in_cluster)) { + crm_trace("Node is down: in_cluster=%s", + pcmk__s(in_cluster, "")); + + } else if (pcmk__str_eq(is_peer, ONLINESTATUS, pcmk__str_casei)) { + if (pcmk__str_eq(join, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) { + online = TRUE; + } else { + crm_debug("Node is not ready to run resources: %s", join); + } + + } else if (this_node->details->expected_up == FALSE) { + crm_trace("Controller is down: " + "in_cluster=%s is_peer=%s join=%s expected=%s", + pcmk__s(in_cluster, ""), pcmk__s(is_peer, ""), + pcmk__s(join, ""), pcmk__s(exp_state, "")); + + } else { + /* mark it unclean */ + pe_fence_node(data_set, this_node, "peer is unexpectedly down", FALSE); + crm_info("in_cluster=%s is_peer=%s join=%s expected=%s", + pcmk__s(in_cluster, ""), pcmk__s(is_peer, ""), + pcmk__s(join, ""), pcmk__s(exp_state, "")); + } + return online; +} + +static gboolean +determine_online_status_fencing(pe_working_set_t *data_set, + const xmlNode *node_state, pe_node_t *this_node) +{ + gboolean online = FALSE; + gboolean do_terminate = FALSE; + bool crmd_online = FALSE; + const char *join = crm_element_value(node_state, XML_NODE_JOIN_STATE); + const char *is_peer = crm_element_value(node_state, XML_NODE_IS_PEER); + const char *in_cluster = crm_element_value(node_state, XML_NODE_IN_CLUSTER); + const char *exp_state = crm_element_value(node_state, XML_NODE_EXPECTED); + const char *terminate = pe_node_attribute_raw(this_node, "terminate"); + +/* + - XML_NODE_IN_CLUSTER ::= true|false + - XML_NODE_IS_PEER ::= online|offline + - XML_NODE_JOIN_STATE ::= member|down|pending|banned + - XML_NODE_EXPECTED ::= member|down +*/ + + if (crm_is_true(terminate)) { + do_terminate = TRUE; + + } else if (terminate != NULL && strlen(terminate) > 0) { + /* could be a time() value */ + char t = terminate[0]; + + if (t != '0' && isdigit(t)) { + do_terminate = TRUE; + } + } + + crm_trace("%s: in_cluster=%s is_peer=%s join=%s expected=%s term=%d", + pe__node_name(this_node), pcmk__s(in_cluster, ""), + pcmk__s(is_peer, ""), pcmk__s(join, ""), + pcmk__s(exp_state, ""), do_terminate); + + online = crm_is_true(in_cluster); + crmd_online = pcmk__str_eq(is_peer, ONLINESTATUS, pcmk__str_casei); + if (exp_state == NULL) { + exp_state = CRMD_JOINSTATE_DOWN; + } + + if (this_node->details->shutdown) { + crm_debug("%s is shutting down", pe__node_name(this_node)); + + /* Slightly different criteria since we can't shut down a dead peer */ + online = crmd_online; + + } else if (in_cluster == NULL) { + pe_fence_node(data_set, this_node, "peer has not been seen by the cluster", FALSE); + + } else if (pcmk__str_eq(join, CRMD_JOINSTATE_NACK, pcmk__str_casei)) { + pe_fence_node(data_set, this_node, + "peer failed Pacemaker membership criteria", FALSE); + + } else if (do_terminate == FALSE && pcmk__str_eq(exp_state, CRMD_JOINSTATE_DOWN, pcmk__str_casei)) { + + if (crm_is_true(in_cluster) || crmd_online) { + crm_info("- %s is not ready to run resources", + pe__node_name(this_node)); + this_node->details->standby = TRUE; + this_node->details->pending = TRUE; + + } else { + crm_trace("%s is down or still coming up", + pe__node_name(this_node)); + } + + } else if (do_terminate && pcmk__str_eq(join, CRMD_JOINSTATE_DOWN, pcmk__str_casei) + && crm_is_true(in_cluster) == FALSE && !crmd_online) { + crm_info("%s was just shot", pe__node_name(this_node)); + online = FALSE; + + } else if (crm_is_true(in_cluster) == FALSE) { + // Consider `priority-fencing-delay` for lost nodes + pe_fence_node(data_set, this_node, "peer is no longer part of the cluster", TRUE); + + } else if (!crmd_online) { + pe_fence_node(data_set, this_node, "peer process is no longer available", FALSE); + + /* Everything is running at this point, now check join state */ + } else if (do_terminate) { + pe_fence_node(data_set, this_node, "termination was requested", FALSE); + + } else if (pcmk__str_eq(join, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) { + crm_info("%s is active", pe__node_name(this_node)); + + } else if (pcmk__strcase_any_of(join, CRMD_JOINSTATE_PENDING, CRMD_JOINSTATE_DOWN, NULL)) { + crm_info("%s is not ready to run resources", pe__node_name(this_node)); + this_node->details->standby = TRUE; + this_node->details->pending = TRUE; + + } else { + pe_fence_node(data_set, this_node, "peer was in an unknown state", FALSE); + crm_warn("%s: in-cluster=%s is-peer=%s join=%s expected=%s term=%d shutdown=%d", + pe__node_name(this_node), pcmk__s(in_cluster, ""), + pcmk__s(is_peer, ""), pcmk__s(join, ""), + pcmk__s(exp_state, ""), do_terminate, + this_node->details->shutdown); + } + + return online; +} + +static void +determine_remote_online_status(pe_working_set_t * data_set, pe_node_t * this_node) +{ + pe_resource_t *rsc = this_node->details->remote_rsc; + pe_resource_t *container = NULL; + pe_node_t *host = NULL; + + /* If there is a node state entry for a (former) Pacemaker Remote node + * but no resource creating that node, the node's connection resource will + * be NULL. Consider it an offline remote node in that case. + */ + if (rsc == NULL) { + this_node->details->online = FALSE; + goto remote_online_done; + } + + container = rsc->container; + + if (container && pcmk__list_of_1(rsc->running_on)) { + host = rsc->running_on->data; + } + + /* If the resource is currently started, mark it online. */ + if (rsc->role == RSC_ROLE_STARTED) { + crm_trace("%s node %s presumed ONLINE because connection resource is started", + (container? "Guest" : "Remote"), this_node->details->id); + this_node->details->online = TRUE; + } + + /* consider this node shutting down if transitioning start->stop */ + if (rsc->role == RSC_ROLE_STARTED && rsc->next_role == RSC_ROLE_STOPPED) { + crm_trace("%s node %s shutting down because connection resource is stopping", + (container? "Guest" : "Remote"), this_node->details->id); + this_node->details->shutdown = TRUE; + } + + /* Now check all the failure conditions. */ + if(container && pcmk_is_set(container->flags, pe_rsc_failed)) { + crm_trace("Guest node %s UNCLEAN because guest resource failed", + this_node->details->id); + this_node->details->online = FALSE; + this_node->details->remote_requires_reset = TRUE; + + } else if (pcmk_is_set(rsc->flags, pe_rsc_failed)) { + crm_trace("%s node %s OFFLINE because connection resource failed", + (container? "Guest" : "Remote"), this_node->details->id); + this_node->details->online = FALSE; + + } else if (rsc->role == RSC_ROLE_STOPPED + || (container && container->role == RSC_ROLE_STOPPED)) { + + crm_trace("%s node %s OFFLINE because its resource is stopped", + (container? "Guest" : "Remote"), this_node->details->id); + this_node->details->online = FALSE; + this_node->details->remote_requires_reset = FALSE; + + } else if (host && (host->details->online == FALSE) + && host->details->unclean) { + crm_trace("Guest node %s UNCLEAN because host is unclean", + this_node->details->id); + this_node->details->online = FALSE; + this_node->details->remote_requires_reset = TRUE; + } + +remote_online_done: + crm_trace("Remote node %s online=%s", + this_node->details->id, this_node->details->online ? "TRUE" : "FALSE"); +} + +static void +determine_online_status(const xmlNode *node_state, pe_node_t *this_node, + pe_working_set_t *data_set) +{ + gboolean online = FALSE; + const char *exp_state = crm_element_value(node_state, XML_NODE_EXPECTED); + + CRM_CHECK(this_node != NULL, return); + + this_node->details->shutdown = FALSE; + this_node->details->expected_up = FALSE; + + if (pe__shutdown_requested(this_node)) { + this_node->details->shutdown = TRUE; + + } else if (pcmk__str_eq(exp_state, CRMD_JOINSTATE_MEMBER, pcmk__str_casei)) { + this_node->details->expected_up = TRUE; + } + + if (this_node->details->type == node_ping) { + this_node->details->unclean = FALSE; + online = FALSE; /* As far as resource management is concerned, + * the node is safely offline. + * Anyone caught abusing this logic will be shot + */ + + } else if (!pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + online = determine_online_status_no_fencing(data_set, node_state, this_node); + + } else { + online = determine_online_status_fencing(data_set, node_state, this_node); + } + + if (online) { + this_node->details->online = TRUE; + + } else { + /* remove node from contention */ + this_node->fixed = TRUE; // @COMPAT deprecated and unused + this_node->weight = -INFINITY; + } + + if (online && this_node->details->shutdown) { + /* don't run resources here */ + this_node->fixed = TRUE; // @COMPAT deprecated and unused + this_node->weight = -INFINITY; + } + + if (this_node->details->type == node_ping) { + crm_info("%s is not a Pacemaker node", pe__node_name(this_node)); + + } else if (this_node->details->unclean) { + pe_proc_warn("%s is unclean", pe__node_name(this_node)); + + } else if (this_node->details->online) { + crm_info("%s is %s", pe__node_name(this_node), + this_node->details->shutdown ? "shutting down" : + this_node->details->pending ? "pending" : + this_node->details->standby ? "standby" : + this_node->details->maintenance ? "maintenance" : "online"); + + } else { + crm_trace("%s is offline", pe__node_name(this_node)); + } +} + +/*! + * \internal + * \brief Find the end of a resource's name, excluding any clone suffix + * + * \param[in] id Resource ID to check + * + * \return Pointer to last character of resource's base name + */ +const char * +pe_base_name_end(const char *id) +{ + if (!pcmk__str_empty(id)) { + const char *end = id + strlen(id) - 1; + + for (const char *s = end; s > id; --s) { + switch (*s) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + break; + case ':': + return (s == end)? s : (s - 1); + default: + return end; + } + } + return end; + } + return NULL; +} + +/*! + * \internal + * \brief Get a resource name excluding any clone suffix + * + * \param[in] last_rsc_id Resource ID to check + * + * \return Pointer to newly allocated string with resource's base name + * \note It is the caller's responsibility to free() the result. + * This asserts on error, so callers can assume result is not NULL. + */ +char * +clone_strip(const char *last_rsc_id) +{ + const char *end = pe_base_name_end(last_rsc_id); + char *basename = NULL; + + CRM_ASSERT(end); + basename = strndup(last_rsc_id, end - last_rsc_id + 1); + CRM_ASSERT(basename); + return basename; +} + +/*! + * \internal + * \brief Get the name of the first instance of a cloned resource + * + * \param[in] last_rsc_id Resource ID to check + * + * \return Pointer to newly allocated string with resource's base name plus :0 + * \note It is the caller's responsibility to free() the result. + * This asserts on error, so callers can assume result is not NULL. + */ +char * +clone_zero(const char *last_rsc_id) +{ + const char *end = pe_base_name_end(last_rsc_id); + size_t base_name_len = end - last_rsc_id + 1; + char *zero = NULL; + + CRM_ASSERT(end); + zero = calloc(base_name_len + 3, sizeof(char)); + CRM_ASSERT(zero); + memcpy(zero, last_rsc_id, base_name_len); + zero[base_name_len] = ':'; + zero[base_name_len + 1] = '0'; + return zero; +} + +static pe_resource_t * +create_fake_resource(const char *rsc_id, const xmlNode *rsc_entry, + pe_working_set_t *data_set) +{ + pe_resource_t *rsc = NULL; + xmlNode *xml_rsc = create_xml_node(NULL, XML_CIB_TAG_RESOURCE); + + copy_in_properties(xml_rsc, rsc_entry); + crm_xml_add(xml_rsc, XML_ATTR_ID, rsc_id); + crm_log_xml_debug(xml_rsc, "Orphan resource"); + + if (pe__unpack_resource(xml_rsc, &rsc, NULL, data_set) != pcmk_rc_ok) { + return NULL; + } + + if (xml_contains_remote_node(xml_rsc)) { + pe_node_t *node; + + crm_debug("Detected orphaned remote node %s", rsc_id); + node = pe_find_node(data_set->nodes, rsc_id); + if (node == NULL) { + node = pe_create_node(rsc_id, rsc_id, "remote", NULL, data_set); + } + link_rsc2remotenode(data_set, rsc); + + if (node) { + crm_trace("Setting node %s as shutting down due to orphaned connection resource", rsc_id); + node->details->shutdown = TRUE; + } + } + + if (crm_element_value(rsc_entry, XML_RSC_ATTR_CONTAINER)) { + /* This orphaned rsc needs to be mapped to a container. */ + crm_trace("Detected orphaned container filler %s", rsc_id); + pe__set_resource_flags(rsc, pe_rsc_orphan_container_filler); + } + pe__set_resource_flags(rsc, pe_rsc_orphan); + data_set->resources = g_list_append(data_set->resources, rsc); + return rsc; +} + +/*! + * \internal + * \brief Create orphan instance for anonymous clone resource history + * + * \param[in,out] parent Clone resource that orphan will be added to + * \param[in] rsc_id Orphan's resource ID + * \param[in] node Where orphan is active (for logging only) + * \param[in,out] data_set Cluster working set + * + * \return Newly added orphaned instance of \p parent + */ +static pe_resource_t * +create_anonymous_orphan(pe_resource_t *parent, const char *rsc_id, + const pe_node_t *node, pe_working_set_t *data_set) +{ + pe_resource_t *top = pe__create_clone_child(parent, data_set); + + // find_rsc() because we might be a cloned group + pe_resource_t *orphan = top->fns->find_rsc(top, rsc_id, NULL, pe_find_clone); + + pe_rsc_debug(parent, "Created orphan %s for %s: %s on %s", + top->id, parent->id, rsc_id, pe__node_name(node)); + return orphan; +} + +/*! + * \internal + * \brief Check a node for an instance of an anonymous clone + * + * Return a child instance of the specified anonymous clone, in order of + * preference: (1) the instance running on the specified node, if any; + * (2) an inactive instance (i.e. within the total of clone-max instances); + * (3) a newly created orphan (i.e. clone-max instances are already active). + * + * \param[in,out] data_set Cluster information + * \param[in] node Node on which to check for instance + * \param[in,out] parent Clone to check + * \param[in] rsc_id Name of cloned resource in history (without instance) + */ +static pe_resource_t * +find_anonymous_clone(pe_working_set_t *data_set, const pe_node_t *node, + pe_resource_t *parent, const char *rsc_id) +{ + GList *rIter = NULL; + pe_resource_t *rsc = NULL; + pe_resource_t *inactive_instance = NULL; + gboolean skip_inactive = FALSE; + + CRM_ASSERT(parent != NULL); + CRM_ASSERT(pe_rsc_is_clone(parent)); + CRM_ASSERT(!pcmk_is_set(parent->flags, pe_rsc_unique)); + + // Check for active (or partially active, for cloned groups) instance + pe_rsc_trace(parent, "Looking for %s on %s in %s", + rsc_id, pe__node_name(node), parent->id); + for (rIter = parent->children; rsc == NULL && rIter; rIter = rIter->next) { + GList *locations = NULL; + pe_resource_t *child = rIter->data; + + /* Check whether this instance is already known to be active or pending + * anywhere, at this stage of unpacking. Because this function is called + * for a resource before the resource's individual operation history + * entries are unpacked, locations will generally not contain the + * desired node. + * + * However, there are three exceptions: + * (1) when child is a cloned group and we have already unpacked the + * history of another member of the group on the same node; + * (2) when we've already unpacked the history of another numbered + * instance on the same node (which can happen if globally-unique + * was flipped from true to false); and + * (3) when we re-run calculations on the same data set as part of a + * simulation. + */ + child->fns->location(child, &locations, 2); + if (locations) { + /* We should never associate the same numbered anonymous clone + * instance with multiple nodes, and clone instances can't migrate, + * so there must be only one location, regardless of history. + */ + CRM_LOG_ASSERT(locations->next == NULL); + + if (((pe_node_t *)locations->data)->details == node->details) { + /* This child instance is active on the requested node, so check + * for a corresponding configured resource. We use find_rsc() + * instead of child because child may be a cloned group, and we + * need the particular member corresponding to rsc_id. + * + * If the history entry is orphaned, rsc will be NULL. + */ + rsc = parent->fns->find_rsc(child, rsc_id, NULL, pe_find_clone); + if (rsc) { + /* If there are multiple instance history entries for an + * anonymous clone in a single node's history (which can + * happen if globally-unique is switched from true to + * false), we want to consider the instances beyond the + * first as orphans, even if there are inactive instance + * numbers available. + */ + if (rsc->running_on) { + crm_notice("Active (now-)anonymous clone %s has " + "multiple (orphan) instance histories on %s", + parent->id, pe__node_name(node)); + skip_inactive = TRUE; + rsc = NULL; + } else { + pe_rsc_trace(parent, "Resource %s, active", rsc->id); + } + } + } + g_list_free(locations); + + } else { + pe_rsc_trace(parent, "Resource %s, skip inactive", child->id); + if (!skip_inactive && !inactive_instance + && !pcmk_is_set(child->flags, pe_rsc_block)) { + // Remember one inactive instance in case we don't find active + inactive_instance = parent->fns->find_rsc(child, rsc_id, NULL, + pe_find_clone); + + /* ... but don't use it if it was already associated with a + * pending action on another node + */ + if (inactive_instance && inactive_instance->pending_node + && (inactive_instance->pending_node->details != node->details)) { + inactive_instance = NULL; + } + } + } + } + + if ((rsc == NULL) && !skip_inactive && (inactive_instance != NULL)) { + pe_rsc_trace(parent, "Resource %s, empty slot", inactive_instance->id); + rsc = inactive_instance; + } + + /* If the resource has "requires" set to "quorum" or "nothing", and we don't + * have a clone instance for every node, we don't want to consume a valid + * instance number for unclean nodes. Such instances may appear to be active + * according to the history, but should be considered inactive, so we can + * start an instance elsewhere. Treat such instances as orphans. + * + * An exception is instances running on guest nodes -- since guest node + * "fencing" is actually just a resource stop, requires shouldn't apply. + * + * @TODO Ideally, we'd use an inactive instance number if it is not needed + * for any clean instances. However, we don't know that at this point. + */ + if ((rsc != NULL) && !pcmk_is_set(rsc->flags, pe_rsc_needs_fencing) + && (!node->details->online || node->details->unclean) + && !pe__is_guest_node(node) + && !pe__is_universal_clone(parent, data_set)) { + + rsc = NULL; + } + + if (rsc == NULL) { + rsc = create_anonymous_orphan(parent, rsc_id, node, data_set); + pe_rsc_trace(parent, "Resource %s, orphan", rsc->id); + } + return rsc; +} + +static pe_resource_t * +unpack_find_resource(pe_working_set_t *data_set, const pe_node_t *node, + const char *rsc_id) +{ + pe_resource_t *rsc = NULL; + pe_resource_t *parent = NULL; + + crm_trace("looking for %s", rsc_id); + rsc = pe_find_resource(data_set->resources, rsc_id); + + if (rsc == NULL) { + /* If we didn't find the resource by its name in the operation history, + * check it again as a clone instance. Even when clone-max=0, we create + * a single :0 orphan to match against here. + */ + char *clone0_id = clone_zero(rsc_id); + pe_resource_t *clone0 = pe_find_resource(data_set->resources, clone0_id); + + if (clone0 && !pcmk_is_set(clone0->flags, pe_rsc_unique)) { + rsc = clone0; + parent = uber_parent(clone0); + crm_trace("%s found as %s (%s)", rsc_id, clone0_id, parent->id); + } else { + crm_trace("%s is not known as %s either (orphan)", + rsc_id, clone0_id); + } + free(clone0_id); + + } else if (rsc->variant > pe_native) { + crm_trace("Resource history for %s is orphaned because it is no longer primitive", + rsc_id); + return NULL; + + } else { + parent = uber_parent(rsc); + } + + if (pe_rsc_is_anon_clone(parent)) { + + if (pe_rsc_is_bundled(parent)) { + rsc = pe__find_bundle_replica(parent->parent, node); + } else { + char *base = clone_strip(rsc_id); + + rsc = find_anonymous_clone(data_set, node, parent, base); + free(base); + CRM_ASSERT(rsc != NULL); + } + } + + if (rsc && !pcmk__str_eq(rsc_id, rsc->id, pcmk__str_casei) + && !pcmk__str_eq(rsc_id, rsc->clone_name, pcmk__str_casei)) { + + pcmk__str_update(&rsc->clone_name, rsc_id); + pe_rsc_debug(rsc, "Internally renamed %s on %s to %s%s", + rsc_id, pe__node_name(node), rsc->id, + (pcmk_is_set(rsc->flags, pe_rsc_orphan)? " (ORPHAN)" : "")); + } + return rsc; +} + +static pe_resource_t * +process_orphan_resource(const xmlNode *rsc_entry, const pe_node_t *node, + pe_working_set_t *data_set) +{ + pe_resource_t *rsc = NULL; + const char *rsc_id = crm_element_value(rsc_entry, XML_ATTR_ID); + + crm_debug("Detected orphan resource %s on %s", rsc_id, pe__node_name(node)); + rsc = create_fake_resource(rsc_id, rsc_entry, data_set); + if (rsc == NULL) { + return NULL; + } + + if (!pcmk_is_set(data_set->flags, pe_flag_stop_rsc_orphans)) { + pe__clear_resource_flags(rsc, pe_rsc_managed); + + } else { + CRM_CHECK(rsc != NULL, return NULL); + pe_rsc_trace(rsc, "Added orphan %s", rsc->id); + resource_location(rsc, NULL, -INFINITY, "__orphan_do_not_run__", data_set); + } + return rsc; +} + +static void +process_rsc_state(pe_resource_t * rsc, pe_node_t * node, + enum action_fail_response on_fail) +{ + pe_node_t *tmpnode = NULL; + char *reason = NULL; + enum action_fail_response save_on_fail = action_fail_ignore; + + CRM_ASSERT(rsc); + pe_rsc_trace(rsc, "Resource %s is %s on %s: on_fail=%s", + rsc->id, role2text(rsc->role), pe__node_name(node), + fail2text(on_fail)); + + /* process current state */ + if (rsc->role != RSC_ROLE_UNKNOWN) { + pe_resource_t *iter = rsc; + + while (iter) { + if (g_hash_table_lookup(iter->known_on, node->details->id) == NULL) { + pe_node_t *n = pe__copy_node(node); + + pe_rsc_trace(rsc, "%s%s%s known on %s", + rsc->id, + ((rsc->clone_name == NULL)? "" : " also known as "), + ((rsc->clone_name == NULL)? "" : rsc->clone_name), + pe__node_name(n)); + g_hash_table_insert(iter->known_on, (gpointer) n->details->id, n); + } + if (pcmk_is_set(iter->flags, pe_rsc_unique)) { + break; + } + iter = iter->parent; + } + } + + /* If a managed resource is believed to be running, but node is down ... */ + if (rsc->role > RSC_ROLE_STOPPED + && node->details->online == FALSE + && node->details->maintenance == FALSE + && pcmk_is_set(rsc->flags, pe_rsc_managed)) { + + gboolean should_fence = FALSE; + + /* If this is a guest node, fence it (regardless of whether fencing is + * enabled, because guest node fencing is done by recovery of the + * container resource rather than by the fencer). Mark the resource + * we're processing as failed. When the guest comes back up, its + * operation history in the CIB will be cleared, freeing the affected + * resource to run again once we are sure we know its state. + */ + if (pe__is_guest_node(node)) { + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + should_fence = TRUE; + + } else if (pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled)) { + if (pe__is_remote_node(node) && node->details->remote_rsc + && !pcmk_is_set(node->details->remote_rsc->flags, pe_rsc_failed)) { + + /* Setting unseen means that fencing of the remote node will + * occur only if the connection resource is not going to start + * somewhere. This allows connection resources on a failed + * cluster node to move to another node without requiring the + * remote nodes to be fenced as well. + */ + node->details->unseen = TRUE; + reason = crm_strdup_printf("%s is active there (fencing will be" + " revoked if remote connection can " + "be re-established elsewhere)", + rsc->id); + } + should_fence = TRUE; + } + + if (should_fence) { + if (reason == NULL) { + reason = crm_strdup_printf("%s is thought to be active there", rsc->id); + } + pe_fence_node(rsc->cluster, node, reason, FALSE); + } + free(reason); + } + + /* In order to calculate priority_fencing_delay correctly, save the failure information and pass it to native_add_running(). */ + save_on_fail = on_fail; + + if (node->details->unclean) { + /* No extra processing needed + * Also allows resources to be started again after a node is shot + */ + on_fail = action_fail_ignore; + } + + switch (on_fail) { + case action_fail_ignore: + /* nothing to do */ + break; + + case action_fail_demote: + pe__set_resource_flags(rsc, pe_rsc_failed); + demote_action(rsc, node, FALSE); + break; + + case action_fail_fence: + /* treat it as if it is still running + * but also mark the node as unclean + */ + reason = crm_strdup_printf("%s failed there", rsc->id); + pe_fence_node(rsc->cluster, node, reason, FALSE); + free(reason); + break; + + case action_fail_standby: + node->details->standby = TRUE; + node->details->standby_onfail = TRUE; + break; + + case action_fail_block: + /* is_managed == FALSE will prevent any + * actions being sent for the resource + */ + pe__clear_resource_flags(rsc, pe_rsc_managed); + pe__set_resource_flags(rsc, pe_rsc_block); + break; + + case action_fail_migrate: + /* make sure it comes up somewhere else + * or not at all + */ + resource_location(rsc, node, -INFINITY, "__action_migration_auto__", + rsc->cluster); + break; + + case action_fail_stop: + pe__set_next_role(rsc, RSC_ROLE_STOPPED, "on-fail=stop"); + break; + + case action_fail_recover: + if (rsc->role != RSC_ROLE_STOPPED && rsc->role != RSC_ROLE_UNKNOWN) { + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + stop_action(rsc, node, FALSE); + } + break; + + case action_fail_restart_container: + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + if (rsc->container && pe_rsc_is_bundled(rsc)) { + /* A bundle's remote connection can run on a different node than + * the bundle's container. We don't necessarily know where the + * container is running yet, so remember it and add a stop + * action for it later. + */ + rsc->cluster->stop_needed = + g_list_prepend(rsc->cluster->stop_needed, rsc->container); + } else if (rsc->container) { + stop_action(rsc->container, node, FALSE); + } else if (rsc->role != RSC_ROLE_STOPPED && rsc->role != RSC_ROLE_UNKNOWN) { + stop_action(rsc, node, FALSE); + } + break; + + case action_fail_reset_remote: + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + if (pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled)) { + tmpnode = NULL; + if (rsc->is_remote_node) { + tmpnode = pe_find_node(rsc->cluster->nodes, rsc->id); + } + if (tmpnode && + pe__is_remote_node(tmpnode) && + tmpnode->details->remote_was_fenced == 0) { + + /* The remote connection resource failed in a way that + * should result in fencing the remote node. + */ + pe_fence_node(rsc->cluster, tmpnode, + "remote connection is unrecoverable", FALSE); + } + } + + /* require the stop action regardless if fencing is occurring or not. */ + if (rsc->role > RSC_ROLE_STOPPED) { + stop_action(rsc, node, FALSE); + } + + /* if reconnect delay is in use, prevent the connection from exiting the + * "STOPPED" role until the failure is cleared by the delay timeout. */ + if (rsc->remote_reconnect_ms) { + pe__set_next_role(rsc, RSC_ROLE_STOPPED, "remote reset"); + } + break; + } + + /* ensure a remote-node connection failure forces an unclean remote-node + * to be fenced. By setting unseen = FALSE, the remote-node failure will + * result in a fencing operation regardless if we're going to attempt to + * reconnect to the remote-node in this transition or not. */ + if (pcmk_is_set(rsc->flags, pe_rsc_failed) && rsc->is_remote_node) { + tmpnode = pe_find_node(rsc->cluster->nodes, rsc->id); + if (tmpnode && tmpnode->details->unclean) { + tmpnode->details->unseen = FALSE; + } + } + + if (rsc->role != RSC_ROLE_STOPPED && rsc->role != RSC_ROLE_UNKNOWN) { + if (pcmk_is_set(rsc->flags, pe_rsc_orphan)) { + if (pcmk_is_set(rsc->flags, pe_rsc_managed)) { + pcmk__config_warn("Detected active orphan %s running on %s", + rsc->id, pe__node_name(node)); + } else { + pcmk__config_warn("Resource '%s' must be stopped manually on " + "%s because cluster is configured not to " + "stop active orphans", + rsc->id, pe__node_name(node)); + } + } + + native_add_running(rsc, node, rsc->cluster, + (save_on_fail != action_fail_ignore)); + switch (on_fail) { + case action_fail_ignore: + break; + case action_fail_demote: + case action_fail_block: + pe__set_resource_flags(rsc, pe_rsc_failed); + break; + default: + pe__set_resource_flags(rsc, pe_rsc_failed|pe_rsc_stop); + break; + } + + } else if (rsc->clone_name && strchr(rsc->clone_name, ':') != NULL) { + /* Only do this for older status sections that included instance numbers + * Otherwise stopped instances will appear as orphans + */ + pe_rsc_trace(rsc, "Resetting clone_name %s for %s (stopped)", rsc->clone_name, rsc->id); + free(rsc->clone_name); + rsc->clone_name = NULL; + + } else { + GList *possible_matches = pe__resource_actions(rsc, node, RSC_STOP, + FALSE); + GList *gIter = possible_matches; + + for (; gIter != NULL; gIter = gIter->next) { + pe_action_t *stop = (pe_action_t *) gIter->data; + + pe__set_action_flags(stop, pe_action_optional); + } + + g_list_free(possible_matches); + } + + /* A successful stop after migrate_to on the migration source doesn't make + * the partially migrated resource stopped on the migration target. + */ + if (rsc->role == RSC_ROLE_STOPPED + && rsc->partial_migration_source + && rsc->partial_migration_source->details == node->details + && rsc->partial_migration_target + && rsc->running_on) { + + rsc->role = RSC_ROLE_STARTED; + } +} + +/* create active recurring operations as optional */ +static void +process_recurring(pe_node_t * node, pe_resource_t * rsc, + int start_index, int stop_index, + GList *sorted_op_list, pe_working_set_t * data_set) +{ + int counter = -1; + const char *task = NULL; + const char *status = NULL; + GList *gIter = sorted_op_list; + + CRM_ASSERT(rsc); + pe_rsc_trace(rsc, "%s: Start index %d, stop index = %d", rsc->id, start_index, stop_index); + + for (; gIter != NULL; gIter = gIter->next) { + xmlNode *rsc_op = (xmlNode *) gIter->data; + + guint interval_ms = 0; + char *key = NULL; + const char *id = ID(rsc_op); + + counter++; + + if (node->details->online == FALSE) { + pe_rsc_trace(rsc, "Skipping %s on %s: node is offline", + rsc->id, pe__node_name(node)); + break; + + /* Need to check if there's a monitor for role="Stopped" */ + } else if (start_index < stop_index && counter <= stop_index) { + pe_rsc_trace(rsc, "Skipping %s on %s: resource is not active", + id, pe__node_name(node)); + continue; + + } else if (counter < start_index) { + pe_rsc_trace(rsc, "Skipping %s on %s: old %d", + id, pe__node_name(node), counter); + continue; + } + + crm_element_value_ms(rsc_op, XML_LRM_ATTR_INTERVAL_MS, &interval_ms); + if (interval_ms == 0) { + pe_rsc_trace(rsc, "Skipping %s on %s: non-recurring", + id, pe__node_name(node)); + continue; + } + + status = crm_element_value(rsc_op, XML_LRM_ATTR_OPSTATUS); + if (pcmk__str_eq(status, "-1", pcmk__str_casei)) { + pe_rsc_trace(rsc, "Skipping %s on %s: status", + id, pe__node_name(node)); + continue; + } + task = crm_element_value(rsc_op, XML_LRM_ATTR_TASK); + /* create the action */ + key = pcmk__op_key(rsc->id, task, interval_ms); + pe_rsc_trace(rsc, "Creating %s on %s", key, pe__node_name(node)); + custom_action(rsc, key, task, node, TRUE, TRUE, data_set); + } +} + +void +calculate_active_ops(const GList *sorted_op_list, int *start_index, + int *stop_index) +{ + int counter = -1; + int implied_monitor_start = -1; + int implied_clone_start = -1; + const char *task = NULL; + const char *status = NULL; + + *stop_index = -1; + *start_index = -1; + + for (const GList *iter = sorted_op_list; iter != NULL; iter = iter->next) { + const xmlNode *rsc_op = (const xmlNode *) iter->data; + + counter++; + + task = crm_element_value(rsc_op, XML_LRM_ATTR_TASK); + status = crm_element_value(rsc_op, XML_LRM_ATTR_OPSTATUS); + + if (pcmk__str_eq(task, CRMD_ACTION_STOP, pcmk__str_casei) + && pcmk__str_eq(status, "0", pcmk__str_casei)) { + *stop_index = counter; + + } else if (pcmk__strcase_any_of(task, CRMD_ACTION_START, CRMD_ACTION_MIGRATED, NULL)) { + *start_index = counter; + + } else if ((implied_monitor_start <= *stop_index) && pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_casei)) { + const char *rc = crm_element_value(rsc_op, XML_LRM_ATTR_RC); + + if (pcmk__strcase_any_of(rc, "0", "8", NULL)) { + implied_monitor_start = counter; + } + } else if (pcmk__strcase_any_of(task, CRMD_ACTION_PROMOTE, CRMD_ACTION_DEMOTE, NULL)) { + implied_clone_start = counter; + } + } + + if (*start_index == -1) { + if (implied_clone_start != -1) { + *start_index = implied_clone_start; + } else if (implied_monitor_start != -1) { + *start_index = implied_monitor_start; + } + } +} + +// If resource history entry has shutdown lock, remember lock node and time +static void +unpack_shutdown_lock(const xmlNode *rsc_entry, pe_resource_t *rsc, + const pe_node_t *node, pe_working_set_t *data_set) +{ + time_t lock_time = 0; // When lock started (i.e. node shutdown time) + + if ((crm_element_value_epoch(rsc_entry, XML_CONFIG_ATTR_SHUTDOWN_LOCK, + &lock_time) == pcmk_ok) && (lock_time != 0)) { + + if ((data_set->shutdown_lock > 0) + && (get_effective_time(data_set) + > (lock_time + data_set->shutdown_lock))) { + pe_rsc_info(rsc, "Shutdown lock for %s on %s expired", + rsc->id, pe__node_name(node)); + pe__clear_resource_history(rsc, node, data_set); + } else { + /* @COMPAT I don't like breaking const signatures, but + * rsc->lock_node should really be const -- we just can't change it + * until the next API compatibility break. + */ + rsc->lock_node = (pe_node_t *) node; + rsc->lock_time = lock_time; + } + } +} + +/*! + * \internal + * \brief Unpack one lrm_resource entry from a node's CIB status + * + * \param[in,out] node Node whose status is being unpacked + * \param[in] rsc_entry lrm_resource XML being unpacked + * \param[in,out] data_set Cluster working set + * + * \return Resource corresponding to the entry, or NULL if no operation history + */ +static pe_resource_t * +unpack_lrm_resource(pe_node_t *node, const xmlNode *lrm_resource, + pe_working_set_t *data_set) +{ + GList *gIter = NULL; + int stop_index = -1; + int start_index = -1; + enum rsc_role_e req_role = RSC_ROLE_UNKNOWN; + + const char *rsc_id = ID(lrm_resource); + + pe_resource_t *rsc = NULL; + GList *op_list = NULL; + GList *sorted_op_list = NULL; + + xmlNode *rsc_op = NULL; + xmlNode *last_failure = NULL; + + enum action_fail_response on_fail = action_fail_ignore; + enum rsc_role_e saved_role = RSC_ROLE_UNKNOWN; + + if (rsc_id == NULL) { + crm_warn("Ignoring malformed " XML_LRM_TAG_RESOURCE + " entry without id"); + return NULL; + } + crm_trace("Unpacking " XML_LRM_TAG_RESOURCE " for %s on %s", + rsc_id, pe__node_name(node)); + + // Build a list of individual lrm_rsc_op entries, so we can sort them + for (rsc_op = first_named_child(lrm_resource, XML_LRM_TAG_RSC_OP); + rsc_op != NULL; rsc_op = crm_next_same_xml(rsc_op)) { + + op_list = g_list_prepend(op_list, rsc_op); + } + + if (!pcmk_is_set(data_set->flags, pe_flag_shutdown_lock)) { + if (op_list == NULL) { + // If there are no operations, there is nothing to do + return NULL; + } + } + + /* find the resource */ + rsc = unpack_find_resource(data_set, node, rsc_id); + if (rsc == NULL) { + if (op_list == NULL) { + // If there are no operations, there is nothing to do + return NULL; + } else { + rsc = process_orphan_resource(lrm_resource, node, data_set); + } + } + CRM_ASSERT(rsc != NULL); + + // Check whether the resource is "shutdown-locked" to this node + if (pcmk_is_set(data_set->flags, pe_flag_shutdown_lock)) { + unpack_shutdown_lock(lrm_resource, rsc, node, data_set); + } + + /* process operations */ + saved_role = rsc->role; + rsc->role = RSC_ROLE_UNKNOWN; + sorted_op_list = g_list_sort(op_list, sort_op_by_callid); + + for (gIter = sorted_op_list; gIter != NULL; gIter = gIter->next) { + xmlNode *rsc_op = (xmlNode *) gIter->data; + + unpack_rsc_op(rsc, node, rsc_op, &last_failure, &on_fail); + } + + /* create active recurring operations as optional */ + calculate_active_ops(sorted_op_list, &start_index, &stop_index); + process_recurring(node, rsc, start_index, stop_index, sorted_op_list, data_set); + + /* no need to free the contents */ + g_list_free(sorted_op_list); + + process_rsc_state(rsc, node, on_fail); + + if (get_target_role(rsc, &req_role)) { + if (rsc->next_role == RSC_ROLE_UNKNOWN || req_role < rsc->next_role) { + pe__set_next_role(rsc, req_role, XML_RSC_ATTR_TARGET_ROLE); + + } else if (req_role > rsc->next_role) { + pe_rsc_info(rsc, "%s: Not overwriting calculated next role %s" + " with requested next role %s", + rsc->id, role2text(rsc->next_role), role2text(req_role)); + } + } + + if (saved_role > rsc->role) { + rsc->role = saved_role; + } + + return rsc; +} + +static void +handle_orphaned_container_fillers(const xmlNode *lrm_rsc_list, + pe_working_set_t *data_set) +{ + for (const xmlNode *rsc_entry = pcmk__xe_first_child(lrm_rsc_list); + rsc_entry != NULL; rsc_entry = pcmk__xe_next(rsc_entry)) { + + pe_resource_t *rsc; + pe_resource_t *container; + const char *rsc_id; + const char *container_id; + + if (!pcmk__str_eq((const char *)rsc_entry->name, XML_LRM_TAG_RESOURCE, pcmk__str_casei)) { + continue; + } + + container_id = crm_element_value(rsc_entry, XML_RSC_ATTR_CONTAINER); + rsc_id = crm_element_value(rsc_entry, XML_ATTR_ID); + if (container_id == NULL || rsc_id == NULL) { + continue; + } + + container = pe_find_resource(data_set->resources, container_id); + if (container == NULL) { + continue; + } + + rsc = pe_find_resource(data_set->resources, rsc_id); + if (rsc == NULL || + !pcmk_is_set(rsc->flags, pe_rsc_orphan_container_filler) || + rsc->container != NULL) { + continue; + } + + pe_rsc_trace(rsc, "Mapped container of orphaned resource %s to %s", + rsc->id, container_id); + rsc->container = container; + container->fillers = g_list_append(container->fillers, rsc); + } +} + +/*! + * \internal + * \brief Unpack one node's lrm status section + * + * \param[in,out] node Node whose status is being unpacked + * \param[in] xml CIB node state XML + * \param[in,out] data_set Cluster working set + */ +static void +unpack_node_lrm(pe_node_t *node, const xmlNode *xml, pe_working_set_t *data_set) +{ + bool found_orphaned_container_filler = false; + + // Drill down to lrm_resources section + xml = find_xml_node(xml, XML_CIB_TAG_LRM, FALSE); + if (xml == NULL) { + return; + } + xml = find_xml_node(xml, XML_LRM_TAG_RESOURCES, FALSE); + if (xml == NULL) { + return; + } + + // Unpack each lrm_resource entry + for (const xmlNode *rsc_entry = first_named_child(xml, XML_LRM_TAG_RESOURCE); + rsc_entry != NULL; rsc_entry = crm_next_same_xml(rsc_entry)) { + + pe_resource_t *rsc = unpack_lrm_resource(node, rsc_entry, data_set); + + if ((rsc != NULL) + && pcmk_is_set(rsc->flags, pe_rsc_orphan_container_filler)) { + found_orphaned_container_filler = true; + } + } + + /* Now that all resource state has been unpacked for this node, map any + * orphaned container fillers to their container resource. + */ + if (found_orphaned_container_filler) { + handle_orphaned_container_fillers(xml, data_set); + } +} + +static void +set_active(pe_resource_t * rsc) +{ + const pe_resource_t *top = pe__const_top_resource(rsc, false); + + if (top && pcmk_is_set(top->flags, pe_rsc_promotable)) { + rsc->role = RSC_ROLE_UNPROMOTED; + } else { + rsc->role = RSC_ROLE_STARTED; + } +} + +static void +set_node_score(gpointer key, gpointer value, gpointer user_data) +{ + pe_node_t *node = value; + int *score = user_data; + + node->weight = *score; +} + +#define XPATH_NODE_STATE "/" XML_TAG_CIB "/" XML_CIB_TAG_STATUS \ + "/" XML_CIB_TAG_STATE +#define SUB_XPATH_LRM_RESOURCE "/" XML_CIB_TAG_LRM \ + "/" XML_LRM_TAG_RESOURCES \ + "/" XML_LRM_TAG_RESOURCE +#define SUB_XPATH_LRM_RSC_OP "/" XML_LRM_TAG_RSC_OP + +static xmlNode * +find_lrm_op(const char *resource, const char *op, const char *node, const char *source, + int target_rc, pe_working_set_t *data_set) +{ + GString *xpath = NULL; + xmlNode *xml = NULL; + + CRM_CHECK((resource != NULL) && (op != NULL) && (node != NULL), + return NULL); + + xpath = g_string_sized_new(256); + pcmk__g_strcat(xpath, + XPATH_NODE_STATE "[@" XML_ATTR_UNAME "='", node, "']" + SUB_XPATH_LRM_RESOURCE "[@" XML_ATTR_ID "='", resource, "']" + SUB_XPATH_LRM_RSC_OP "[@" XML_LRM_ATTR_TASK "='", op, "'", + NULL); + + /* Need to check against transition_magic too? */ + if ((source != NULL) && (strcmp(op, CRMD_ACTION_MIGRATE) == 0)) { + pcmk__g_strcat(xpath, + " and @" XML_LRM_ATTR_MIGRATE_TARGET "='", source, "']", + NULL); + + } else if ((source != NULL) && (strcmp(op, CRMD_ACTION_MIGRATED) == 0)) { + pcmk__g_strcat(xpath, + " and @" XML_LRM_ATTR_MIGRATE_SOURCE "='", source, "']", + NULL); + } else { + g_string_append_c(xpath, ']'); + } + + xml = get_xpath_object((const char *) xpath->str, data_set->input, + LOG_DEBUG); + g_string_free(xpath, TRUE); + + if (xml && target_rc >= 0) { + int rc = PCMK_OCF_UNKNOWN_ERROR; + int status = PCMK_EXEC_ERROR; + + crm_element_value_int(xml, XML_LRM_ATTR_RC, &rc); + crm_element_value_int(xml, XML_LRM_ATTR_OPSTATUS, &status); + if ((rc != target_rc) || (status != PCMK_EXEC_DONE)) { + return NULL; + } + } + return xml; +} + +static xmlNode * +find_lrm_resource(const char *rsc_id, const char *node_name, + pe_working_set_t *data_set) +{ + GString *xpath = NULL; + xmlNode *xml = NULL; + + CRM_CHECK((rsc_id != NULL) && (node_name != NULL), return NULL); + + xpath = g_string_sized_new(256); + pcmk__g_strcat(xpath, + XPATH_NODE_STATE "[@" XML_ATTR_UNAME "='", node_name, "']" + SUB_XPATH_LRM_RESOURCE "[@" XML_ATTR_ID "='", rsc_id, "']", + NULL); + + xml = get_xpath_object((const char *) xpath->str, data_set->input, + LOG_DEBUG); + + g_string_free(xpath, TRUE); + return xml; +} + +/*! + * \internal + * \brief Check whether a resource has no completed action history on a node + * + * \param[in,out] rsc Resource to check + * \param[in] node_name Node to check + * + * \return true if \p rsc_id is unknown on \p node_name, otherwise false + */ +static bool +unknown_on_node(pe_resource_t *rsc, const char *node_name) +{ + bool result = false; + xmlXPathObjectPtr search; + GString *xpath = g_string_sized_new(256); + + pcmk__g_strcat(xpath, + XPATH_NODE_STATE "[@" XML_ATTR_UNAME "='", node_name, "']" + SUB_XPATH_LRM_RESOURCE "[@" XML_ATTR_ID "='", rsc->id, "']" + SUB_XPATH_LRM_RSC_OP "[@" XML_LRM_ATTR_RC "!='193']", + NULL); + search = xpath_search(rsc->cluster->input, (const char *) xpath->str); + result = (numXpathResults(search) == 0); + freeXpathObject(search); + g_string_free(xpath, TRUE); + return result; +} + +/*! + * \brief Check whether a probe/monitor indicating the resource was not running + * on a node happened after some event + * + * \param[in] rsc_id Resource being checked + * \param[in] node_name Node being checked + * \param[in] xml_op Event that monitor is being compared to + * \param[in] same_node Whether the operations are on the same node + * \param[in,out] data_set Cluster working set + * + * \return true if such a monitor happened after event, false otherwise + */ +static bool +monitor_not_running_after(const char *rsc_id, const char *node_name, + const xmlNode *xml_op, bool same_node, + pe_working_set_t *data_set) +{ + /* Any probe/monitor operation on the node indicating it was not running + * there + */ + xmlNode *monitor = find_lrm_op(rsc_id, CRMD_ACTION_STATUS, node_name, + NULL, PCMK_OCF_NOT_RUNNING, data_set); + + return (monitor && pe__is_newer_op(monitor, xml_op, same_node) > 0); +} + +/*! + * \brief Check whether any non-monitor operation on a node happened after some + * event + * + * \param[in] rsc_id Resource being checked + * \param[in] node_name Node being checked + * \param[in] xml_op Event that non-monitor is being compared to + * \param[in] same_node Whether the operations are on the same node + * \param[in,out] data_set Cluster working set + * + * \return true if such a operation happened after event, false otherwise + */ +static bool +non_monitor_after(const char *rsc_id, const char *node_name, + const xmlNode *xml_op, bool same_node, + pe_working_set_t *data_set) +{ + xmlNode *lrm_resource = NULL; + + lrm_resource = find_lrm_resource(rsc_id, node_name, data_set); + if (lrm_resource == NULL) { + return false; + } + + for (xmlNode *op = first_named_child(lrm_resource, XML_LRM_TAG_RSC_OP); + op != NULL; op = crm_next_same_xml(op)) { + const char * task = NULL; + + if (op == xml_op) { + continue; + } + + task = crm_element_value(op, XML_LRM_ATTR_TASK); + + if (pcmk__str_any_of(task, CRMD_ACTION_START, CRMD_ACTION_STOP, + CRMD_ACTION_MIGRATE, CRMD_ACTION_MIGRATED, NULL) + && pe__is_newer_op(op, xml_op, same_node) > 0) { + return true; + } + } + + return false; +} + +/*! + * \brief Check whether the resource has newer state on a node after a migration + * attempt + * + * \param[in] rsc_id Resource being checked + * \param[in] node_name Node being checked + * \param[in] migrate_to Any migrate_to event that is being compared to + * \param[in] migrate_from Any migrate_from event that is being compared to + * \param[in,out] data_set Cluster working set + * + * \return true if such a operation happened after event, false otherwise + */ +static bool +newer_state_after_migrate(const char *rsc_id, const char *node_name, + const xmlNode *migrate_to, + const xmlNode *migrate_from, + pe_working_set_t *data_set) +{ + const xmlNode *xml_op = migrate_to; + const char *source = NULL; + const char *target = NULL; + bool same_node = false; + + if (migrate_from) { + xml_op = migrate_from; + } + + source = crm_element_value(xml_op, XML_LRM_ATTR_MIGRATE_SOURCE); + target = crm_element_value(xml_op, XML_LRM_ATTR_MIGRATE_TARGET); + + /* It's preferred to compare to the migrate event on the same node if + * existing, since call ids are more reliable. + */ + if (pcmk__str_eq(node_name, target, pcmk__str_casei)) { + if (migrate_from) { + xml_op = migrate_from; + same_node = true; + + } else { + xml_op = migrate_to; + } + + } else if (pcmk__str_eq(node_name, source, pcmk__str_casei)) { + if (migrate_to) { + xml_op = migrate_to; + same_node = true; + + } else { + xml_op = migrate_from; + } + } + + /* If there's any newer non-monitor operation on the node, or any newer + * probe/monitor operation on the node indicating it was not running there, + * the migration events potentially no longer matter for the node. + */ + return non_monitor_after(rsc_id, node_name, xml_op, same_node, data_set) + || monitor_not_running_after(rsc_id, node_name, xml_op, same_node, + data_set); +} + +/*! + * \internal + * \brief Parse migration source and target node names from history entry + * + * \param[in] entry Resource history entry for a migration action + * \param[in] source_node If not NULL, source must match this node + * \param[in] target_node If not NULL, target must match this node + * \param[out] source_name Where to store migration source node name + * \param[out] target_name Where to store migration target node name + * + * \return Standard Pacemaker return code + */ +static int +get_migration_node_names(const xmlNode *entry, const pe_node_t *source_node, + const pe_node_t *target_node, + const char **source_name, const char **target_name) +{ + *source_name = crm_element_value(entry, XML_LRM_ATTR_MIGRATE_SOURCE); + *target_name = crm_element_value(entry, XML_LRM_ATTR_MIGRATE_TARGET); + if ((*source_name == NULL) || (*target_name == NULL)) { + crm_err("Ignoring resource history entry %s without " + XML_LRM_ATTR_MIGRATE_SOURCE " and " XML_LRM_ATTR_MIGRATE_TARGET, + ID(entry)); + return pcmk_rc_unpack_error; + } + + if ((source_node != NULL) + && !pcmk__str_eq(*source_name, source_node->details->uname, + pcmk__str_casei|pcmk__str_null_matches)) { + crm_err("Ignoring resource history entry %s because " + XML_LRM_ATTR_MIGRATE_SOURCE "='%s' does not match %s", + ID(entry), *source_name, pe__node_name(source_node)); + return pcmk_rc_unpack_error; + } + + if ((target_node != NULL) + && !pcmk__str_eq(*target_name, target_node->details->uname, + pcmk__str_casei|pcmk__str_null_matches)) { + crm_err("Ignoring resource history entry %s because " + XML_LRM_ATTR_MIGRATE_TARGET "='%s' does not match %s", + ID(entry), *target_name, pe__node_name(target_node)); + return pcmk_rc_unpack_error; + } + + return pcmk_rc_ok; +} + +/* + * \internal + * \brief Add a migration source to a resource's list of dangling migrations + * + * If the migrate_to and migrate_from actions in a live migration both + * succeeded, but there is no stop on the source, the migration is considered + * "dangling." Add the source to the resource's dangling migration list, which + * will be used to schedule a stop on the source without affecting the target. + * + * \param[in,out] rsc Resource involved in migration + * \param[in] node Migration source + */ +static void +add_dangling_migration(pe_resource_t *rsc, const pe_node_t *node) +{ + pe_rsc_trace(rsc, "Dangling migration of %s requires stop on %s", + rsc->id, pe__node_name(node)); + rsc->role = RSC_ROLE_STOPPED; + rsc->dangling_migrations = g_list_prepend(rsc->dangling_migrations, + (gpointer) node); +} + +/*! + * \internal + * \brief Update resource role etc. after a successful migrate_to action + * + * \param[in,out] history Parsed action result history + */ +static void +unpack_migrate_to_success(struct action_history *history) +{ + /* A complete migration sequence is: + * 1. migrate_to on source node (which succeeded if we get to this function) + * 2. migrate_from on target node + * 3. stop on source node + * + * If no migrate_from has happened, the migration is considered to be + * "partial". If the migrate_from succeeded but no stop has happened, the + * migration is considered to be "dangling". + * + * If a successful migrate_to and stop have happened on the source node, we + * still need to check for a partial migration, due to scenarios (easier to + * produce with batch-limit=1) like: + * + * - A resource is migrating from node1 to node2, and a migrate_to is + * initiated for it on node1. + * + * - node2 goes into standby mode while the migrate_to is pending, which + * aborts the transition. + * + * - Upon completion of the migrate_to, a new transition schedules a stop + * on both nodes and a start on node1. + * + * - If the new transition is aborted for any reason while the resource is + * stopping on node1, the transition after that stop completes will see + * the migrate_to and stop on the source, but it's still a partial + * migration, and the resource must be stopped on node2 because it is + * potentially active there due to the migrate_to. + * + * We also need to take into account that either node's history may be + * cleared at any point in the migration process. + */ + int from_rc = PCMK_OCF_OK; + int from_status = PCMK_EXEC_PENDING; + pe_node_t *target_node = NULL; + xmlNode *migrate_from = NULL; + const char *source = NULL; + const char *target = NULL; + bool source_newer_op = false; + bool target_newer_state = false; + bool active_on_target = false; + + // Get source and target node names from XML + if (get_migration_node_names(history->xml, history->node, NULL, &source, + &target) != pcmk_rc_ok) { + return; + } + + // Check for newer state on the source + source_newer_op = non_monitor_after(history->rsc->id, source, history->xml, + true, history->rsc->cluster); + + // Check for a migrate_from action from this source on the target + migrate_from = find_lrm_op(history->rsc->id, CRMD_ACTION_MIGRATED, target, + source, -1, history->rsc->cluster); + if (migrate_from != NULL) { + if (source_newer_op) { + /* There's a newer non-monitor operation on the source and a + * migrate_from on the target, so this migrate_to is irrelevant to + * the resource's state. + */ + return; + } + crm_element_value_int(migrate_from, XML_LRM_ATTR_RC, &from_rc); + crm_element_value_int(migrate_from, XML_LRM_ATTR_OPSTATUS, + &from_status); + } + + /* If the resource has newer state on both the source and target after the + * migration events, this migrate_to is irrelevant to the resource's state. + */ + target_newer_state = newer_state_after_migrate(history->rsc->id, target, + history->xml, migrate_from, + history->rsc->cluster); + if (source_newer_op && target_newer_state) { + return; + } + + /* Check for dangling migration (migrate_from succeeded but stop not done). + * We know there's no stop because we already returned if the target has a + * migrate_from and the source has any newer non-monitor operation. + */ + if ((from_rc == PCMK_OCF_OK) && (from_status == PCMK_EXEC_DONE)) { + add_dangling_migration(history->rsc, history->node); + return; + } + + /* Without newer state, this migrate_to implies the resource is active. + * (Clones are not allowed to migrate, so role can't be promoted.) + */ + history->rsc->role = RSC_ROLE_STARTED; + + target_node = pe_find_node(history->rsc->cluster->nodes, target); + active_on_target = !target_newer_state && (target_node != NULL) + && target_node->details->online; + + if (from_status != PCMK_EXEC_PENDING) { // migrate_from failed on target + if (active_on_target) { + native_add_running(history->rsc, target_node, history->rsc->cluster, + TRUE); + } else { + // Mark resource as failed, require recovery, and prevent migration + pe__set_resource_flags(history->rsc, pe_rsc_failed|pe_rsc_stop); + pe__clear_resource_flags(history->rsc, pe_rsc_allow_migrate); + } + return; + } + + // The migrate_from is pending, complete but erased, or to be scheduled + + /* If there is no history at all for the resource on an online target, then + * it was likely cleaned. Just return, and we'll schedule a probe. Once we + * have the probe result, it will be reflected in target_newer_state. + */ + if ((target_node != NULL) && target_node->details->online + && unknown_on_node(history->rsc, target)) { + return; + } + + if (active_on_target) { + pe_node_t *source_node = pe_find_node(history->rsc->cluster->nodes, + source); + + native_add_running(history->rsc, target_node, history->rsc->cluster, + FALSE); + if ((source_node != NULL) && source_node->details->online) { + /* This is a partial migration: the migrate_to completed + * successfully on the source, but the migrate_from has not + * completed. Remember the source and target; if the newly + * chosen target remains the same when we schedule actions + * later, we may continue with the migration. + */ + history->rsc->partial_migration_target = target_node; + history->rsc->partial_migration_source = source_node; + } + + } else if (!source_newer_op) { + // Mark resource as failed, require recovery, and prevent migration + pe__set_resource_flags(history->rsc, pe_rsc_failed|pe_rsc_stop); + pe__clear_resource_flags(history->rsc, pe_rsc_allow_migrate); + } +} + +/*! + * \internal + * \brief Update resource role etc. after a failed migrate_to action + * + * \param[in,out] history Parsed action result history + */ +static void +unpack_migrate_to_failure(struct action_history *history) +{ + xmlNode *target_migrate_from = NULL; + const char *source = NULL; + const char *target = NULL; + + // Get source and target node names from XML + if (get_migration_node_names(history->xml, history->node, NULL, &source, + &target) != pcmk_rc_ok) { + return; + } + + /* If a migration failed, we have to assume the resource is active. Clones + * are not allowed to migrate, so role can't be promoted. + */ + history->rsc->role = RSC_ROLE_STARTED; + + // Check for migrate_from on the target + target_migrate_from = find_lrm_op(history->rsc->id, CRMD_ACTION_MIGRATED, + target, source, PCMK_OCF_OK, + history->rsc->cluster); + + if (/* If the resource state is unknown on the target, it will likely be + * probed there. + * Don't just consider it running there. We will get back here anyway in + * case the probe detects it's running there. + */ + !unknown_on_node(history->rsc, target) + /* If the resource has newer state on the target after the migration + * events, this migrate_to no longer matters for the target. + */ + && !newer_state_after_migrate(history->rsc->id, target, history->xml, + target_migrate_from, + history->rsc->cluster)) { + /* The resource has no newer state on the target, so assume it's still + * active there. + * (if it is up). + */ + pe_node_t *target_node = pe_find_node(history->rsc->cluster->nodes, + target); + + if (target_node && target_node->details->online) { + native_add_running(history->rsc, target_node, history->rsc->cluster, + FALSE); + } + + } else if (!non_monitor_after(history->rsc->id, source, history->xml, true, + history->rsc->cluster)) { + /* We know the resource has newer state on the target, but this + * migrate_to still matters for the source as long as there's no newer + * non-monitor operation there. + */ + + // Mark node as having dangling migration so we can force a stop later + history->rsc->dangling_migrations = + g_list_prepend(history->rsc->dangling_migrations, + (gpointer) history->node); + } +} + +/*! + * \internal + * \brief Update resource role etc. after a failed migrate_from action + * + * \param[in,out] history Parsed action result history + */ +static void +unpack_migrate_from_failure(struct action_history *history) +{ + xmlNode *source_migrate_to = NULL; + const char *source = NULL; + const char *target = NULL; + + // Get source and target node names from XML + if (get_migration_node_names(history->xml, NULL, history->node, &source, + &target) != pcmk_rc_ok) { + return; + } + + /* If a migration failed, we have to assume the resource is active. Clones + * are not allowed to migrate, so role can't be promoted. + */ + history->rsc->role = RSC_ROLE_STARTED; + + // Check for a migrate_to on the source + source_migrate_to = find_lrm_op(history->rsc->id, CRMD_ACTION_MIGRATE, + source, target, PCMK_OCF_OK, + history->rsc->cluster); + + if (/* If the resource state is unknown on the source, it will likely be + * probed there. + * Don't just consider it running there. We will get back here anyway in + * case the probe detects it's running there. + */ + !unknown_on_node(history->rsc, source) + /* If the resource has newer state on the source after the migration + * events, this migrate_from no longer matters for the source. + */ + && !newer_state_after_migrate(history->rsc->id, source, + source_migrate_to, history->xml, + history->rsc->cluster)) { + /* The resource has no newer state on the source, so assume it's still + * active there (if it is up). + */ + pe_node_t *source_node = pe_find_node(history->rsc->cluster->nodes, + source); + + if (source_node && source_node->details->online) { + native_add_running(history->rsc, source_node, history->rsc->cluster, + TRUE); + } + } +} + +/*! + * \internal + * \brief Add an action to cluster's list of failed actions + * + * \param[in,out] history Parsed action result history + */ +static void +record_failed_op(struct action_history *history) +{ + if (!(history->node->details->online)) { + return; + } + + for (const xmlNode *xIter = history->rsc->cluster->failed->children; + xIter != NULL; xIter = xIter->next) { + + const char *key = pe__xe_history_key(xIter); + const char *uname = crm_element_value(xIter, XML_ATTR_UNAME); + + if (pcmk__str_eq(history->key, key, pcmk__str_none) + && pcmk__str_eq(uname, history->node->details->uname, + pcmk__str_casei)) { + crm_trace("Skipping duplicate entry %s on %s", + history->key, pe__node_name(history->node)); + return; + } + } + + crm_trace("Adding entry for %s on %s to failed action list", + history->key, pe__node_name(history->node)); + crm_xml_add(history->xml, XML_ATTR_UNAME, history->node->details->uname); + crm_xml_add(history->xml, XML_LRM_ATTR_RSCID, history->rsc->id); + add_node_copy(history->rsc->cluster->failed, history->xml); +} + +static char * +last_change_str(const xmlNode *xml_op) +{ + time_t when; + char *result = NULL; + + if (crm_element_value_epoch(xml_op, XML_RSC_OP_LAST_CHANGE, + &when) == pcmk_ok) { + char *when_s = pcmk__epoch2str(&when, 0); + const char *p = strchr(when_s, ' '); + + // Skip day of week to make message shorter + if ((p != NULL) && (*(++p) != '\0')) { + result = strdup(p); + CRM_ASSERT(result != NULL); + } + free(when_s); + } + + if (result == NULL) { + result = strdup("unknown time"); + CRM_ASSERT(result != NULL); + } + + return result; +} + +/*! + * \internal + * \brief Compare two on-fail values + * + * \param[in] first One on-fail value to compare + * \param[in] second The other on-fail value to compare + * + * \return A negative number if second is more severe than first, zero if they + * are equal, or a positive number if first is more severe than second. + * \note This is only needed until the action_fail_response values can be + * renumbered at the next API compatibility break. + */ +static int +cmp_on_fail(enum action_fail_response first, enum action_fail_response second) +{ + switch (first) { + case action_fail_demote: + switch (second) { + case action_fail_ignore: + return 1; + case action_fail_demote: + return 0; + default: + return -1; + } + break; + + case action_fail_reset_remote: + switch (second) { + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + return 1; + case action_fail_reset_remote: + return 0; + default: + return -1; + } + break; + + case action_fail_restart_container: + switch (second) { + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + case action_fail_reset_remote: + return 1; + case action_fail_restart_container: + return 0; + default: + return -1; + } + break; + + default: + break; + } + switch (second) { + case action_fail_demote: + return (first == action_fail_ignore)? -1 : 1; + + case action_fail_reset_remote: + switch (first) { + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + return -1; + default: + return 1; + } + break; + + case action_fail_restart_container: + switch (first) { + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + case action_fail_reset_remote: + return -1; + default: + return 1; + } + break; + + default: + break; + } + return first - second; +} + +/*! + * \internal + * \brief Ban a resource (or its clone if an anonymous instance) from all nodes + * + * \param[in,out] rsc Resource to ban + */ +static void +ban_from_all_nodes(pe_resource_t *rsc) +{ + int score = -INFINITY; + pe_resource_t *fail_rsc = rsc; + + if (fail_rsc->parent != NULL) { + pe_resource_t *parent = uber_parent(fail_rsc); + + if (pe_rsc_is_anon_clone(parent)) { + /* For anonymous clones, if an operation with on-fail=stop fails for + * any instance, the entire clone must stop. + */ + fail_rsc = parent; + } + } + + // Ban the resource from all nodes + crm_notice("%s will not be started under current conditions", fail_rsc->id); + if (fail_rsc->allowed_nodes != NULL) { + g_hash_table_destroy(fail_rsc->allowed_nodes); + } + fail_rsc->allowed_nodes = pe__node_list2table(rsc->cluster->nodes); + g_hash_table_foreach(fail_rsc->allowed_nodes, set_node_score, &score); +} + +/*! + * \internal + * \brief Update resource role, failure handling, etc., after a failed action + * + * \param[in,out] history Parsed action result history + * \param[out] last_failure Set this to action XML + * \param[in,out] on_fail What should be done about the result + */ +static void +unpack_rsc_op_failure(struct action_history *history, xmlNode **last_failure, + enum action_fail_response *on_fail) +{ + bool is_probe = false; + pe_action_t *action = NULL; + char *last_change_s = NULL; + + *last_failure = history->xml; + + is_probe = pcmk_xe_is_probe(history->xml); + last_change_s = last_change_str(history->xml); + + if (!pcmk_is_set(history->rsc->cluster->flags, pe_flag_symmetric_cluster) + && (history->exit_status == PCMK_OCF_NOT_INSTALLED)) { + crm_trace("Unexpected result (%s%s%s) was recorded for " + "%s of %s on %s at %s " CRM_XS " exit-status=%d id=%s", + services_ocf_exitcode_str(history->exit_status), + (pcmk__str_empty(history->exit_reason)? "" : ": "), + pcmk__s(history->exit_reason, ""), + (is_probe? "probe" : history->task), history->rsc->id, + pe__node_name(history->node), last_change_s, + history->exit_status, history->id); + } else { + crm_warn("Unexpected result (%s%s%s) was recorded for " + "%s of %s on %s at %s " CRM_XS " exit-status=%d id=%s", + services_ocf_exitcode_str(history->exit_status), + (pcmk__str_empty(history->exit_reason)? "" : ": "), + pcmk__s(history->exit_reason, ""), + (is_probe? "probe" : history->task), history->rsc->id, + pe__node_name(history->node), last_change_s, + history->exit_status, history->id); + + if (is_probe && (history->exit_status != PCMK_OCF_OK) + && (history->exit_status != PCMK_OCF_NOT_RUNNING) + && (history->exit_status != PCMK_OCF_RUNNING_PROMOTED)) { + + /* A failed (not just unexpected) probe result could mean the user + * didn't know resources will be probed even where they can't run. + */ + crm_notice("If it is not possible for %s to run on %s, see " + "the resource-discovery option for location constraints", + history->rsc->id, pe__node_name(history->node)); + } + + record_failed_op(history); + } + + free(last_change_s); + + action = custom_action(history->rsc, strdup(history->key), history->task, + NULL, TRUE, FALSE, history->rsc->cluster); + if (cmp_on_fail(*on_fail, action->on_fail) < 0) { + pe_rsc_trace(history->rsc, "on-fail %s -> %s for %s (%s)", + fail2text(*on_fail), fail2text(action->on_fail), + action->uuid, history->key); + *on_fail = action->on_fail; + } + + if (strcmp(history->task, CRMD_ACTION_STOP) == 0) { + resource_location(history->rsc, history->node, -INFINITY, + "__stop_fail__", history->rsc->cluster); + + } else if (strcmp(history->task, CRMD_ACTION_MIGRATE) == 0) { + unpack_migrate_to_failure(history); + + } else if (strcmp(history->task, CRMD_ACTION_MIGRATED) == 0) { + unpack_migrate_from_failure(history); + + } else if (strcmp(history->task, CRMD_ACTION_PROMOTE) == 0) { + history->rsc->role = RSC_ROLE_PROMOTED; + + } else if (strcmp(history->task, CRMD_ACTION_DEMOTE) == 0) { + if (action->on_fail == action_fail_block) { + history->rsc->role = RSC_ROLE_PROMOTED; + pe__set_next_role(history->rsc, RSC_ROLE_STOPPED, + "demote with on-fail=block"); + + } else if (history->exit_status == PCMK_OCF_NOT_RUNNING) { + history->rsc->role = RSC_ROLE_STOPPED; + + } else { + /* Staying in the promoted role would put the scheduler and + * controller into a loop. Setting the role to unpromoted is not + * dangerous because the resource will be stopped as part of + * recovery, and any promotion will be ordered after that stop. + */ + history->rsc->role = RSC_ROLE_UNPROMOTED; + } + } + + if (is_probe && (history->exit_status == PCMK_OCF_NOT_INSTALLED)) { + /* leave stopped */ + pe_rsc_trace(history->rsc, "Leaving %s stopped", history->rsc->id); + history->rsc->role = RSC_ROLE_STOPPED; + + } else if (history->rsc->role < RSC_ROLE_STARTED) { + pe_rsc_trace(history->rsc, "Setting %s active", history->rsc->id); + set_active(history->rsc); + } + + pe_rsc_trace(history->rsc, + "Resource %s: role=%s, unclean=%s, on_fail=%s, fail_role=%s", + history->rsc->id, role2text(history->rsc->role), + pcmk__btoa(history->node->details->unclean), + fail2text(action->on_fail), role2text(action->fail_role)); + + if ((action->fail_role != RSC_ROLE_STARTED) + && (history->rsc->next_role < action->fail_role)) { + pe__set_next_role(history->rsc, action->fail_role, "failure"); + } + + if (action->fail_role == RSC_ROLE_STOPPED) { + ban_from_all_nodes(history->rsc); + } + + pe_free_action(action); +} + +/*! + * \internal + * \brief Block a resource with a failed action if it cannot be recovered + * + * If resource action is a failed stop and fencing is not possible, mark the + * resource as unmanaged and blocked, since recovery cannot be done. + * + * \param[in,out] history Parsed action history entry + */ +static void +block_if_unrecoverable(struct action_history *history) +{ + char *last_change_s = NULL; + + if (strcmp(history->task, CRMD_ACTION_STOP) != 0) { + return; // All actions besides stop are always recoverable + } + if (pe_can_fence(history->node->details->data_set, history->node)) { + return; // Failed stops are recoverable via fencing + } + + last_change_s = last_change_str(history->xml); + pe_proc_err("No further recovery can be attempted for %s " + "because %s on %s failed (%s%s%s) at %s " + CRM_XS " rc=%d id=%s", + history->rsc->id, history->task, pe__node_name(history->node), + services_ocf_exitcode_str(history->exit_status), + (pcmk__str_empty(history->exit_reason)? "" : ": "), + pcmk__s(history->exit_reason, ""), + last_change_s, history->exit_status, history->id); + + free(last_change_s); + + pe__clear_resource_flags(history->rsc, pe_rsc_managed); + pe__set_resource_flags(history->rsc, pe_rsc_block); +} + +/*! + * \internal + * \brief Update action history's execution status and why + * + * \param[in,out] history Parsed action history entry + * \param[out] why Where to store reason for update + * \param[in] value New value + * \param[in] reason Description of why value was changed + */ +static inline void +remap_because(struct action_history *history, const char **why, int value, + const char *reason) +{ + if (history->execution_status != value) { + history->execution_status = value; + *why = reason; + } +} + +/*! + * \internal + * \brief Remap informational monitor results and operation status + * + * For the monitor results, certain OCF codes are for providing extended information + * to the user about services that aren't yet failed but not entirely healthy either. + * These must be treated as the "normal" result by Pacemaker. + * + * For operation status, the action result can be used to determine an appropriate + * status for the purposes of responding to the action. The status provided by the + * executor is not directly usable since the executor does not know what was expected. + * + * \param[in,out] history Parsed action history entry + * \param[in,out] on_fail What should be done about the result + * \param[in] expired Whether result is expired + * + * \note If the result is remapped and the node is not shutting down or failed, + * the operation will be recorded in the data set's list of failed operations + * to highlight it for the user. + * + * \note This may update the resource's current and next role. + */ +static void +remap_operation(struct action_history *history, + enum action_fail_response *on_fail, bool expired) +{ + bool is_probe = false; + int orig_exit_status = history->exit_status; + int orig_exec_status = history->execution_status; + const char *why = NULL; + const char *task = history->task; + + // Remap degraded results to their successful counterparts + history->exit_status = pcmk__effective_rc(history->exit_status); + if (history->exit_status != orig_exit_status) { + why = "degraded result"; + if (!expired && (!history->node->details->shutdown + || history->node->details->online)) { + record_failed_op(history); + } + } + + if (!pe_rsc_is_bundled(history->rsc) + && pcmk_xe_mask_probe_failure(history->xml) + && ((history->execution_status != PCMK_EXEC_DONE) + || (history->exit_status != PCMK_OCF_NOT_RUNNING))) { + history->execution_status = PCMK_EXEC_DONE; + history->exit_status = PCMK_OCF_NOT_RUNNING; + why = "equivalent probe result"; + } + + /* If the executor reported an execution status of anything but done or + * error, consider that final. But for done or error, we know better whether + * it should be treated as a failure or not, because we know the expected + * result. + */ + switch (history->execution_status) { + case PCMK_EXEC_DONE: + case PCMK_EXEC_ERROR: + break; + + // These should be treated as node-fatal + case PCMK_EXEC_NO_FENCE_DEVICE: + case PCMK_EXEC_NO_SECRETS: + remap_because(history, &why, PCMK_EXEC_ERROR_HARD, + "node-fatal error"); + goto remap_done; + + default: + goto remap_done; + } + + is_probe = pcmk_xe_is_probe(history->xml); + if (is_probe) { + task = "probe"; + } + + if (history->expected_exit_status < 0) { + /* Pre-1.0 Pacemaker versions, and Pacemaker 1.1.6 or earlier with + * Heartbeat 2.0.7 or earlier as the cluster layer, did not include the + * expected exit status in the transition key, which (along with the + * similar case of a corrupted transition key in the CIB) will be + * reported to this function as -1. Pacemaker 2.0+ does not support + * rolling upgrades from those versions or processing of saved CIB files + * from those versions, so we do not need to care much about this case. + */ + remap_because(history, &why, PCMK_EXEC_ERROR, + "obsolete history format"); + crm_warn("Expected result not found for %s on %s " + "(corrupt or obsolete CIB?)", + history->key, pe__node_name(history->node)); + + } else if (history->exit_status == history->expected_exit_status) { + remap_because(history, &why, PCMK_EXEC_DONE, "expected result"); + + } else { + remap_because(history, &why, PCMK_EXEC_ERROR, "unexpected result"); + pe_rsc_debug(history->rsc, + "%s on %s: expected %d (%s), got %d (%s%s%s)", + history->key, pe__node_name(history->node), + history->expected_exit_status, + services_ocf_exitcode_str(history->expected_exit_status), + history->exit_status, + services_ocf_exitcode_str(history->exit_status), + (pcmk__str_empty(history->exit_reason)? "" : ": "), + pcmk__s(history->exit_reason, "")); + } + + switch (history->exit_status) { + case PCMK_OCF_OK: + if (is_probe + && (history->expected_exit_status == PCMK_OCF_NOT_RUNNING)) { + char *last_change_s = last_change_str(history->xml); + + remap_because(history, &why, PCMK_EXEC_DONE, "probe"); + pe_rsc_info(history->rsc, "Probe found %s active on %s at %s", + history->rsc->id, pe__node_name(history->node), + last_change_s); + free(last_change_s); + } + break; + + case PCMK_OCF_NOT_RUNNING: + if (is_probe + || (history->expected_exit_status == history->exit_status) + || !pcmk_is_set(history->rsc->flags, pe_rsc_managed)) { + + /* For probes, recurring monitors for the Stopped role, and + * unmanaged resources, "not running" is not considered a + * failure. + */ + remap_because(history, &why, PCMK_EXEC_DONE, "exit status"); + history->rsc->role = RSC_ROLE_STOPPED; + *on_fail = action_fail_ignore; + pe__set_next_role(history->rsc, RSC_ROLE_UNKNOWN, + "not running"); + } + break; + + case PCMK_OCF_RUNNING_PROMOTED: + if (is_probe + && (history->exit_status != history->expected_exit_status)) { + char *last_change_s = last_change_str(history->xml); + + remap_because(history, &why, PCMK_EXEC_DONE, "probe"); + pe_rsc_info(history->rsc, + "Probe found %s active and promoted on %s at %s", + history->rsc->id, pe__node_name(history->node), + last_change_s); + free(last_change_s); + } + if (!expired + || (history->exit_status == history->expected_exit_status)) { + history->rsc->role = RSC_ROLE_PROMOTED; + } + break; + + case PCMK_OCF_FAILED_PROMOTED: + if (!expired) { + history->rsc->role = RSC_ROLE_PROMOTED; + } + remap_because(history, &why, PCMK_EXEC_ERROR, "exit status"); + break; + + case PCMK_OCF_NOT_CONFIGURED: + remap_because(history, &why, PCMK_EXEC_ERROR_FATAL, "exit status"); + break; + + case PCMK_OCF_UNIMPLEMENT_FEATURE: + { + guint interval_ms = 0; + crm_element_value_ms(history->xml, XML_LRM_ATTR_INTERVAL_MS, + &interval_ms); + + if (interval_ms == 0) { + if (!expired) { + block_if_unrecoverable(history); + } + remap_because(history, &why, PCMK_EXEC_ERROR_HARD, + "exit status"); + } else { + remap_because(history, &why, PCMK_EXEC_NOT_SUPPORTED, + "exit status"); + } + } + break; + + case PCMK_OCF_NOT_INSTALLED: + case PCMK_OCF_INVALID_PARAM: + case PCMK_OCF_INSUFFICIENT_PRIV: + if (!expired) { + block_if_unrecoverable(history); + } + remap_because(history, &why, PCMK_EXEC_ERROR_HARD, "exit status"); + break; + + default: + if (history->execution_status == PCMK_EXEC_DONE) { + char *last_change_s = last_change_str(history->xml); + + crm_info("Treating unknown exit status %d from %s of %s " + "on %s at %s as failure", + history->exit_status, task, history->rsc->id, + pe__node_name(history->node), last_change_s); + remap_because(history, &why, PCMK_EXEC_ERROR, + "unknown exit status"); + free(last_change_s); + } + break; + } + +remap_done: + if (why != NULL) { + pe_rsc_trace(history->rsc, + "Remapped %s result from [%s: %s] to [%s: %s] " + "because of %s", + history->key, pcmk_exec_status_str(orig_exec_status), + crm_exit_str(orig_exit_status), + pcmk_exec_status_str(history->execution_status), + crm_exit_str(history->exit_status), why); + } +} + +// return TRUE if start or monitor last failure but parameters changed +static bool +should_clear_for_param_change(const xmlNode *xml_op, const char *task, + pe_resource_t *rsc, pe_node_t *node) +{ + if (!strcmp(task, "start") || !strcmp(task, "monitor")) { + + if (pe__bundle_needs_remote_name(rsc)) { + /* We haven't allocated resources yet, so we can't reliably + * substitute addr parameters for the REMOTE_CONTAINER_HACK. + * When that's needed, defer the check until later. + */ + pe__add_param_check(xml_op, rsc, node, pe_check_last_failure, + rsc->cluster); + + } else { + op_digest_cache_t *digest_data = NULL; + + digest_data = rsc_action_digest_cmp(rsc, xml_op, node, + rsc->cluster); + switch (digest_data->rc) { + case RSC_DIGEST_UNKNOWN: + crm_trace("Resource %s history entry %s on %s" + " has no digest to compare", + rsc->id, pe__xe_history_key(xml_op), + node->details->id); + break; + case RSC_DIGEST_MATCH: + break; + default: + return TRUE; + } + } + } + return FALSE; +} + +// Order action after fencing of remote node, given connection rsc +static void +order_after_remote_fencing(pe_action_t *action, pe_resource_t *remote_conn, + pe_working_set_t *data_set) +{ + pe_node_t *remote_node = pe_find_node(data_set->nodes, remote_conn->id); + + if (remote_node) { + pe_action_t *fence = pe_fence_op(remote_node, NULL, TRUE, NULL, + FALSE, data_set); + + order_actions(fence, action, pe_order_implies_then); + } +} + +static bool +should_ignore_failure_timeout(const pe_resource_t *rsc, const char *task, + guint interval_ms, bool is_last_failure) +{ + /* Clearing failures of recurring monitors has special concerns. The + * executor reports only changes in the monitor result, so if the + * monitor is still active and still getting the same failure result, + * that will go undetected after the failure is cleared. + * + * Also, the operation history will have the time when the recurring + * monitor result changed to the given code, not the time when the + * result last happened. + * + * @TODO We probably should clear such failures only when the failure + * timeout has passed since the last occurrence of the failed result. + * However we don't record that information. We could maybe approximate + * that by clearing only if there is a more recent successful monitor or + * stop result, but we don't even have that information at this point + * since we are still unpacking the resource's operation history. + * + * This is especially important for remote connection resources with a + * reconnect interval, so in that case, we skip clearing failures + * if the remote node hasn't been fenced. + */ + if (rsc->remote_reconnect_ms + && pcmk_is_set(rsc->cluster->flags, pe_flag_stonith_enabled) + && (interval_ms != 0) && pcmk__str_eq(task, CRMD_ACTION_STATUS, pcmk__str_casei)) { + + pe_node_t *remote_node = pe_find_node(rsc->cluster->nodes, rsc->id); + + if (remote_node && !remote_node->details->remote_was_fenced) { + if (is_last_failure) { + crm_info("Waiting to clear monitor failure for remote node %s" + " until fencing has occurred", rsc->id); + } + return TRUE; + } + } + return FALSE; +} + +/*! + * \internal + * \brief Check operation age and schedule failure clearing when appropriate + * + * This function has two distinct purposes. The first is to check whether an + * operation history entry is expired (i.e. the resource has a failure timeout, + * the entry is older than the timeout, and the resource either has no fail + * count or its fail count is entirely older than the timeout). The second is to + * schedule fail count clearing when appropriate (i.e. the operation is expired + * and either the resource has an expired fail count or the operation is a + * last_failure for a remote connection resource with a reconnect interval, + * or the operation is a last_failure for a start or monitor operation and the + * resource's parameters have changed since the operation). + * + * \param[in,out] history Parsed action result history + * + * \return true if operation history entry is expired, otherwise false + */ +static bool +check_operation_expiry(struct action_history *history) +{ + bool expired = false; + bool is_last_failure = pcmk__ends_with(history->id, "_last_failure_0"); + time_t last_run = 0; + int unexpired_fail_count = 0; + const char *clear_reason = NULL; + + if (history->execution_status == PCMK_EXEC_NOT_INSTALLED) { + pe_rsc_trace(history->rsc, + "Resource history entry %s on %s is not expired: " + "Not Installed does not expire", + history->id, pe__node_name(history->node)); + return false; // "Not installed" must always be cleared manually + } + + if ((history->rsc->failure_timeout > 0) + && (crm_element_value_epoch(history->xml, XML_RSC_OP_LAST_CHANGE, + &last_run) == 0)) { + + // Resource has a failure-timeout, and history entry has a timestamp + + time_t now = get_effective_time(history->rsc->cluster); + time_t last_failure = 0; + + // Is this particular operation history older than the failure timeout? + if ((now >= (last_run + history->rsc->failure_timeout)) + && !should_ignore_failure_timeout(history->rsc, history->task, + history->interval_ms, + is_last_failure)) { + expired = true; + } + + // Does the resource as a whole have an unexpired fail count? + unexpired_fail_count = pe_get_failcount(history->node, history->rsc, + &last_failure, pe_fc_effective, + history->xml); + + // Update scheduler recheck time according to *last* failure + crm_trace("%s@%lld is %sexpired @%lld with unexpired_failures=%d timeout=%ds" + " last-failure@%lld", + history->id, (long long) last_run, (expired? "" : "not "), + (long long) now, unexpired_fail_count, + history->rsc->failure_timeout, (long long) last_failure); + last_failure += history->rsc->failure_timeout + 1; + if (unexpired_fail_count && (now < last_failure)) { + pe__update_recheck_time(last_failure, history->rsc->cluster); + } + } + + if (expired) { + if (pe_get_failcount(history->node, history->rsc, NULL, pe_fc_default, + history->xml)) { + // There is a fail count ignoring timeout + + if (unexpired_fail_count == 0) { + // There is no fail count considering timeout + clear_reason = "it expired"; + + } else { + /* This operation is old, but there is an unexpired fail count. + * In a properly functioning cluster, this should only be + * possible if this operation is not a failure (otherwise the + * fail count should be expired too), so this is really just a + * failsafe. + */ + pe_rsc_trace(history->rsc, + "Resource history entry %s on %s is not expired: " + "Unexpired fail count", + history->id, pe__node_name(history->node)); + expired = false; + } + + } else if (is_last_failure + && (history->rsc->remote_reconnect_ms != 0)) { + /* Clear any expired last failure when reconnect interval is set, + * even if there is no fail count. + */ + clear_reason = "reconnect interval is set"; + } + } + + if (!expired && is_last_failure + && should_clear_for_param_change(history->xml, history->task, + history->rsc, history->node)) { + clear_reason = "resource parameters have changed"; + } + + if (clear_reason != NULL) { + // Schedule clearing of the fail count + pe_action_t *clear_op = pe__clear_failcount(history->rsc, history->node, + clear_reason, + history->rsc->cluster); + + if (pcmk_is_set(history->rsc->cluster->flags, pe_flag_stonith_enabled) + && (history->rsc->remote_reconnect_ms != 0)) { + /* If we're clearing a remote connection due to a reconnect + * interval, we want to wait until any scheduled fencing + * completes. + * + * We could limit this to remote_node->details->unclean, but at + * this point, that's always true (it won't be reliable until + * after unpack_node_history() is done). + */ + crm_info("Clearing %s failure will wait until any scheduled " + "fencing of %s completes", + history->task, history->rsc->id); + order_after_remote_fencing(clear_op, history->rsc, + history->rsc->cluster); + } + } + + if (expired && (history->interval_ms == 0) + && pcmk__str_eq(history->task, CRMD_ACTION_STATUS, pcmk__str_none)) { + switch (history->exit_status) { + case PCMK_OCF_OK: + case PCMK_OCF_NOT_RUNNING: + case PCMK_OCF_RUNNING_PROMOTED: + case PCMK_OCF_DEGRADED: + case PCMK_OCF_DEGRADED_PROMOTED: + // Don't expire probes that return these values + pe_rsc_trace(history->rsc, + "Resource history entry %s on %s is not expired: " + "Probe result", + history->id, pe__node_name(history->node)); + expired = false; + break; + } + } + + return expired; +} + +int +pe__target_rc_from_xml(const xmlNode *xml_op) +{ + int target_rc = 0; + const char *key = crm_element_value(xml_op, XML_ATTR_TRANSITION_KEY); + + if (key == NULL) { + return -1; + } + decode_transition_key(key, NULL, NULL, NULL, &target_rc); + return target_rc; +} + +/*! + * \internal + * \brief Get the failure handling for an action + * + * \param[in,out] history Parsed action history entry + * + * \return Failure handling appropriate to action + */ +static enum action_fail_response +get_action_on_fail(struct action_history *history) +{ + enum action_fail_response result = action_fail_recover; + pe_action_t *action = custom_action(history->rsc, strdup(history->key), + history->task, NULL, TRUE, FALSE, + history->rsc->cluster); + + result = action->on_fail; + pe_free_action(action); + return result; +} + +/*! + * \internal + * \brief Update a resource's state for an action result + * + * \param[in,out] history Parsed action history entry + * \param[in] exit_status Exit status to base new state on + * \param[in] last_failure Resource's last_failure entry, if known + * \param[in,out] on_fail Resource's current failure handling + */ +static void +update_resource_state(struct action_history *history, int exit_status, + const xmlNode *last_failure, + enum action_fail_response *on_fail) +{ + bool clear_past_failure = false; + + if ((exit_status == PCMK_OCF_NOT_INSTALLED) + || (!pe_rsc_is_bundled(history->rsc) + && pcmk_xe_mask_probe_failure(history->xml))) { + history->rsc->role = RSC_ROLE_STOPPED; + + } else if (exit_status == PCMK_OCF_NOT_RUNNING) { + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_STATUS, + pcmk__str_none)) { + if ((last_failure != NULL) + && pcmk__str_eq(history->key, pe__xe_history_key(last_failure), + pcmk__str_none)) { + clear_past_failure = true; + } + if (history->rsc->role < RSC_ROLE_STARTED) { + set_active(history->rsc); + } + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_START, pcmk__str_none)) { + history->rsc->role = RSC_ROLE_STARTED; + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_STOP, pcmk__str_none)) { + history->rsc->role = RSC_ROLE_STOPPED; + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_PROMOTE, + pcmk__str_none)) { + history->rsc->role = RSC_ROLE_PROMOTED; + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_DEMOTE, + pcmk__str_none)) { + if (*on_fail == action_fail_demote) { + // Demote clears an error only if on-fail=demote + clear_past_failure = true; + } + history->rsc->role = RSC_ROLE_UNPROMOTED; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_MIGRATED, + pcmk__str_none)) { + history->rsc->role = RSC_ROLE_STARTED; + clear_past_failure = true; + + } else if (pcmk__str_eq(history->task, CRMD_ACTION_MIGRATE, + pcmk__str_none)) { + unpack_migrate_to_success(history); + + } else if (history->rsc->role < RSC_ROLE_STARTED) { + pe_rsc_trace(history->rsc, "%s active on %s", + history->rsc->id, pe__node_name(history->node)); + set_active(history->rsc); + } + + if (!clear_past_failure) { + return; + } + + switch (*on_fail) { + case action_fail_stop: + case action_fail_fence: + case action_fail_migrate: + case action_fail_standby: + pe_rsc_trace(history->rsc, + "%s (%s) is not cleared by a completed %s", + history->rsc->id, fail2text(*on_fail), history->task); + break; + + case action_fail_block: + case action_fail_ignore: + case action_fail_demote: + case action_fail_recover: + case action_fail_restart_container: + *on_fail = action_fail_ignore; + pe__set_next_role(history->rsc, RSC_ROLE_UNKNOWN, + "clear past failures"); + break; + + case action_fail_reset_remote: + if (history->rsc->remote_reconnect_ms == 0) { + /* With no reconnect interval, the connection is allowed to + * start again after the remote node is fenced and + * completely stopped. (With a reconnect interval, we wait + * for the failure to be cleared entirely before attempting + * to reconnect.) + */ + *on_fail = action_fail_ignore; + pe__set_next_role(history->rsc, RSC_ROLE_UNKNOWN, + "clear past failures and reset remote"); + } + break; + } +} + +/*! + * \internal + * \brief Check whether a given history entry matters for resource state + * + * \param[in] history Parsed action history entry + * + * \return true if action can affect resource state, otherwise false + */ +static inline bool +can_affect_state(struct action_history *history) +{ +#if 0 + /* @COMPAT It might be better to parse only actions we know we're interested + * in, rather than exclude a couple we don't. However that would be a + * behavioral change that should be done at a major or minor series release. + * Currently, unknown operations can affect whether a resource is considered + * active and/or failed. + */ + return pcmk__str_any_of(history->task, CRMD_ACTION_STATUS, + CRMD_ACTION_START, CRMD_ACTION_STOP, + CRMD_ACTION_PROMOTE, CRMD_ACTION_DEMOTE, + CRMD_ACTION_MIGRATE, CRMD_ACTION_MIGRATED, + "asyncmon", NULL); +#else + return !pcmk__str_any_of(history->task, CRMD_ACTION_NOTIFY, + CRMD_ACTION_METADATA, NULL); +#endif +} + +/*! + * \internal + * \brief Unpack execution/exit status and exit reason from a history entry + * + * \param[in,out] history Action history entry to unpack + * + * \return Standard Pacemaker return code + */ +static int +unpack_action_result(struct action_history *history) +{ + if ((crm_element_value_int(history->xml, XML_LRM_ATTR_OPSTATUS, + &(history->execution_status)) < 0) + || (history->execution_status < PCMK_EXEC_PENDING) + || (history->execution_status > PCMK_EXEC_MAX) + || (history->execution_status == PCMK_EXEC_CANCELLED)) { + crm_err("Ignoring resource history entry %s for %s on %s " + "with invalid " XML_LRM_ATTR_OPSTATUS " '%s'", + history->id, history->rsc->id, pe__node_name(history->node), + pcmk__s(crm_element_value(history->xml, XML_LRM_ATTR_OPSTATUS), + "")); + return pcmk_rc_unpack_error; + } + if ((crm_element_value_int(history->xml, XML_LRM_ATTR_RC, + &(history->exit_status)) < 0) + || (history->exit_status < 0) || (history->exit_status > CRM_EX_MAX)) { +#if 0 + /* @COMPAT We should ignore malformed entries, but since that would + * change behavior, it should be done at a major or minor series + * release. + */ + crm_err("Ignoring resource history entry %s for %s on %s " + "with invalid " XML_LRM_ATTR_RC " '%s'", + history->id, history->rsc->id, pe__node_name(history->node), + pcmk__s(crm_element_value(history->xml, XML_LRM_ATTR_RC), + "")); + return pcmk_rc_unpack_error; +#else + history->exit_status = CRM_EX_ERROR; +#endif + } + history->exit_reason = crm_element_value(history->xml, + XML_LRM_ATTR_EXIT_REASON); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Process an action history entry whose result expired + * + * \param[in,out] history Parsed action history entry + * \param[in] orig_exit_status Action exit status before remapping + * + * \return Standard Pacemaker return code (in particular, pcmk_rc_ok means the + * entry needs no further processing) + */ +static int +process_expired_result(struct action_history *history, int orig_exit_status) +{ + if (!pe_rsc_is_bundled(history->rsc) + && pcmk_xe_mask_probe_failure(history->xml) + && (orig_exit_status != history->expected_exit_status)) { + + if (history->rsc->role <= RSC_ROLE_STOPPED) { + history->rsc->role = RSC_ROLE_UNKNOWN; + } + crm_trace("Ignoring resource history entry %s for probe of %s on %s: " + "Masked failure expired", + history->id, history->rsc->id, + pe__node_name(history->node)); + return pcmk_rc_ok; + } + + if (history->exit_status == history->expected_exit_status) { + return pcmk_rc_undetermined; // Only failures expire + } + + if (history->interval_ms == 0) { + crm_notice("Ignoring resource history entry %s for %s of %s on %s: " + "Expired failure", + history->id, history->task, history->rsc->id, + pe__node_name(history->node)); + return pcmk_rc_ok; + } + + if (history->node->details->online && !history->node->details->unclean) { + /* Reschedule the recurring action. schedule_cancel() won't work at + * this stage, so as a hacky workaround, forcibly change the restart + * digest so pcmk__check_action_config() does what we want later. + * + * @TODO We should skip this if there is a newer successful monitor. + * Also, this causes rescheduling only if the history entry + * has an op-digest (which the expire-non-blocked-failure + * scheduler regression test doesn't, but that may not be a + * realistic scenario in production). + */ + crm_notice("Rescheduling %s-interval %s of %s on %s " + "after failure expired", + pcmk__readable_interval(history->interval_ms), history->task, + history->rsc->id, pe__node_name(history->node)); + crm_xml_add(history->xml, XML_LRM_ATTR_RESTART_DIGEST, + "calculated-failure-timeout"); + return pcmk_rc_ok; + } + + return pcmk_rc_undetermined; +} + +/*! + * \internal + * \brief Process a masked probe failure + * + * \param[in,out] history Parsed action history entry + * \param[in] orig_exit_status Action exit status before remapping + * \param[in] last_failure Resource's last_failure entry, if known + * \param[in,out] on_fail Resource's current failure handling + */ +static void +mask_probe_failure(struct action_history *history, int orig_exit_status, + const xmlNode *last_failure, + enum action_fail_response *on_fail) +{ + pe_resource_t *ban_rsc = history->rsc; + + if (!pcmk_is_set(history->rsc->flags, pe_rsc_unique)) { + ban_rsc = uber_parent(history->rsc); + } + + crm_notice("Treating probe result '%s' for %s on %s as 'not running'", + services_ocf_exitcode_str(orig_exit_status), history->rsc->id, + pe__node_name(history->node)); + update_resource_state(history, history->expected_exit_status, last_failure, + on_fail); + crm_xml_add(history->xml, XML_ATTR_UNAME, history->node->details->uname); + + record_failed_op(history); + resource_location(ban_rsc, history->node, -INFINITY, "masked-probe-failure", + history->rsc->cluster); +} + +/*! + * \internal Check whether a given failure is for a given pending action + * + * \param[in] history Parsed history entry for pending action + * \param[in] last_failure Resource's last_failure entry, if known + * + * \return true if \p last_failure is failure of pending action in \p history, + * otherwise false + * \note Both \p history and \p last_failure must come from the same + * lrm_resource block, as node and resource are assumed to be the same. + */ +static bool +failure_is_newer(const struct action_history *history, + const xmlNode *last_failure) +{ + guint failure_interval_ms = 0U; + long long failure_change = 0LL; + long long this_change = 0LL; + + if (last_failure == NULL) { + return false; // Resource has no last_failure entry + } + + if (!pcmk__str_eq(history->task, + crm_element_value(last_failure, XML_LRM_ATTR_TASK), + pcmk__str_none)) { + return false; // last_failure is for different action + } + + if ((crm_element_value_ms(last_failure, XML_LRM_ATTR_INTERVAL_MS, + &failure_interval_ms) != pcmk_ok) + || (history->interval_ms != failure_interval_ms)) { + return false; // last_failure is for action with different interval + } + + if ((pcmk__scan_ll(crm_element_value(history->xml, XML_RSC_OP_LAST_CHANGE), + &this_change, 0LL) != pcmk_rc_ok) + || (pcmk__scan_ll(crm_element_value(last_failure, + XML_RSC_OP_LAST_CHANGE), + &failure_change, 0LL) != pcmk_rc_ok) + || (failure_change < this_change)) { + return false; // Failure is not known to be newer + } + + return true; +} + +/*! + * \internal + * \brief Update a resource's role etc. for a pending action + * + * \param[in,out] history Parsed history entry for pending action + * \param[in] last_failure Resource's last_failure entry, if known + */ +static void +process_pending_action(struct action_history *history, + const xmlNode *last_failure) +{ + /* For recurring monitors, a failure is recorded only in RSC_last_failure_0, + * and there might be a RSC_monitor_INTERVAL entry with the last successful + * or pending result. + * + * If last_failure contains the failure of the pending recurring monitor + * we're processing here, and is newer, the action is no longer pending. + * (Pending results have call ID -1, which sorts last, so the last failure + * if any should be known.) + */ + if (failure_is_newer(history, last_failure)) { + return; + } + + if (strcmp(history->task, CRMD_ACTION_START) == 0) { + pe__set_resource_flags(history->rsc, pe_rsc_start_pending); + set_active(history->rsc); + + } else if (strcmp(history->task, CRMD_ACTION_PROMOTE) == 0) { + history->rsc->role = RSC_ROLE_PROMOTED; + + } else if ((strcmp(history->task, CRMD_ACTION_MIGRATE) == 0) + && history->node->details->unclean) { + /* A migrate_to action is pending on a unclean source, so force a stop + * on the target. + */ + const char *migrate_target = NULL; + pe_node_t *target = NULL; + + migrate_target = crm_element_value(history->xml, + XML_LRM_ATTR_MIGRATE_TARGET); + target = pe_find_node(history->rsc->cluster->nodes, migrate_target); + if (target != NULL) { + stop_action(history->rsc, target, FALSE); + } + } + + if (history->rsc->pending_task != NULL) { + /* There should never be multiple pending actions, but as a failsafe, + * just remember the first one processed for display purposes. + */ + return; + } + + if (pcmk_is_probe(history->task, history->interval_ms)) { + /* Pending probes are currently never displayed, even if pending + * operations are requested. If we ever want to change that, + * enable the below and the corresponding part of + * native.c:native_pending_task(). + */ +#if 0 + history->rsc->pending_task = strdup("probe"); + history->rsc->pending_node = history->node; +#endif + } else { + history->rsc->pending_task = strdup(history->task); + history->rsc->pending_node = history->node; + } +} + +static void +unpack_rsc_op(pe_resource_t *rsc, pe_node_t *node, xmlNode *xml_op, + xmlNode **last_failure, enum action_fail_response *on_fail) +{ + int old_rc = 0; + bool expired = false; + pe_resource_t *parent = rsc; + enum action_fail_response failure_strategy = action_fail_recover; + + struct action_history history = { + .rsc = rsc, + .node = node, + .xml = xml_op, + .execution_status = PCMK_EXEC_UNKNOWN, + }; + + CRM_CHECK(rsc && node && xml_op, return); + + history.id = ID(xml_op); + if (history.id == NULL) { + crm_err("Ignoring resource history entry for %s on %s without ID", + rsc->id, pe__node_name(node)); + return; + } + + // Task and interval + history.task = crm_element_value(xml_op, XML_LRM_ATTR_TASK); + if (history.task == NULL) { + crm_err("Ignoring resource history entry %s for %s on %s without " + XML_LRM_ATTR_TASK, history.id, rsc->id, pe__node_name(node)); + return; + } + crm_element_value_ms(xml_op, XML_LRM_ATTR_INTERVAL_MS, + &(history.interval_ms)); + if (!can_affect_state(&history)) { + pe_rsc_trace(rsc, + "Ignoring resource history entry %s for %s on %s " + "with irrelevant action '%s'", + history.id, rsc->id, pe__node_name(node), history.task); + return; + } + + if (unpack_action_result(&history) != pcmk_rc_ok) { + return; // Error already logged + } + + history.expected_exit_status = pe__target_rc_from_xml(xml_op); + history.key = pe__xe_history_key(xml_op); + crm_element_value_int(xml_op, XML_LRM_ATTR_CALLID, &(history.call_id)); + + pe_rsc_trace(rsc, "Unpacking %s (%s call %d on %s): %s (%s)", + history.id, history.task, history.call_id, pe__node_name(node), + pcmk_exec_status_str(history.execution_status), + crm_exit_str(history.exit_status)); + + if (node->details->unclean) { + pe_rsc_trace(rsc, + "%s is running on %s, which is unclean (further action " + "depends on value of stop's on-fail attribute)", + rsc->id, pe__node_name(node)); + } + + expired = check_operation_expiry(&history); + old_rc = history.exit_status; + + remap_operation(&history, on_fail, expired); + + if (expired && (process_expired_result(&history, old_rc) == pcmk_rc_ok)) { + goto done; + } + + if (!pe_rsc_is_bundled(rsc) && pcmk_xe_mask_probe_failure(xml_op)) { + mask_probe_failure(&history, old_rc, *last_failure, on_fail); + goto done; + } + + if (!pcmk_is_set(rsc->flags, pe_rsc_unique)) { + parent = uber_parent(rsc); + } + + switch (history.execution_status) { + case PCMK_EXEC_PENDING: + process_pending_action(&history, *last_failure); + goto done; + + case PCMK_EXEC_DONE: + update_resource_state(&history, history.exit_status, *last_failure, + on_fail); + goto done; + + case PCMK_EXEC_NOT_INSTALLED: + failure_strategy = get_action_on_fail(&history); + if (failure_strategy == action_fail_ignore) { + crm_warn("Cannot ignore failed %s of %s on %s: " + "Resource agent doesn't exist " + CRM_XS " status=%d rc=%d id=%s", + history.task, rsc->id, pe__node_name(node), + history.execution_status, history.exit_status, + history.id); + /* Also for printing it as "FAILED" by marking it as pe_rsc_failed later */ + *on_fail = action_fail_migrate; + } + resource_location(parent, node, -INFINITY, "hard-error", + rsc->cluster); + unpack_rsc_op_failure(&history, last_failure, on_fail); + goto done; + + case PCMK_EXEC_NOT_CONNECTED: + if (pe__is_guest_or_remote_node(node) + && pcmk_is_set(node->details->remote_rsc->flags, pe_rsc_managed)) { + /* We should never get into a situation where a managed remote + * connection resource is considered OK but a resource action + * behind the connection gets a "not connected" status. But as a + * fail-safe in case a bug or unusual circumstances do lead to + * that, ensure the remote connection is considered failed. + */ + pe__set_resource_flags(node->details->remote_rsc, + pe_rsc_failed|pe_rsc_stop); + } + break; // Not done, do error handling + + case PCMK_EXEC_ERROR: + case PCMK_EXEC_ERROR_HARD: + case PCMK_EXEC_ERROR_FATAL: + case PCMK_EXEC_TIMEOUT: + case PCMK_EXEC_NOT_SUPPORTED: + case PCMK_EXEC_INVALID: + break; // Not done, do error handling + + default: // No other value should be possible at this point + break; + } + + failure_strategy = get_action_on_fail(&history); + if ((failure_strategy == action_fail_ignore) + || (failure_strategy == action_fail_restart_container + && (strcmp(history.task, CRMD_ACTION_STOP) == 0))) { + + char *last_change_s = last_change_str(xml_op); + + crm_warn("Pretending failed %s (%s%s%s) of %s on %s at %s succeeded " + CRM_XS " %s", + history.task, services_ocf_exitcode_str(history.exit_status), + (pcmk__str_empty(history.exit_reason)? "" : ": "), + pcmk__s(history.exit_reason, ""), rsc->id, pe__node_name(node), + last_change_s, history.id); + free(last_change_s); + + update_resource_state(&history, history.expected_exit_status, + *last_failure, on_fail); + crm_xml_add(xml_op, XML_ATTR_UNAME, node->details->uname); + pe__set_resource_flags(rsc, pe_rsc_failure_ignored); + + record_failed_op(&history); + + if ((failure_strategy == action_fail_restart_container) + && cmp_on_fail(*on_fail, action_fail_recover) <= 0) { + *on_fail = failure_strategy; + } + + } else { + unpack_rsc_op_failure(&history, last_failure, on_fail); + + if (history.execution_status == PCMK_EXEC_ERROR_HARD) { + uint8_t log_level = LOG_ERR; + + if (history.exit_status == PCMK_OCF_NOT_INSTALLED) { + log_level = LOG_NOTICE; + } + do_crm_log(log_level, + "Preventing %s from restarting on %s because " + "of hard failure (%s%s%s) " CRM_XS " %s", + parent->id, pe__node_name(node), + services_ocf_exitcode_str(history.exit_status), + (pcmk__str_empty(history.exit_reason)? "" : ": "), + pcmk__s(history.exit_reason, ""), history.id); + resource_location(parent, node, -INFINITY, "hard-error", + rsc->cluster); + + } else if (history.execution_status == PCMK_EXEC_ERROR_FATAL) { + crm_err("Preventing %s from restarting anywhere because " + "of fatal failure (%s%s%s) " CRM_XS " %s", + parent->id, services_ocf_exitcode_str(history.exit_status), + (pcmk__str_empty(history.exit_reason)? "" : ": "), + pcmk__s(history.exit_reason, ""), history.id); + resource_location(parent, NULL, -INFINITY, "fatal-error", + rsc->cluster); + } + } + +done: + pe_rsc_trace(rsc, "%s role on %s after %s is %s (next %s)", + rsc->id, pe__node_name(node), history.id, + role2text(rsc->role), role2text(rsc->next_role)); +} + +static void +add_node_attrs(const xmlNode *xml_obj, pe_node_t *node, bool overwrite, + pe_working_set_t *data_set) +{ + const char *cluster_name = NULL; + + pe_rule_eval_data_t rule_data = { + .node_hash = NULL, + .role = RSC_ROLE_UNKNOWN, + .now = data_set->now, + .match_data = NULL, + .rsc_data = NULL, + .op_data = NULL + }; + + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_UNAME), strdup(node->details->uname)); + + g_hash_table_insert(node->details->attrs, strdup(CRM_ATTR_ID), + strdup(node->details->id)); + if (pcmk__str_eq(node->details->id, data_set->dc_uuid, pcmk__str_casei)) { + data_set->dc_node = node; + node->details->is_dc = TRUE; + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_IS_DC), strdup(XML_BOOLEAN_TRUE)); + } else { + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_IS_DC), strdup(XML_BOOLEAN_FALSE)); + } + + cluster_name = g_hash_table_lookup(data_set->config_hash, "cluster-name"); + if (cluster_name) { + g_hash_table_insert(node->details->attrs, strdup(CRM_ATTR_CLUSTER_NAME), + strdup(cluster_name)); + } + + pe__unpack_dataset_nvpairs(xml_obj, XML_TAG_ATTR_SETS, &rule_data, + node->details->attrs, NULL, overwrite, data_set); + + pe__unpack_dataset_nvpairs(xml_obj, XML_TAG_UTILIZATION, &rule_data, + node->details->utilization, NULL, + FALSE, data_set); + + if (pe_node_attribute_raw(node, CRM_ATTR_SITE_NAME) == NULL) { + const char *site_name = pe_node_attribute_raw(node, "site-name"); + + if (site_name) { + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_SITE_NAME), + strdup(site_name)); + + } else if (cluster_name) { + /* Default to cluster-name if unset */ + g_hash_table_insert(node->details->attrs, + strdup(CRM_ATTR_SITE_NAME), + strdup(cluster_name)); + } + } +} + +static GList * +extract_operations(const char *node, const char *rsc, xmlNode * rsc_entry, gboolean active_filter) +{ + int counter = -1; + int stop_index = -1; + int start_index = -1; + + xmlNode *rsc_op = NULL; + + GList *gIter = NULL; + GList *op_list = NULL; + GList *sorted_op_list = NULL; + + /* extract operations */ + op_list = NULL; + sorted_op_list = NULL; + + for (rsc_op = pcmk__xe_first_child(rsc_entry); + rsc_op != NULL; rsc_op = pcmk__xe_next(rsc_op)) { + + if (pcmk__str_eq((const char *)rsc_op->name, XML_LRM_TAG_RSC_OP, + pcmk__str_none)) { + crm_xml_add(rsc_op, "resource", rsc); + crm_xml_add(rsc_op, XML_ATTR_UNAME, node); + op_list = g_list_prepend(op_list, rsc_op); + } + } + + if (op_list == NULL) { + /* if there are no operations, there is nothing to do */ + return NULL; + } + + sorted_op_list = g_list_sort(op_list, sort_op_by_callid); + + /* create active recurring operations as optional */ + if (active_filter == FALSE) { + return sorted_op_list; + } + + op_list = NULL; + + calculate_active_ops(sorted_op_list, &start_index, &stop_index); + + for (gIter = sorted_op_list; gIter != NULL; gIter = gIter->next) { + xmlNode *rsc_op = (xmlNode *) gIter->data; + + counter++; + + if (start_index < stop_index) { + crm_trace("Skipping %s: not active", ID(rsc_entry)); + break; + + } else if (counter < start_index) { + crm_trace("Skipping %s: old", ID(rsc_op)); + continue; + } + op_list = g_list_append(op_list, rsc_op); + } + + g_list_free(sorted_op_list); + return op_list; +} + +GList * +find_operations(const char *rsc, const char *node, gboolean active_filter, + pe_working_set_t * data_set) +{ + GList *output = NULL; + GList *intermediate = NULL; + + xmlNode *tmp = NULL; + xmlNode *status = find_xml_node(data_set->input, XML_CIB_TAG_STATUS, TRUE); + + pe_node_t *this_node = NULL; + + xmlNode *node_state = NULL; + + for (node_state = pcmk__xe_first_child(status); node_state != NULL; + node_state = pcmk__xe_next(node_state)) { + + if (pcmk__str_eq((const char *)node_state->name, XML_CIB_TAG_STATE, pcmk__str_none)) { + const char *uname = crm_element_value(node_state, XML_ATTR_UNAME); + + if (node != NULL && !pcmk__str_eq(uname, node, pcmk__str_casei)) { + continue; + } + + this_node = pe_find_node(data_set->nodes, uname); + if(this_node == NULL) { + CRM_LOG_ASSERT(this_node != NULL); + continue; + + } else if (pe__is_guest_or_remote_node(this_node)) { + determine_remote_online_status(data_set, this_node); + + } else { + determine_online_status(node_state, this_node, data_set); + } + + if (this_node->details->online + || pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + /* offline nodes run no resources... + * unless stonith is enabled in which case we need to + * make sure rsc start events happen after the stonith + */ + xmlNode *lrm_rsc = NULL; + + tmp = find_xml_node(node_state, XML_CIB_TAG_LRM, FALSE); + tmp = find_xml_node(tmp, XML_LRM_TAG_RESOURCES, FALSE); + + for (lrm_rsc = pcmk__xe_first_child(tmp); lrm_rsc != NULL; + lrm_rsc = pcmk__xe_next(lrm_rsc)) { + + if (pcmk__str_eq((const char *)lrm_rsc->name, + XML_LRM_TAG_RESOURCE, pcmk__str_none)) { + + const char *rsc_id = crm_element_value(lrm_rsc, XML_ATTR_ID); + + if (rsc != NULL && !pcmk__str_eq(rsc_id, rsc, pcmk__str_casei)) { + continue; + } + + intermediate = extract_operations(uname, rsc_id, lrm_rsc, active_filter); + output = g_list_concat(output, intermediate); + } + } + } + } + } + + return output; +} diff --git a/lib/pengine/utils.c b/lib/pengine/utils.c new file mode 100644 index 0000000..ef0a092 --- /dev/null +++ b/lib/pengine/utils.c @@ -0,0 +1,938 @@ +/* + * Copyright 2004-2023 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include + +#include +#include +#include +#include + +#include "pe_status_private.h" + +extern bool pcmk__is_daemon; + +gboolean ghash_free_str_str(gpointer key, gpointer value, gpointer user_data); + +/*! + * \internal + * \brief Check whether we can fence a particular node + * + * \param[in] data_set Working set for cluster + * \param[in] node Name of node to check + * + * \return true if node can be fenced, false otherwise + */ +bool +pe_can_fence(const pe_working_set_t *data_set, const pe_node_t *node) +{ + if (pe__is_guest_node(node)) { + /* Guest nodes are fenced by stopping their container resource. We can + * do that if the container's host is either online or fenceable. + */ + pe_resource_t *rsc = node->details->remote_rsc->container; + + for (GList *n = rsc->running_on; n != NULL; n = n->next) { + pe_node_t *container_node = n->data; + + if (!container_node->details->online + && !pe_can_fence(data_set, container_node)) { + return false; + } + } + return true; + + } else if (!pcmk_is_set(data_set->flags, pe_flag_stonith_enabled)) { + return false; /* Turned off */ + + } else if (!pcmk_is_set(data_set->flags, pe_flag_have_stonith_resource)) { + return false; /* No devices */ + + } else if (pcmk_is_set(data_set->flags, pe_flag_have_quorum)) { + return true; + + } else if (data_set->no_quorum_policy == no_quorum_ignore) { + return true; + + } else if(node == NULL) { + return false; + + } else if(node->details->online) { + crm_notice("We can fence %s without quorum because they're in our membership", + pe__node_name(node)); + return true; + } + + crm_trace("Cannot fence %s", pe__node_name(node)); + return false; +} + +/*! + * \internal + * \brief Copy a node object + * + * \param[in] this_node Node object to copy + * + * \return Newly allocated shallow copy of this_node + * \note This function asserts on errors and is guaranteed to return non-NULL. + */ +pe_node_t * +pe__copy_node(const pe_node_t *this_node) +{ + pe_node_t *new_node = NULL; + + CRM_ASSERT(this_node != NULL); + + new_node = calloc(1, sizeof(pe_node_t)); + CRM_ASSERT(new_node != NULL); + + new_node->rsc_discover_mode = this_node->rsc_discover_mode; + new_node->weight = this_node->weight; + new_node->fixed = this_node->fixed; // @COMPAT deprecated and unused + new_node->details = this_node->details; + + return new_node; +} + +/* any node in list1 or list2 and not in the other gets a score of -INFINITY */ +void +node_list_exclude(GHashTable * hash, GList *list, gboolean merge_scores) +{ + GHashTable *result = hash; + pe_node_t *other_node = NULL; + GList *gIter = list; + + GHashTableIter iter; + pe_node_t *node = NULL; + + g_hash_table_iter_init(&iter, hash); + while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) { + + other_node = pe_find_node_id(list, node->details->id); + if (other_node == NULL) { + node->weight = -INFINITY; + crm_trace("Banning dependent from %s (no primary instance)", + pe__node_name(node)); + } else if (merge_scores) { + node->weight = pcmk__add_scores(node->weight, other_node->weight); + crm_trace("Added primary's score %s to dependent's score for %s " + "(now %s)", pcmk_readable_score(other_node->weight), + pe__node_name(node), pcmk_readable_score(node->weight)); + } + } + + for (; gIter != NULL; gIter = gIter->next) { + pe_node_t *node = (pe_node_t *) gIter->data; + + other_node = pe_hash_table_lookup(result, node->details->id); + + if (other_node == NULL) { + pe_node_t *new_node = pe__copy_node(node); + + new_node->weight = -INFINITY; + g_hash_table_insert(result, (gpointer) new_node->details->id, new_node); + } + } +} + +/*! + * \internal + * \brief Create a node hash table from a node list + * + * \param[in] list Node list + * + * \return Hash table equivalent of node list + */ +GHashTable * +pe__node_list2table(const GList *list) +{ + GHashTable *result = NULL; + + result = pcmk__strkey_table(NULL, free); + for (const GList *gIter = list; gIter != NULL; gIter = gIter->next) { + pe_node_t *new_node = pe__copy_node((const pe_node_t *) gIter->data); + + g_hash_table_insert(result, (gpointer) new_node->details->id, new_node); + } + return result; +} + +/*! + * \internal + * \brief Compare two nodes by name, with numeric portions sorted numerically + * + * Sort two node names case-insensitively like strcasecmp(), but with any + * numeric portions of the name sorted numerically. For example, "node10" will + * sort higher than "node9" but lower than "remotenode9". + * + * \param[in] a First node to compare (can be \c NULL) + * \param[in] b Second node to compare (can be \c NULL) + * + * \retval -1 \c a comes before \c b (or \c a is \c NULL and \c b is not) + * \retval 0 \c a and \c b are equal (or both are \c NULL) + * \retval 1 \c a comes after \c b (or \c b is \c NULL and \c a is not) + */ +gint +pe__cmp_node_name(gconstpointer a, gconstpointer b) +{ + const pe_node_t *node1 = (const pe_node_t *) a; + const pe_node_t *node2 = (const pe_node_t *) b; + + if ((node1 == NULL) && (node2 == NULL)) { + return 0; + } + + if (node1 == NULL) { + return -1; + } + + if (node2 == NULL) { + return 1; + } + + return pcmk__numeric_strcasecmp(node1->details->uname, + node2->details->uname); +} + +/*! + * \internal + * \brief Output node weights to stdout + * + * \param[in] rsc Use allowed nodes for this resource + * \param[in] comment Text description to prefix lines with + * \param[in] nodes If rsc is not specified, use these nodes + * \param[in,out] data_set Cluster working set + */ +static void +pe__output_node_weights(const pe_resource_t *rsc, const char *comment, + GHashTable *nodes, pe_working_set_t *data_set) +{ + pcmk__output_t *out = data_set->priv; + + // Sort the nodes so the output is consistent for regression tests + GList *list = g_list_sort(g_hash_table_get_values(nodes), + pe__cmp_node_name); + + for (const GList *gIter = list; gIter != NULL; gIter = gIter->next) { + const pe_node_t *node = (const pe_node_t *) gIter->data; + + out->message(out, "node-weight", rsc, comment, node->details->uname, + pcmk_readable_score(node->weight)); + } + g_list_free(list); +} + +/*! + * \internal + * \brief Log node weights at trace level + * + * \param[in] file Caller's filename + * \param[in] function Caller's function name + * \param[in] line Caller's line number + * \param[in] rsc If not NULL, include this resource's ID in logs + * \param[in] comment Text description to prefix lines with + * \param[in] nodes Nodes whose scores should be logged + */ +static void +pe__log_node_weights(const char *file, const char *function, int line, + const pe_resource_t *rsc, const char *comment, + GHashTable *nodes) +{ + GHashTableIter iter; + pe_node_t *node = NULL; + + // Don't waste time if we're not tracing at this point + pcmk__if_tracing({}, return); + + g_hash_table_iter_init(&iter, nodes); + while (g_hash_table_iter_next(&iter, NULL, (void **) &node)) { + if (rsc) { + qb_log_from_external_source(function, file, + "%s: %s allocation score on %s: %s", + LOG_TRACE, line, 0, + comment, rsc->id, + pe__node_name(node), + pcmk_readable_score(node->weight)); + } else { + qb_log_from_external_source(function, file, "%s: %s = %s", + LOG_TRACE, line, 0, + comment, pe__node_name(node), + pcmk_readable_score(node->weight)); + } + } +} + +/*! + * \internal + * \brief Log or output node weights + * + * \param[in] file Caller's filename + * \param[in] function Caller's function name + * \param[in] line Caller's line number + * \param[in] to_log Log if true, otherwise output + * \param[in] rsc If not NULL, use this resource's ID in logs, + * and show scores recursively for any children + * \param[in] comment Text description to prefix lines with + * \param[in] nodes Nodes whose scores should be shown + * \param[in,out] data_set Cluster working set + */ +void +pe__show_node_weights_as(const char *file, const char *function, int line, + bool to_log, const pe_resource_t *rsc, + const char *comment, GHashTable *nodes, + pe_working_set_t *data_set) +{ + if (rsc != NULL && pcmk_is_set(rsc->flags, pe_rsc_orphan)) { + // Don't show allocation scores for orphans + return; + } + if (nodes == NULL) { + // Nothing to show + return; + } + + if (to_log) { + pe__log_node_weights(file, function, line, rsc, comment, nodes); + } else { + pe__output_node_weights(rsc, comment, nodes, data_set); + } + + // If this resource has children, repeat recursively for each + if (rsc && rsc->children) { + for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child = (pe_resource_t *) gIter->data; + + pe__show_node_weights_as(file, function, line, to_log, child, + comment, child->allowed_nodes, data_set); + } + } +} + +/*! + * \internal + * \brief Compare two resources by priority + * + * \param[in] a First resource to compare (can be \c NULL) + * \param[in] b Second resource to compare (can be \c NULL) + * + * \retval -1 \c a->priority > \c b->priority (or \c b is \c NULL and \c a is + * not) + * \retval 0 \c a->priority == \c b->priority (or both \c a and \c b are + * \c NULL) + * \retval 1 \c a->priority < \c b->priority (or \c a is \c NULL and \c b is + * not) + */ +gint +pe__cmp_rsc_priority(gconstpointer a, gconstpointer b) +{ + const pe_resource_t *resource1 = (const pe_resource_t *)a; + const pe_resource_t *resource2 = (const pe_resource_t *)b; + + if (a == NULL && b == NULL) { + return 0; + } + if (a == NULL) { + return 1; + } + if (b == NULL) { + return -1; + } + + if (resource1->priority > resource2->priority) { + return -1; + } + + if (resource1->priority < resource2->priority) { + return 1; + } + + return 0; +} + +static void +resource_node_score(pe_resource_t *rsc, const pe_node_t *node, int score, + const char *tag) +{ + pe_node_t *match = NULL; + + if ((rsc->exclusive_discover || (node->rsc_discover_mode == pe_discover_never)) + && pcmk__str_eq(tag, "symmetric_default", pcmk__str_casei)) { + /* This string comparision may be fragile, but exclusive resources and + * exclusive nodes should not have the symmetric_default constraint + * applied to them. + */ + return; + + } else if (rsc->children) { + GList *gIter = rsc->children; + + for (; gIter != NULL; gIter = gIter->next) { + pe_resource_t *child_rsc = (pe_resource_t *) gIter->data; + + resource_node_score(child_rsc, node, score, tag); + } + } + + match = pe_hash_table_lookup(rsc->allowed_nodes, node->details->id); + if (match == NULL) { + match = pe__copy_node(node); + g_hash_table_insert(rsc->allowed_nodes, (gpointer) match->details->id, match); + } + match->weight = pcmk__add_scores(match->weight, score); + pe_rsc_trace(rsc, + "Enabling %s preference (%s) for %s on %s (now %s)", + tag, pcmk_readable_score(score), rsc->id, pe__node_name(node), + pcmk_readable_score(match->weight)); +} + +void +resource_location(pe_resource_t *rsc, const pe_node_t *node, int score, + const char *tag, pe_working_set_t *data_set) +{ + if (node != NULL) { + resource_node_score(rsc, node, score, tag); + + } else if (data_set != NULL) { + GList *gIter = data_set->nodes; + + for (; gIter != NULL; gIter = gIter->next) { + pe_node_t *node_iter = (pe_node_t *) gIter->data; + + resource_node_score(rsc, node_iter, score, tag); + } + + } else { + GHashTableIter iter; + pe_node_t *node_iter = NULL; + + g_hash_table_iter_init(&iter, rsc->allowed_nodes); + while (g_hash_table_iter_next(&iter, NULL, (void **)&node_iter)) { + resource_node_score(rsc, node_iter, score, tag); + } + } + + if (node == NULL && score == -INFINITY) { + if (rsc->allocated_to) { + crm_info("Deallocating %s from %s", + rsc->id, pe__node_name(rsc->allocated_to)); + free(rsc->allocated_to); + rsc->allocated_to = NULL; + } + } +} + +time_t +get_effective_time(pe_working_set_t * data_set) +{ + if(data_set) { + if (data_set->now == NULL) { + crm_trace("Recording a new 'now'"); + data_set->now = crm_time_new(NULL); + } + return crm_time_get_seconds_since_epoch(data_set->now); + } + + crm_trace("Defaulting to 'now'"); + return time(NULL); +} + +gboolean +get_target_role(const pe_resource_t *rsc, enum rsc_role_e *role) +{ + enum rsc_role_e local_role = RSC_ROLE_UNKNOWN; + const char *value = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE); + + CRM_CHECK(role != NULL, return FALSE); + + if (pcmk__str_eq(value, "started", pcmk__str_null_matches | pcmk__str_casei) + || pcmk__str_eq("default", value, pcmk__str_casei)) { + return FALSE; + } + + local_role = text2role(value); + if (local_role == RSC_ROLE_UNKNOWN) { + pcmk__config_err("Ignoring '" XML_RSC_ATTR_TARGET_ROLE "' for %s " + "because '%s' is not valid", rsc->id, value); + return FALSE; + + } else if (local_role > RSC_ROLE_STARTED) { + if (pcmk_is_set(pe__const_top_resource(rsc, false)->flags, + pe_rsc_promotable)) { + if (local_role > RSC_ROLE_UNPROMOTED) { + /* This is what we'd do anyway, just leave the default to avoid messing up the placement algorithm */ + return FALSE; + } + + } else { + pcmk__config_err("Ignoring '" XML_RSC_ATTR_TARGET_ROLE "' for %s " + "because '%s' only makes sense for promotable " + "clones", rsc->id, value); + return FALSE; + } + } + + *role = local_role; + return TRUE; +} + +gboolean +order_actions(pe_action_t * lh_action, pe_action_t * rh_action, enum pe_ordering order) +{ + GList *gIter = NULL; + pe_action_wrapper_t *wrapper = NULL; + GList *list = NULL; + + if (order == pe_order_none) { + return FALSE; + } + + if (lh_action == NULL || rh_action == NULL) { + return FALSE; + } + + crm_trace("Creating action wrappers for ordering: %s then %s", + lh_action->uuid, rh_action->uuid); + + /* Ensure we never create a dependency on ourselves... it's happened */ + CRM_ASSERT(lh_action != rh_action); + + /* Filter dups, otherwise update_action_states() has too much work to do */ + gIter = lh_action->actions_after; + for (; gIter != NULL; gIter = gIter->next) { + pe_action_wrapper_t *after = (pe_action_wrapper_t *) gIter->data; + + if (after->action == rh_action && (after->type & order)) { + return FALSE; + } + } + + wrapper = calloc(1, sizeof(pe_action_wrapper_t)); + wrapper->action = rh_action; + wrapper->type = order; + list = lh_action->actions_after; + list = g_list_prepend(list, wrapper); + lh_action->actions_after = list; + + wrapper = calloc(1, sizeof(pe_action_wrapper_t)); + wrapper->action = lh_action; + wrapper->type = order; + list = rh_action->actions_before; + list = g_list_prepend(list, wrapper); + rh_action->actions_before = list; + return TRUE; +} + +void +destroy_ticket(gpointer data) +{ + pe_ticket_t *ticket = data; + + if (ticket->state) { + g_hash_table_destroy(ticket->state); + } + free(ticket->id); + free(ticket); +} + +pe_ticket_t * +ticket_new(const char *ticket_id, pe_working_set_t * data_set) +{ + pe_ticket_t *ticket = NULL; + + if (pcmk__str_empty(ticket_id)) { + return NULL; + } + + if (data_set->tickets == NULL) { + data_set->tickets = pcmk__strkey_table(free, destroy_ticket); + } + + ticket = g_hash_table_lookup(data_set->tickets, ticket_id); + if (ticket == NULL) { + + ticket = calloc(1, sizeof(pe_ticket_t)); + if (ticket == NULL) { + crm_err("Cannot allocate ticket '%s'", ticket_id); + return NULL; + } + + crm_trace("Creaing ticket entry for %s", ticket_id); + + ticket->id = strdup(ticket_id); + ticket->granted = FALSE; + ticket->last_granted = -1; + ticket->standby = FALSE; + ticket->state = pcmk__strkey_table(free, free); + + g_hash_table_insert(data_set->tickets, strdup(ticket->id), ticket); + } + + return ticket; +} + +const char * +rsc_printable_id(const pe_resource_t *rsc) +{ + return pcmk_is_set(rsc->flags, pe_rsc_unique)? rsc->id : ID(rsc->xml); +} + +void +pe__clear_resource_flags_recursive(pe_resource_t *rsc, uint64_t flags) +{ + pe__clear_resource_flags(rsc, flags); + for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) { + pe__clear_resource_flags_recursive((pe_resource_t *) gIter->data, flags); + } +} + +void +pe__clear_resource_flags_on_all(pe_working_set_t *data_set, uint64_t flag) +{ + for (GList *lpc = data_set->resources; lpc != NULL; lpc = lpc->next) { + pe_resource_t *r = (pe_resource_t *) lpc->data; + pe__clear_resource_flags_recursive(r, flag); + } +} + +void +pe__set_resource_flags_recursive(pe_resource_t *rsc, uint64_t flags) +{ + pe__set_resource_flags(rsc, flags); + for (GList *gIter = rsc->children; gIter != NULL; gIter = gIter->next) { + pe__set_resource_flags_recursive((pe_resource_t *) gIter->data, flags); + } +} + +void +trigger_unfencing(pe_resource_t *rsc, pe_node_t *node, const char *reason, + pe_action_t *dependency, pe_working_set_t *data_set) +{ + if (!pcmk_is_set(data_set->flags, pe_flag_enable_unfencing)) { + /* No resources require it */ + return; + + } else if ((rsc != NULL) + && !pcmk_is_set(rsc->flags, pe_rsc_fence_device)) { + /* Wasn't a stonith device */ + return; + + } else if(node + && node->details->online + && node->details->unclean == FALSE + && node->details->shutdown == FALSE) { + pe_action_t *unfence = pe_fence_op(node, "on", FALSE, reason, FALSE, data_set); + + if(dependency) { + order_actions(unfence, dependency, pe_order_optional); + } + + } else if(rsc) { + GHashTableIter iter; + + g_hash_table_iter_init(&iter, rsc->allowed_nodes); + while (g_hash_table_iter_next(&iter, NULL, (void **)&node)) { + if(node->details->online && node->details->unclean == FALSE && node->details->shutdown == FALSE) { + trigger_unfencing(rsc, node, reason, dependency, data_set); + } + } + } +} + +gboolean +add_tag_ref(GHashTable * tags, const char * tag_name, const char * obj_ref) +{ + pe_tag_t *tag = NULL; + GList *gIter = NULL; + gboolean is_existing = FALSE; + + CRM_CHECK(tags && tag_name && obj_ref, return FALSE); + + tag = g_hash_table_lookup(tags, tag_name); + if (tag == NULL) { + tag = calloc(1, sizeof(pe_tag_t)); + if (tag == NULL) { + return FALSE; + } + tag->id = strdup(tag_name); + tag->refs = NULL; + g_hash_table_insert(tags, strdup(tag_name), tag); + } + + for (gIter = tag->refs; gIter != NULL; gIter = gIter->next) { + const char *existing_ref = (const char *) gIter->data; + + if (pcmk__str_eq(existing_ref, obj_ref, pcmk__str_none)){ + is_existing = TRUE; + break; + } + } + + if (is_existing == FALSE) { + tag->refs = g_list_append(tag->refs, strdup(obj_ref)); + crm_trace("Added: tag=%s ref=%s", tag->id, obj_ref); + } + + return TRUE; +} + +/*! + * \internal + * \brief Check whether shutdown has been requested for a node + * + * \param[in] node Node to check + * + * \return TRUE if node has shutdown attribute set and nonzero, FALSE otherwise + * \note This differs from simply using node->details->shutdown in that it can + * be used before that has been determined (and in fact to determine it), + * and it can also be used to distinguish requested shutdown from implicit + * shutdown of remote nodes by virtue of their connection stopping. + */ +bool +pe__shutdown_requested(const pe_node_t *node) +{ + const char *shutdown = pe_node_attribute_raw(node, XML_CIB_ATTR_SHUTDOWN); + + return !pcmk__str_eq(shutdown, "0", pcmk__str_null_matches); +} + +/*! + * \internal + * \brief Update a data set's "recheck by" time + * + * \param[in] recheck Epoch time when recheck should happen + * \param[in,out] data_set Current working set + */ +void +pe__update_recheck_time(time_t recheck, pe_working_set_t *data_set) +{ + if ((recheck > get_effective_time(data_set)) + && ((data_set->recheck_by == 0) + || (data_set->recheck_by > recheck))) { + data_set->recheck_by = recheck; + } +} + +/*! + * \internal + * \brief Extract nvpair blocks contained by a CIB XML element into a hash table + * + * \param[in] xml_obj XML element containing blocks of nvpair elements + * \param[in] set_name If not NULL, only use blocks of this element + * \param[in] rule_data Matching parameters to use when unpacking + * \param[out] hash Where to store extracted name/value pairs + * \param[in] always_first If not NULL, process block with this ID first + * \param[in] overwrite Whether to replace existing values with same name + * \param[in,out] data_set Cluster working set containing \p xml_obj + */ +void +pe__unpack_dataset_nvpairs(const xmlNode *xml_obj, const char *set_name, + const pe_rule_eval_data_t *rule_data, + GHashTable *hash, const char *always_first, + gboolean overwrite, pe_working_set_t *data_set) +{ + crm_time_t *next_change = crm_time_new_undefined(); + + pe_eval_nvpairs(data_set->input, xml_obj, set_name, rule_data, hash, + always_first, overwrite, next_change); + if (crm_time_is_defined(next_change)) { + time_t recheck = (time_t) crm_time_get_seconds_since_epoch(next_change); + + pe__update_recheck_time(recheck, data_set); + } + crm_time_free(next_change); +} + +bool +pe__resource_is_disabled(const pe_resource_t *rsc) +{ + const char *target_role = NULL; + + CRM_CHECK(rsc != NULL, return false); + target_role = g_hash_table_lookup(rsc->meta, XML_RSC_ATTR_TARGET_ROLE); + if (target_role) { + enum rsc_role_e target_role_e = text2role(target_role); + + if ((target_role_e == RSC_ROLE_STOPPED) + || ((target_role_e == RSC_ROLE_UNPROMOTED) + && pcmk_is_set(pe__const_top_resource(rsc, false)->flags, + pe_rsc_promotable))) { + return true; + } + } + return false; +} + +/*! + * \internal + * \brief Check whether a resource is running only on given node + * + * \param[in] rsc Resource to check + * \param[in] node Node to check + * + * \return true if \p rsc is running only on \p node, otherwise false + */ +bool +pe__rsc_running_on_only(const pe_resource_t *rsc, const pe_node_t *node) +{ + return (rsc != NULL) && pcmk__list_of_1(rsc->running_on) + && pe__same_node((const pe_node_t *) rsc->running_on->data, node); +} + +bool +pe__rsc_running_on_any(pe_resource_t *rsc, GList *node_list) +{ + for (GList *ele = rsc->running_on; ele; ele = ele->next) { + pe_node_t *node = (pe_node_t *) ele->data; + if (pcmk__str_in_list(node->details->uname, node_list, + pcmk__str_star_matches|pcmk__str_casei)) { + return true; + } + } + + return false; +} + +bool +pcmk__rsc_filtered_by_node(pe_resource_t *rsc, GList *only_node) +{ + return (rsc->fns->active(rsc, FALSE) && !pe__rsc_running_on_any(rsc, only_node)); +} + +GList * +pe__filter_rsc_list(GList *rscs, GList *filter) +{ + GList *retval = NULL; + + for (GList *gIter = rscs; gIter; gIter = gIter->next) { + pe_resource_t *rsc = (pe_resource_t *) gIter->data; + + /* I think the second condition is safe here for all callers of this + * function. If not, it needs to move into pe__node_text. + */ + if (pcmk__str_in_list(rsc_printable_id(rsc), filter, pcmk__str_star_matches) || + (rsc->parent && pcmk__str_in_list(rsc_printable_id(rsc->parent), filter, pcmk__str_star_matches))) { + retval = g_list_prepend(retval, rsc); + } + } + + return retval; +} + +GList * +pe__build_node_name_list(pe_working_set_t *data_set, const char *s) { + GList *nodes = NULL; + + if (pcmk__str_eq(s, "*", pcmk__str_null_matches)) { + /* Nothing was given so return a list of all node names. Or, '*' was + * given. This would normally fall into the pe__unames_with_tag branch + * where it will return an empty list. Catch it here instead. + */ + nodes = g_list_prepend(nodes, strdup("*")); + } else { + pe_node_t *node = pe_find_node(data_set->nodes, s); + + if (node) { + /* The given string was a valid uname for a node. Return a + * singleton list containing just that uname. + */ + nodes = g_list_prepend(nodes, strdup(s)); + } else { + /* The given string was not a valid uname. It's either a tag or + * it's a typo or something. In the first case, we'll return a + * list of all the unames of the nodes with the given tag. In the + * second case, we'll return a NULL pointer and nothing will + * get displayed. + */ + nodes = pe__unames_with_tag(data_set, s); + } + } + + return nodes; +} + +GList * +pe__build_rsc_list(pe_working_set_t *data_set, const char *s) { + GList *resources = NULL; + + if (pcmk__str_eq(s, "*", pcmk__str_null_matches)) { + resources = g_list_prepend(resources, strdup("*")); + } else { + pe_resource_t *rsc = pe_find_resource_with_flags(data_set->resources, s, + pe_find_renamed|pe_find_any); + + if (rsc) { + /* A colon in the name we were given means we're being asked to filter + * on a specific instance of a cloned resource. Put that exact string + * into the filter list. Otherwise, use the printable ID of whatever + * resource was found that matches what was asked for. + */ + if (strstr(s, ":") != NULL) { + resources = g_list_prepend(resources, strdup(rsc->id)); + } else { + resources = g_list_prepend(resources, strdup(rsc_printable_id(rsc))); + } + } else { + /* The given string was not a valid resource name. It's a tag or a + * typo or something. See pe__build_node_name_list() for more + * detail. + */ + resources = pe__rscs_with_tag(data_set, s); + } + } + + return resources; +} + +xmlNode * +pe__failed_probe_for_rsc(const pe_resource_t *rsc, const char *name) +{ + const pe_resource_t *parent = pe__const_top_resource(rsc, false); + const char *rsc_id = rsc->id; + + if (parent->variant == pe_clone) { + rsc_id = pe__clone_child_id(parent); + } + + for (xmlNode *xml_op = pcmk__xml_first_child(rsc->cluster->failed); xml_op != NULL; + xml_op = pcmk__xml_next(xml_op)) { + const char *value = NULL; + char *op_id = NULL; + + /* This resource operation is not a failed probe. */ + if (!pcmk_xe_mask_probe_failure(xml_op)) { + continue; + } + + /* This resource operation was not run on the given node. Note that if name is + * NULL, this will always succeed. + */ + value = crm_element_value(xml_op, XML_LRM_ATTR_TARGET); + if (value == NULL || !pcmk__str_eq(value, name, pcmk__str_casei|pcmk__str_null_matches)) { + continue; + } + + if (!parse_op_key(pe__xe_history_key(xml_op), &op_id, NULL, NULL)) { + continue; // This history entry is missing an operation key + } + + /* This resource operation's ID does not match the rsc_id we are looking for. */ + if (!pcmk__str_eq(op_id, rsc_id, pcmk__str_none)) { + free(op_id); + continue; + } + + free(op_id); + return xml_op; + } + + return NULL; +} diff --git a/lib/pengine/variant.h b/lib/pengine/variant.h new file mode 100644 index 0000000..daa3781 --- /dev/null +++ b/lib/pengine/variant.h @@ -0,0 +1,91 @@ +/* + * Copyright 2004-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PE_VARIANT__H +# define PE_VARIANT__H + +# if PE__VARIANT_BUNDLE + +typedef struct { + int offset; + char *ipaddr; + pe_node_t *node; + pe_resource_t *ip; + pe_resource_t *child; + pe_resource_t *container; + pe_resource_t *remote; +} pe__bundle_replica_t; + +enum pe__bundle_mount_flags { + pe__bundle_mount_none = 0x00, + + // mount instance-specific subdirectory rather than source directly + pe__bundle_mount_subdir = 0x01 +}; + +typedef struct { + char *source; + char *target; + char *options; + uint32_t flags; // bitmask of pe__bundle_mount_flags +} pe__bundle_mount_t; + +typedef struct { + char *source; + char *target; +} pe__bundle_port_t; + +enum pe__container_agent { + PE__CONTAINER_AGENT_UNKNOWN, + PE__CONTAINER_AGENT_DOCKER, + PE__CONTAINER_AGENT_RKT, + PE__CONTAINER_AGENT_PODMAN, +}; + +#define PE__CONTAINER_AGENT_UNKNOWN_S "unknown" +#define PE__CONTAINER_AGENT_DOCKER_S "docker" +#define PE__CONTAINER_AGENT_RKT_S "rkt" +#define PE__CONTAINER_AGENT_PODMAN_S "podman" + +typedef struct pe__bundle_variant_data_s { + int promoted_max; + int nreplicas; + int nreplicas_per_host; + char *prefix; + char *image; + const char *ip_last; + char *host_network; + char *host_netmask; + char *control_port; + char *container_network; + char *ip_range_start; + gboolean add_host; + gchar *container_host_options; + char *container_command; + char *launcher_options; + const char *attribute_target; + + pe_resource_t *child; + + GList *replicas; // pe__bundle_replica_t * + GList *ports; // pe__bundle_port_t * + GList *mounts; // pe__bundle_mount_t * + + enum pe__container_agent agent_type; +} pe__bundle_variant_data_t; + +# define get_bundle_variant_data(data, rsc) \ + CRM_ASSERT(rsc != NULL); \ + CRM_ASSERT(rsc->variant == pe_container); \ + CRM_ASSERT(rsc->variant_opaque != NULL); \ + data = (pe__bundle_variant_data_t *)rsc->variant_opaque; \ + +# endif + +#endif diff --git a/lib/services/Makefile.am b/lib/services/Makefile.am new file mode 100644 index 0000000..a7e10c9 --- /dev/null +++ b/lib/services/Makefile.am @@ -0,0 +1,43 @@ +# +# Copyright 2012-2021 the Pacemaker project contributors +# +# The version control history for this file may have further details. +# +# This source code is licensed under the GNU Lesser General Public License +# version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. +# + +MAINTAINERCLEANFILES = Makefile.in + +AM_CPPFLAGS = -I$(top_srcdir)/include + +lib_LTLIBRARIES = libcrmservice.la +noinst_HEADERS = pcmk-dbus.h upstart.h systemd.h \ + services_lsb.h services_nagios.h \ + services_ocf.h \ + services_private.h + +libcrmservice_la_LDFLAGS = -version-info 31:2:3 +libcrmservice_la_CFLAGS = + +libcrmservice_la_CFLAGS += $(CFLAGS_HARDENED_LIB) +libcrmservice_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) + +libcrmservice_la_LIBADD = $(top_builddir)/lib/common/libcrmcommon.la $(DBUS_LIBS) + +libcrmservice_la_SOURCES = services.c +libcrmservice_la_SOURCES += services_linux.c +libcrmservice_la_SOURCES += services_lsb.c +libcrmservice_la_SOURCES += services_ocf.c +if BUILD_DBUS +libcrmservice_la_SOURCES += dbus.c +endif +if BUILD_UPSTART +libcrmservice_la_SOURCES += upstart.c +endif +if BUILD_SYSTEMD +libcrmservice_la_SOURCES += systemd.c +endif +if BUILD_NAGIOS +libcrmservice_la_SOURCES += services_nagios.c +endif diff --git a/lib/services/dbus.c b/lib/services/dbus.c new file mode 100644 index 0000000..f052c0a --- /dev/null +++ b/lib/services/dbus.c @@ -0,0 +1,776 @@ +/* + * Copyright 2014-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include + +/* + * DBus message dispatch + */ + +// List of DBus connections (DBusConnection*) with messages available +static GList *conn_dispatches = NULL; + +/*! + * \internal + * \brief Save an indication that DBus messages need dispatching + * + * \param[in] connection DBus connection with messages to dispatch + * \param[in] new_status Dispatch status as reported by DBus library + * \param[in] data Ignored + * + * \note This is suitable to be used as a DBus status dispatch function. + * As mentioned in the DBus documentation, dbus_connection_dispatch() must + * not be called from within this function, and any re-entrancy is a bad + * idea. Instead, this should just flag the main loop that messages need + * to be dispatched. + */ +static void +update_dispatch_status(DBusConnection *connection, + DBusDispatchStatus new_status, void *data) +{ + if (new_status == DBUS_DISPATCH_DATA_REMAINS) { + crm_trace("DBus connection has messages available for dispatch"); + conn_dispatches = g_list_prepend(conn_dispatches, connection); + } else { + crm_trace("DBus connection has no messages available for dispatch " + "(status %d)", new_status); + } +} + +/*! + * \internal + * \brief Dispatch available messages on all DBus connections + */ +static void +dispatch_messages(void) +{ + for (GList *gIter = conn_dispatches; gIter != NULL; gIter = gIter->next) { + DBusConnection *connection = gIter->data; + + while (dbus_connection_get_dispatch_status(connection) + == DBUS_DISPATCH_DATA_REMAINS) { + crm_trace("Dispatching available messages on DBus connection"); + dbus_connection_dispatch(connection); + } + } + g_list_free(conn_dispatches); + conn_dispatches = NULL; +} + + +/* + * DBus file descriptor watches + * + * The DBus library allows the caller to register functions for the library to + * use for file descriptor notifications via a main loop. + */ + +/* Copied from dbus-watch.c */ +static const char* +dbus_watch_flags_to_string(int flags) +{ + const char *watch_type; + + if ((flags & DBUS_WATCH_READABLE) && (flags & DBUS_WATCH_WRITABLE)) { + watch_type = "read/write"; + } else if (flags & DBUS_WATCH_READABLE) { + watch_type = "read"; + } else if (flags & DBUS_WATCH_WRITABLE) { + watch_type = "write"; + } else { + watch_type = "neither read nor write"; + } + return watch_type; +} + +/*! + * \internal + * \brief Dispatch data available on a DBus file descriptor watch + * + * \param[in,out] userdata Pointer to the DBus watch + * + * \return Always 0 + * \note This is suitable for use as a dispatch function in + * struct mainloop_fd_callbacks (which means that a negative return value + * would indicate the file descriptor is no longer required). + */ +static int +dispatch_fd_data(gpointer userdata) +{ + bool oom = FALSE; + DBusWatch *watch = userdata; + int flags = dbus_watch_get_flags(watch); + bool enabled = dbus_watch_get_enabled (watch); + + crm_trace("Dispatching DBus watch for file descriptor %d " + "with flags %#x (%s)", + dbus_watch_get_unix_fd(watch), flags, + dbus_watch_flags_to_string(flags)); + + if (enabled && (flags & (DBUS_WATCH_READABLE|DBUS_WATCH_WRITABLE))) { + oom = !dbus_watch_handle(watch, flags); + + } else if (enabled) { + oom = !dbus_watch_handle(watch, DBUS_WATCH_ERROR); + } + + if (flags != dbus_watch_get_flags(watch)) { + flags = dbus_watch_get_flags(watch); + crm_trace("Dispatched DBus file descriptor watch: now %#x (%s)", + flags, dbus_watch_flags_to_string(flags)); + } + + if (oom) { + crm_crit("Could not dispatch DBus file descriptor data: Out of memory"); + } else { + dispatch_messages(); + } + return 0; +} + +static void +watch_fd_closed(gpointer userdata) +{ + crm_trace("DBus watch for file descriptor %d is now closed", + dbus_watch_get_unix_fd((DBusWatch *) userdata)); +} + +static struct mainloop_fd_callbacks pcmk_dbus_cb = { + .dispatch = dispatch_fd_data, + .destroy = watch_fd_closed, +}; + +static dbus_bool_t +add_dbus_watch(DBusWatch *watch, void *data) +{ + int fd = dbus_watch_get_unix_fd(watch); + + mainloop_io_t *client = mainloop_add_fd("dbus", G_PRIORITY_DEFAULT, fd, + watch, &pcmk_dbus_cb); + + crm_trace("Added DBus watch for file descriptor %d", fd); + dbus_watch_set_data(watch, client, NULL); + return TRUE; +} + +static void +toggle_dbus_watch(DBusWatch *watch, void *data) +{ + // @TODO Should this do something more? + crm_debug("DBus watch for file descriptor %d is now %s", + dbus_watch_get_unix_fd(watch), + (dbus_watch_get_enabled(watch)? "enabled" : "disabled")); +} + +static void +remove_dbus_watch(DBusWatch *watch, void *data) +{ + crm_trace("Removed DBus watch for file descriptor %d", + dbus_watch_get_unix_fd(watch)); + mainloop_del_fd((mainloop_io_t *) dbus_watch_get_data(watch)); +} + +static void +register_watch_functions(DBusConnection *connection) +{ + dbus_connection_set_watch_functions(connection, add_dbus_watch, + remove_dbus_watch, + toggle_dbus_watch, NULL, NULL); +} + +/* + * DBus main loop timeouts + * + * The DBus library allows the caller to register functions for the library to + * use for managing timers via a main loop. + */ + +static gboolean +timer_popped(gpointer data) +{ + crm_debug("%dms DBus timer expired", + dbus_timeout_get_interval((DBusTimeout *) data)); + dbus_timeout_handle(data); + return FALSE; +} + +static dbus_bool_t +add_dbus_timer(DBusTimeout *timeout, void *data) +{ + int interval_ms = dbus_timeout_get_interval(timeout); + guint id = g_timeout_add(interval_ms, timer_popped, timeout); + + if (id) { + dbus_timeout_set_data(timeout, GUINT_TO_POINTER(id), NULL); + } + crm_trace("Added %dms DBus timer", interval_ms); + return TRUE; +} + +static void +remove_dbus_timer(DBusTimeout *timeout, void *data) +{ + void *vid = dbus_timeout_get_data(timeout); + guint id = GPOINTER_TO_UINT(vid); + + crm_trace("Removing %dms DBus timer", dbus_timeout_get_interval(timeout)); + if (id) { + g_source_remove(id); + dbus_timeout_set_data(timeout, 0, NULL); + } +} + +static void +toggle_dbus_timer(DBusTimeout *timeout, void *data) +{ + bool enabled = dbus_timeout_get_enabled(timeout); + + crm_trace("Toggling %dms DBus timer %s", + dbus_timeout_get_interval(timeout), (enabled? "off": "on")); + if (enabled) { + add_dbus_timer(timeout, data); + } else { + remove_dbus_timer(timeout, data); + } +} + +static void +register_timer_functions(DBusConnection *connection) +{ + dbus_connection_set_timeout_functions(connection, add_dbus_timer, + remove_dbus_timer, + toggle_dbus_timer, NULL, NULL); +} + +/* + * General DBus utilities + */ + +DBusConnection * +pcmk_dbus_connect(void) +{ + DBusError err; + DBusConnection *connection; + + dbus_error_init(&err); + connection = dbus_bus_get(DBUS_BUS_SYSTEM, &err); + if (dbus_error_is_set(&err)) { + crm_err("Could not connect to DBus: %s", err.message); + dbus_error_free(&err); + return NULL; + } + if (connection == NULL) { + return NULL; + } + + /* Tell libdbus not to exit the process when a disconnect happens. This + * defaults to FALSE but is toggled on by the dbus_bus_get() call above. + */ + dbus_connection_set_exit_on_disconnect(connection, FALSE); + + // Set custom handlers for various situations + register_timer_functions(connection); + register_watch_functions(connection); + dbus_connection_set_dispatch_status_function(connection, + update_dispatch_status, + NULL, NULL); + + // Call the dispatch function to check for any messages waiting already + update_dispatch_status(connection, + dbus_connection_get_dispatch_status(connection), + NULL); + return connection; +} + +void +pcmk_dbus_disconnect(DBusConnection *connection) +{ + /* Per the DBus documentation, connections created with + * dbus_connection_open() are owned by libdbus and should never be closed. + * + * @TODO Should we call dbus_connection_unref() here? + */ + return; +} + +// Custom DBus error names to use +#define ERR_NO_REQUEST "org.clusterlabs.pacemaker.NoRequest" +#define ERR_NO_REPLY "org.clusterlabs.pacemaker.NoReply" +#define ERR_INVALID_REPLY "org.clusterlabs.pacemaker.InvalidReply" +#define ERR_INVALID_REPLY_METHOD "org.clusterlabs.pacemaker.InvalidReply.Method" +#define ERR_INVALID_REPLY_SIGNAL "org.clusterlabs.pacemaker.InvalidReply.Signal" +#define ERR_INVALID_REPLY_TYPE "org.clusterlabs.pacemaker.InvalidReply.Type" +#define ERR_SEND_FAILED "org.clusterlabs.pacemaker.SendFailed" + +/*! + * \internal + * \brief Check whether a DBus reply indicates an error occurred + * + * \param[in] pending If non-NULL, indicates that a DBus request was sent + * \param[in] reply Reply received from DBus + * \param[out] ret If non-NULL, will be set to DBus error, if any + * + * \return TRUE if an error was found, FALSE otherwise + * + * \note Following the DBus API convention, a TRUE return is exactly equivalent + * to ret being set. If ret is provided and this function returns TRUE, + * the caller is responsible for calling dbus_error_free() on ret when + * done using it. + */ +bool +pcmk_dbus_find_error(const DBusPendingCall *pending, DBusMessage *reply, + DBusError *ret) +{ + DBusError error; + + dbus_error_init(&error); + + if (pending == NULL) { + dbus_set_error_const(&error, ERR_NO_REQUEST, "No request sent"); + + } else if (reply == NULL) { + dbus_set_error_const(&error, ERR_NO_REPLY, "No reply"); + + } else { + DBusMessageIter args; + int dtype = dbus_message_get_type(reply); + + switch (dtype) { + case DBUS_MESSAGE_TYPE_METHOD_RETURN: + { + char *sig = NULL; + + dbus_message_iter_init(reply, &args); + crm_trace("Received DBus reply with argument type '%s'", + (sig = dbus_message_iter_get_signature(&args))); + if (sig != NULL) { + dbus_free(sig); + } + } + break; + case DBUS_MESSAGE_TYPE_INVALID: + dbus_set_error_const(&error, ERR_INVALID_REPLY, + "Invalid reply"); + break; + case DBUS_MESSAGE_TYPE_METHOD_CALL: + dbus_set_error_const(&error, ERR_INVALID_REPLY_METHOD, + "Invalid reply (method call)"); + break; + case DBUS_MESSAGE_TYPE_SIGNAL: + dbus_set_error_const(&error, ERR_INVALID_REPLY_SIGNAL, + "Invalid reply (signal)"); + break; + case DBUS_MESSAGE_TYPE_ERROR: + dbus_set_error_from_message(&error, reply); + break; + default: + dbus_set_error(&error, ERR_INVALID_REPLY_TYPE, + "Unknown reply type %d", dtype); + } + } + + if (dbus_error_is_set(&error)) { + crm_trace("DBus reply indicated error '%s' (%s)", + error.name, error.message); + if (ret) { + dbus_error_init(ret); + dbus_move_error(&error, ret); + } else { + dbus_error_free(&error); + } + return TRUE; + } + + return FALSE; +} + +/*! + * \internal + * \brief Send a DBus request and wait for the reply + * + * \param[in,out] msg DBus request to send + * \param[in,out] connection DBus connection to use + * \param[out] error If non-NULL, will be set to error, if any + * \param[in] timeout Timeout to use for request + * + * \return DBus reply + * + * \note If error is non-NULL, it is initialized, so the caller may always use + * dbus_error_is_set() to determine whether an error occurred; the caller + * is responsible for calling dbus_error_free() in this case. + */ +DBusMessage * +pcmk_dbus_send_recv(DBusMessage *msg, DBusConnection *connection, + DBusError *error, int timeout) +{ + const char *method = NULL; + DBusMessage *reply = NULL; + DBusPendingCall* pending = NULL; + + CRM_ASSERT(dbus_message_get_type (msg) == DBUS_MESSAGE_TYPE_METHOD_CALL); + method = dbus_message_get_member (msg); + + /* Ensure caller can reliably check whether error is set */ + if (error) { + dbus_error_init(error); + } + + if (timeout <= 0) { + /* DBUS_TIMEOUT_USE_DEFAULT (-1) tells DBus to use a sane default */ + timeout = DBUS_TIMEOUT_USE_DEFAULT; + } + + // send message and get a handle for a reply + if (!dbus_connection_send_with_reply(connection, msg, &pending, timeout)) { + if (error) { + dbus_set_error(error, ERR_SEND_FAILED, + "Could not queue DBus '%s' request", method); + } + return NULL; + } + + dbus_connection_flush(connection); + + if (pending) { + /* block until we receive a reply */ + dbus_pending_call_block(pending); + + /* get the reply message */ + reply = dbus_pending_call_steal_reply(pending); + } + + (void) pcmk_dbus_find_error(pending, reply, error); + + if (pending) { + /* free the pending message handle */ + dbus_pending_call_unref(pending); + } + + return reply; +} + +/*! + * \internal + * \brief Send a DBus message with a callback for the reply + * + * \param[in,out] msg DBus message to send + * \param[in,out] connection DBus connection to send on + * \param[in] done Function to call when pending call completes + * \param[in] user_data Data to pass to done callback + * + * \return Handle for reply on success, NULL on error + * \note The caller can assume that the done callback is called always and + * only when the return value is non-NULL. (This allows the caller to + * know where it should free dynamically allocated user_data.) + */ +DBusPendingCall * +pcmk_dbus_send(DBusMessage *msg, DBusConnection *connection, + void (*done)(DBusPendingCall *pending, void *user_data), + void *user_data, int timeout) +{ + const char *method = NULL; + DBusPendingCall* pending = NULL; + + CRM_ASSERT(done); + CRM_ASSERT(dbus_message_get_type(msg) == DBUS_MESSAGE_TYPE_METHOD_CALL); + method = dbus_message_get_member(msg); + + if (timeout <= 0) { + /* DBUS_TIMEOUT_USE_DEFAULT (-1) tells DBus to use a sane default */ + timeout = DBUS_TIMEOUT_USE_DEFAULT; + } + + // send message and get a handle for a reply + if (!dbus_connection_send_with_reply(connection, msg, &pending, timeout)) { + crm_err("Could not send DBus %s message: failed", method); + return NULL; + + } else if (pending == NULL) { + crm_err("Could not send DBus %s message: connection may be closed", + method); + return NULL; + } + + if (dbus_pending_call_get_completed(pending)) { + crm_info("DBus %s message completed too soon", method); + /* Calling done() directly in this case instead of setting notify below + * breaks things + */ + } + if (!dbus_pending_call_set_notify(pending, done, user_data, NULL)) { + return NULL; + } + return pending; +} + +bool +pcmk_dbus_type_check(DBusMessage *msg, DBusMessageIter *field, int expected, + const char *function, int line) +{ + int dtype = 0; + DBusMessageIter lfield; + + if (field == NULL) { + if (dbus_message_iter_init(msg, &lfield)) { + field = &lfield; + } + } + + if (field == NULL) { + do_crm_log_alias(LOG_INFO, __FILE__, function, line, + "DBus reply has empty parameter list (expected '%c')", + expected); + return FALSE; + } + + dtype = dbus_message_iter_get_arg_type(field); + + if (dtype != expected) { + DBusMessageIter args; + char *sig; + + dbus_message_iter_init(msg, &args); + sig = dbus_message_iter_get_signature(&args); + do_crm_log_alias(LOG_INFO, __FILE__, function, line, + "DBus reply has unexpected type " + "(expected '%c' not '%c' in '%s')", + expected, dtype, sig); + dbus_free(sig); + return FALSE; + } + + return TRUE; +} + + +/* + * Property queries + */ + +/* DBus APIs often provide queryable properties that use this standard + * interface. See: + * https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties + */ +#define BUS_PROPERTY_IFACE "org.freedesktop.DBus.Properties" + +// Callback prototype for when a DBus property query result is received +typedef void (*property_callback_func)(const char *name, // Property name + const char *value, // Property value + void *userdata); // Caller-provided data + +// Data needed by DBus property queries +struct property_query { + char *name; // Property name being queried + char *target; // Name of DBus bus that query should be sent to + char *object; // DBus object path for object with the property + void *userdata; // Caller-provided data to supply to callback + property_callback_func callback; // Function to call when result is received +}; + +static void +free_property_query(struct property_query *data) +{ + free(data->target); + free(data->object); + free(data->name); + free(data); +} + +static char * +handle_query_result(DBusMessage *reply, struct property_query *data) +{ + DBusError error; + char *output = NULL; + DBusMessageIter args; + DBusMessageIter variant_iter; + DBusBasicValue value; + + // First, check if the reply contains an error + if (pcmk_dbus_find_error((void*)&error, reply, &error)) { + crm_err("DBus query for %s property '%s' failed: %s", + data->object, data->name, error.message); + dbus_error_free(&error); + goto cleanup; + } + + // The lone output argument should be a DBus variant type + dbus_message_iter_init(reply, &args); + if (!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_VARIANT, + __func__, __LINE__)) { + crm_err("DBus query for %s property '%s' failed: Unexpected reply type", + data->object, data->name); + goto cleanup; + } + + // The variant should be a string + dbus_message_iter_recurse(&args, &variant_iter); + if (!pcmk_dbus_type_check(reply, &variant_iter, DBUS_TYPE_STRING, + __func__, __LINE__)) { + crm_err("DBus query for %s property '%s' failed: " + "Unexpected variant type", data->object, data->name); + goto cleanup; + } + dbus_message_iter_get_basic(&variant_iter, &value); + + // There should be no more arguments (in variant or reply) + dbus_message_iter_next(&variant_iter); + if (dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_INVALID) { + crm_err("DBus query for %s property '%s' failed: " + "Too many arguments in reply", + data->object, data->name); + goto cleanup; + } + dbus_message_iter_next(&args); + if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_INVALID) { + crm_err("DBus query for %s property '%s' failed: " + "Too many arguments in reply", data->object, data->name); + goto cleanup; + } + + crm_trace("DBus query result for %s: %s='%s'", + data->object, data->name, (value.str? value.str : "")); + + if (data->callback) { // Query was asynchronous + data->callback(data->name, (value.str? value.str : ""), data->userdata); + + } else { // Query was synchronous + output = strdup(value.str? value.str : ""); + } + + cleanup: + free_property_query(data); + return output; +} + +static void +async_query_result_cb(DBusPendingCall *pending, void *user_data) +{ + DBusMessage *reply = NULL; + char *value = NULL; + + if (pending) { + reply = dbus_pending_call_steal_reply(pending); + } + + value = handle_query_result(reply, user_data); + free(value); + + if (reply) { + dbus_message_unref(reply); + } +} + +/*! + * \internal + * \brief Query a property on a DBus object + * + * \param[in,out] connection An active connection to DBus + * \param[in] target DBus name that the query should be sent to + * \param[in] obj DBus object path for object with the property + * \param[in] iface DBus interface for property to query + * \param[in] name Name of property to query + * \param[in] callback If not NULL, perform query asynchronously and call + * this function when query completes + * \param[in,out] userdata Caller-provided data to provide to \p callback + * \param[out] pending If \p callback is not NULL, this will be set to + * handle for the reply (or NULL on error) + * \param[in] timeout Abort query if it takes longer than this (ms) + * + * \return NULL if \p callback is non-NULL (i.e. asynchronous), otherwise a + * newly allocated string with property value + * \note It is the caller's responsibility to free the result with free(). + */ +char * +pcmk_dbus_get_property(DBusConnection *connection, const char *target, + const char *obj, const gchar * iface, const char *name, + property_callback_func callback, void *userdata, + DBusPendingCall **pending, int timeout) +{ + DBusMessage *msg; + char *output = NULL; + struct property_query *query_data = NULL; + + CRM_CHECK((connection != NULL) && (target != NULL) && (obj != NULL) + && (iface != NULL) && (name != NULL), return NULL); + + crm_trace("Querying DBus %s for %s property '%s'", + target, obj, name); + + // Create a new message to use to invoke method + msg = dbus_message_new_method_call(target, obj, BUS_PROPERTY_IFACE, "Get"); + if (msg == NULL) { + crm_err("DBus query for %s property '%s' failed: " + "Unable to create message", obj, name); + return NULL; + } + + // Add the interface name and property name as message arguments + if (!dbus_message_append_args(msg, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)) { + crm_err("DBus query for %s property '%s' failed: " + "Could not append arguments", obj, name); + dbus_message_unref(msg); + return NULL; + } + + query_data = malloc(sizeof(struct property_query)); + if (query_data == NULL) { + crm_crit("DBus query for %s property '%s' failed: Out of memory", + obj, name); + dbus_message_unref(msg); + return NULL; + } + + query_data->target = strdup(target); + query_data->object = strdup(obj); + query_data->callback = callback; + query_data->userdata = userdata; + query_data->name = strdup(name); + CRM_CHECK((query_data->target != NULL) + && (query_data->object != NULL) + && (query_data->name != NULL), + free_property_query(query_data); + dbus_message_unref(msg); + return NULL); + + if (query_data->callback) { // Asynchronous + DBusPendingCall *local_pending; + + local_pending = pcmk_dbus_send(msg, connection, async_query_result_cb, + query_data, timeout); + if (local_pending == NULL) { + // async_query_result_cb() was not called in this case + free_property_query(query_data); + query_data = NULL; + } + + if (pending) { + *pending = local_pending; + } + + } else { // Synchronous + DBusMessage *reply = pcmk_dbus_send_recv(msg, connection, NULL, + timeout); + + output = handle_query_result(reply, query_data); + + if (reply) { + dbus_message_unref(reply); + } + } + + dbus_message_unref(msg); + + return output; +} diff --git a/lib/services/pcmk-dbus.h b/lib/services/pcmk-dbus.h new file mode 100644 index 0000000..a9d0cbc --- /dev/null +++ b/lib/services/pcmk-dbus.h @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PCMK_DBUS__H +# define PCMK_DBUS__H + +# include + +# ifndef DBUS_TIMEOUT_USE_DEFAULT +# define DBUS_TIMEOUT_USE_DEFAULT -1 +# endif + +G_GNUC_INTERNAL +DBusConnection *pcmk_dbus_connect(void); + +G_GNUC_INTERNAL +void pcmk_dbus_disconnect(DBusConnection *connection); + +G_GNUC_INTERNAL +DBusPendingCall *pcmk_dbus_send(DBusMessage *msg, DBusConnection *connection, + void(*done)(DBusPendingCall *pending, void *user_data), void *user_data, int timeout); + +G_GNUC_INTERNAL +DBusMessage *pcmk_dbus_send_recv(DBusMessage *msg, DBusConnection *connection, DBusError *error, int timeout); + +G_GNUC_INTERNAL +bool pcmk_dbus_type_check(DBusMessage *msg, DBusMessageIter *field, int expected, const char *function, int line); + +G_GNUC_INTERNAL +char *pcmk_dbus_get_property( + DBusConnection *connection, const char *target, const char *obj, const gchar * iface, const char *name, + void (*callback)(const char *name, const char *value, void *userdata), void *userdata, + DBusPendingCall **pending, int timeout); + +G_GNUC_INTERNAL +bool pcmk_dbus_find_error(const DBusPendingCall *pending, DBusMessage *reply, + DBusError *error); + +#endif /* PCMK_DBUS__H */ diff --git a/lib/services/services.c b/lib/services/services.c new file mode 100644 index 0000000..b60d8bd --- /dev/null +++ b/lib/services/services.c @@ -0,0 +1,1417 @@ +/* + * Copyright 2010-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include "services_private.h" +#include "services_ocf.h" +#include "services_lsb.h" + +#if SUPPORT_UPSTART +# include +#endif + +#if SUPPORT_SYSTEMD +# include +#endif + +#if SUPPORT_NAGIOS +# include +#endif + +/* TODO: Develop a rollover strategy */ + +static int operations = 0; +static GHashTable *recurring_actions = NULL; + +/* ops waiting to run async because of conflicting active + * pending ops */ +static GList *blocked_ops = NULL; + +/* ops currently active (in-flight) */ +static GList *inflight_ops = NULL; + +static void handle_blocked_ops(void); + +/*! + * \brief Find first service class that can provide a specified agent + * + * \param[in] agent Name of agent to search for + * + * \return Service class if found, NULL otherwise + * + * \note The priority is LSB, then systemd, then upstart. It would be preferable + * to put systemd first, but LSB merely requires a file existence check, + * while systemd requires contacting D-Bus. + */ +const char * +resources_find_service_class(const char *agent) +{ + if (services__lsb_agent_exists(agent)) { + return PCMK_RESOURCE_CLASS_LSB; + } + +#if SUPPORT_SYSTEMD + if (systemd_unit_exists(agent)) { + return PCMK_RESOURCE_CLASS_SYSTEMD; + } +#endif + +#if SUPPORT_UPSTART + if (upstart_job_exists(agent)) { + return PCMK_RESOURCE_CLASS_UPSTART; + } +#endif + return NULL; +} + +static inline void +init_recurring_actions(void) +{ + if (recurring_actions == NULL) { + recurring_actions = pcmk__strkey_table(NULL, NULL); + } +} + +/*! + * \internal + * \brief Check whether op is in-flight systemd or upstart op + * + * \param[in] op Operation to check + * + * \return TRUE if op is in-flight systemd or upstart op + */ +static inline gboolean +inflight_systemd_or_upstart(const svc_action_t *op) +{ + return pcmk__strcase_any_of(op->standard, PCMK_RESOURCE_CLASS_SYSTEMD, + PCMK_RESOURCE_CLASS_UPSTART, NULL) && + g_list_find(inflight_ops, op) != NULL; +} + +/*! + * \internal + * \brief Expand "service" alias to an actual resource class + * + * \param[in] rsc Resource name (for logging only) + * \param[in] standard Resource class as configured + * \param[in] agent Agent name to look for + * + * \return Newly allocated string with actual resource class + * + * \note The caller is responsible for calling free() on the result. + */ +static char * +expand_resource_class(const char *rsc, const char *standard, const char *agent) +{ + char *expanded_class = NULL; + + if (strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE) == 0) { + const char *found_class = resources_find_service_class(agent); + + if (found_class) { + crm_debug("Found %s agent %s for %s", found_class, agent, rsc); + expanded_class = strdup(found_class); + } else { + crm_info("Assuming resource class lsb for agent %s for %s", + agent, rsc); + expanded_class = strdup(PCMK_RESOURCE_CLASS_LSB); + } + } else { + expanded_class = strdup(standard); + } + CRM_ASSERT(expanded_class); + return expanded_class; +} + +/*! + * \internal + * \brief Create a simple svc_action_t instance + * + * \return Newly allocated instance (or NULL if not enough memory) + */ +static svc_action_t * +new_action(void) +{ + svc_action_t *op = calloc(1, sizeof(svc_action_t)); + + if (op == NULL) { + return NULL; + } + + op->opaque = calloc(1, sizeof(svc_action_private_t)); + if (op->opaque == NULL) { + free(op); + return NULL; + } + + // Initialize result + services__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_UNKNOWN, NULL); + return op; +} + +static bool +required_argument_missing(uint32_t ra_caps, const char *name, + const char *standard, const char *provider, + const char *agent, const char *action) +{ + if (pcmk__str_empty(name)) { + crm_info("Cannot create operation without resource name (bug?)"); + return true; + } + + if (pcmk__str_empty(standard)) { + crm_info("Cannot create operation for %s without resource class (bug?)", + name); + return true; + } + + if (pcmk_is_set(ra_caps, pcmk_ra_cap_provider) + && pcmk__str_empty(provider)) { + crm_info("Cannot create operation for %s resource %s " + "without provider (bug?)", standard, name); + return true; + } + + if (pcmk__str_empty(agent)) { + crm_info("Cannot create operation for %s without agent name (bug?)", + name); + return true; + } + + if (pcmk__str_empty(action)) { + crm_info("Cannot create operation for %s without action name (bug?)", + name); + return true; + } + return false; +} + +// \return Standard Pacemaker return code (pcmk_rc_ok or ENOMEM) +static int +copy_action_arguments(svc_action_t *op, uint32_t ra_caps, const char *name, + const char *standard, const char *provider, + const char *agent, const char *action) +{ + op->rsc = strdup(name); + if (op->rsc == NULL) { + return ENOMEM; + } + + op->agent = strdup(agent); + if (op->agent == NULL) { + return ENOMEM; + } + + op->standard = expand_resource_class(name, standard, agent); + if (op->standard == NULL) { + return ENOMEM; + } + + if (pcmk_is_set(ra_caps, pcmk_ra_cap_status) + && pcmk__str_eq(action, "monitor", pcmk__str_casei)) { + action = "status"; + } + op->action = strdup(action); + if (op->action == NULL) { + return ENOMEM; + } + + if (pcmk_is_set(ra_caps, pcmk_ra_cap_provider)) { + op->provider = strdup(provider); + if (op->provider == NULL) { + return ENOMEM; + } + } + return pcmk_rc_ok; +} + +svc_action_t * +services__create_resource_action(const char *name, const char *standard, + const char *provider, const char *agent, + const char *action, guint interval_ms, int timeout, + GHashTable *params, enum svc_action_flags flags) +{ + svc_action_t *op = NULL; + uint32_t ra_caps = pcmk_get_ra_caps(standard); + int rc = pcmk_rc_ok; + + op = new_action(); + if (op == NULL) { + crm_crit("Cannot prepare action: %s", strerror(ENOMEM)); + if (params != NULL) { + g_hash_table_destroy(params); + } + return NULL; + } + + op->interval_ms = interval_ms; + op->timeout = timeout; + op->flags = flags; + op->sequence = ++operations; + + // Take ownership of params + if (pcmk_is_set(ra_caps, pcmk_ra_cap_params)) { + op->params = params; + } else if (params != NULL) { + g_hash_table_destroy(params); + params = NULL; + } + + if (required_argument_missing(ra_caps, name, standard, provider, agent, + action)) { + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR_FATAL, + "Required agent or action information missing"); + return op; + } + + op->id = pcmk__op_key(name, action, interval_ms); + + if (copy_action_arguments(op, ra_caps, name, standard, provider, agent, + action) != pcmk_rc_ok) { + crm_crit("Cannot prepare %s action for %s: %s", + action, name, strerror(ENOMEM)); + services__handle_exec_error(op, ENOMEM); + return op; + } + + if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_OCF) == 0) { + rc = services__ocf_prepare(op); + + } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_LSB) == 0) { + rc = services__lsb_prepare(op); + +#if SUPPORT_SYSTEMD + } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_SYSTEMD) == 0) { + rc = services__systemd_prepare(op); +#endif +#if SUPPORT_UPSTART + } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_UPSTART) == 0) { + rc = services__upstart_prepare(op); +#endif +#if SUPPORT_NAGIOS + } else if (strcasecmp(op->standard, PCMK_RESOURCE_CLASS_NAGIOS) == 0) { + rc = services__nagios_prepare(op); +#endif + } else { + crm_info("Unknown resource standard: %s", op->standard); + rc = ENOENT; + } + + if (rc != pcmk_rc_ok) { + crm_info("Cannot prepare %s operation for %s: %s", + action, name, strerror(rc)); + services__handle_exec_error(op, rc); + } + return op; +} + +svc_action_t * +resources_action_create(const char *name, const char *standard, + const char *provider, const char *agent, + const char *action, guint interval_ms, int timeout, + GHashTable *params, enum svc_action_flags flags) +{ + svc_action_t *op = services__create_resource_action(name, standard, + provider, agent, action, interval_ms, timeout, + params, flags); + if (op == NULL || op->rc != 0) { + services_action_free(op); + return NULL; + } else { + // Preserve public API backward compatibility + op->rc = PCMK_OCF_OK; + op->status = PCMK_EXEC_DONE; + + return op; + } +} + +svc_action_t * +services_action_create_generic(const char *exec, const char *args[]) +{ + svc_action_t *op = new_action(); + + CRM_ASSERT(op != NULL); + + op->opaque->exec = strdup(exec); + op->opaque->args[0] = strdup(exec); + if ((op->opaque->exec == NULL) || (op->opaque->args[0] == NULL)) { + crm_crit("Cannot prepare action for '%s': %s", exec, strerror(ENOMEM)); + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + strerror(ENOMEM)); + return op; + } + + if (args == NULL) { + return op; + } + + for (int cur_arg = 1; args[cur_arg - 1] != NULL; cur_arg++) { + + if (cur_arg == PCMK__NELEM(op->opaque->args)) { + crm_info("Cannot prepare action for '%s': Too many arguments", + exec); + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, + PCMK_EXEC_ERROR_HARD, "Too many arguments"); + break; + } + + op->opaque->args[cur_arg] = strdup(args[cur_arg - 1]); + if (op->opaque->args[cur_arg] == NULL) { + crm_crit("Cannot prepare action for '%s': %s", + exec, strerror(ENOMEM)); + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + strerror(ENOMEM)); + break; + } + } + + return op; +} + +/*! + * \brief Create an alert agent action + * + * \param[in] id Alert ID + * \param[in] exec Path to alert agent executable + * \param[in] timeout Action timeout + * \param[in] params Parameters to use with action + * \param[in] sequence Action sequence number + * \param[in] cb_data Data to pass to callback function + * + * \return New action on success, NULL on error + * \note It is the caller's responsibility to free cb_data. + * The caller should not free params explicitly. + */ +svc_action_t * +services_alert_create(const char *id, const char *exec, int timeout, + GHashTable *params, int sequence, void *cb_data) +{ + svc_action_t *action = services_action_create_generic(exec, NULL); + + action->id = strdup(id); + action->standard = strdup(PCMK_RESOURCE_CLASS_ALERT); + CRM_ASSERT((action->id != NULL) && (action->standard != NULL)); + + action->timeout = timeout; + action->params = params; + action->sequence = sequence; + action->cb_data = cb_data; + return action; +} + +/*! + * \brief Set the user and group that an action will execute as + * + * \param[in,out] op Action to modify + * \param[in] user Name of user to execute action as + * \param[in] group Name of group to execute action as + * + * \return pcmk_ok on success, -errno otherwise + * + * \note This will have no effect unless the process executing the action runs + * as root, and the action is not a systemd or upstart action. + * We could implement this for systemd by adding User= and Group= to + * [Service] in the override file, but that seems more likely to cause + * problems than be useful. + */ +int +services_action_user(svc_action_t *op, const char *user) +{ + CRM_CHECK((op != NULL) && (user != NULL), return -EINVAL); + return crm_user_lookup(user, &(op->opaque->uid), &(op->opaque->gid)); +} + +/*! + * \brief Execute an alert agent action + * + * \param[in,out] action Action to execute + * \param[in] cb Function to call when action completes + * + * \return TRUE if the library will free action, FALSE otherwise + * + * \note If this function returns FALSE, it is the caller's responsibility to + * free the action with services_action_free(). However, unless someone + * intentionally creates a recurring alert action, this will never return + * FALSE. + */ +gboolean +services_alert_async(svc_action_t *action, void (*cb)(svc_action_t *op)) +{ + action->synchronous = false; + action->opaque->callback = cb; + return services__execute_file(action) == pcmk_rc_ok; +} + +#if HAVE_DBUS +/*! + * \internal + * \brief Update operation's pending DBus call, unreferencing old one if needed + * + * \param[in,out] op Operation to modify + * \param[in] pending Pending call to set + */ +void +services_set_op_pending(svc_action_t *op, DBusPendingCall *pending) +{ + if (op->opaque->pending && (op->opaque->pending != pending)) { + if (pending) { + crm_info("Lost pending %s DBus call (%p)", op->id, op->opaque->pending); + } else { + crm_trace("Done with pending %s DBus call (%p)", op->id, op->opaque->pending); + } + dbus_pending_call_unref(op->opaque->pending); + } + op->opaque->pending = pending; + if (pending) { + crm_trace("Updated pending %s DBus call (%p)", op->id, pending); + } else { + crm_trace("Cleared pending %s DBus call", op->id); + } +} +#endif + +void +services_action_cleanup(svc_action_t * op) +{ + if ((op == NULL) || (op->opaque == NULL)) { + return; + } + +#if HAVE_DBUS + if(op->opaque->timerid != 0) { + crm_trace("Removing timer for call %s to %s", op->action, op->rsc); + g_source_remove(op->opaque->timerid); + op->opaque->timerid = 0; + } + + if(op->opaque->pending) { + if (dbus_pending_call_get_completed(op->opaque->pending)) { + // This should never be the case + crm_warn("Result of %s op %s was unhandled", + op->standard, op->id); + } else { + crm_debug("Will ignore any result of canceled %s op %s", + op->standard, op->id); + } + dbus_pending_call_cancel(op->opaque->pending); + services_set_op_pending(op, NULL); + } +#endif + + if (op->opaque->stderr_gsource) { + mainloop_del_fd(op->opaque->stderr_gsource); + op->opaque->stderr_gsource = NULL; + } + + if (op->opaque->stdout_gsource) { + mainloop_del_fd(op->opaque->stdout_gsource); + op->opaque->stdout_gsource = NULL; + } +} + +/*! + * \internal + * \brief Map an actual resource action result to a standard OCF result + * + * \param[in] standard Agent standard (must not be "service") + * \param[in] action Action that result is for + * \param[in] exit_status Actual agent exit status + * + * \return Standard OCF result + */ +enum ocf_exitcode +services_result2ocf(const char *standard, const char *action, int exit_status) +{ + if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { + return services__ocf2ocf(exit_status); + +#if SUPPORT_SYSTEMD + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_SYSTEMD, + pcmk__str_casei)) { + return services__systemd2ocf(exit_status); +#endif + +#if SUPPORT_UPSTART + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_UPSTART, + pcmk__str_casei)) { + return services__upstart2ocf(exit_status); +#endif + +#if SUPPORT_NAGIOS + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_NAGIOS, + pcmk__str_casei)) { + return services__nagios2ocf(exit_status); +#endif + + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_LSB, + pcmk__str_casei)) { + return services__lsb2ocf(action, exit_status); + + } else { + crm_warn("Treating result from unknown standard '%s' as OCF", + ((standard == NULL)? "unspecified" : standard)); + return services__ocf2ocf(exit_status); + } +} + +void +services_action_free(svc_action_t * op) +{ + unsigned int i; + + if (op == NULL) { + return; + } + + /* The operation should be removed from all tracking lists by this point. + * If it's not, we have a bug somewhere, so bail. That may lead to a + * memory leak, but it's better than a use-after-free segmentation fault. + */ + CRM_CHECK(g_list_find(inflight_ops, op) == NULL, return); + CRM_CHECK(g_list_find(blocked_ops, op) == NULL, return); + CRM_CHECK((recurring_actions == NULL) + || (g_hash_table_lookup(recurring_actions, op->id) == NULL), + return); + + services_action_cleanup(op); + + if (op->opaque->repeat_timer) { + g_source_remove(op->opaque->repeat_timer); + op->opaque->repeat_timer = 0; + } + + free(op->id); + free(op->opaque->exec); + + for (i = 0; i < PCMK__NELEM(op->opaque->args); i++) { + free(op->opaque->args[i]); + } + + free(op->opaque->exit_reason); + free(op->opaque); + free(op->rsc); + free(op->action); + + free(op->standard); + free(op->agent); + free(op->provider); + + free(op->stdout_data); + free(op->stderr_data); + + if (op->params) { + g_hash_table_destroy(op->params); + op->params = NULL; + } + + free(op); +} + +gboolean +cancel_recurring_action(svc_action_t * op) +{ + crm_info("Cancelling %s operation %s", op->standard, op->id); + + if (recurring_actions) { + g_hash_table_remove(recurring_actions, op->id); + } + + if (op->opaque->repeat_timer) { + g_source_remove(op->opaque->repeat_timer); + op->opaque->repeat_timer = 0; + } + + return TRUE; +} + +/*! + * \brief Cancel a recurring action + * + * \param[in] name Name of resource that operation is for + * \param[in] action Name of operation to cancel + * \param[in] interval_ms Interval of operation to cancel + * + * \return TRUE if action was successfully cancelled, FALSE otherwise + */ +gboolean +services_action_cancel(const char *name, const char *action, guint interval_ms) +{ + gboolean cancelled = FALSE; + char *id = pcmk__op_key(name, action, interval_ms); + svc_action_t *op = NULL; + + /* We can only cancel a recurring action */ + init_recurring_actions(); + op = g_hash_table_lookup(recurring_actions, id); + if (op == NULL) { + goto done; + } + + // Tell services__finalize_async_op() not to reschedule the operation + op->cancel = TRUE; + + /* Stop tracking it as a recurring operation, and stop its repeat timer */ + cancel_recurring_action(op); + + /* If the op has a PID, it's an in-flight child process, so kill it. + * + * Whether the kill succeeds or fails, the main loop will send the op to + * async_action_complete() (and thus services__finalize_async_op()) when the + * process goes away. + */ + if (op->pid != 0) { + crm_info("Terminating in-flight op %s[%d] early because it was cancelled", + id, op->pid); + cancelled = mainloop_child_kill(op->pid); + if (cancelled == FALSE) { + crm_err("Termination of %s[%d] failed", id, op->pid); + } + goto done; + } + +#if HAVE_DBUS + // In-flight systemd and upstart ops don't have a pid + if (inflight_systemd_or_upstart(op)) { + inflight_ops = g_list_remove(inflight_ops, op); + + /* This will cause any result that comes in later to be discarded, so we + * don't call the callback and free the operation twice. + */ + services_action_cleanup(op); + } +#endif + + /* The rest of this is essentially equivalent to + * services__finalize_async_op(), minus the handle_blocked_ops() call. + */ + + // Report operation as cancelled + services__set_cancelled(op); + if (op->opaque->callback) { + op->opaque->callback(op); + } + + blocked_ops = g_list_remove(blocked_ops, op); + services_action_free(op); + cancelled = TRUE; + // @TODO Initiate handle_blocked_ops() asynchronously + +done: + free(id); + return cancelled; +} + +gboolean +services_action_kick(const char *name, const char *action, guint interval_ms) +{ + svc_action_t * op = NULL; + char *id = pcmk__op_key(name, action, interval_ms); + + init_recurring_actions(); + op = g_hash_table_lookup(recurring_actions, id); + free(id); + + if (op == NULL) { + return FALSE; + } + + + if (op->pid || inflight_systemd_or_upstart(op)) { + return TRUE; + } else { + if (op->opaque->repeat_timer) { + g_source_remove(op->opaque->repeat_timer); + op->opaque->repeat_timer = 0; + } + recurring_action_timer(op); + return TRUE; + } + +} + +/*! + * \internal + * \brief Add a new recurring operation, checking for duplicates + * + * \param[in,out] op Operation to add + * + * \return TRUE if duplicate found (and reschedule), FALSE otherwise + */ +static gboolean +handle_duplicate_recurring(svc_action_t *op) +{ + svc_action_t * dup = NULL; + + /* check for duplicates */ + dup = g_hash_table_lookup(recurring_actions, op->id); + + if (dup && (dup != op)) { + /* update user data */ + if (op->opaque->callback) { + dup->opaque->callback = op->opaque->callback; + dup->cb_data = op->cb_data; + op->cb_data = NULL; + } + /* immediately execute the next interval */ + if (dup->pid != 0) { + if (op->opaque->repeat_timer) { + g_source_remove(op->opaque->repeat_timer); + op->opaque->repeat_timer = 0; + } + recurring_action_timer(dup); + } + /* free the duplicate */ + services_action_free(op); + return TRUE; + } + + return FALSE; +} + +/*! + * \internal + * \brief Execute an action appropriately according to its standard + * + * \param[in,out] op Action to execute + * + * \return Standard Pacemaker return code + * \retval EBUSY Recurring operation could not be initiated + * \retval pcmk_rc_error Synchronous action failed + * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action + * should not be freed (because it's pending or because + * it failed to execute and was already freed) + * + * \note If the return value for an asynchronous action is not pcmk_rc_ok, the + * caller is responsible for freeing the action. + */ +static int +execute_action(svc_action_t *op) +{ +#if SUPPORT_UPSTART + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_UPSTART, + pcmk__str_casei)) { + return services__execute_upstart(op); + } +#endif + +#if SUPPORT_SYSTEMD + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_SYSTEMD, + pcmk__str_casei)) { + return services__execute_systemd(op); + } +#endif + + return services__execute_file(op); +} + +void +services_add_inflight_op(svc_action_t * op) +{ + if (op == NULL) { + return; + } + + CRM_ASSERT(op->synchronous == FALSE); + + /* keep track of ops that are in-flight to avoid collisions in the same namespace */ + if (op->rsc) { + inflight_ops = g_list_append(inflight_ops, op); + } +} + +/*! + * \internal + * \brief Stop tracking an operation that completed + * + * \param[in] op Operation to stop tracking + */ +void +services_untrack_op(const svc_action_t *op) +{ + /* Op is no longer in-flight or blocked */ + inflight_ops = g_list_remove(inflight_ops, op); + blocked_ops = g_list_remove(blocked_ops, op); + + /* Op is no longer blocking other ops, so check if any need to run */ + handle_blocked_ops(); +} + +gboolean +services_action_async_fork_notify(svc_action_t * op, + void (*action_callback) (svc_action_t *), + void (*action_fork_callback) (svc_action_t *)) +{ + CRM_CHECK(op != NULL, return TRUE); + + op->synchronous = false; + if (action_callback != NULL) { + op->opaque->callback = action_callback; + } + if (action_fork_callback != NULL) { + op->opaque->fork_callback = action_fork_callback; + } + + if (op->interval_ms > 0) { + init_recurring_actions(); + if (handle_duplicate_recurring(op)) { + /* entry rescheduled, dup freed */ + /* exit early */ + return TRUE; + } + g_hash_table_replace(recurring_actions, op->id, op); + } + + if (!pcmk_is_set(op->flags, SVC_ACTION_NON_BLOCKED) + && op->rsc && is_op_blocked(op->rsc)) { + blocked_ops = g_list_append(blocked_ops, op); + return TRUE; + } + + return execute_action(op) == pcmk_rc_ok; +} + +gboolean +services_action_async(svc_action_t * op, + void (*action_callback) (svc_action_t *)) +{ + return services_action_async_fork_notify(op, action_callback, NULL); +} + +static gboolean processing_blocked_ops = FALSE; + +gboolean +is_op_blocked(const char *rsc) +{ + GList *gIter = NULL; + svc_action_t *op = NULL; + + for (gIter = inflight_ops; gIter != NULL; gIter = gIter->next) { + op = gIter->data; + if (pcmk__str_eq(op->rsc, rsc, pcmk__str_casei)) { + return TRUE; + } + } + + return FALSE; +} + +static void +handle_blocked_ops(void) +{ + GList *executed_ops = NULL; + GList *gIter = NULL; + svc_action_t *op = NULL; + + if (processing_blocked_ops) { + /* avoid nested calling of this function */ + return; + } + + processing_blocked_ops = TRUE; + + /* n^2 operation here, but blocked ops are incredibly rare. this list + * will be empty 99% of the time. */ + for (gIter = blocked_ops; gIter != NULL; gIter = gIter->next) { + op = gIter->data; + if (is_op_blocked(op->rsc)) { + continue; + } + executed_ops = g_list_append(executed_ops, op); + if (execute_action(op) != pcmk_rc_ok) { + /* this can cause this function to be called recursively + * which is why we have processing_blocked_ops static variable */ + services__finalize_async_op(op); + } + } + + for (gIter = executed_ops; gIter != NULL; gIter = gIter->next) { + op = gIter->data; + blocked_ops = g_list_remove(blocked_ops, op); + } + g_list_free(executed_ops); + + processing_blocked_ops = FALSE; +} + +/*! + * \internal + * \brief Execute a meta-data action appropriately to standard + * + * \param[in,out] op Meta-data action to execute + * + * \return Standard Pacemaker return code + */ +static int +execute_metadata_action(svc_action_t *op) +{ + const char *class = op->standard; + + if (op->agent == NULL) { + crm_info("Meta-data requested without specifying agent"); + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR_FATAL, "Agent not specified"); + return EINVAL; + } + + if (class == NULL) { + crm_info("Meta-data requested for agent %s without specifying class", + op->agent); + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR_FATAL, + "Agent standard not specified"); + return EINVAL; + } + + if (!strcmp(class, PCMK_RESOURCE_CLASS_SERVICE)) { + class = resources_find_service_class(op->agent); + } + if (class == NULL) { + crm_info("Meta-data requested for %s, but could not determine class", + op->agent); + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR_HARD, + "Agent standard could not be determined"); + return EINVAL; + } + + if (pcmk__str_eq(class, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei)) { + return pcmk_legacy2rc(services__get_lsb_metadata(op->agent, + &op->stdout_data)); + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(class, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return pcmk_legacy2rc(services__get_nagios_metadata(op->agent, + &op->stdout_data)); + } +#endif + + return execute_action(op); +} + +gboolean +services_action_sync(svc_action_t * op) +{ + gboolean rc = TRUE; + + if (op == NULL) { + crm_trace("No operation to execute"); + return FALSE; + } + + op->synchronous = true; + + if (pcmk__str_eq(op->action, "meta-data", pcmk__str_casei)) { + /* Synchronous meta-data operations are handled specially. Since most + * resource classes don't provide any meta-data, it has to be + * synthesized from available information about the agent. + * + * services_action_async() doesn't treat meta-data actions specially, so + * it will result in an error for classes that don't support the action. + */ + rc = (execute_metadata_action(op) == pcmk_rc_ok); + } else { + rc = (execute_action(op) == pcmk_rc_ok); + } + crm_trace(" > " PCMK__OP_FMT ": %s = %d", + op->rsc, op->action, op->interval_ms, op->opaque->exec, op->rc); + if (op->stdout_data) { + crm_trace(" > stdout: %s", op->stdout_data); + } + if (op->stderr_data) { + crm_trace(" > stderr: %s", op->stderr_data); + } + return rc; +} + +GList * +get_directory_list(const char *root, gboolean files, gboolean executable) +{ + return services_os_get_directory_list(root, files, executable); +} + +GList * +resources_list_standards(void) +{ + GList *standards = NULL; + + standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_OCF)); + standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_LSB)); + standards = g_list_append(standards, strdup(PCMK_RESOURCE_CLASS_SERVICE)); + +#if SUPPORT_SYSTEMD + { + GList *agents = systemd_unit_listall(); + + if (agents != NULL) { + standards = g_list_append(standards, + strdup(PCMK_RESOURCE_CLASS_SYSTEMD)); + g_list_free_full(agents, free); + } + } +#endif + +#if SUPPORT_UPSTART + { + GList *agents = upstart_job_listall(); + + if (agents != NULL) { + standards = g_list_append(standards, + strdup(PCMK_RESOURCE_CLASS_UPSTART)); + g_list_free_full(agents, free); + } + } +#endif + +#if SUPPORT_NAGIOS + { + GList *agents = services__list_nagios_agents(); + + if (agents != NULL) { + standards = g_list_append(standards, + strdup(PCMK_RESOURCE_CLASS_NAGIOS)); + g_list_free_full(agents, free); + } + } +#endif + + return standards; +} + +GList * +resources_list_providers(const char *standard) +{ + if (pcmk_is_set(pcmk_get_ra_caps(standard), pcmk_ra_cap_provider)) { + return resources_os_list_ocf_providers(); + } + + return NULL; +} + +GList * +resources_list_agents(const char *standard, const char *provider) +{ + if ((standard == NULL) + || (strcasecmp(standard, PCMK_RESOURCE_CLASS_SERVICE) == 0)) { + + GList *tmp1; + GList *tmp2; + GList *result = services__list_lsb_agents(); + + if (standard == NULL) { + tmp1 = result; + tmp2 = resources_os_list_ocf_agents(NULL); + if (tmp2) { + result = g_list_concat(tmp1, tmp2); + } + } +#if SUPPORT_SYSTEMD + tmp1 = result; + tmp2 = systemd_unit_listall(); + if (tmp2) { + result = g_list_concat(tmp1, tmp2); + } +#endif + +#if SUPPORT_UPSTART + tmp1 = result; + tmp2 = upstart_job_listall(); + if (tmp2) { + result = g_list_concat(tmp1, tmp2); + } +#endif + + return result; + + } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_OCF) == 0) { + return resources_os_list_ocf_agents(provider); + } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_LSB) == 0) { + return services__list_lsb_agents(); +#if SUPPORT_SYSTEMD + } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_SYSTEMD) == 0) { + return systemd_unit_listall(); +#endif +#if SUPPORT_UPSTART + } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_UPSTART) == 0) { + return upstart_job_listall(); +#endif +#if SUPPORT_NAGIOS + } else if (strcasecmp(standard, PCMK_RESOURCE_CLASS_NAGIOS) == 0) { + return services__list_nagios_agents(); +#endif + } + + return NULL; +} + +gboolean +resources_agent_exists(const char *standard, const char *provider, const char *agent) +{ + GList *standards = NULL; + GList *providers = NULL; + GList *iter = NULL; + gboolean rc = FALSE; + gboolean has_providers = FALSE; + + standards = resources_list_standards(); + for (iter = standards; iter != NULL; iter = iter->next) { + if (pcmk__str_eq(iter->data, standard, pcmk__str_none)) { + rc = TRUE; + break; + } + } + + if (rc == FALSE) { + goto done; + } + + rc = FALSE; + + has_providers = pcmk_is_set(pcmk_get_ra_caps(standard), pcmk_ra_cap_provider); + if (has_providers == TRUE && provider != NULL) { + providers = resources_list_providers(standard); + for (iter = providers; iter != NULL; iter = iter->next) { + if (pcmk__str_eq(iter->data, provider, pcmk__str_none)) { + rc = TRUE; + break; + } + } + } else if (has_providers == FALSE && provider == NULL) { + rc = TRUE; + } + + if (rc == FALSE) { + goto done; + } + + if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_SERVICE, pcmk__str_casei)) { + if (services__lsb_agent_exists(agent)) { + rc = TRUE; +#if SUPPORT_SYSTEMD + } else if (systemd_unit_exists(agent)) { + rc = TRUE; +#endif + +#if SUPPORT_UPSTART + } else if (upstart_job_exists(agent)) { + rc = TRUE; +#endif + } else { + rc = FALSE; + } + + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { + rc = services__ocf_agent_exists(provider, agent); + + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei)) { + rc = services__lsb_agent_exists(agent); + +#if SUPPORT_SYSTEMD + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_SYSTEMD, pcmk__str_casei)) { + rc = systemd_unit_exists(agent); +#endif + +#if SUPPORT_UPSTART + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_UPSTART, pcmk__str_casei)) { + rc = upstart_job_exists(agent); +#endif + +#if SUPPORT_NAGIOS + } else if (pcmk__str_eq(standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + rc = services__nagios_agent_exists(agent); +#endif + + } else { + rc = FALSE; + } + +done: + g_list_free(standards); + g_list_free(providers); + return rc; +} + +/*! + * \internal + * \brief Set the result of an action + * + * \param[out] action Where to set action result + * \param[in] agent_status Exit status to set + * \param[in] exec_status Execution status to set + * \param[in] reason Human-friendly description of event to set + */ +void +services__set_result(svc_action_t *action, int agent_status, + enum pcmk_exec_status exec_status, const char *reason) +{ + if (action == NULL) { + return; + } + + action->rc = agent_status; + action->status = exec_status; + + if (!pcmk__str_eq(action->opaque->exit_reason, reason, + pcmk__str_none)) { + free(action->opaque->exit_reason); + action->opaque->exit_reason = (reason == NULL)? NULL : strdup(reason); + } +} + +/*! + * \internal + * \brief Set the result of an action, with a formatted exit reason + * + * \param[out] action Where to set action result + * \param[in] agent_status Exit status to set + * \param[in] exec_status Execution status to set + * \param[in] format printf-style format for a human-friendly + * description of reason for result + * \param[in] ... arguments for \p format + */ +void +services__format_result(svc_action_t *action, int agent_status, + enum pcmk_exec_status exec_status, + const char *format, ...) +{ + va_list ap; + int len = 0; + char *reason = NULL; + + if (action == NULL) { + return; + } + + action->rc = agent_status; + action->status = exec_status; + + if (format != NULL) { + va_start(ap, format); + len = vasprintf(&reason, format, ap); + CRM_ASSERT(len > 0); + va_end(ap); + } + free(action->opaque->exit_reason); + action->opaque->exit_reason = reason; +} + +/*! + * \internal + * \brief Set the result of an action to cancelled + * + * \param[out] action Where to set action result + * + * \note This sets execution status but leaves the exit status unchanged + */ +void +services__set_cancelled(svc_action_t *action) +{ + if (action != NULL) { + action->status = PCMK_EXEC_CANCELLED; + free(action->opaque->exit_reason); + action->opaque->exit_reason = NULL; + } +} + +/*! + * \internal + * \brief Get a readable description of what an action is for + * + * \param[in] action Action to check + * + * \return Readable name for the kind of \p action + */ +const char * +services__action_kind(const svc_action_t *action) +{ + if ((action == NULL) || (action->standard == NULL)) { + return "Process"; + } else if (pcmk__str_eq(action->standard, PCMK_RESOURCE_CLASS_STONITH, + pcmk__str_none)) { + return "Fence agent"; + } else if (pcmk__str_eq(action->standard, PCMK_RESOURCE_CLASS_ALERT, + pcmk__str_none)) { + return "Alert agent"; + } else { + return "Resource agent"; + } +} + +/*! + * \internal + * \brief Get the exit reason of an action + * + * \param[in] action Action to check + * + * \return Action's exit reason (or NULL if none) + */ +const char * +services__exit_reason(const svc_action_t *action) +{ + return action->opaque->exit_reason; +} + +/*! + * \internal + * \brief Steal stdout from an action + * + * \param[in,out] action Action whose stdout is desired + * + * \return Action's stdout (which may be NULL) + * \note Upon return, \p action will no longer track the output, so it is the + * caller's responsibility to free the return value. + */ +char * +services__grab_stdout(svc_action_t *action) +{ + char *output = action->stdout_data; + + action->stdout_data = NULL; + return output; +} + +/*! + * \internal + * \brief Steal stderr from an action + * + * \param[in,out] action Action whose stderr is desired + * + * \return Action's stderr (which may be NULL) + * \note Upon return, \p action will no longer track the output, so it is the + * caller's responsibility to free the return value. + */ +char * +services__grab_stderr(svc_action_t *action) +{ + char *output = action->stderr_data; + + action->stderr_data = NULL; + return output; +} diff --git a/lib/services/services_linux.c b/lib/services/services_linux.c new file mode 100644 index 0000000..fb12f73 --- /dev/null +++ b/lib/services/services_linux.c @@ -0,0 +1,1438 @@ +/* + * Copyright 2010-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "crm/crm.h" +#include "crm/common/mainloop.h" +#include "crm/services.h" +#include "crm/services_internal.h" + +#include "services_private.h" + +static void close_pipe(int fildes[]); + +/* We have two alternative ways of handling SIGCHLD when synchronously waiting + * for spawned processes to complete. Both rely on polling a file descriptor to + * discover SIGCHLD events. + * + * If sys/signalfd.h is available (e.g. on Linux), we call signalfd() to + * generate the file descriptor. Otherwise, we use the "self-pipe trick" + * (opening a pipe and writing a byte to it when SIGCHLD is received). + */ +#ifdef HAVE_SYS_SIGNALFD_H + +// signalfd() implementation + +#include + +// Everything needed to manage SIGCHLD handling +struct sigchld_data_s { + sigset_t mask; // Signals to block now (including SIGCHLD) + sigset_t old_mask; // Previous set of blocked signals +}; + +// Initialize SIGCHLD data and prepare for use +static bool +sigchld_setup(struct sigchld_data_s *data) +{ + sigemptyset(&(data->mask)); + sigaddset(&(data->mask), SIGCHLD); + + sigemptyset(&(data->old_mask)); + + // Block SIGCHLD (saving previous set of blocked signals to restore later) + if (sigprocmask(SIG_BLOCK, &(data->mask), &(data->old_mask)) < 0) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=sigprocmask", pcmk_rc_str(errno)); + return false; + } + return true; +} + +// Get a file descriptor suitable for polling for SIGCHLD events +static int +sigchld_open(struct sigchld_data_s *data) +{ + int fd; + + CRM_CHECK(data != NULL, return -1); + + fd = signalfd(-1, &(data->mask), SFD_NONBLOCK); + if (fd < 0) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=signalfd", pcmk_rc_str(errno)); + } + return fd; +} + +// Close a file descriptor returned by sigchld_open() +static void +sigchld_close(int fd) +{ + if (fd > 0) { + close(fd); + } +} + +// Return true if SIGCHLD was received from polled fd +static bool +sigchld_received(int fd) +{ + struct signalfd_siginfo fdsi; + ssize_t s; + + if (fd < 0) { + return false; + } + s = read(fd, &fdsi, sizeof(struct signalfd_siginfo)); + if (s != sizeof(struct signalfd_siginfo)) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=read", pcmk_rc_str(errno)); + + } else if (fdsi.ssi_signo == SIGCHLD) { + return true; + } + return false; +} + +// Do anything needed after done waiting for SIGCHLD +static void +sigchld_cleanup(struct sigchld_data_s *data) +{ + // Restore the original set of blocked signals + if ((sigismember(&(data->old_mask), SIGCHLD) == 0) + && (sigprocmask(SIG_UNBLOCK, &(data->mask), NULL) < 0)) { + crm_warn("Could not clean up after child process completion: %s", + pcmk_rc_str(errno)); + } +} + +#else // HAVE_SYS_SIGNALFD_H not defined + +// Self-pipe implementation (see above for function descriptions) + +struct sigchld_data_s { + int pipe_fd[2]; // Pipe file descriptors + struct sigaction sa; // Signal handling info (with SIGCHLD) + struct sigaction old_sa; // Previous signal handling info +}; + +// We need a global to use in the signal handler +volatile struct sigchld_data_s *last_sigchld_data = NULL; + +static void +sigchld_handler(void) +{ + // We received a SIGCHLD, so trigger pipe polling + if ((last_sigchld_data != NULL) + && (last_sigchld_data->pipe_fd[1] >= 0) + && (write(last_sigchld_data->pipe_fd[1], "", 1) == -1)) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=write", pcmk_rc_str(errno)); + } +} + +static bool +sigchld_setup(struct sigchld_data_s *data) +{ + int rc; + + data->pipe_fd[0] = data->pipe_fd[1] = -1; + + if (pipe(data->pipe_fd) == -1) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=pipe", pcmk_rc_str(errno)); + return false; + } + + rc = pcmk__set_nonblocking(data->pipe_fd[0]); + if (rc != pcmk_rc_ok) { + crm_info("Could not set pipe input non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + } + rc = pcmk__set_nonblocking(data->pipe_fd[1]); + if (rc != pcmk_rc_ok) { + crm_info("Could not set pipe output non-blocking: %s " CRM_XS " rc=%d", + pcmk_rc_str(rc), rc); + } + + // Set SIGCHLD handler + data->sa.sa_handler = (sighandler_t) sigchld_handler; + data->sa.sa_flags = 0; + sigemptyset(&(data->sa.sa_mask)); + if (sigaction(SIGCHLD, &(data->sa), &(data->old_sa)) < 0) { + crm_info("Wait for child process completion failed: %s " + CRM_XS " source=sigaction", pcmk_rc_str(errno)); + } + + // Remember data for use in signal handler + last_sigchld_data = data; + return true; +} + +static int +sigchld_open(struct sigchld_data_s *data) +{ + CRM_CHECK(data != NULL, return -1); + return data->pipe_fd[0]; +} + +static void +sigchld_close(int fd) +{ + // Pipe will be closed in sigchld_cleanup() + return; +} + +static bool +sigchld_received(int fd) +{ + char ch; + + if (fd < 0) { + return false; + } + + // Clear out the self-pipe + while (read(fd, &ch, 1) == 1) /*omit*/; + return true; +} + +static void +sigchld_cleanup(struct sigchld_data_s *data) +{ + // Restore the previous SIGCHLD handler + if (sigaction(SIGCHLD, &(data->old_sa), NULL) < 0) { + crm_warn("Could not clean up after child process completion: %s", + pcmk_rc_str(errno)); + } + + close_pipe(data->pipe_fd); +} + +#endif + +/*! + * \internal + * \brief Close the two file descriptors of a pipe + * + * \param[in,out] fildes Array of file descriptors opened by pipe() + */ +static void +close_pipe(int fildes[]) +{ + if (fildes[0] >= 0) { + close(fildes[0]); + fildes[0] = -1; + } + if (fildes[1] >= 0) { + close(fildes[1]); + fildes[1] = -1; + } +} + +static gboolean +svc_read_output(int fd, svc_action_t * op, bool is_stderr) +{ + char *data = NULL; + int rc = 0, len = 0; + char buf[500]; + static const size_t buf_read_len = sizeof(buf) - 1; + + + if (fd < 0) { + crm_trace("No fd for %s", op->id); + return FALSE; + } + + if (is_stderr && op->stderr_data) { + len = strlen(op->stderr_data); + data = op->stderr_data; + crm_trace("Reading %s stderr into offset %d", op->id, len); + + } else if (is_stderr == FALSE && op->stdout_data) { + len = strlen(op->stdout_data); + data = op->stdout_data; + crm_trace("Reading %s stdout into offset %d", op->id, len); + + } else { + crm_trace("Reading %s %s into offset %d", op->id, is_stderr?"stderr":"stdout", len); + } + + do { + rc = read(fd, buf, buf_read_len); + if (rc > 0) { + buf[rc] = 0; + crm_trace("Got %d chars: %.80s", rc, buf); + data = pcmk__realloc(data, len + rc + 1); + len += sprintf(data + len, "%s", buf); + + } else if (errno != EINTR) { + /* error or EOF + * Cleanup happens in pipe_done() + */ + rc = FALSE; + break; + } + + } while (rc == buf_read_len || rc < 0); + + if (is_stderr) { + op->stderr_data = data; + } else { + op->stdout_data = data; + } + + return rc; +} + +static int +dispatch_stdout(gpointer userdata) +{ + svc_action_t *op = (svc_action_t *) userdata; + + return svc_read_output(op->opaque->stdout_fd, op, FALSE); +} + +static int +dispatch_stderr(gpointer userdata) +{ + svc_action_t *op = (svc_action_t *) userdata; + + return svc_read_output(op->opaque->stderr_fd, op, TRUE); +} + +static void +pipe_out_done(gpointer user_data) +{ + svc_action_t *op = (svc_action_t *) user_data; + + crm_trace("%p", op); + + op->opaque->stdout_gsource = NULL; + if (op->opaque->stdout_fd > STDOUT_FILENO) { + close(op->opaque->stdout_fd); + } + op->opaque->stdout_fd = -1; +} + +static void +pipe_err_done(gpointer user_data) +{ + svc_action_t *op = (svc_action_t *) user_data; + + op->opaque->stderr_gsource = NULL; + if (op->opaque->stderr_fd > STDERR_FILENO) { + close(op->opaque->stderr_fd); + } + op->opaque->stderr_fd = -1; +} + +static struct mainloop_fd_callbacks stdout_callbacks = { + .dispatch = dispatch_stdout, + .destroy = pipe_out_done, +}; + +static struct mainloop_fd_callbacks stderr_callbacks = { + .dispatch = dispatch_stderr, + .destroy = pipe_err_done, +}; + +static void +set_ocf_env(const char *key, const char *value, gpointer user_data) +{ + if (setenv(key, value, 1) != 0) { + crm_perror(LOG_ERR, "setenv failed for key:%s and value:%s", key, value); + } +} + +static void +set_ocf_env_with_prefix(gpointer key, gpointer value, gpointer user_data) +{ + char buffer[500]; + + snprintf(buffer, sizeof(buffer), strcmp(key, "OCF_CHECK_LEVEL") != 0 ? "OCF_RESKEY_%s" : "%s", (char *)key); + set_ocf_env(buffer, value, user_data); +} + +static void +set_alert_env(gpointer key, gpointer value, gpointer user_data) +{ + int rc; + + if (value != NULL) { + rc = setenv(key, value, 1); + } else { + rc = unsetenv(key); + } + + if (rc < 0) { + crm_perror(LOG_ERR, "setenv %s=%s", + (char*)key, (value? (char*)value : "")); + } else { + crm_trace("setenv %s=%s", (char*)key, (value? (char*)value : "")); + } +} + +/*! + * \internal + * \brief Add environment variables suitable for an action + * + * \param[in] op Action to use + */ +static void +add_action_env_vars(const svc_action_t *op) +{ + void (*env_setter)(gpointer, gpointer, gpointer) = NULL; + if (op->agent == NULL) { + env_setter = set_alert_env; /* we deal with alert handler */ + + } else if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_casei)) { + env_setter = set_ocf_env_with_prefix; + } + + if (env_setter != NULL && op->params != NULL) { + g_hash_table_foreach(op->params, env_setter, NULL); + } + + if (env_setter == NULL || env_setter == set_alert_env) { + return; + } + + set_ocf_env("OCF_RA_VERSION_MAJOR", PCMK_OCF_MAJOR_VERSION, NULL); + set_ocf_env("OCF_RA_VERSION_MINOR", PCMK_OCF_MINOR_VERSION, NULL); + set_ocf_env("OCF_ROOT", OCF_ROOT_DIR, NULL); + set_ocf_env("OCF_EXIT_REASON_PREFIX", PCMK_OCF_REASON_PREFIX, NULL); + + if (op->rsc) { + set_ocf_env("OCF_RESOURCE_INSTANCE", op->rsc, NULL); + } + + if (op->agent != NULL) { + set_ocf_env("OCF_RESOURCE_TYPE", op->agent, NULL); + } + + /* Notes: this is not added to specification yet. Sept 10,2004 */ + if (op->provider != NULL) { + set_ocf_env("OCF_RESOURCE_PROVIDER", op->provider, NULL); + } +} + +static void +pipe_in_single_parameter(gpointer key, gpointer value, gpointer user_data) +{ + svc_action_t *op = user_data; + char *buffer = crm_strdup_printf("%s=%s\n", (char *)key, (char *) value); + int ret, total = 0, len = strlen(buffer); + + do { + errno = 0; + ret = write(op->opaque->stdin_fd, buffer + total, len - total); + if (ret > 0) { + total += ret; + } + + } while ((errno == EINTR) && (total < len)); + free(buffer); +} + +/*! + * \internal + * \brief Pipe parameters in via stdin for action + * + * \param[in] op Action to use + */ +static void +pipe_in_action_stdin_parameters(const svc_action_t *op) +{ + if (op->params) { + g_hash_table_foreach(op->params, pipe_in_single_parameter, (gpointer) op); + } +} + +gboolean +recurring_action_timer(gpointer data) +{ + svc_action_t *op = data; + + crm_debug("Scheduling another invocation of %s", op->id); + + /* Clean out the old result */ + free(op->stdout_data); + op->stdout_data = NULL; + free(op->stderr_data); + op->stderr_data = NULL; + op->opaque->repeat_timer = 0; + + services_action_async(op, NULL); + return FALSE; +} + +/*! + * \internal + * \brief Finalize handling of an asynchronous operation + * + * Given a completed asynchronous operation, cancel or reschedule it as + * appropriate if recurring, call its callback if registered, stop tracking it, + * and clean it up. + * + * \param[in,out] op Operation to finalize + * + * \return Standard Pacemaker return code + * \retval EINVAL Caller supplied NULL or invalid \p op + * \retval EBUSY Uncanceled recurring action has only been cleaned up + * \retval pcmk_rc_ok Action has been freed + * + * \note If the return value is not pcmk_rc_ok, the caller is responsible for + * freeing the action. + */ +int +services__finalize_async_op(svc_action_t *op) +{ + CRM_CHECK((op != NULL) && !(op->synchronous), return EINVAL); + + if (op->interval_ms != 0) { + // Recurring operations must be either cancelled or rescheduled + if (op->cancel) { + services__set_cancelled(op); + cancel_recurring_action(op); + } else { + op->opaque->repeat_timer = g_timeout_add(op->interval_ms, + recurring_action_timer, + (void *) op); + } + } + + if (op->opaque->callback != NULL) { + op->opaque->callback(op); + } + + // Stop tracking the operation (as in-flight or blocked) + op->pid = 0; + services_untrack_op(op); + + if ((op->interval_ms != 0) && !(op->cancel)) { + // Do not free recurring actions (they will get freed when cancelled) + services_action_cleanup(op); + return EBUSY; + } + + services_action_free(op); + return pcmk_rc_ok; +} + +static void +close_op_input(svc_action_t *op) +{ + if (op->opaque->stdin_fd >= 0) { + close(op->opaque->stdin_fd); + } +} + +static void +finish_op_output(svc_action_t *op, bool is_stderr) +{ + mainloop_io_t **source; + int fd; + + if (is_stderr) { + source = &(op->opaque->stderr_gsource); + fd = op->opaque->stderr_fd; + } else { + source = &(op->opaque->stdout_gsource); + fd = op->opaque->stdout_fd; + } + + if (op->synchronous || *source) { + crm_trace("Finish reading %s[%d] %s", + op->id, op->pid, (is_stderr? "stderr" : "stdout")); + svc_read_output(fd, op, is_stderr); + if (op->synchronous) { + close(fd); + } else { + mainloop_del_fd(*source); + *source = NULL; + } + } +} + +// Log an operation's stdout and stderr +static void +log_op_output(svc_action_t *op) +{ + char *prefix = crm_strdup_printf("%s[%d] error output", op->id, op->pid); + + /* The library caller has better context to know how important the output + * is, so log it at info and debug severity here. They can log it again at + * higher severity if appropriate. + */ + crm_log_output(LOG_INFO, prefix, op->stderr_data); + strcpy(prefix + strlen(prefix) - strlen("error output"), "output"); + crm_log_output(LOG_DEBUG, prefix, op->stdout_data); + free(prefix); +} + +// Truncate exit reasons at this many characters +#define EXIT_REASON_MAX_LEN 128 + +static void +parse_exit_reason_from_stderr(svc_action_t *op) +{ + const char *reason_start = NULL; + const char *reason_end = NULL; + const int prefix_len = strlen(PCMK_OCF_REASON_PREFIX); + + if ((op->stderr_data == NULL) || + // Only OCF agents have exit reasons in stderr + !pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, pcmk__str_none)) { + return; + } + + // Find the last occurrence of the magic string indicating an exit reason + for (const char *cur = strstr(op->stderr_data, PCMK_OCF_REASON_PREFIX); + cur != NULL; cur = strstr(cur, PCMK_OCF_REASON_PREFIX)) { + + cur += prefix_len; // Skip over magic string + reason_start = cur; + } + + if ((reason_start == NULL) || (reason_start[0] == '\n') + || (reason_start[0] == '\0')) { + return; // No or empty exit reason + } + + // Exit reason goes to end of line (or end of output) + reason_end = strchr(reason_start, '\n'); + if (reason_end == NULL) { + reason_end = reason_start + strlen(reason_start); + } + + // Limit size of exit reason to something reasonable + if (reason_end > (reason_start + EXIT_REASON_MAX_LEN)) { + reason_end = reason_start + EXIT_REASON_MAX_LEN; + } + + free(op->opaque->exit_reason); + op->opaque->exit_reason = strndup(reason_start, reason_end - reason_start); +} + +/*! + * \internal + * \brief Process the completion of an asynchronous child process + * + * \param[in,out] p Child process that completed + * \param[in] pid Process ID of child + * \param[in] core (Unused) + * \param[in] signo Signal that interrupted child, if any + * \param[in] exitcode Exit status of child process + */ +static void +async_action_complete(mainloop_child_t *p, pid_t pid, int core, int signo, + int exitcode) +{ + svc_action_t *op = mainloop_child_userdata(p); + + mainloop_clear_child_userdata(p); + CRM_CHECK(op->pid == pid, + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR, "Bug in mainloop handling"); + return); + + /* Depending on the priority the mainloop gives the stdout and stderr + * file descriptors, this function could be called before everything has + * been read from them, so force a final read now. + */ + finish_op_output(op, true); + finish_op_output(op, false); + + close_op_input(op); + + if (signo == 0) { + crm_debug("%s[%d] exited with status %d", op->id, op->pid, exitcode); + services__set_result(op, exitcode, PCMK_EXEC_DONE, NULL); + log_op_output(op); + parse_exit_reason_from_stderr(op); + + } else if (mainloop_child_timeout(p)) { + const char *kind = services__action_kind(op); + + crm_info("%s %s[%d] timed out after %s", + kind, op->id, op->pid, pcmk__readable_interval(op->timeout)); + services__format_result(op, services__generic_error(op), + PCMK_EXEC_TIMEOUT, + "%s did not complete within %s", + kind, pcmk__readable_interval(op->timeout)); + + } else if (op->cancel) { + /* If an in-flight recurring operation was killed because it was + * cancelled, don't treat that as a failure. + */ + crm_info("%s[%d] terminated with signal %d (%s)", + op->id, op->pid, signo, strsignal(signo)); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_CANCELLED, NULL); + + } else { + crm_info("%s[%d] terminated with signal %d (%s)", + op->id, op->pid, signo, strsignal(signo)); + services__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "%s interrupted by %s signal", + services__action_kind(op), strsignal(signo)); + } + + services__finalize_async_op(op); +} + +/*! + * \internal + * \brief Return agent standard's exit status for "generic error" + * + * When returning an internal error for an action, a value that is appropriate + * to the action's agent standard must be used. This function returns a value + * appropriate for errors in general. + * + * \param[in] op Action that error is for + * + * \return Exit status appropriate to agent standard + * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. + */ +int +services__generic_error(const svc_action_t *op) +{ + if ((op == NULL) || (op->standard == NULL)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) + && pcmk__str_eq(op->action, "status", pcmk__str_casei)) { + + return PCMK_LSB_STATUS_UNKNOWN; + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return NAGIOS_STATE_UNKNOWN; + } +#endif + + return PCMK_OCF_UNKNOWN_ERROR; +} + +/*! + * \internal + * \brief Return agent standard's exit status for "not installed" + * + * When returning an internal error for an action, a value that is appropriate + * to the action's agent standard must be used. This function returns a value + * appropriate for "not installed" errors. + * + * \param[in] op Action that error is for + * + * \return Exit status appropriate to agent standard + * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. + */ +int +services__not_installed_error(const svc_action_t *op) +{ + if ((op == NULL) || (op->standard == NULL)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) + && pcmk__str_eq(op->action, "status", pcmk__str_casei)) { + + return PCMK_LSB_STATUS_NOT_INSTALLED; + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return NAGIOS_STATE_UNKNOWN; + } +#endif + + return PCMK_OCF_NOT_INSTALLED; +} + +/*! + * \internal + * \brief Return agent standard's exit status for "insufficient privileges" + * + * When returning an internal error for an action, a value that is appropriate + * to the action's agent standard must be used. This function returns a value + * appropriate for "insufficient privileges" errors. + * + * \param[in] op Action that error is for + * + * \return Exit status appropriate to agent standard + * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. + */ +int +services__authorization_error(const svc_action_t *op) +{ + if ((op == NULL) || (op->standard == NULL)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) + && pcmk__str_eq(op->action, "status", pcmk__str_casei)) { + + return PCMK_LSB_STATUS_INSUFFICIENT_PRIV; + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return NAGIOS_INSUFFICIENT_PRIV; + } +#endif + + return PCMK_OCF_INSUFFICIENT_PRIV; +} + +/*! + * \internal + * \brief Return agent standard's exit status for "not configured" + * + * When returning an internal error for an action, a value that is appropriate + * to the action's agent standard must be used. This function returns a value + * appropriate for "not configured" errors. + * + * \param[in] op Action that error is for + * \param[in] is_fatal Whether problem is cluster-wide instead of only local + * + * \return Exit status appropriate to agent standard + * \note Actions without a standard will get PCMK_OCF_UNKNOWN_ERROR. + */ +int +services__configuration_error(const svc_action_t *op, bool is_fatal) +{ + if ((op == NULL) || (op->standard == NULL)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_LSB, pcmk__str_casei) + && pcmk__str_eq(op->action, "status", pcmk__str_casei)) { + + return PCMK_LSB_NOT_CONFIGURED; + } + +#if SUPPORT_NAGIOS + if (pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_NAGIOS, pcmk__str_casei)) { + return NAGIOS_STATE_UNKNOWN; + } +#endif + + return is_fatal? PCMK_OCF_NOT_CONFIGURED : PCMK_OCF_INVALID_PARAM; +} + + +/*! + * \internal + * \brief Set operation rc and status per errno from stat(), fork() or execvp() + * + * \param[in,out] op Operation to set rc and status for + * \param[in] error Value of errno after system call + * + * \return void + */ +void +services__handle_exec_error(svc_action_t * op, int error) +{ + const char *name = op->opaque->exec; + + if (name == NULL) { + name = op->agent; + if (name == NULL) { + name = op->id; + } + } + + switch (error) { /* see execve(2), stat(2) and fork(2) */ + case ENOENT: /* No such file or directory */ + case EISDIR: /* Is a directory */ + case ENOTDIR: /* Path component is not a directory */ + case EINVAL: /* Invalid executable format */ + case ENOEXEC: /* Invalid executable format */ + services__format_result(op, services__not_installed_error(op), + PCMK_EXEC_NOT_INSTALLED, "%s: %s", + name, pcmk_rc_str(error)); + break; + case EACCES: /* permission denied (various errors) */ + case EPERM: /* permission denied (various errors) */ + services__format_result(op, services__authorization_error(op), + PCMK_EXEC_ERROR, "%s: %s", + name, pcmk_rc_str(error)); + break; + default: + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR, pcmk_rc_str(error)); + } +} + +/*! + * \internal + * \brief Exit a child process that failed before executing agent + * + * \param[in] op Action that failed + * \param[in] exit_status Exit status code to use + * \param[in] exit_reason Exit reason to output if for OCF agent + */ +static void +exit_child(const svc_action_t *op, int exit_status, const char *exit_reason) +{ + if ((op != NULL) && (exit_reason != NULL) + && pcmk__str_eq(op->standard, PCMK_RESOURCE_CLASS_OCF, + pcmk__str_none)) { + fprintf(stderr, PCMK_OCF_REASON_PREFIX "%s\n", exit_reason); + } + _exit(exit_status); +} + +static void +action_launch_child(svc_action_t *op) +{ + int rc; + + /* SIGPIPE is ignored (which is different from signal blocking) by the gnutls library. + * Depending on the libqb version in use, libqb may set SIGPIPE to be ignored as well. + * We do not want this to be inherited by the child process. By resetting this the signal + * to the default behavior, we avoid some potential odd problems that occur during OCF + * scripts when SIGPIPE is ignored by the environment. */ + signal(SIGPIPE, SIG_DFL); + +#if defined(HAVE_SCHED_SETSCHEDULER) + if (sched_getscheduler(0) != SCHED_OTHER) { + struct sched_param sp; + + memset(&sp, 0, sizeof(sp)); + sp.sched_priority = 0; + + if (sched_setscheduler(0, SCHED_OTHER, &sp) == -1) { + crm_info("Could not reset scheduling policy for %s", op->id); + } + } +#endif + if (setpriority(PRIO_PROCESS, 0, 0) == -1) { + crm_info("Could not reset process priority for %s", op->id); + } + + /* Man: The call setpgrp() is equivalent to setpgid(0,0) + * _and_ compiles on BSD variants too + * need to investigate if it works the same too. + */ + setpgid(0, 0); + + pcmk__close_fds_in_child(false); + + /* It would be nice if errors in this function could be reported as + * execution status (for example, PCMK_EXEC_NO_SECRETS for the secrets error + * below) instead of exit status. However, we've already forked, so + * exit status is all we have. At least for OCF actions, we can output an + * exit reason for the parent to parse. + */ + +#if SUPPORT_CIBSECRETS + rc = pcmk__substitute_secrets(op->rsc, op->params); + if (rc != pcmk_rc_ok) { + if (pcmk__str_eq(op->action, "stop", pcmk__str_casei)) { + crm_info("Proceeding with stop operation for %s " + "despite being unable to load CIB secrets (%s)", + op->rsc, pcmk_rc_str(rc)); + } else { + crm_err("Considering %s unconfigured " + "because unable to load CIB secrets: %s", + op->rsc, pcmk_rc_str(rc)); + exit_child(op, services__configuration_error(op, false), + "Unable to load CIB secrets"); + } + } +#endif + + add_action_env_vars(op); + + /* Become the desired user */ + if (op->opaque->uid && (geteuid() == 0)) { + + // If requested, set effective group + if (op->opaque->gid && (setgid(op->opaque->gid) < 0)) { + crm_err("Considering %s unauthorized because could not set " + "child group to %d: %s", + op->id, op->opaque->gid, strerror(errno)); + exit_child(op, services__authorization_error(op), + "Could not set group for child process"); + } + + // Erase supplementary group list + // (We could do initgroups() if we kept a copy of the username) + if (setgroups(0, NULL) < 0) { + crm_err("Considering %s unauthorized because could not " + "clear supplementary groups: %s", op->id, strerror(errno)); + exit_child(op, services__authorization_error(op), + "Could not clear supplementary groups for child process"); + } + + // Set effective user + if (setuid(op->opaque->uid) < 0) { + crm_err("Considering %s unauthorized because could not set user " + "to %d: %s", op->id, op->opaque->uid, strerror(errno)); + exit_child(op, services__authorization_error(op), + "Could not set user for child process"); + } + } + + // Execute the agent (doesn't return if successful) + execvp(op->opaque->exec, op->opaque->args); + + // An earlier stat() should have avoided most possible errors + rc = errno; + services__handle_exec_error(op, rc); + crm_err("Unable to execute %s: %s", op->id, strerror(rc)); + exit_child(op, op->rc, "Child process was unable to execute file"); +} + +/*! + * \internal + * \brief Wait for synchronous action to complete, and set its result + * + * \param[in,out] op Action to wait for + * \param[in,out] data Child signal data + */ +static void +wait_for_sync_result(svc_action_t *op, struct sigchld_data_s *data) +{ + int status = 0; + int timeout = op->timeout; + time_t start = time(NULL); + struct pollfd fds[3]; + int wait_rc = 0; + const char *wait_reason = NULL; + + fds[0].fd = op->opaque->stdout_fd; + fds[0].events = POLLIN; + fds[0].revents = 0; + + fds[1].fd = op->opaque->stderr_fd; + fds[1].events = POLLIN; + fds[1].revents = 0; + + fds[2].fd = sigchld_open(data); + fds[2].events = POLLIN; + fds[2].revents = 0; + + crm_trace("Waiting for %s[%d]", op->id, op->pid); + do { + int poll_rc = poll(fds, 3, timeout); + + wait_reason = NULL; + + if (poll_rc > 0) { + if (fds[0].revents & POLLIN) { + svc_read_output(op->opaque->stdout_fd, op, FALSE); + } + + if (fds[1].revents & POLLIN) { + svc_read_output(op->opaque->stderr_fd, op, TRUE); + } + + if ((fds[2].revents & POLLIN) && sigchld_received(fds[2].fd)) { + wait_rc = waitpid(op->pid, &status, WNOHANG); + + if ((wait_rc > 0) || ((wait_rc < 0) && (errno == ECHILD))) { + // Child process exited or doesn't exist + break; + + } else if (wait_rc < 0) { + wait_reason = pcmk_rc_str(errno); + crm_info("Wait for completion of %s[%d] failed: %s " + CRM_XS " source=waitpid", + op->id, op->pid, wait_reason); + wait_rc = 0; // Act as if process is still running + } + } + + } else if (poll_rc == 0) { + // Poll timed out with no descriptors ready + timeout = 0; + break; + + } else if ((poll_rc < 0) && (errno != EINTR)) { + wait_reason = pcmk_rc_str(errno); + crm_info("Wait for completion of %s[%d] failed: %s " + CRM_XS " source=poll", op->id, op->pid, wait_reason); + break; + } + + timeout = op->timeout - (time(NULL) - start) * 1000; + + } while ((op->timeout < 0 || timeout > 0)); + + crm_trace("Stopped waiting for %s[%d]", op->id, op->pid); + finish_op_output(op, true); + finish_op_output(op, false); + close_op_input(op); + sigchld_close(fds[2].fd); + + if (wait_rc <= 0) { + + if ((op->timeout > 0) && (timeout <= 0)) { + services__format_result(op, services__generic_error(op), + PCMK_EXEC_TIMEOUT, + "%s did not exit within specified timeout", + services__action_kind(op)); + crm_info("%s[%d] timed out after %dms", + op->id, op->pid, op->timeout); + + } else { + services__set_result(op, services__generic_error(op), + PCMK_EXEC_ERROR, wait_reason); + } + + /* If only child hasn't been successfully waited for, yet. + This is to limit killing wrong target a bit more. */ + if ((wait_rc == 0) && (waitpid(op->pid, &status, WNOHANG) == 0)) { + if (kill(op->pid, SIGKILL)) { + crm_warn("Could not kill rogue child %s[%d]: %s", + op->id, op->pid, pcmk_rc_str(errno)); + } + /* Safe to skip WNOHANG here as we sent non-ignorable signal. */ + while ((waitpid(op->pid, &status, 0) == (pid_t) -1) + && (errno == EINTR)) { + /* keep waiting */; + } + } + + } else if (WIFEXITED(status)) { + services__set_result(op, WEXITSTATUS(status), PCMK_EXEC_DONE, NULL); + parse_exit_reason_from_stderr(op); + crm_info("%s[%d] exited with status %d", op->id, op->pid, op->rc); + + } else if (WIFSIGNALED(status)) { + int signo = WTERMSIG(status); + + services__format_result(op, services__generic_error(op), + PCMK_EXEC_ERROR, "%s interrupted by %s signal", + services__action_kind(op), strsignal(signo)); + crm_info("%s[%d] terminated with signal %d (%s)", + op->id, op->pid, signo, strsignal(signo)); + +#ifdef WCOREDUMP + if (WCOREDUMP(status)) { + crm_warn("%s[%d] dumped core", op->id, op->pid); + } +#endif + + } else { + // Shouldn't be possible to get here + services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, + "Unable to wait for child to complete"); + } +} + +/*! + * \internal + * \brief Execute an action whose standard uses executable files + * + * \param[in,out] op Action to execute + * + * \return Standard Pacemaker return value + * \retval EBUSY Recurring operation could not be initiated + * \retval pcmk_rc_error Synchronous action failed + * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action + * should not be freed (because it's pending or because + * it failed to execute and was already freed) + * + * \note If the return value for an asynchronous action is not pcmk_rc_ok, the + * caller is responsible for freeing the action. + */ +int +services__execute_file(svc_action_t *op) +{ + int stdout_fd[2]; + int stderr_fd[2]; + int stdin_fd[2] = {-1, -1}; + int rc; + struct stat st; + struct sigchld_data_s data; + + // Catch common failure conditions early + if (stat(op->opaque->exec, &st) != 0) { + rc = errno; + crm_info("Cannot execute '%s': %s " CRM_XS " stat rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + goto done; + } + + if (pipe(stdout_fd) < 0) { + rc = errno; + crm_info("Cannot execute '%s': %s " CRM_XS " pipe(stdout) rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + goto done; + } + + if (pipe(stderr_fd) < 0) { + rc = errno; + + close_pipe(stdout_fd); + + crm_info("Cannot execute '%s': %s " CRM_XS " pipe(stderr) rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + goto done; + } + + if (pcmk_is_set(pcmk_get_ra_caps(op->standard), pcmk_ra_cap_stdin)) { + if (pipe(stdin_fd) < 0) { + rc = errno; + + close_pipe(stdout_fd); + close_pipe(stderr_fd); + + crm_info("Cannot execute '%s': %s " CRM_XS " pipe(stdin) rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + goto done; + } + } + + if (op->synchronous && !sigchld_setup(&data)) { + close_pipe(stdin_fd); + close_pipe(stdout_fd); + close_pipe(stderr_fd); + sigchld_cleanup(&data); + services__set_result(op, services__generic_error(op), PCMK_EXEC_ERROR, + "Could not manage signals for child process"); + goto done; + } + + op->pid = fork(); + switch (op->pid) { + case -1: + rc = errno; + close_pipe(stdin_fd); + close_pipe(stdout_fd); + close_pipe(stderr_fd); + + crm_info("Cannot execute '%s': %s " CRM_XS " fork rc=%d", + op->opaque->exec, pcmk_strerror(rc), rc); + services__handle_exec_error(op, rc); + if (op->synchronous) { + sigchld_cleanup(&data); + } + goto done; + break; + + case 0: /* Child */ + close(stdout_fd[0]); + close(stderr_fd[0]); + if (stdin_fd[1] >= 0) { + close(stdin_fd[1]); + } + if (STDOUT_FILENO != stdout_fd[1]) { + if (dup2(stdout_fd[1], STDOUT_FILENO) != STDOUT_FILENO) { + crm_warn("Can't redirect output from '%s': %s " + CRM_XS " errno=%d", + op->opaque->exec, pcmk_rc_str(errno), errno); + } + close(stdout_fd[1]); + } + if (STDERR_FILENO != stderr_fd[1]) { + if (dup2(stderr_fd[1], STDERR_FILENO) != STDERR_FILENO) { + crm_warn("Can't redirect error output from '%s': %s " + CRM_XS " errno=%d", + op->opaque->exec, pcmk_rc_str(errno), errno); + } + close(stderr_fd[1]); + } + if ((stdin_fd[0] >= 0) && + (STDIN_FILENO != stdin_fd[0])) { + if (dup2(stdin_fd[0], STDIN_FILENO) != STDIN_FILENO) { + crm_warn("Can't redirect input to '%s': %s " + CRM_XS " errno=%d", + op->opaque->exec, pcmk_rc_str(errno), errno); + } + close(stdin_fd[0]); + } + + if (op->synchronous) { + sigchld_cleanup(&data); + } + + action_launch_child(op); + CRM_ASSERT(0); /* action_launch_child is effectively noreturn */ + } + + /* Only the parent reaches here */ + close(stdout_fd[1]); + close(stderr_fd[1]); + if (stdin_fd[0] >= 0) { + close(stdin_fd[0]); + } + + op->opaque->stdout_fd = stdout_fd[0]; + rc = pcmk__set_nonblocking(op->opaque->stdout_fd); + if (rc != pcmk_rc_ok) { + crm_info("Could not set '%s' output non-blocking: %s " + CRM_XS " rc=%d", + op->opaque->exec, pcmk_rc_str(rc), rc); + } + + op->opaque->stderr_fd = stderr_fd[0]; + rc = pcmk__set_nonblocking(op->opaque->stderr_fd); + if (rc != pcmk_rc_ok) { + crm_info("Could not set '%s' error output non-blocking: %s " + CRM_XS " rc=%d", + op->opaque->exec, pcmk_rc_str(rc), rc); + } + + op->opaque->stdin_fd = stdin_fd[1]; + if (op->opaque->stdin_fd >= 0) { + // using buffer behind non-blocking-fd here - that could be improved + // as long as no other standard uses stdin_fd assume stonith + rc = pcmk__set_nonblocking(op->opaque->stdin_fd); + if (rc != pcmk_rc_ok) { + crm_info("Could not set '%s' input non-blocking: %s " + CRM_XS " fd=%d,rc=%d", op->opaque->exec, + pcmk_rc_str(rc), op->opaque->stdin_fd, rc); + } + pipe_in_action_stdin_parameters(op); + // as long as we are handling parameters directly in here just close + close(op->opaque->stdin_fd); + op->opaque->stdin_fd = -1; + } + + // after fds are setup properly and before we plug anything into mainloop + if (op->opaque->fork_callback) { + op->opaque->fork_callback(op); + } + + if (op->synchronous) { + wait_for_sync_result(op, &data); + sigchld_cleanup(&data); + goto done; + } + + crm_trace("Waiting async for '%s'[%d]", op->opaque->exec, op->pid); + mainloop_child_add_with_flags(op->pid, op->timeout, op->id, op, + pcmk_is_set(op->flags, SVC_ACTION_LEAVE_GROUP)? mainloop_leave_pid_group : 0, + async_action_complete); + + op->opaque->stdout_gsource = mainloop_add_fd(op->id, + G_PRIORITY_LOW, + op->opaque->stdout_fd, op, + &stdout_callbacks); + op->opaque->stderr_gsource = mainloop_add_fd(op->id, + G_PRIORITY_LOW, + op->opaque->stderr_fd, op, + &stderr_callbacks); + services_add_inflight_op(op); + return pcmk_rc_ok; + +done: + if (op->synchronous) { + return (op->rc == PCMK_OCF_OK)? pcmk_rc_ok : pcmk_rc_error; + } else { + return services__finalize_async_op(op); + } +} + +GList * +services_os_get_single_directory_list(const char *root, gboolean files, gboolean executable) +{ + GList *list = NULL; + struct dirent **namelist; + int entries = 0, lpc = 0; + char buffer[PATH_MAX]; + + entries = scandir(root, &namelist, NULL, alphasort); + if (entries <= 0) { + return list; + } + + for (lpc = 0; lpc < entries; lpc++) { + struct stat sb; + + if ('.' == namelist[lpc]->d_name[0]) { + free(namelist[lpc]); + continue; + } + + snprintf(buffer, sizeof(buffer), "%s/%s", root, namelist[lpc]->d_name); + + if (stat(buffer, &sb)) { + continue; + } + + if (S_ISDIR(sb.st_mode)) { + if (files) { + free(namelist[lpc]); + continue; + } + + } else if (S_ISREG(sb.st_mode)) { + if (files == FALSE) { + free(namelist[lpc]); + continue; + + } else if (executable + && (sb.st_mode & S_IXUSR) == 0 + && (sb.st_mode & S_IXGRP) == 0 && (sb.st_mode & S_IXOTH) == 0) { + free(namelist[lpc]); + continue; + } + } + + list = g_list_append(list, strdup(namelist[lpc]->d_name)); + + free(namelist[lpc]); + } + + free(namelist); + return list; +} + +GList * +services_os_get_directory_list(const char *root, gboolean files, gboolean executable) +{ + GList *result = NULL; + char *dirs = strdup(root); + char *dir = NULL; + + if (pcmk__str_empty(dirs)) { + free(dirs); + return result; + } + + for (dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { + GList *tmp = services_os_get_single_directory_list(dir, files, executable); + + if (tmp) { + result = g_list_concat(result, tmp); + } + } + + free(dirs); + + return result; +} diff --git a/lib/services/services_lsb.c b/lib/services/services_lsb.c new file mode 100644 index 0000000..134cc70 --- /dev/null +++ b/lib/services/services_lsb.c @@ -0,0 +1,341 @@ +/* + * Copyright 2010-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include + +#include +#include +#include "services_private.h" +#include "services_lsb.h" + +#define lsb_metadata_template \ + "\n" \ + "\n" \ + "\n" \ + " 1.0\n" \ + " \n" \ + "%s" \ + " \n" \ + " %s\n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " %s\n" \ + " %s\n" \ + " %s\n" \ + " %s\n" \ + " %s\n" \ + " %s\n" \ + " %s\n" \ + " \n" \ + "\n" + +/* See "Comment Conventions for Init Scripts" in the LSB core specification at: + * http://refspecs.linuxfoundation.org/lsb.shtml + */ +#define LSB_INITSCRIPT_INFOBEGIN_TAG "### BEGIN INIT INFO" +#define LSB_INITSCRIPT_INFOEND_TAG "### END INIT INFO" +#define PROVIDES "# Provides:" +#define REQ_START "# Required-Start:" +#define REQ_STOP "# Required-Stop:" +#define SHLD_START "# Should-Start:" +#define SHLD_STOP "# Should-Stop:" +#define DFLT_START "# Default-Start:" +#define DFLT_STOP "# Default-Stop:" +#define SHORT_DSCR "# Short-Description:" +#define DESCRIPTION "# Description:" + +#define lsb_meta_helper_free_value(m) \ + do { \ + if ((m) != NULL) { \ + xmlFree(m); \ + (m) = NULL; \ + } \ + } while(0) + +/*! + * \internal + * \brief Grab an LSB header value + * + * \param[in] line Line read from LSB init script + * \param[in,out] value If not set, will be set to XML-safe copy of value + * \param[in] prefix Set value if line starts with this pattern + * + * \return TRUE if value was set, FALSE otherwise + */ +static inline gboolean +lsb_meta_helper_get_value(const char *line, char **value, const char *prefix) +{ + if (!*value && pcmk__starts_with(line, prefix)) { + *value = (char *)xmlEncodeEntitiesReentrant(NULL, BAD_CAST line+strlen(prefix)); + return TRUE; + } + return FALSE; +} + +int +services__get_lsb_metadata(const char *type, char **output) +{ + char ra_pathname[PATH_MAX] = { 0, }; + FILE *fp = NULL; + char buffer[1024] = { 0, }; + char *provides = NULL; + char *req_start = NULL; + char *req_stop = NULL; + char *shld_start = NULL; + char *shld_stop = NULL; + char *dflt_start = NULL; + char *dflt_stop = NULL; + char *s_dscrpt = NULL; + char *xml_l_dscrpt = NULL; + bool in_header = FALSE; + + if (type[0] == '/') { + snprintf(ra_pathname, sizeof(ra_pathname), "%s", type); + } else { + snprintf(ra_pathname, sizeof(ra_pathname), "%s/%s", + PCMK__LSB_INIT_DIR, type); + } + + crm_trace("Looking into %s", ra_pathname); + fp = fopen(ra_pathname, "r"); + if (fp == NULL) { + return -errno; + } + + /* Enter into the LSB-compliant comment block */ + while (fgets(buffer, sizeof(buffer), fp)) { + + // Ignore lines up to and including the block delimiter + if (pcmk__starts_with(buffer, LSB_INITSCRIPT_INFOBEGIN_TAG)) { + in_header = TRUE; + continue; + } + if (!in_header) { + continue; + } + + /* Assume each of the following eight arguments contain one line */ + if (lsb_meta_helper_get_value(buffer, &provides, PROVIDES)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &req_start, REQ_START)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &req_stop, REQ_STOP)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &shld_start, SHLD_START)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &shld_stop, SHLD_STOP)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &dflt_start, DFLT_START)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &dflt_stop, DFLT_STOP)) { + continue; + } + if (lsb_meta_helper_get_value(buffer, &s_dscrpt, SHORT_DSCR)) { + continue; + } + + /* Long description may cross multiple lines */ + if ((xml_l_dscrpt == NULL) // haven't already found long description + && pcmk__starts_with(buffer, DESCRIPTION)) { + bool processed_line = TRUE; + GString *desc = g_string_sized_new(2048); + + // Get remainder of description line itself + g_string_append(desc, buffer + sizeof(DESCRIPTION) - 1); + + // Read any continuation lines of the description + buffer[0] = '\0'; + while (fgets(buffer, sizeof(buffer), fp)) { + if (pcmk__starts_with(buffer, "# ") + || pcmk__starts_with(buffer, "#\t")) { + /* '#' followed by a tab or more than one space indicates a + * continuation of the long description. + */ + g_string_append(desc, buffer + 1); + } else { + /* This line is not part of the long description, + * so continue with normal processing. + */ + processed_line = FALSE; + break; + } + } + + // Make long description safe to use in XML + xml_l_dscrpt = + (char *) xmlEncodeEntitiesReentrant(NULL, + (pcmkXmlStr) desc->str); + g_string_free(desc, TRUE); + + if (processed_line) { + // We grabbed the line into the long description + continue; + } + } + + // Stop if we leave the header block + if (pcmk__starts_with(buffer, LSB_INITSCRIPT_INFOEND_TAG)) { + break; + } + if (buffer[0] != '#') { + break; + } + } + fclose(fp); + + *output = crm_strdup_printf(lsb_metadata_template, type, + (xml_l_dscrpt? xml_l_dscrpt : type), + (s_dscrpt? s_dscrpt : type), + (provides? provides : ""), + (req_start? req_start : ""), + (req_stop? req_stop : ""), + (shld_start? shld_start : ""), + (shld_stop? shld_stop : ""), + (dflt_start? dflt_start : ""), + (dflt_stop? dflt_stop : "")); + + lsb_meta_helper_free_value(xml_l_dscrpt); + lsb_meta_helper_free_value(s_dscrpt); + lsb_meta_helper_free_value(provides); + lsb_meta_helper_free_value(req_start); + lsb_meta_helper_free_value(req_stop); + lsb_meta_helper_free_value(shld_start); + lsb_meta_helper_free_value(shld_stop); + lsb_meta_helper_free_value(dflt_start); + lsb_meta_helper_free_value(dflt_stop); + + crm_trace("Created fake metadata: %llu", + (unsigned long long) strlen(*output)); + return pcmk_ok; +} + +GList * +services__list_lsb_agents(void) +{ + return services_os_get_directory_list(PCMK__LSB_INIT_DIR, TRUE, TRUE); +} + +bool +services__lsb_agent_exists(const char *agent) +{ + bool rc = FALSE; + struct stat st; + char *path = pcmk__full_path(agent, PCMK__LSB_INIT_DIR); + + rc = (stat(path, &st) == 0); + free(path); + return rc; +} + +/*! + * \internal + * \brief Prepare an LSB action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__lsb_prepare(svc_action_t *op) +{ + op->opaque->exec = pcmk__full_path(op->agent, PCMK__LSB_INIT_DIR); + op->opaque->args[0] = strdup(op->opaque->exec); + op->opaque->args[1] = strdup(op->action); + if ((op->opaque->args[0] == NULL) || (op->opaque->args[1] == NULL)) { + return ENOMEM; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map an LSB result to a standard OCF result + * + * \param[in] action Action that result is for + * \param[in] exit_status LSB agent exit status + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__lsb2ocf(const char *action, int exit_status) +{ + // For non-status actions, LSB and OCF share error codes <= 7 + if (!pcmk__str_any_of(action, "status", "monitor", NULL)) { + if ((exit_status < 0) || (exit_status > PCMK_LSB_NOT_RUNNING)) { + return PCMK_OCF_UNKNOWN_ERROR; + } + return (enum ocf_exitcode) exit_status; + } + + // LSB status actions have their own codes + switch (exit_status) { + case PCMK_LSB_STATUS_OK: + return PCMK_OCF_OK; + + case PCMK_LSB_STATUS_NOT_INSTALLED: + return PCMK_OCF_NOT_INSTALLED; + + case PCMK_LSB_STATUS_INSUFFICIENT_PRIV: + return PCMK_OCF_INSUFFICIENT_PRIV; + + case PCMK_LSB_STATUS_VAR_PID: + case PCMK_LSB_STATUS_VAR_LOCK: + case PCMK_LSB_STATUS_NOT_RUNNING: + return PCMK_OCF_NOT_RUNNING; + + default: + return PCMK_OCF_UNKNOWN_ERROR; + } +} + +// Deprecated functions kept only for backward API compatibility +// LCOV_EXCL_START + +#include + +svc_action_t * +services_action_create(const char *name, const char *action, + guint interval_ms, int timeout) +{ + return resources_action_create(name, PCMK_RESOURCE_CLASS_LSB, NULL, name, + action, interval_ms, timeout, NULL, 0); +} + +GList * +services_list(void) +{ + return resources_list_agents(PCMK_RESOURCE_CLASS_LSB, NULL); +} + +// LCOV_EXCL_STOP +// End deprecated API diff --git a/lib/services/services_lsb.h b/lib/services/services_lsb.h new file mode 100644 index 0000000..8174833 --- /dev/null +++ b/lib/services/services_lsb.h @@ -0,0 +1,21 @@ +/* + * Copyright 2010-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef SERVICES_LSB__H +# define SERVICES_LSB__H + +G_GNUC_INTERNAL int services__get_lsb_metadata(const char *type, char **output); +G_GNUC_INTERNAL GList *services__list_lsb_agents(void); +G_GNUC_INTERNAL bool services__lsb_agent_exists(const char *agent); +G_GNUC_INTERNAL int services__lsb_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__lsb2ocf(const char *action, int exit_status); + +#endif diff --git a/lib/services/services_nagios.c b/lib/services/services_nagios.c new file mode 100644 index 0000000..abddca8 --- /dev/null +++ b/lib/services/services_nagios.c @@ -0,0 +1,220 @@ +/* + * Copyright 2010-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "crm/crm.h" +#include +#include "crm/common/mainloop.h" +#include "crm/services.h" + +#include "services_private.h" +#include "services_nagios.h" + +/*! + * \internal + * \brief Prepare a Nagios action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__nagios_prepare(svc_action_t *op) +{ + op->opaque->exec = pcmk__full_path(op->agent, NAGIOS_PLUGIN_DIR); + op->opaque->args[0] = strdup(op->opaque->exec); + if (op->opaque->args[0] == NULL) { + return ENOMEM; + } + + if (pcmk__str_eq(op->action, "monitor", pcmk__str_casei) + && (op->interval_ms == 0)) { + + // Invoke --version for a nagios probe + op->opaque->args[1] = strdup("--version"); + if (op->opaque->args[1] == NULL) { + return ENOMEM; + } + + } else if (op->params != NULL) { + GHashTableIter iter; + char *key = NULL; + char *value = NULL; + int index = 1; // 0 is already set to executable name + + g_hash_table_iter_init(&iter, op->params); + + while (g_hash_table_iter_next(&iter, (gpointer *) & key, + (gpointer *) & value)) { + + if (index > (PCMK__NELEM(op->opaque->args) - 2)) { + return E2BIG; + } + + if (pcmk__str_eq(key, XML_ATTR_CRM_VERSION, pcmk__str_casei) + || strstr(key, CRM_META "_")) { + continue; + } + + op->opaque->args[index++] = crm_strdup_printf("--%s", key); + op->opaque->args[index++] = strdup(value); + if (op->opaque->args[index - 1] == NULL) { + return ENOMEM; + } + } + } + + // Nagios actions don't need to keep the parameters + if (op->params != NULL) { + g_hash_table_destroy(op->params); + op->params = NULL; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map a Nagios result to a standard OCF result + * + * \param[in] exit_status Nagios exit status + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__nagios2ocf(int exit_status) +{ + switch (exit_status) { + case NAGIOS_STATE_OK: + return PCMK_OCF_OK; + + case NAGIOS_INSUFFICIENT_PRIV: + return PCMK_OCF_INSUFFICIENT_PRIV; + + case NAGIOS_STATE_WARNING: + return PCMK_OCF_DEGRADED; + + case NAGIOS_STATE_CRITICAL: + case NAGIOS_STATE_UNKNOWN: + default: + return PCMK_OCF_UNKNOWN_ERROR; + } +} + +static inline char * +nagios_metadata_name(const char *plugin) +{ + return crm_strdup_printf(NAGIOS_METADATA_DIR "/%s.xml", plugin); +} + +GList * +services__list_nagios_agents(void) +{ + GList *plugin_list = NULL; + GList *result = NULL; + + plugin_list = services_os_get_directory_list(NAGIOS_PLUGIN_DIR, TRUE, TRUE); + + // Return only the plugins that have metadata + for (GList *gIter = plugin_list; gIter != NULL; gIter = gIter->next) { + struct stat st; + const char *plugin = gIter->data; + char *metadata = nagios_metadata_name(plugin); + + if (stat(metadata, &st) == 0) { + result = g_list_append(result, strdup(plugin)); + } + free(metadata); + } + g_list_free_full(plugin_list, free); + return result; +} + +gboolean +services__nagios_agent_exists(const char *name) +{ + char *buf = NULL; + gboolean rc = FALSE; + struct stat st; + + if (name == NULL) { + return rc; + } + + buf = crm_strdup_printf(NAGIOS_PLUGIN_DIR "/%s", name); + if (stat(buf, &st) == 0) { + rc = TRUE; + } + + free(buf); + return rc; +} + +int +services__get_nagios_metadata(const char *type, char **output) +{ + int rc = pcmk_ok; + FILE *file_strm = NULL; + int start = 0, length = 0, read_len = 0; + char *metadata_file = nagios_metadata_name(type); + + file_strm = fopen(metadata_file, "r"); + if (file_strm == NULL) { + crm_err("Metadata file %s does not exist", metadata_file); + free(metadata_file); + return -EIO; + } + + /* see how big the file is */ + start = ftell(file_strm); + fseek(file_strm, 0L, SEEK_END); + length = ftell(file_strm); + fseek(file_strm, 0L, start); + + CRM_ASSERT(length >= 0); + CRM_ASSERT(start == ftell(file_strm)); + + if (length <= 0) { + crm_info("%s was not valid", metadata_file); + free(*output); + *output = NULL; + rc = -EIO; + + } else { + crm_trace("Reading %d bytes from file", length); + *output = calloc(1, (length + 1)); + read_len = fread(*output, 1, length, file_strm); + if (read_len != length) { + crm_err("Calculated and read bytes differ: %d vs. %d", + length, read_len); + free(*output); + *output = NULL; + rc = -EIO; + } + } + + fclose(file_strm); + free(metadata_file); + return rc; +} diff --git a/lib/services/services_nagios.h b/lib/services/services_nagios.h new file mode 100644 index 0000000..2e447e0 --- /dev/null +++ b/lib/services/services_nagios.h @@ -0,0 +1,28 @@ +/* + * Copyright 2010-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef SERVICES_NAGIOS__H +# define SERVICES_NAGIOS__H + +G_GNUC_INTERNAL +int services__nagios_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__nagios2ocf(int exit_status); + +G_GNUC_INTERNAL +GList *services__list_nagios_agents(void); + +G_GNUC_INTERNAL +gboolean services__nagios_agent_exists(const char *agent); + +G_GNUC_INTERNAL +int services__get_nagios_metadata(const char *type, char **output); + +#endif diff --git a/lib/services/services_ocf.c b/lib/services/services_ocf.c new file mode 100644 index 0000000..d7fb9bd --- /dev/null +++ b/lib/services/services_ocf.c @@ -0,0 +1,179 @@ +/* + * Copyright 2012-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include +#include +#include + +#include +#include +#include + +#include "services_private.h" +#include "services_ocf.h" + +GList * +resources_os_list_ocf_providers(void) +{ + return get_directory_list(OCF_RA_PATH, FALSE, TRUE); +} + +static GList * +services_os_get_directory_list_provider(const char *root, const char *provider, + gboolean files, gboolean executable) +{ + GList *result = NULL; + char *dirs = strdup(root); + char *dir = NULL; + char buffer[PATH_MAX]; + + if (pcmk__str_empty(dirs)) { + free(dirs); + return result; + } + + for (dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { + GList *tmp = NULL; + + sprintf(buffer, "%s/%s", dir, provider); + tmp = services_os_get_single_directory_list(buffer, files, executable); + + if (tmp) { + result = g_list_concat(result, tmp); + } + } + + free(dirs); + + return result; +} + +GList * +resources_os_list_ocf_agents(const char *provider) +{ + GList *gIter = NULL; + GList *result = NULL; + GList *providers = NULL; + + if (provider) { + return services_os_get_directory_list_provider(OCF_RA_PATH, provider, + TRUE, TRUE); + } + + providers = resources_os_list_ocf_providers(); + for (gIter = providers; gIter != NULL; gIter = gIter->next) { + GList *tmp1 = result; + GList *tmp2 = resources_os_list_ocf_agents(gIter->data); + + if (tmp2) { + result = g_list_concat(tmp1, tmp2); + } + } + g_list_free_full(providers, free); + return result; +} + +gboolean +services__ocf_agent_exists(const char *provider, const char *agent) +{ + gboolean rc = FALSE; + struct stat st; + char *dirs = strdup(OCF_RA_PATH); + char *dir = NULL; + char *buf = NULL; + + if (provider == NULL || agent == NULL || pcmk__str_empty(dirs)) { + free(dirs); + return rc; + } + + for (dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { + buf = crm_strdup_printf("%s/%s/%s", dir, provider, agent); + if (stat(buf, &st) == 0) { + free(buf); + rc = TRUE; + break; + } + + free(buf); + } + + free(dirs); + + return rc; +} + +/*! + * \internal + * \brief Prepare an OCF action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__ocf_prepare(svc_action_t *op) +{ + char *dirs = strdup(OCF_RA_PATH); + struct stat st; + + if (dirs == NULL) { + return ENOMEM; + } + + // Look for agent on path + for (char *dir = strtok(dirs, ":"); dir != NULL; dir = strtok(NULL, ":")) { + char *buf = crm_strdup_printf("%s/%s/%s", dir, op->provider, op->agent); + + if (stat(buf, &st) == 0) { + op->opaque->exec = buf; + break; + } + free(buf); + } + free(dirs); + + if (op->opaque->exec == NULL) { + return ENOENT; + } + + op->opaque->args[0] = strdup(op->opaque->exec); + op->opaque->args[1] = strdup(op->action); + if ((op->opaque->args[0] == NULL) || (op->opaque->args[1] == NULL)) { + return ENOMEM; + } + + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map an actual OCF result to a standard OCF result + * + * \param[in] exit_status Actual OCF agent exit status + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__ocf2ocf(int exit_status) +{ + switch (exit_status) { + case PCMK_OCF_DEGRADED: + case PCMK_OCF_DEGRADED_PROMOTED: + break; + default: + if ((exit_status < 0) || (exit_status > PCMK_OCF_FAILED_PROMOTED)) { + exit_status = PCMK_OCF_UNKNOWN_ERROR; + } + break; + } + return (enum ocf_exitcode) exit_status; +} diff --git a/lib/services/services_ocf.h b/lib/services/services_ocf.h new file mode 100644 index 0000000..1c40552 --- /dev/null +++ b/lib/services/services_ocf.h @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2011 Red Hat, Inc. + * Later changes copyright 2012-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef PCMK__SERVICES_OCF__H +#define PCMK__SERVICES_OCF__H + +#include + +G_GNUC_INTERNAL +GList *resources_os_list_ocf_providers(void); + +G_GNUC_INTERNAL +GList *resources_os_list_ocf_agents(const char *provider); + +G_GNUC_INTERNAL +gboolean services__ocf_agent_exists(const char *provider, const char *agent); + +G_GNUC_INTERNAL +int services__ocf_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__ocf2ocf(int exit_status); + +#endif // PCMK__SERVICES_OCF__H diff --git a/lib/services/services_private.h b/lib/services/services_private.h new file mode 100644 index 0000000..48269b8 --- /dev/null +++ b/lib/services/services_private.h @@ -0,0 +1,101 @@ +/* + * Copyright 2010-2011 Red Hat, Inc. + * Later changes copyright 2012-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef SERVICES_PRIVATE__H +# define SERVICES_PRIVATE__H + +# include +# include "crm/services.h" + +#if HAVE_DBUS +# include +#endif + +#define MAX_ARGC 255 +struct svc_action_private_s { + char *exec; + char *exit_reason; + char *args[MAX_ARGC]; + + uid_t uid; + gid_t gid; + + guint repeat_timer; + void (*callback) (svc_action_t * op); + void (*fork_callback) (svc_action_t * op); + + int stderr_fd; + mainloop_io_t *stderr_gsource; + + int stdout_fd; + mainloop_io_t *stdout_gsource; + + int stdin_fd; +#if HAVE_DBUS + DBusPendingCall* pending; + unsigned timerid; +#endif +}; + +G_GNUC_INTERNAL +const char *services__action_kind(const svc_action_t *action); + +G_GNUC_INTERNAL +GList *services_os_get_single_directory_list(const char *root, gboolean files, + gboolean executable); + +G_GNUC_INTERNAL +GList *services_os_get_directory_list(const char *root, gboolean files, gboolean executable); + +G_GNUC_INTERNAL +int services__execute_file(svc_action_t *op); + +G_GNUC_INTERNAL +gboolean cancel_recurring_action(svc_action_t * op); + +G_GNUC_INTERNAL +gboolean recurring_action_timer(gpointer data); + +G_GNUC_INTERNAL +int services__finalize_async_op(svc_action_t *op); + +G_GNUC_INTERNAL +int services__generic_error(const svc_action_t *op); + +G_GNUC_INTERNAL +int services__not_installed_error(const svc_action_t *op); + +G_GNUC_INTERNAL +int services__authorization_error(const svc_action_t *op); + +G_GNUC_INTERNAL +int services__configuration_error(const svc_action_t *op, bool is_fatal); + +G_GNUC_INTERNAL +void services__handle_exec_error(svc_action_t * op, int error); + +G_GNUC_INTERNAL +void services__set_cancelled(svc_action_t *action); + +G_GNUC_INTERNAL +void services_add_inflight_op(svc_action_t *op); + +G_GNUC_INTERNAL +void services_untrack_op(const svc_action_t *op); + +G_GNUC_INTERNAL +gboolean is_op_blocked(const char *rsc); + +#if HAVE_DBUS +G_GNUC_INTERNAL +void services_set_op_pending(svc_action_t *op, DBusPendingCall *pending); +#endif + +#endif /* SERVICES_PRIVATE__H */ diff --git a/lib/services/systemd.c b/lib/services/systemd.c new file mode 100644 index 0000000..0c38ae0 --- /dev/null +++ b/lib/services/systemd.c @@ -0,0 +1,1100 @@ +/* + * Copyright 2012-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +static void invoke_unit_by_path(svc_action_t *op, const char *unit); + +#define BUS_NAME "org.freedesktop.systemd1" +#define BUS_NAME_MANAGER BUS_NAME ".Manager" +#define BUS_NAME_UNIT BUS_NAME ".Unit" +#define BUS_PATH "/org/freedesktop/systemd1" + +/*! + * \internal + * \brief Prepare a systemd action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__systemd_prepare(svc_action_t *op) +{ + op->opaque->exec = strdup("systemd-dbus"); + if (op->opaque->exec == NULL) { + return ENOMEM; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map a systemd result to a standard OCF result + * + * \param[in] exit_status Systemd result + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__systemd2ocf(int exit_status) +{ + // This library uses OCF codes for systemd actions + return (enum ocf_exitcode) exit_status; +} + +static inline DBusMessage * +systemd_new_method(const char *method) +{ + crm_trace("Calling: %s on " BUS_NAME_MANAGER, method); + return dbus_message_new_method_call(BUS_NAME, BUS_PATH, BUS_NAME_MANAGER, + method); +} + +/* + * Functions to manage a static DBus connection + */ + +static DBusConnection* systemd_proxy = NULL; + +static inline DBusPendingCall * +systemd_send(DBusMessage *msg, + void(*done)(DBusPendingCall *pending, void *user_data), + void *user_data, int timeout) +{ + return pcmk_dbus_send(msg, systemd_proxy, done, user_data, timeout); +} + +static inline DBusMessage * +systemd_send_recv(DBusMessage *msg, DBusError *error, int timeout) +{ + return pcmk_dbus_send_recv(msg, systemd_proxy, error, timeout); +} + +/*! + * \internal + * \brief Send a method to systemd without arguments, and wait for reply + * + * \param[in] method Method to send + * + * \return Systemd reply on success, NULL (and error will be logged) otherwise + * + * \note The caller must call dbus_message_unref() on the reply after + * handling it. + */ +static DBusMessage * +systemd_call_simple_method(const char *method) +{ + DBusMessage *msg = systemd_new_method(method); + DBusMessage *reply = NULL; + DBusError error; + + /* Don't call systemd_init() here, because that calls this */ + CRM_CHECK(systemd_proxy, return NULL); + + if (msg == NULL) { + crm_err("Could not create message to send %s to systemd", method); + return NULL; + } + + dbus_error_init(&error); + reply = systemd_send_recv(msg, &error, DBUS_TIMEOUT_USE_DEFAULT); + dbus_message_unref(msg); + + if (dbus_error_is_set(&error)) { + crm_err("Could not send %s to systemd: %s (%s)", + method, error.message, error.name); + dbus_error_free(&error); + return NULL; + + } else if (reply == NULL) { + crm_err("Could not send %s to systemd: no reply received", method); + return NULL; + } + + return reply; +} + +static gboolean +systemd_init(void) +{ + static int need_init = 1; + // https://dbus.freedesktop.org/doc/api/html/group__DBusConnection.html + + if (systemd_proxy + && dbus_connection_get_is_connected(systemd_proxy) == FALSE) { + crm_warn("Connection to System DBus is closed. Reconnecting..."); + pcmk_dbus_disconnect(systemd_proxy); + systemd_proxy = NULL; + need_init = 1; + } + + if (need_init) { + need_init = 0; + systemd_proxy = pcmk_dbus_connect(); + } + if (systemd_proxy == NULL) { + return FALSE; + } + return TRUE; +} + +static inline char * +systemd_get_property(const char *unit, const char *name, + void (*callback)(const char *name, const char *value, void *userdata), + void *userdata, DBusPendingCall **pending, int timeout) +{ + return systemd_proxy? + pcmk_dbus_get_property(systemd_proxy, BUS_NAME, unit, BUS_NAME_UNIT, + name, callback, userdata, pending, timeout) + : NULL; +} + +void +systemd_cleanup(void) +{ + if (systemd_proxy) { + pcmk_dbus_disconnect(systemd_proxy); + systemd_proxy = NULL; + } +} + +/* + * end of systemd_proxy functions + */ + +/*! + * \internal + * \brief Check whether a file name represents a manageable systemd unit + * + * \param[in] name File name to check + * + * \return Pointer to "dot" before filename extension if so, NULL otherwise + */ +static const char * +systemd_unit_extension(const char *name) +{ + if (name) { + const char *dot = strrchr(name, '.'); + + if (dot && (!strcmp(dot, ".service") + || !strcmp(dot, ".socket") + || !strcmp(dot, ".mount") + || !strcmp(dot, ".timer") + || !strcmp(dot, ".path"))) { + return dot; + } + } + return NULL; +} + +static char * +systemd_service_name(const char *name, bool add_instance_name) +{ + const char *dot = NULL; + + if (pcmk__str_empty(name)) { + return NULL; + } + + /* Services that end with an @ sign are systemd templates. They expect an + * instance name to follow the service name. If no instance name was + * provided, just add "pacemaker" to the string as the instance name. It + * doesn't seem to matter for purposes of looking up whether a service + * exists or not. + * + * A template can be specified either with or without the unit extension, + * so this block handles both cases. + */ + dot = systemd_unit_extension(name); + + if (dot) { + if (dot != name && *(dot-1) == '@') { + char *s = NULL; + + if (asprintf(&s, "%.*spacemaker%s", (int) (dot-name), name, dot) == -1) { + /* If asprintf fails, just return name. */ + return strdup(name); + } + + return s; + } else { + return strdup(name); + } + + } else if (add_instance_name && *(name+strlen(name)-1) == '@') { + return crm_strdup_printf("%spacemaker.service", name); + + } else { + return crm_strdup_printf("%s.service", name); + } +} + +static void +systemd_daemon_reload_complete(DBusPendingCall *pending, void *user_data) +{ + DBusError error; + DBusMessage *reply = NULL; + unsigned int reload_count = GPOINTER_TO_UINT(user_data); + + dbus_error_init(&error); + if(pending) { + reply = dbus_pending_call_steal_reply(pending); + } + + if (pcmk_dbus_find_error(pending, reply, &error)) { + crm_warn("Could not issue systemd reload %d: %s", + reload_count, error.message); + dbus_error_free(&error); + + } else { + crm_trace("Reload %d complete", reload_count); + } + + if(pending) { + dbus_pending_call_unref(pending); + } + if(reply) { + dbus_message_unref(reply); + } +} + +static bool +systemd_daemon_reload(int timeout) +{ + static unsigned int reload_count = 0; + DBusMessage *msg = systemd_new_method("Reload"); + + reload_count++; + CRM_ASSERT(msg != NULL); + systemd_send(msg, systemd_daemon_reload_complete, + GUINT_TO_POINTER(reload_count), timeout); + dbus_message_unref(msg); + + return TRUE; +} + +/*! + * \internal + * \brief Set an action result based on a method error + * + * \param[in,out] op Action to set result for + * \param[in] error Method error + */ +static void +set_result_from_method_error(svc_action_t *op, const DBusError *error) +{ + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Unable to invoke systemd DBus method"); + + if (strstr(error->name, "org.freedesktop.systemd1.InvalidName") + || strstr(error->name, "org.freedesktop.systemd1.LoadFailed") + || strstr(error->name, "org.freedesktop.systemd1.NoSuchUnit")) { + + if (pcmk__str_eq(op->action, "stop", pcmk__str_casei)) { + crm_trace("Masking systemd stop failure (%s) for %s " + "because unknown service can be considered stopped", + error->name, pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + return; + } + + services__format_result(op, PCMK_OCF_NOT_INSTALLED, + PCMK_EXEC_NOT_INSTALLED, + "systemd unit %s not found", op->agent); + } + + crm_info("DBus request for %s of systemd unit %s%s%s failed: %s", + op->action, op->agent, + ((op->rsc == NULL)? "" : " for resource "), pcmk__s(op->rsc, ""), + error->message); +} + +/*! + * \internal + * \brief Extract unit path from LoadUnit reply, and execute action + * + * \param[in] reply LoadUnit reply + * \param[in,out] op Action to execute (or NULL to just return path) + * + * \return DBus object path for specified unit if successful (only valid for + * lifetime of \p reply), otherwise NULL + */ +static const char * +execute_after_loadunit(DBusMessage *reply, svc_action_t *op) +{ + const char *path = NULL; + DBusError error; + + /* path here is not used other than as a non-NULL flag to indicate that a + * request was indeed sent + */ + if (pcmk_dbus_find_error((void *) &path, reply, &error)) { + if (op != NULL) { + set_result_from_method_error(op, &error); + } + dbus_error_free(&error); + + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + if (op != NULL) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "systemd DBus method had unexpected reply"); + crm_info("Could not load systemd unit %s for %s: " + "DBus reply has unexpected type", op->agent, op->id); + } else { + crm_info("Could not load systemd unit: " + "DBus reply has unexpected type"); + } + + } else { + dbus_message_get_args (reply, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID); + } + + if (op != NULL) { + if (path != NULL) { + invoke_unit_by_path(op, path); + + } else if (!(op->synchronous)) { + services__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "No DBus object found for systemd unit %s", + op->agent); + services__finalize_async_op(op); + } + } + + return path; +} + +/*! + * \internal + * \brief Execute a systemd action after its LoadUnit completes + * + * \param[in,out] pending If not NULL, DBus call associated with LoadUnit + * \param[in,out] user_data Action to execute + */ +static void +loadunit_completed(DBusPendingCall *pending, void *user_data) +{ + DBusMessage *reply = NULL; + svc_action_t *op = user_data; + + crm_trace("LoadUnit result for %s arrived", op->id); + + // Grab the reply + if (pending != NULL) { + reply = dbus_pending_call_steal_reply(pending); + } + + // The call is no longer pending + CRM_LOG_ASSERT(pending == op->opaque->pending); + services_set_op_pending(op, NULL); + + // Execute the desired action based on the reply + execute_after_loadunit(reply, user_data); + if (reply != NULL) { + dbus_message_unref(reply); + } +} + +/*! + * \internal + * \brief Execute a systemd action, given the unit name + * + * \param[in] arg_name Unit name (possibly without ".service" extension) + * \param[in,out] op Action to execute (if NULL, just get object path) + * \param[out] path If non-NULL and \p op is NULL or synchronous, where + * to store DBus object path for specified unit + * + * \return Standard Pacemaker return code (for NULL \p op, pcmk_rc_ok means unit + * was found; for synchronous actions, pcmk_rc_ok means unit was + * executed, with the actual result stored in \p op; for asynchronous + * actions, pcmk_rc_ok means action was initiated) + * \note It is the caller's responsibility to free the path. + */ +static int +invoke_unit_by_name(const char *arg_name, svc_action_t *op, char **path) +{ + DBusMessage *msg; + DBusMessage *reply = NULL; + DBusPendingCall *pending = NULL; + char *name = NULL; + + if (!systemd_init()) { + if (op != NULL) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "No DBus connection"); + } + return ENOTCONN; + } + + /* Create a LoadUnit DBus method (equivalent to GetUnit if already loaded), + * which makes the unit usable via further DBus methods. + * + * + * + * + * + */ + msg = systemd_new_method("LoadUnit"); + CRM_ASSERT(msg != NULL); + + // Add the (expanded) unit name as the argument + name = systemd_service_name(arg_name, op == NULL || pcmk__str_eq(op->action, "meta-data", pcmk__str_none)); + CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &name, + DBUS_TYPE_INVALID)); + free(name); + + if ((op == NULL) || op->synchronous) { + // For synchronous ops, wait for a reply and extract the result + const char *unit = NULL; + int rc = pcmk_rc_ok; + + reply = systemd_send_recv(msg, NULL, + (op? op->timeout : DBUS_TIMEOUT_USE_DEFAULT)); + dbus_message_unref(msg); + + unit = execute_after_loadunit(reply, op); + if (unit == NULL) { + rc = ENOENT; + if (path != NULL) { + *path = NULL; + } + } else if (path != NULL) { + *path = strdup(unit); + if (*path == NULL) { + rc = ENOMEM; + } + } + + if (reply != NULL) { + dbus_message_unref(reply); + } + return rc; + } + + // For asynchronous ops, initiate the LoadUnit call and return + pending = systemd_send(msg, loadunit_completed, op, op->timeout); + if (pending == NULL) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Unable to send DBus message"); + dbus_message_unref(msg); + return ECOMM; + } + + // LoadUnit was successfully initiated + services__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL); + services_set_op_pending(op, pending); + dbus_message_unref(msg); + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Compare two strings alphabetically (case-insensitive) + * + * \param[in] a First string to compare + * \param[in] b Second string to compare + * + * \return 0 if strings are equal, -1 if a < b, 1 if a > b + * + * \note Usable as a GCompareFunc with g_list_sort(). + * NULL is considered less than non-NULL. + */ +static gint +sort_str(gconstpointer a, gconstpointer b) +{ + if (!a && !b) { + return 0; + } else if (!a) { + return -1; + } else if (!b) { + return 1; + } + return strcasecmp(a, b); +} + +GList * +systemd_unit_listall(void) +{ + int nfiles = 0; + GList *units = NULL; + DBusMessageIter args; + DBusMessageIter unit; + DBusMessageIter elem; + DBusMessage *reply = NULL; + + if (systemd_init() == FALSE) { + return NULL; + } + +/* + " \n" \ + " \n" \ + " \n" \ +*/ + + reply = systemd_call_simple_method("ListUnitFiles"); + if (reply == NULL) { + return NULL; + } + if (!dbus_message_iter_init(reply, &args)) { + crm_err("Could not list systemd unit files: systemd reply has no arguments"); + dbus_message_unref(reply); + return NULL; + } + if (!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_ARRAY, + __func__, __LINE__)) { + crm_err("Could not list systemd unit files: systemd reply has invalid arguments"); + dbus_message_unref(reply); + return NULL; + } + + dbus_message_iter_recurse(&args, &unit); + for (; dbus_message_iter_get_arg_type(&unit) != DBUS_TYPE_INVALID; + dbus_message_iter_next(&unit)) { + + DBusBasicValue value; + const char *match = NULL; + char *unit_name = NULL; + char *basename = NULL; + + if(!pcmk_dbus_type_check(reply, &unit, DBUS_TYPE_STRUCT, __func__, __LINE__)) { + crm_warn("Skipping systemd reply argument with unexpected type"); + continue; + } + + dbus_message_iter_recurse(&unit, &elem); + if(!pcmk_dbus_type_check(reply, &elem, DBUS_TYPE_STRING, __func__, __LINE__)) { + crm_warn("Skipping systemd reply argument with no string"); + continue; + } + + dbus_message_iter_get_basic(&elem, &value); + if (value.str == NULL) { + crm_debug("ListUnitFiles reply did not provide a string"); + continue; + } + crm_trace("DBus ListUnitFiles listed: %s", value.str); + + match = systemd_unit_extension(value.str); + if (match == NULL) { + // This is not a unit file type we know how to manage + crm_debug("ListUnitFiles entry '%s' is not supported as resource", + value.str); + continue; + } + + // ListUnitFiles returns full path names, we just want base name + basename = strrchr(value.str, '/'); + if (basename) { + basename = basename + 1; + } else { + basename = value.str; + } + + if (!strcmp(match, ".service")) { + // Service is the "default" unit type, so strip it + unit_name = strndup(basename, match - basename); + } else { + unit_name = strdup(basename); + } + + nfiles++; + units = g_list_prepend(units, unit_name); + } + + dbus_message_unref(reply); + + crm_trace("Found %d manageable systemd unit files", nfiles); + units = g_list_sort(units, sort_str); + return units; +} + +gboolean +systemd_unit_exists(const char *name) +{ + char *path = NULL; + char *state = NULL; + + /* Note: Makes a blocking dbus calls + * Used by resources_find_service_class() when resource class=service + */ + if ((invoke_unit_by_name(name, NULL, &path) != pcmk_rc_ok) + || (path == NULL)) { + return FALSE; + } + + /* A successful LoadUnit is not sufficient to determine the unit's + * existence; it merely means the LoadUnit request received a reply. + * We must make another blocking call to check the LoadState property. + */ + state = systemd_get_property(path, "LoadState", NULL, NULL, NULL, + DBUS_TIMEOUT_USE_DEFAULT); + free(path); + if (pcmk__str_any_of(state, "loaded", "masked", NULL)) { + free(state); + return TRUE; + } + free(state); + return FALSE; +} + +#define METADATA_FORMAT \ + "\n" \ + "\n" \ + "\n" \ + " 1.1\n" \ + " \n" \ + " %s\n" \ + " \n" \ + " systemd unit file for %s\n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + "\n" + +static char * +systemd_unit_metadata(const char *name, int timeout) +{ + char *meta = NULL; + char *desc = NULL; + char *path = NULL; + + char *escaped = NULL; + + if (invoke_unit_by_name(name, NULL, &path) == pcmk_rc_ok) { + /* TODO: Worth a making blocking call for? Probably not. Possibly if cached. */ + desc = systemd_get_property(path, "Description", NULL, NULL, NULL, + timeout); + } else { + desc = crm_strdup_printf("Systemd unit file for %s", name); + } + + escaped = crm_xml_escape(desc); + + meta = crm_strdup_printf(METADATA_FORMAT, name, escaped, name); + free(desc); + free(path); + free(escaped); + return meta; +} + +/*! + * \internal + * \brief Determine result of method from reply + * + * \param[in] reply Reply to start, stop, or restart request + * \param[in,out] op Action that was executed + */ +static void +process_unit_method_reply(DBusMessage *reply, svc_action_t *op) +{ + DBusError error; + + /* The first use of error here is not used other than as a non-NULL flag to + * indicate that a request was indeed sent + */ + if (pcmk_dbus_find_error((void *) &error, reply, &error)) { + set_result_from_method_error(op, &error); + dbus_error_free(&error); + + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + crm_info("DBus request for %s of %s succeeded but " + "return type was unexpected", + op->action, pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, + "systemd DBus method had unexpected reply"); + + } else { + const char *path = NULL; + + dbus_message_get_args(reply, NULL, + DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID); + crm_debug("DBus request for %s of %s using %s succeeded", + op->action, pcmk__s(op->rsc, "unknown resource"), path); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + } +} + +/*! + * \internal + * \brief Process the completion of an asynchronous unit start, stop, or restart + * + * \param[in,out] pending If not NULL, DBus call associated with request + * \param[in,out] user_data Action that was executed + */ +static void +unit_method_complete(DBusPendingCall *pending, void *user_data) +{ + DBusMessage *reply = NULL; + svc_action_t *op = user_data; + + crm_trace("Result for %s arrived", op->id); + + // Grab the reply + if (pending != NULL) { + reply = dbus_pending_call_steal_reply(pending); + } + + // The call is no longer pending + CRM_LOG_ASSERT(pending == op->opaque->pending); + services_set_op_pending(op, NULL); + + // Determine result and finalize action + process_unit_method_reply(reply, op); + services__finalize_async_op(op); + if (reply != NULL) { + dbus_message_unref(reply); + } +} + +#define SYSTEMD_OVERRIDE_ROOT "/run/systemd/system/" + +/* When the cluster manages a systemd resource, we create a unit file override + * to order the service "before" pacemaker. The "before" relationship won't + * actually be used, since systemd won't ever start the resource -- we're + * interested in the reverse shutdown ordering it creates, to ensure that + * systemd doesn't stop the resource at shutdown while pacemaker is still + * running. + * + * @TODO Add start timeout + */ +#define SYSTEMD_OVERRIDE_TEMPLATE \ + "[Unit]\n" \ + "Description=Cluster Controlled %s\n" \ + "Before=pacemaker.service pacemaker_remote.service\n" \ + "\n" \ + "[Service]\n" \ + "Restart=no\n" + +// Temporarily use rwxr-xr-x umask when opening a file for writing +static FILE * +create_world_readable(const char *filename) +{ + mode_t orig_umask = umask(S_IWGRP | S_IWOTH); + FILE *fp = fopen(filename, "w"); + + umask(orig_umask); + return fp; +} + +static void +create_override_dir(const char *agent) +{ + char *override_dir = crm_strdup_printf(SYSTEMD_OVERRIDE_ROOT + "/%s.service.d", agent); + int rc = pcmk__build_path(override_dir, 0755); + + if (rc != pcmk_rc_ok) { + crm_warn("Could not create systemd override directory %s: %s", + override_dir, pcmk_rc_str(rc)); + } + free(override_dir); +} + +static char * +get_override_filename(const char *agent) +{ + return crm_strdup_printf(SYSTEMD_OVERRIDE_ROOT + "/%s.service.d/50-pacemaker.conf", agent); +} + +static void +systemd_create_override(const char *agent, int timeout) +{ + FILE *file_strm = NULL; + char *override_file = get_override_filename(agent); + + create_override_dir(agent); + + /* Ensure the override file is world-readable. This is not strictly + * necessary, but it avoids a systemd warning in the logs. + */ + file_strm = create_world_readable(override_file); + if (file_strm == NULL) { + crm_err("Cannot open systemd override file %s for writing", + override_file); + } else { + char *override = crm_strdup_printf(SYSTEMD_OVERRIDE_TEMPLATE, agent); + + int rc = fprintf(file_strm, "%s\n", override); + + free(override); + if (rc < 0) { + crm_perror(LOG_WARNING, "Cannot write to systemd override file %s", + override_file); + } + fflush(file_strm); + fclose(file_strm); + systemd_daemon_reload(timeout); + } + + free(override_file); +} + +static void +systemd_remove_override(const char *agent, int timeout) +{ + char *override_file = get_override_filename(agent); + int rc = unlink(override_file); + + if (rc < 0) { + // Stop may be called when already stopped, which is fine + crm_perror(LOG_DEBUG, "Cannot remove systemd override file %s", + override_file); + } else { + systemd_daemon_reload(timeout); + } + free(override_file); +} + +/*! + * \internal + * \brief Parse result of systemd status check + * + * Set a status action's exit status and execution status based on a DBus + * property check result, and finalize the action if asynchronous. + * + * \param[in] name DBus interface name for property that was checked + * \param[in] state Property value + * \param[in,out] userdata Status action that check was done for + */ +static void +parse_status_result(const char *name, const char *state, void *userdata) +{ + svc_action_t *op = userdata; + + crm_trace("Resource %s has %s='%s'", + pcmk__s(op->rsc, "(unspecified)"), name, + pcmk__s(state, "")); + + if (pcmk__str_eq(state, "active", pcmk__str_none)) { + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else if (pcmk__str_eq(state, "reloading", pcmk__str_none)) { + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else if (pcmk__str_eq(state, "activating", pcmk__str_none)) { + services__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL); + + } else if (pcmk__str_eq(state, "deactivating", pcmk__str_none)) { + services__set_result(op, PCMK_OCF_UNKNOWN, PCMK_EXEC_PENDING, NULL); + + } else { + services__set_result(op, PCMK_OCF_NOT_RUNNING, PCMK_EXEC_DONE, state); + } + + if (!(op->synchronous)) { + services_set_op_pending(op, NULL); + services__finalize_async_op(op); + } +} + +/*! + * \internal + * \brief Invoke a systemd unit, given its DBus object path + * + * \param[in,out] op Action to execute + * \param[in] unit DBus object path of systemd unit to invoke + */ +static void +invoke_unit_by_path(svc_action_t *op, const char *unit) +{ + const char *method = NULL; + DBusMessage *msg = NULL; + DBusMessage *reply = NULL; + + if (pcmk__str_any_of(op->action, "monitor", "status", NULL)) { + DBusPendingCall *pending = NULL; + char *state; + + state = systemd_get_property(unit, "ActiveState", + (op->synchronous? NULL : parse_status_result), + op, (op->synchronous? NULL : &pending), + op->timeout); + if (op->synchronous) { + parse_status_result("ActiveState", state, op); + free(state); + + } else if (pending == NULL) { // Could not get ActiveState property + services__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Could not get state for unit %s from DBus", + op->agent); + services__finalize_async_op(op); + + } else { + services_set_op_pending(op, pending); + } + return; + + } else if (pcmk__str_eq(op->action, "start", pcmk__str_none)) { + method = "StartUnit"; + systemd_create_override(op->agent, op->timeout); + + } else if (pcmk__str_eq(op->action, "stop", pcmk__str_none)) { + method = "StopUnit"; + systemd_remove_override(op->agent, op->timeout); + + } else if (pcmk__str_eq(op->action, "restart", pcmk__str_none)) { + method = "RestartUnit"; + + } else { + services__format_result(op, PCMK_OCF_UNIMPLEMENT_FEATURE, + PCMK_EXEC_ERROR, + "Action %s not implemented " + "for systemd resources", + pcmk__s(op->action, "(unspecified)")); + if (!(op->synchronous)) { + services__finalize_async_op(op); + } + return; + } + + crm_trace("Calling %s for unit path %s%s%s", + method, unit, + ((op->rsc == NULL)? "" : " for resource "), pcmk__s(op->rsc, "")); + + msg = systemd_new_method(method); + CRM_ASSERT(msg != NULL); + + /* (ss) */ + { + const char *replace_s = "replace"; + char *name = systemd_service_name(op->agent, pcmk__str_eq(op->action, "meta-data", pcmk__str_none)); + + CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &name, DBUS_TYPE_INVALID)); + CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &replace_s, DBUS_TYPE_INVALID)); + + free(name); + } + + if (op->synchronous) { + reply = systemd_send_recv(msg, NULL, op->timeout); + dbus_message_unref(msg); + process_unit_method_reply(reply, op); + if (reply != NULL) { + dbus_message_unref(reply); + } + + } else { + DBusPendingCall *pending = systemd_send(msg, unit_method_complete, op, + op->timeout); + + dbus_message_unref(msg); + if (pending == NULL) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Unable to send DBus message"); + services__finalize_async_op(op); + + } else { + services_set_op_pending(op, pending); + } + } +} + +static gboolean +systemd_timeout_callback(gpointer p) +{ + svc_action_t * op = p; + + op->opaque->timerid = 0; + crm_info("%s action for systemd unit %s named '%s' timed out", + op->action, op->agent, op->rsc); + services__format_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_TIMEOUT, + "%s action for systemd unit %s " + "did not complete in time", op->action, op->agent); + services__finalize_async_op(op); + return FALSE; +} + +/*! + * \internal + * \brief Execute a systemd action + * + * \param[in,out] op Action to execute + * + * \return Standard Pacemaker return code + * \retval EBUSY Recurring operation could not be initiated + * \retval pcmk_rc_error Synchronous action failed + * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action + * should not be freed (because it's pending or because + * it failed to execute and was already freed) + * + * \note If the return value for an asynchronous action is not pcmk_rc_ok, the + * caller is responsible for freeing the action. + */ +int +services__execute_systemd(svc_action_t *op) +{ + CRM_ASSERT(op != NULL); + + if ((op->action == NULL) || (op->agent == NULL)) { + services__set_result(op, PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_FATAL, + "Bug in action caller"); + goto done; + } + + if (!systemd_init()) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "No DBus connection"); + goto done; + } + + crm_debug("Performing %ssynchronous %s op on systemd unit %s%s%s", + (op->synchronous? "" : "a"), op->action, op->agent, + ((op->rsc == NULL)? "" : " for resource "), pcmk__s(op->rsc, "")); + + if (pcmk__str_eq(op->action, "meta-data", pcmk__str_casei)) { + op->stdout_data = systemd_unit_metadata(op->agent, op->timeout); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + goto done; + } + + /* invoke_unit_by_name() should always override these values, which are here + * just as a fail-safe in case there are any code paths that neglect to + */ + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Bug in service library"); + + if (invoke_unit_by_name(op->agent, op, NULL) == pcmk_rc_ok) { + op->opaque->timerid = g_timeout_add(op->timeout + 5000, + systemd_timeout_callback, op); + services_add_inflight_op(op); + return pcmk_rc_ok; + } + +done: + if (op->synchronous) { + return (op->rc == PCMK_OCF_OK)? pcmk_rc_ok : pcmk_rc_error; + } else { + return services__finalize_async_op(op); + } +} diff --git a/lib/services/systemd.h b/lib/services/systemd.h new file mode 100644 index 0000000..0d3dbe6 --- /dev/null +++ b/lib/services/systemd.h @@ -0,0 +1,30 @@ +/* + * Copyright 2012-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#ifndef SYSTEMD__H +# define SYSTEMD__H + +# include +# include "crm/services.h" + +G_GNUC_INTERNAL GList *systemd_unit_listall(void); + +G_GNUC_INTERNAL +int services__systemd_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__systemd2ocf(int exit_status); + +G_GNUC_INTERNAL +int services__execute_systemd(svc_action_t *op); + +G_GNUC_INTERNAL gboolean systemd_unit_exists(const gchar * name); +G_GNUC_INTERNAL void systemd_cleanup(void); + +#endif /* SYSTEMD__H */ diff --git a/lib/services/upstart.c b/lib/services/upstart.c new file mode 100644 index 0000000..459b572 --- /dev/null +++ b/lib/services/upstart.c @@ -0,0 +1,701 @@ +/* + * Original copyright 2010 Senko Rasic + * and Ante Karamatic + * Later changes copyright 2012-2022 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#define BUS_NAME "com.ubuntu.Upstart" +#define BUS_PATH "/com/ubuntu/Upstart" + +#define UPSTART_06_API BUS_NAME"0_6" +#define UPSTART_JOB_IFACE UPSTART_06_API".Job" +#define BUS_PROPERTY_IFACE "org.freedesktop.DBus.Properties" + +/* + http://upstart.ubuntu.com/wiki/DBusInterface +*/ +static DBusConnection *upstart_proxy = NULL; + +/*! + * \internal + * \brief Prepare an Upstart action + * + * \param[in,out] op Action to prepare + * + * \return Standard Pacemaker return code + */ +int +services__upstart_prepare(svc_action_t *op) +{ + op->opaque->exec = strdup("upstart-dbus"); + if (op->opaque->exec == NULL) { + return ENOMEM; + } + return pcmk_rc_ok; +} + +/*! + * \internal + * \brief Map a Upstart result to a standard OCF result + * + * \param[in] exit_status Upstart result + * + * \return Standard OCF result + */ +enum ocf_exitcode +services__upstart2ocf(int exit_status) +{ + // This library uses OCF codes for Upstart actions + return (enum ocf_exitcode) exit_status; +} + +static gboolean +upstart_init(void) +{ + static int need_init = 1; + + if (need_init) { + need_init = 0; + upstart_proxy = pcmk_dbus_connect(); + } + if (upstart_proxy == NULL) { + return FALSE; + } + return TRUE; +} + +void +upstart_cleanup(void) +{ + if (upstart_proxy) { + pcmk_dbus_disconnect(upstart_proxy); + upstart_proxy = NULL; + } +} + +/*! + * \internal + * \brief Get the DBus object path corresponding to a job name + * + * \param[in] arg_name Name of job to get path for + * \param[out] path If not NULL, where to store DBus object path + * \param[in] timeout Give up after this many seconds + * + * \return true if object path was found, false otherwise + * \note The caller is responsible for freeing *path if it is non-NULL. + */ +static bool +object_path_for_job(const gchar *arg_name, char **path, int timeout) +{ + /* + com.ubuntu.Upstart0_6.GetJobByName (in String name, out ObjectPath job) + */ + DBusError error; + DBusMessage *msg; + DBusMessage *reply = NULL; + bool rc = false; + + if (path != NULL) { + *path = NULL; + } + + if (!upstart_init()) { + return false; + } + msg = dbus_message_new_method_call(BUS_NAME, // target for the method call + BUS_PATH, // object to call on + UPSTART_06_API, // interface to call on + "GetJobByName"); // method name + + dbus_error_init(&error); + CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg_name, + DBUS_TYPE_INVALID)); + reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, timeout); + dbus_message_unref(msg); + + if (dbus_error_is_set(&error)) { + crm_err("Could not get DBus object path for %s: %s", + arg_name, error.message); + dbus_error_free(&error); + + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + crm_err("Could not get DBus object path for %s: Invalid return type", + arg_name); + + } else { + if (path != NULL) { + dbus_message_get_args(reply, NULL, DBUS_TYPE_OBJECT_PATH, path, + DBUS_TYPE_INVALID); + if (*path != NULL) { + *path = strdup(*path); + } + } + rc = true; + } + + if (reply != NULL) { + dbus_message_unref(reply); + } + return rc; +} + +static void +fix(char *input, const char *search, char replace) +{ + char *match = NULL; + int shuffle = strlen(search) - 1; + + while (TRUE) { + int len, lpc; + + match = strstr(input, search); + if (match == NULL) { + break; + } + crm_trace("Found: %s", match); + match[0] = replace; + len = strlen(match) - shuffle; + for (lpc = 1; lpc <= len; lpc++) { + match[lpc] = match[lpc + shuffle]; + } + } +} + +static char * +fix_upstart_name(const char *input) +{ + char *output = strdup(input); + + fix(output, "_2b", '+'); + fix(output, "_2c", ','); + fix(output, "_2d", '-'); + fix(output, "_2e", '.'); + fix(output, "_40", '@'); + fix(output, "_5f", '_'); + return output; +} + +GList * +upstart_job_listall(void) +{ + GList *units = NULL; + DBusMessageIter args; + DBusMessageIter unit; + DBusMessage *msg = NULL; + DBusMessage *reply = NULL; + const char *method = "GetAllJobs"; + DBusError error; + int lpc = 0; + + if (upstart_init() == FALSE) { + return NULL; + } + +/* + com.ubuntu.Upstart0_6.GetAllJobs (out jobs) +*/ + + dbus_error_init(&error); + msg = dbus_message_new_method_call(BUS_NAME, // target for the method call + BUS_PATH, // object to call on + UPSTART_06_API, // interface to call on + method); // method name + CRM_ASSERT(msg != NULL); + + reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, DBUS_TIMEOUT_USE_DEFAULT); + dbus_message_unref(msg); + + if (dbus_error_is_set(&error)) { + crm_err("Call to %s failed: %s", method, error.message); + dbus_error_free(&error); + return NULL; + + } else if (!dbus_message_iter_init(reply, &args)) { + crm_err("Call to %s failed: Message has no arguments", method); + dbus_message_unref(reply); + return NULL; + } + + if(!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_ARRAY, __func__, __LINE__)) { + crm_err("Call to %s failed: Message has invalid arguments", method); + dbus_message_unref(reply); + return NULL; + } + + dbus_message_iter_recurse(&args, &unit); + while (dbus_message_iter_get_arg_type (&unit) != DBUS_TYPE_INVALID) { + DBusBasicValue value; + const char *job = NULL; + char *path = NULL; + + if(!pcmk_dbus_type_check(reply, &unit, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { + crm_warn("Skipping Upstart reply argument with unexpected type"); + continue; + } + + dbus_message_iter_get_basic(&unit, &value); + + if(value.str) { + int llpc = 0; + path = value.str; + job = value.str; + while (path[llpc] != 0) { + if (path[llpc] == '/') { + job = path + llpc + 1; + } + llpc++; + } + lpc++; + crm_trace("%s -> %s", path, job); + units = g_list_append(units, fix_upstart_name(job)); + } + dbus_message_iter_next (&unit); + } + + dbus_message_unref(reply); + crm_trace("Found %d upstart jobs", lpc); + return units; +} + +gboolean +upstart_job_exists(const char *name) +{ + return object_path_for_job(name, NULL, DBUS_TIMEOUT_USE_DEFAULT); +} + +static char * +get_first_instance(const gchar * job, int timeout) +{ + char *instance = NULL; + const char *method = "GetAllInstances"; + DBusError error; + DBusMessage *msg; + DBusMessage *reply; + DBusMessageIter args; + DBusMessageIter unit; + + dbus_error_init(&error); + msg = dbus_message_new_method_call(BUS_NAME, // target for the method call + job, // object to call on + UPSTART_JOB_IFACE, // interface to call on + method); // method name + CRM_ASSERT(msg != NULL); + + dbus_message_append_args(msg, DBUS_TYPE_INVALID); + reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, timeout); + dbus_message_unref(msg); + + if (dbus_error_is_set(&error)) { + crm_info("Call to %s failed: %s", method, error.message); + dbus_error_free(&error); + goto done; + + } else if(reply == NULL) { + crm_info("Call to %s failed: no reply", method); + goto done; + + } else if (!dbus_message_iter_init(reply, &args)) { + crm_info("Call to %s failed: Message has no arguments", method); + goto done; + } + + if(!pcmk_dbus_type_check(reply, &args, DBUS_TYPE_ARRAY, __func__, __LINE__)) { + crm_info("Call to %s failed: Message has invalid arguments", method); + goto done; + } + + dbus_message_iter_recurse(&args, &unit); + if(pcmk_dbus_type_check(reply, &unit, DBUS_TYPE_OBJECT_PATH, __func__, __LINE__)) { + DBusBasicValue value; + + dbus_message_iter_get_basic(&unit, &value); + + if(value.str) { + instance = strdup(value.str); + crm_trace("Result: %s", instance); + } + } + + done: + if(reply) { + dbus_message_unref(reply); + } + return instance; +} + +/*! + * \internal + * \brief Parse result of Upstart status check + * + * \param[in] name DBus interface name for property that was checked + * \param[in] state Property value + * \param[in,out] userdata Status action that check was done for + */ +static void +parse_status_result(const char *name, const char *state, void *userdata) +{ + svc_action_t *op = userdata; + + if (pcmk__str_eq(state, "running", pcmk__str_none)) { + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + } else { + services__set_result(op, PCMK_OCF_NOT_RUNNING, PCMK_EXEC_DONE, state); + } + + if (!(op->synchronous)) { + services_set_op_pending(op, NULL); + services__finalize_async_op(op); + } +} + +#define METADATA_FORMAT \ + "\n" \ + "\n" \ + "\n" \ + " 1.1\n" \ + " \n" \ + " Upstart agent for controlling the system %s service\n" \ + " \n" \ + " Upstart job for %s\n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + "\n" + +static char * +upstart_job_metadata(const char *name) +{ + return crm_strdup_printf(METADATA_FORMAT, name, name, name); +} + +/*! + * \internal + * \brief Set an action result based on a method error + * + * \param[in,out] op Action to set result for + * \param[in] error Method error + */ +static void +set_result_from_method_error(svc_action_t *op, const DBusError *error) +{ + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Unable to invoke Upstart DBus method"); + + if (strstr(error->name, UPSTART_06_API ".Error.UnknownInstance")) { + + if (pcmk__str_eq(op->action, "stop", pcmk__str_casei)) { + crm_trace("Masking stop failure (%s) for %s " + "because unknown service can be considered stopped", + error->name, pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + return; + } + + services__set_result(op, PCMK_OCF_NOT_INSTALLED, + PCMK_EXEC_NOT_INSTALLED, "Upstart job not found"); + + } else if (pcmk__str_eq(op->action, "start", pcmk__str_casei) + && strstr(error->name, UPSTART_06_API ".Error.AlreadyStarted")) { + crm_trace("Masking start failure (%s) for %s " + "because already started resource is OK", + error->name, pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + return; + } + + crm_info("DBus request for %s of Upstart job %s for resource %s failed: %s", + op->action, op->agent, pcmk__s(op->rsc, "with unknown name"), + error->message); +} + +/*! + * \internal + * \brief Process the completion of an asynchronous job start, stop, or restart + * + * \param[in,out] pending If not NULL, DBus call associated with request + * \param[in,out] user_data Action that was executed + */ +static void +job_method_complete(DBusPendingCall *pending, void *user_data) +{ + DBusError error; + DBusMessage *reply = NULL; + svc_action_t *op = user_data; + + // Grab the reply + if (pending != NULL) { + reply = dbus_pending_call_steal_reply(pending); + } + + // Determine result + dbus_error_init(&error); + if (pcmk_dbus_find_error(pending, reply, &error)) { + set_result_from_method_error(op, &error); + dbus_error_free(&error); + + } else if (pcmk__str_eq(op->action, "stop", pcmk__str_none)) { + // Call has no return value + crm_debug("DBus request for stop of %s succeeded", + pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + crm_info("DBus request for %s of %s succeeded but " + "return type was unexpected", op->action, + pcmk__s(op->rsc, "unknown resource")); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else { + const char *path = NULL; + + dbus_message_get_args(reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID); + crm_debug("DBus request for %s of %s using %s succeeded", + op->action, pcmk__s(op->rsc, "unknown resource"), path); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + } + + // The call is no longer pending + CRM_LOG_ASSERT(pending == op->opaque->pending); + services_set_op_pending(op, NULL); + + // Finalize action + services__finalize_async_op(op); + if (reply != NULL) { + dbus_message_unref(reply); + } +} + +/*! + * \internal + * \brief Execute an Upstart action + * + * \param[in,out] op Action to execute + * + * \return Standard Pacemaker return code + * \retval EBUSY Recurring operation could not be initiated + * \retval pcmk_rc_error Synchronous action failed + * \retval pcmk_rc_ok Synchronous action succeeded, or asynchronous action + * should not be freed (because it's pending or because + * it failed to execute and was already freed) + * + * \note If the return value for an asynchronous action is not pcmk_rc_ok, the + * caller is responsible for freeing the action. + */ +int +services__execute_upstart(svc_action_t *op) +{ + char *job = NULL; + int arg_wait = TRUE; + const char *arg_env = "pacemaker=1"; + const char *action = op->action; + + DBusError error; + DBusMessage *msg = NULL; + DBusMessage *reply = NULL; + DBusMessageIter iter, array_iter; + + CRM_ASSERT(op != NULL); + + if ((op->action == NULL) || (op->agent == NULL)) { + services__set_result(op, PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_FATAL, + "Bug in action caller"); + goto cleanup; + } + + if (!upstart_init()) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "No DBus connection"); + goto cleanup; + } + + if (pcmk__str_eq(op->action, "meta-data", pcmk__str_casei)) { + op->stdout_data = upstart_job_metadata(op->agent); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + goto cleanup; + } + + if (!object_path_for_job(op->agent, &job, op->timeout)) { + if (pcmk__str_eq(action, "stop", pcmk__str_none)) { + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + } else { + services__set_result(op, PCMK_OCF_NOT_INSTALLED, + PCMK_EXEC_NOT_INSTALLED, + "Upstart job not found"); + } + goto cleanup; + } + + if (job == NULL) { + // Shouldn't normally be possible -- maybe a memory error + op->rc = PCMK_OCF_UNKNOWN_ERROR; + op->status = PCMK_EXEC_ERROR; + goto cleanup; + } + + if (pcmk__strcase_any_of(op->action, "monitor", "status", NULL)) { + DBusPendingCall *pending = NULL; + char *state = NULL; + char *path = get_first_instance(job, op->timeout); + + services__set_result(op, PCMK_OCF_NOT_RUNNING, PCMK_EXEC_DONE, + "No Upstart job instances found"); + if (path == NULL) { + goto cleanup; + } + state = pcmk_dbus_get_property(upstart_proxy, BUS_NAME, path, + UPSTART_06_API ".Instance", "state", + op->synchronous? NULL : parse_status_result, + op, + op->synchronous? NULL : &pending, + op->timeout); + free(path); + + if (op->synchronous) { + parse_status_result("state", state, op); + free(state); + + } else if (pending == NULL) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Could not get job state from DBus"); + + } else { // Successfully initiated async op + free(job); + services_set_op_pending(op, pending); + services_add_inflight_op(op); + return pcmk_rc_ok; + } + + goto cleanup; + + } else if (pcmk__str_eq(action, "start", pcmk__str_none)) { + action = "Start"; + + } else if (pcmk__str_eq(action, "stop", pcmk__str_none)) { + action = "Stop"; + + } else if (pcmk__str_eq(action, "restart", pcmk__str_none)) { + action = "Restart"; + + } else { + services__set_result(op, PCMK_OCF_UNIMPLEMENT_FEATURE, + PCMK_EXEC_ERROR_HARD, + "Action not implemented for Upstart resources"); + goto cleanup; + } + + // Initialize rc/status in case called functions don't set them + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_DONE, + "Bug in service library"); + + crm_debug("Calling %s for %s on %s", + action, pcmk__s(op->rsc, "unknown resource"), job); + + msg = dbus_message_new_method_call(BUS_NAME, // target for the method call + job, // object to call on + UPSTART_JOB_IFACE, // interface to call on + action); // method name + CRM_ASSERT(msg != NULL); + + dbus_message_iter_init_append (msg, &iter); + CRM_LOG_ASSERT(dbus_message_iter_open_container(&iter, + DBUS_TYPE_ARRAY, + DBUS_TYPE_STRING_AS_STRING, + &array_iter)); + CRM_LOG_ASSERT(dbus_message_iter_append_basic(&array_iter, + DBUS_TYPE_STRING, &arg_env)); + CRM_LOG_ASSERT(dbus_message_iter_close_container(&iter, &array_iter)); + CRM_LOG_ASSERT(dbus_message_append_args(msg, DBUS_TYPE_BOOLEAN, &arg_wait, + DBUS_TYPE_INVALID)); + + if (!(op->synchronous)) { + DBusPendingCall *pending = pcmk_dbus_send(msg, upstart_proxy, + job_method_complete, op, + op->timeout); + + if (pending == NULL) { + services__set_result(op, PCMK_OCF_UNKNOWN_ERROR, PCMK_EXEC_ERROR, + "Unable to send DBus message"); + goto cleanup; + + } else { // Successfully initiated async op + free(job); + services_set_op_pending(op, pending); + services_add_inflight_op(op); + return pcmk_rc_ok; + } + } + + // Synchronous call + + dbus_error_init(&error); + reply = pcmk_dbus_send_recv(msg, upstart_proxy, &error, op->timeout); + + if (dbus_error_is_set(&error)) { + set_result_from_method_error(op, &error); + dbus_error_free(&error); + + } else if (pcmk__str_eq(op->action, "stop", pcmk__str_none)) { + // DBus call does not return a value + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else if (!pcmk_dbus_type_check(reply, NULL, DBUS_TYPE_OBJECT_PATH, + __func__, __LINE__)) { + crm_info("Call to %s passed but return type was unexpected", + op->action); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + + } else { + const char *path = NULL; + + dbus_message_get_args(reply, NULL, DBUS_TYPE_OBJECT_PATH, &path, + DBUS_TYPE_INVALID); + crm_debug("Call to %s passed: %s", op->action, path); + services__set_result(op, PCMK_OCF_OK, PCMK_EXEC_DONE, NULL); + } + +cleanup: + free(job); + if (msg != NULL) { + dbus_message_unref(msg); + } + if (reply != NULL) { + dbus_message_unref(reply); + } + + if (op->synchronous) { + return (op->rc == PCMK_OCF_OK)? pcmk_rc_ok : pcmk_rc_error; + } else { + return services__finalize_async_op(op); + } +} diff --git a/lib/services/upstart.h b/lib/services/upstart.h new file mode 100644 index 0000000..b6c4eff --- /dev/null +++ b/lib/services/upstart.h @@ -0,0 +1,31 @@ +/* + * Copyright 2010 Senko Rasic + * Copyright 2010 Ante Karamatic + * Later changes copyright 2012-2021 the Pacemaker project contributors + * + * The version control history for this file may have further details. + * + * This source code is licensed under the GNU Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ +#ifndef UPSTART__H +# define UPSTART__H + +# include +# include "crm/services.h" + +G_GNUC_INTERNAL GList *upstart_job_listall(void); + +G_GNUC_INTERNAL +int services__upstart_prepare(svc_action_t *op); + +G_GNUC_INTERNAL +enum ocf_exitcode services__upstart2ocf(int exit_status); + +G_GNUC_INTERNAL +int services__execute_upstart(svc_action_t *op); + +G_GNUC_INTERNAL gboolean upstart_job_exists(const gchar * name); +G_GNUC_INTERNAL void upstart_cleanup(void); + +#endif /* UPSTART__H */ -- cgit v1.2.3