root/lib/common/digest.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. dump_xml_for_digest
  2. calculate_xml_digest_v1
  3. pcmk__digest_on_disk_cib
  4. pcmk__digest_operation
  5. pcmk__digest_xml
  6. pcmk__verify_digest
  7. pcmk__xa_filterable
  8. crm_md5sum
  9. should_filter_for_digest
  10. pcmk__filter_op_for_digest
  11. calculate_on_disk_digest
  12. calculate_operation_digest
  13. calculate_xml_versioned_digest

   1 /*
   2  * Copyright 2015-2024 the Pacemaker project contributors
   3  *
   4  * The version control history for this file may have further details.
   5  *
   6  * This source code is licensed under the GNU Lesser General Public License
   7  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
   8  */
   9 
  10 #include <crm_internal.h>
  11 
  12 #include <stdbool.h>
  13 #include <stdio.h>
  14 #include <unistd.h>
  15 #include <string.h>
  16 #include <stdlib.h>
  17 
  18 #include <glib.h>               // GString, etc.
  19 #include <gnutls/crypto.h>      // gnutls_hash_fast(), gnutls_hash_get_len()
  20 #include <gnutls/gnutls.h>      // gnutls_strerror()
  21 
  22 #include <crm/crm.h>
  23 #include <crm/common/xml.h>
  24 #include "crmcommon_private.h"
  25 
  26 #define BEST_EFFORT_STATUS 0
  27 
  28 /*
  29  * Pacemaker uses digests (MD5 hashes) of stringified XML to detect changes in
  30  * the CIB as a whole, a particular resource's agent parameters, and the device
  31  * parameters last used to unfence a particular node.
  32  *
  33  * "v2" digests hash pcmk__xml_string() directly, while less efficient "v1"
  34  * digests do the same with a prefixed space, suffixed newline, and optional
  35  * pre-sorting.
  36  *
  37  * On-disk CIB digests use v1 without sorting.
  38  *
  39  * Operation digests use v1 with sorting, and are stored in a resource's
  40  * operation history in the CIB status section. They come in three flavors:
  41  * - a digest of (nearly) all resource parameters and options, used to detect
  42  *   any resource configuration change;
  43  * - a digest of resource parameters marked as nonreloadable, used to decide
  44  *   whether a reload or full restart is needed after a configuration change;
  45  * - and a digest of resource parameters not marked as private, used in
  46  *   simulations where private parameters have been removed from the input.
  47  *
  48  * Unfencing digests are set as node attributes, and are used to require
  49  * that nodes be unfenced again after a device's configuration changes.
  50  */
  51 
  52 /*!
  53  * \internal
  54  * \brief Dump XML in a format used with v1 digests
  55  *
  56  * \param[in] xml  Root of XML to dump
  57  *
  58  * \return Newly allocated buffer containing dumped XML
  59  */
  60 static GString *
  61 dump_xml_for_digest(xmlNodePtr xml)
     /* [previous][next][first][last][top][bottom][index][help] */
  62 {
  63     GString *buffer = g_string_sized_new(1024);
  64 
  65     /* for compatibility with the old result which is used for v1 digests */
  66     g_string_append_c(buffer, ' ');
  67     pcmk__xml_string(xml, 0, buffer, 0);
  68     g_string_append_c(buffer, '\n');
  69 
  70     return buffer;
  71 }
  72 
  73 /*!
  74  * \internal
  75  * \brief Calculate and return v1 digest of XML tree
  76  *
  77  * \param[in] input  Root of XML to digest
  78  *
  79  * \return Newly allocated string containing digest
  80  *
  81  * \note Example return value: "c048eae664dba840e1d2060f00299e9d"
  82  */
  83 static char *
  84 calculate_xml_digest_v1(xmlNode *input)
     /* [previous][next][first][last][top][bottom][index][help] */
  85 {
  86     GString *buffer = dump_xml_for_digest(input);
  87     char *digest = NULL;
  88 
  89     // buffer->len > 2 for initial space and trailing newline
  90     CRM_CHECK(buffer->len > 2,
  91               g_string_free(buffer, TRUE);
  92               return NULL);
  93 
  94     digest = crm_md5sum((const char *) buffer->str);
  95     crm_log_xml_trace(input, "digest:source");
  96 
  97     g_string_free(buffer, TRUE);
  98     return digest;
  99 }
 100 
 101 /*!
 102  * \internal
 103  * \brief Calculate and return the digest of a CIB, suitable for storing on disk
 104  *
 105  * \param[in] input  Root of XML to digest
 106  *
 107  * \return Newly allocated string containing digest
 108  */
 109 char *
 110 pcmk__digest_on_disk_cib(xmlNode *input)
     /* [previous][next][first][last][top][bottom][index][help] */
 111 {
 112     /* Always use the v1 format for on-disk digests.
 113      * * Switching to v2 affects even full-restart upgrades, so it would be a
 114      *   compatibility nightmare.
 115      * * We only use this once at startup. All other invocations are in a
 116      *   separate child process.
 117      */
 118     return calculate_xml_digest_v1(input);
 119 }
 120 
 121 /*!
 122  * \internal
 123  * \brief Calculate and return digest of an operation XML element
 124  *
 125  * The digest is invariant to changes in the order of XML attributes, provided
 126  * that \p input has no children.
 127  *
 128  * \param[in] input  Root of XML to digest
 129  *
 130  * \return Newly allocated string containing digest
 131  */
 132 char *
 133 pcmk__digest_operation(xmlNode *input)
     /* [previous][next][first][last][top][bottom][index][help] */
 134 {
 135     /* Switching to v2 digests would likely cause restarts during rolling
 136      * upgrades.
 137      *
 138      * @TODO Confirm this. Switch to v2 if safe, or drop this TODO otherwise.
 139      */
 140     xmlNode *sorted = pcmk__xml_copy(NULL, input);
 141     char *digest = NULL;
 142 
 143     pcmk__xe_sort_attrs(sorted);
 144     digest = calculate_xml_digest_v1(sorted);
 145 
 146     pcmk__xml_free(sorted);
 147     return digest;
 148 }
 149 
 150 /*!
 151  * \internal
 152  * \brief Calculate and return the digest of an XML tree
 153  *
 154  * \param[in] xml     XML tree to digest
 155  * \param[in] filter  Whether to filter certain XML attributes
 156  *
 157  * \return Newly allocated string containing digest
 158  */
 159 char *
 160 pcmk__digest_xml(xmlNode *xml, bool filter)
     /* [previous][next][first][last][top][bottom][index][help] */
 161 {
 162     /* @TODO Filtering accounts for significant CPU usage. Consider removing if
 163      * possible.
 164      */
 165     char *digest = NULL;
 166     GString *buf = g_string_sized_new(1024);
 167 
 168     pcmk__xml_string(xml, (filter? pcmk__xml_fmt_filtered : 0), buf, 0);
 169     digest = crm_md5sum(buf->str);
 170 
 171     pcmk__if_tracing(
 172         {
 173             char *trace_file = crm_strdup_printf("%s/digest-%s",
 174                                                  pcmk__get_tmpdir(), digest);
 175 
 176             crm_trace("Saving %s.%s.%s to %s",
 177                       crm_element_value(xml, PCMK_XA_ADMIN_EPOCH),
 178                       crm_element_value(xml, PCMK_XA_EPOCH),
 179                       crm_element_value(xml, PCMK_XA_NUM_UPDATES),
 180                       trace_file);
 181             save_xml_to_file(xml, "digest input", trace_file);
 182             free(trace_file);
 183         },
 184         {}
 185     );
 186     g_string_free(buf, TRUE);
 187     return digest;
 188 }
 189 
 190 /*!
 191  * \internal
 192  * \brief Check whether calculated digest of given XML matches expected digest
 193  *
 194  * \param[in] input     Root of XML tree to digest
 195  * \param[in] expected  Expected digest in on-disk format
 196  *
 197  * \return true if digests match, false on mismatch or error
 198  */
 199 bool
 200 pcmk__verify_digest(xmlNode *input, const char *expected)
     /* [previous][next][first][last][top][bottom][index][help] */
 201 {
 202     char *calculated = NULL;
 203     bool passed;
 204 
 205     if (input != NULL) {
 206         calculated = pcmk__digest_on_disk_cib(input);
 207         if (calculated == NULL) {
 208             crm_perror(LOG_ERR, "Could not calculate digest for comparison");
 209             return false;
 210         }
 211     }
 212     passed = pcmk__str_eq(expected, calculated, pcmk__str_casei);
 213     if (passed) {
 214         crm_trace("Digest comparison passed: %s", calculated);
 215     } else {
 216         crm_err("Digest comparison failed: expected %s, calculated %s",
 217                 expected, calculated);
 218     }
 219     free(calculated);
 220     return passed;
 221 }
 222 
 223 /*!
 224  * \internal
 225  * \brief Check whether an XML attribute should be excluded from CIB digests
 226  *
 227  * \param[in] name  XML attribute name
 228  *
 229  * \return true if XML attribute should be excluded from CIB digest calculation
 230  */
 231 bool
 232 pcmk__xa_filterable(const char *name)
     /* [previous][next][first][last][top][bottom][index][help] */
 233 {
 234     static const char *filter[] = {
 235         PCMK_XA_CRM_DEBUG_ORIGIN,
 236         PCMK_XA_CIB_LAST_WRITTEN,
 237         PCMK_XA_UPDATE_ORIGIN,
 238         PCMK_XA_UPDATE_CLIENT,
 239         PCMK_XA_UPDATE_USER,
 240     };
 241 
 242     for (int i = 0; i < PCMK__NELEM(filter); i++) {
 243         if (strcmp(name, filter[i]) == 0) {
 244             return true;
 245         }
 246     }
 247     return false;
 248 }
 249 
 250 char *
 251 crm_md5sum(const char *buffer)
     /* [previous][next][first][last][top][bottom][index][help] */
 252 {
 253     char *digest = NULL;
 254     gchar *raw_digest = NULL;
 255 
 256     if (buffer == NULL) {
 257         return NULL;
 258     }
 259 
 260     raw_digest = g_compute_checksum_for_string(G_CHECKSUM_MD5, buffer, -1);
 261 
 262     if (raw_digest == NULL) {
 263         crm_err("Failed to calculate hash");
 264         return NULL;
 265     }
 266 
 267     digest = pcmk__str_copy(raw_digest);
 268     g_free(raw_digest);
 269 
 270     crm_trace("Digest %s.", digest);
 271     return digest;
 272 }
 273 
 274 // Return true if a is an attribute that should be filtered
 275 static bool
 276 should_filter_for_digest(xmlAttrPtr a, void *user_data)
     /* [previous][next][first][last][top][bottom][index][help] */
 277 {
 278     if (strncmp((const char *) a->name, CRM_META "_",
 279                 sizeof(CRM_META " ") - 1) == 0) {
 280         return true;
 281     }
 282     return pcmk__str_any_of((const char *) a->name,
 283                             PCMK_XA_ID,
 284                             PCMK_XA_CRM_FEATURE_SET,
 285                             PCMK__XA_OP_DIGEST,
 286                             PCMK__META_ON_NODE,
 287                             PCMK__META_ON_NODE_UUID,
 288                             "pcmk_external_ip",
 289                             NULL);
 290 }
 291 
 292 /*!
 293  * \internal
 294  * \brief Remove XML attributes not needed for operation digest
 295  *
 296  * \param[in,out] param_set  XML with operation parameters
 297  */
 298 void
 299 pcmk__filter_op_for_digest(xmlNode *param_set)
     /* [previous][next][first][last][top][bottom][index][help] */
 300 {
 301     char *key = NULL;
 302     char *timeout = NULL;
 303     guint interval_ms = 0;
 304 
 305     if (param_set == NULL) {
 306         return;
 307     }
 308 
 309     /* Timeout is useful for recurring operation digests, so grab it before
 310      * removing meta-attributes
 311      */
 312     key = crm_meta_name(PCMK_META_INTERVAL);
 313     if (crm_element_value_ms(param_set, key, &interval_ms) != pcmk_ok) {
 314         interval_ms = 0;
 315     }
 316     free(key);
 317     key = NULL;
 318     if (interval_ms != 0) {
 319         key = crm_meta_name(PCMK_META_TIMEOUT);
 320         timeout = crm_element_value_copy(param_set, key);
 321     }
 322 
 323     // Remove all CRM_meta_* attributes and certain other attributes
 324     pcmk__xe_remove_matching_attrs(param_set, should_filter_for_digest, NULL);
 325 
 326     // Add timeout back for recurring operation digests
 327     if (timeout != NULL) {
 328         crm_xml_add(param_set, key, timeout);
 329     }
 330     free(timeout);
 331     free(key);
 332 }
 333 
 334 // Deprecated functions kept only for backward API compatibility
 335 // LCOV_EXCL_START
 336 
 337 #include <crm/common/xml_compat.h>
 338 #include <crm/common/xml_element_compat.h>
 339 
 340 char *
 341 calculate_on_disk_digest(xmlNode *input)
     /* [previous][next][first][last][top][bottom][index][help] */
 342 {
 343     return calculate_xml_digest_v1(input);
 344 }
 345 
 346 char *
 347 calculate_operation_digest(xmlNode *input, const char *version)
     /* [previous][next][first][last][top][bottom][index][help] */
 348 {
 349     xmlNode *sorted = sorted_xml(input, NULL, true);
 350     char *digest = calculate_xml_digest_v1(sorted);
 351 
 352     pcmk__xml_free(sorted);
 353     return digest;
 354 }
 355 
 356 char *
 357 calculate_xml_versioned_digest(xmlNode *input, gboolean sort,
     /* [previous][next][first][last][top][bottom][index][help] */
 358                                gboolean do_filter, const char *version)
 359 {
 360     if ((version == NULL) || (compare_version("3.0.5", version) > 0)) {
 361         xmlNode *sorted = NULL;
 362         char *digest = NULL;
 363 
 364         if (sort) {
 365             xmlNode *sorted = sorted_xml(input, NULL, true);
 366 
 367             input = sorted;
 368         }
 369 
 370         crm_trace("Using v1 digest algorithm for %s",
 371                   pcmk__s(version, "unknown feature set"));
 372 
 373         digest = calculate_xml_digest_v1(input);
 374 
 375         pcmk__xml_free(sorted);
 376         return digest;
 377     }
 378     crm_trace("Using v2 digest algorithm for %s", version);
 379     return pcmk__digest_xml(input, do_filter);
 380 }
 381 
 382 // LCOV_EXCL_STOP
 383 // End deprecated API

/* [previous][next][first][last][top][bottom][index][help] */