root/tools/crm_shadow.c

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

DEFINITIONS

This source file includes following definitions.
  1. PCMK__OUTPUT_ARGS
  2. PCMK__OUTPUT_ARGS
  3. PCMK__OUTPUT_ARGS
  4. PCMK__OUTPUT_ARGS
  5. PCMK__OUTPUT_ARGS
  6. set_danger_error
  7. get_instance_from_env
  8. check_file_exists
  9. connect_real_cib
  10. query_real_cib
  11. read_xml
  12. write_shadow_file
  13. get_shadow_prompt
  14. shadow_setup
  15. shadow_teardown
  16. commit_shadow_file
  17. create_shadow_empty
  18. create_shadow_from_cib
  19. delete_shadow_file
  20. edit_shadow_file
  21. show_shadow_contents
  22. show_shadow_diff
  23. show_shadow_filename
  24. show_shadow_instance
  25. switch_shadow_instance
  26. command_cb
  27. build_arg_context
  28. main

   1 /*
   2  * Copyright 2004-2025 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 General Public License version 2
   7  * or later (GPLv2+) WITHOUT ANY WARRANTY.
   8  */
   9 
  10 #include <crm_internal.h>
  11 
  12 #include <stdio.h>
  13 #include <unistd.h>
  14 
  15 #include <sys/param.h>
  16 #include <crm/crm.h>
  17 #include <sys/stat.h>
  18 #include <sys/types.h>
  19 
  20 #include <stdlib.h>
  21 #include <errno.h>
  22 #include <fcntl.h>
  23 
  24 #include <crm/common/cmdline_internal.h>
  25 #include <crm/common/ipc.h>
  26 #include <crm/common/output_internal.h>
  27 #include <crm/common/xml.h>
  28 
  29 #include <crm/cib.h>
  30 #include <crm/cib/internal.h>
  31 
  32 #define SUMMARY "perform Pacemaker configuration changes in a sandbox\n\n"  \
  33                 "This command sets up an environment in which "             \
  34                 "configuration tools (cibadmin,\n"                          \
  35                 "crm_resource, etc.) work offline instead of against a "    \
  36                 "live cluster, allowing\n"                                  \
  37                 "changes to be previewed and tested for side effects."
  38 
  39 #define INDENT "                              "
  40 
  41 enum shadow_command {
  42     shadow_cmd_none = 0,
  43     shadow_cmd_which,
  44     shadow_cmd_display,
  45     shadow_cmd_diff,
  46     shadow_cmd_file,
  47     shadow_cmd_create,
  48     shadow_cmd_create_empty,
  49     shadow_cmd_commit,
  50     shadow_cmd_delete,
  51     shadow_cmd_edit,
  52     shadow_cmd_reset,
  53     shadow_cmd_switch,
  54 };
  55 
  56 /*!
  57  * \internal
  58  * \brief Bit flags to control which fields of shadow CIB info are displayed
  59  *
  60  * \note Ignored for XML output.
  61  */
  62 enum shadow_disp_flags {
  63     shadow_disp_instance = (1 << 0),
  64     shadow_disp_file     = (1 << 1),
  65     shadow_disp_content  = (1 << 2),
  66     shadow_disp_diff     = (1 << 3),
  67 };
  68 
  69 static crm_exit_t exit_code = CRM_EX_OK;
  70 
  71 static struct {
  72     enum shadow_command cmd;
  73     int cmd_options;
  74     char *instance;
  75     gboolean force;
  76     gboolean batch;
  77     gboolean full_upload;
  78     gchar *validate_with;
  79 } options = {
  80     .cmd_options = cib_sync_call,
  81 };
  82 
  83 /*!
  84  * \internal
  85  * \brief Display an instruction to the user
  86  *
  87  * \param[in,out] out   Output object
  88  * \param[in]     args  Message-specific arguments
  89  *
  90  * \return Standard Pacemaker return code
  91  *
  92  * \note \p args should contain the following:
  93  *       -# Instructional message
  94  */
  95 PCMK__OUTPUT_ARGS("instruction", "const char *")
     /* [previous][next][first][last][top][bottom][index][help] */
  96 static int
  97 instruction_default(pcmk__output_t *out, va_list args)
  98 {
  99     const char *msg = va_arg(args, const char *);
 100 
 101     if (msg == NULL) {
 102         return pcmk_rc_no_output;
 103     }
 104     return out->info(out, "%s", msg);
 105 }
 106 
 107 /*!
 108  * \internal
 109  * \brief Display an instruction to the user
 110  *
 111  * \param[in,out] out   Output object
 112  * \param[in]     args  Message-specific arguments
 113  *
 114  * \return Standard Pacemaker return code
 115  *
 116  * \note \p args should contain the following:
 117  *       -# Instructional message
 118  */
 119 PCMK__OUTPUT_ARGS("instruction", "const char *")
     /* [previous][next][first][last][top][bottom][index][help] */
 120 static int
 121 instruction_xml(pcmk__output_t *out, va_list args)
 122 {
 123     const char *msg = va_arg(args, const char *);
 124 
 125     if (msg == NULL) {
 126         return pcmk_rc_no_output;
 127     }
 128     pcmk__output_create_xml_text_node(out, "instruction", msg);
 129     return pcmk_rc_ok;
 130 }
 131 
 132 /*!
 133  * \internal
 134  * \brief Display information about a shadow CIB instance
 135  *
 136  * \param[in,out] out   Output object
 137  * \param[in]     args  Message-specific arguments
 138  *
 139  * \return Standard Pacemaker return code
 140  *
 141  * \note \p args should contain the following:
 142  *       -# Instance name (can be \p NULL)
 143  *       -# Shadow file name (can be \p NULL)
 144  *       -# Shadow file content (can be \p NULL)
 145  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
 146  *       -# Group of \p shadow_disp_flags indicating which fields to display
 147  */
 148 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
     /* [previous][next][first][last][top][bottom][index][help] */
 149                   "const xmlNode *", "enum shadow_disp_flags")
 150 static int
 151 shadow_default(pcmk__output_t *out, va_list args)
 152 {
 153     const char *instance = va_arg(args, const char *);
 154     const char *filename = va_arg(args, const char *);
 155     const xmlNode *content = va_arg(args, const xmlNode *);
 156     const xmlNode *diff = va_arg(args, const xmlNode *);
 157     enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
 158 
 159     int rc = pcmk_rc_no_output;
 160 
 161     if (pcmk_is_set(flags, shadow_disp_instance)) {
 162         rc = out->info(out, "Instance: %s", pcmk__s(instance, "<unknown>"));
 163     }
 164     if (pcmk_is_set(flags, shadow_disp_file)) {
 165         rc = out->info(out, "File name: %s", pcmk__s(filename, "<unknown>"));
 166     }
 167     if (pcmk_is_set(flags, shadow_disp_content)) {
 168         rc = out->info(out, "Content:");
 169 
 170         if (content != NULL) {
 171             GString *buf = g_string_sized_new(1024);
 172             gchar *str = NULL;
 173 
 174             pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
 175                              buf, 0);
 176 
 177             str = g_string_free(buf, FALSE);
 178             str = pcmk__trim(str);
 179             if (!pcmk__str_empty(str)) {
 180                 out->info(out, "%s", str);
 181             }
 182             g_free(str);
 183 
 184         } else {
 185             out->info(out, "<unknown>");
 186         }
 187     }
 188     if (pcmk_is_set(flags, shadow_disp_diff)) {
 189         rc = out->info(out, "Diff:");
 190 
 191         if (diff != NULL) {
 192             out->message(out, "xml-patchset", diff);
 193         } else {
 194             out->info(out, "<empty>");
 195         }
 196     }
 197 
 198     return rc;
 199 }
 200 
 201 /*!
 202  * \internal
 203  * \brief Display information about a shadow CIB instance
 204  *
 205  * \param[in,out] out   Output object
 206  * \param[in]     args  Message-specific arguments
 207  *
 208  * \return Standard Pacemaker return code
 209  *
 210  * \note \p args should contain the following:
 211  *       -# Instance name (can be \p NULL)
 212  *       -# Shadow file name (can be \p NULL)
 213  *       -# Shadow file content (can be \p NULL)
 214  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
 215  *       -# Group of \p shadow_disp_flags indicating which fields to display
 216  */
 217 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
     /* [previous][next][first][last][top][bottom][index][help] */
 218                   "const xmlNode *", "enum shadow_disp_flags")
 219 static int
 220 shadow_text(pcmk__output_t *out, va_list args)
 221 {
 222     if (!out->is_quiet(out)) {
 223         return shadow_default(out, args);
 224 
 225     } else {
 226         const char *instance = va_arg(args, const char *);
 227         const char *filename = va_arg(args, const char *);
 228         const xmlNode *content = va_arg(args, const xmlNode *);
 229         const xmlNode *diff = va_arg(args, const xmlNode *);
 230         enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
 231 
 232         int rc = pcmk_rc_no_output;
 233         bool quiet_orig = out->quiet;
 234 
 235         /* We have to disable quiet mode for the "xml-patchset" message if we
 236          * call it, so we might as well do so for this whole section.
 237          */
 238         out->quiet = false;
 239 
 240         if (pcmk_is_set(flags, shadow_disp_instance) && (instance != NULL)) {
 241             rc = out->info(out, "%s", instance);
 242         }
 243         if (pcmk_is_set(flags, shadow_disp_file) && (filename != NULL)) {
 244             rc = out->info(out, "%s", filename);
 245         }
 246         if (pcmk_is_set(flags, shadow_disp_content) && (content != NULL)) {
 247             GString *buf = g_string_sized_new(1024);
 248             gchar *str = NULL;
 249 
 250             pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
 251                              buf, 0);
 252 
 253             str = g_string_free(buf, FALSE);
 254             str = pcmk__trim(str);
 255             rc = out->info(out, "%s", str);
 256             g_free(str);
 257         }
 258         if (pcmk_is_set(flags, shadow_disp_diff) && (diff != NULL)) {
 259             rc = out->message(out, "xml-patchset", diff);
 260         }
 261 
 262         out->quiet = quiet_orig;
 263         return rc;
 264     }
 265 }
 266 
 267 /*!
 268  * \internal
 269  * \brief Display information about a shadow CIB instance
 270  *
 271  * \param[in,out] out   Output object
 272  * \param[in]     args  Message-specific arguments
 273  *
 274  * \return Standard Pacemaker return code
 275  *
 276  * \note \p args should contain the following:
 277  *       -# Instance name (can be \p NULL)
 278  *       -# Shadow file name (can be \p NULL)
 279  *       -# Shadow file content (can be \p NULL)
 280  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
 281  *       -# Group of \p shadow_disp_flags indicating which fields to display
 282  *          (ignored)
 283  */
 284 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
     /* [previous][next][first][last][top][bottom][index][help] */
 285                   "const xmlNode *", "enum shadow_disp_flags")
 286 static int
 287 shadow_xml(pcmk__output_t *out, va_list args)
 288 {
 289     const char *instance = va_arg(args, const char *);
 290     const char *filename = va_arg(args, const char *);
 291     const xmlNode *content = va_arg(args, const xmlNode *);
 292     const xmlNode *diff = va_arg(args, const xmlNode *);
 293     enum shadow_disp_flags flags G_GNUC_UNUSED =
 294         (enum shadow_disp_flags) va_arg(args, int);
 295 
 296     pcmk__output_xml_create_parent(out, PCMK_XE_SHADOW,
 297                                    PCMK_XA_INSTANCE, instance,
 298                                    PCMK_XA_FILE, filename,
 299                                    NULL);
 300 
 301     if (content != NULL) {
 302         GString *buf = g_string_sized_new(1024);
 303 
 304         pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
 305                          0);
 306 
 307         out->output_xml(out, PCMK_XE_CONTENT, buf->str);
 308         g_string_free(buf, TRUE);
 309     }
 310 
 311     if (diff != NULL) {
 312         out->message(out, "xml-patchset", diff);
 313     }
 314 
 315     pcmk__output_xml_pop_parent(out);
 316     return pcmk_rc_ok;
 317 }
 318 
 319 static const pcmk__supported_format_t formats[] = {
 320     PCMK__SUPPORTED_FORMAT_NONE,
 321     PCMK__SUPPORTED_FORMAT_TEXT,
 322     PCMK__SUPPORTED_FORMAT_XML,
 323     { NULL, NULL, NULL }
 324 };
 325 
 326 static const pcmk__message_entry_t fmt_functions[] = {
 327     { "instruction", "default", instruction_default },
 328     { "instruction", "xml", instruction_xml },
 329     { "shadow", "default", shadow_default },
 330     { "shadow", "text", shadow_text },
 331     { "shadow", "xml", shadow_xml },
 332 
 333     { NULL, NULL, NULL }
 334 };
 335 
 336 /*!
 337  * \internal
 338  * \brief Set the error when \p --force is not passed with a dangerous command
 339  *
 340  * \param[in]  reason         Why command is dangerous
 341  * \param[in]  for_shadow     If true, command is dangerous to the shadow file.
 342  *                            Otherwise, command is dangerous to the active
 343  *                            cluster.
 344  * \param[in]  show_mismatch  If true and the supplied shadow instance is not
 345  *                            the same as the active shadow instance, report
 346  *                            this
 347  * \param[out] error          Where to store error
 348  */
 349 static void
 350 set_danger_error(const char *reason, bool for_shadow, bool show_mismatch,
     /* [previous][next][first][last][top][bottom][index][help] */
 351                  GError **error)
 352 {
 353     const char *active = getenv("CIB_shadow");
 354     char *full = NULL;
 355 
 356     if (show_mismatch
 357         && !pcmk__str_eq(active, options.instance, pcmk__str_null_matches)) {
 358 
 359         full = crm_strdup_printf("%s.\nAdditionally, the supplied shadow "
 360                                  "instance (%s) is not the same as the active "
 361                                  "one (%s)",
 362                                 reason, options.instance, active);
 363         reason = full;
 364     }
 365 
 366     g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 367                 "%s%sTo prevent accidental destruction of the %s, the --force "
 368                 "flag is required in order to proceed.",
 369                 pcmk__s(reason, ""), ((reason != NULL)? ".\n" : ""),
 370                 (for_shadow? "shadow file" : "cluster"));
 371     free(full);
 372 }
 373 
 374 /*!
 375  * \internal
 376  * \brief Get the active shadow instance from the environment
 377  *
 378  * This sets \p options.instance to the value of the \p CIB_shadow env variable.
 379  *
 380  * \param[out] error  Where to store error
 381  */
 382 static int
 383 get_instance_from_env(GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 384 {
 385     int rc = pcmk_rc_ok;
 386 
 387     pcmk__str_update(&options.instance, getenv("CIB_shadow"));
 388     if (options.instance == NULL) {
 389         rc = ENXIO;
 390         exit_code = pcmk_rc2exitc(rc);
 391         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 392                     "No active shadow configuration defined");
 393     }
 394     return rc;
 395 }
 396 
 397 /*!
 398  * \internal
 399  * \brief Validate that the shadow file does or does not exist, as appropriate
 400  *
 401  * \param[in]  filename      Absolute path of shadow file
 402  * \param[in]  should_exist  Whether the shadow file is expected to exist
 403  * \param[out] error         Where to store error
 404  *
 405  * \return Standard Pacemaker return code
 406  */
 407 static int
 408 check_file_exists(const char *filename, bool should_exist, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 409 {
 410     struct stat buf;
 411 
 412     if (!should_exist && (stat(filename, &buf) == 0)) {
 413         char *reason = crm_strdup_printf("A shadow instance '%s' already "
 414                                          "exists", options.instance);
 415 
 416         exit_code = CRM_EX_CANTCREAT;
 417         set_danger_error(reason, true, false, error);
 418         free(reason);
 419         return EEXIST;
 420     }
 421 
 422     if (should_exist && (stat(filename, &buf) < 0)) {
 423         int rc = errno;
 424 
 425         exit_code = pcmk_rc2exitc(rc);
 426         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 427                     "Could not access shadow instance '%s': %s",
 428                     options.instance, strerror(rc));
 429         return errno;
 430     }
 431 
 432     return pcmk_rc_ok;
 433 }
 434 
 435 /*!
 436  * \internal
 437  * \brief Connect to the "real" (non-shadow) CIB
 438  *
 439  * \param[out] real_cib  Where to store CIB connection
 440  * \param[out] error     Where to store error
 441  *
 442  * \return Standard Pacemaker return code
 443  */
 444 static int
 445 connect_real_cib(cib_t **real_cib, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 446 {
 447     int rc = pcmk_rc_ok;
 448 
 449     *real_cib = cib_new_no_shadow();
 450     if (*real_cib == NULL) {
 451         rc = ENOMEM;
 452         exit_code = pcmk_rc2exitc(rc);
 453         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 454                     "Could not create a CIB connection object");
 455         return rc;
 456     }
 457 
 458     rc = cib__signon_attempts(*real_cib, cib_command, 5);
 459     rc = pcmk_legacy2rc(rc);
 460     if (rc != pcmk_rc_ok) {
 461         exit_code = pcmk_rc2exitc(rc);
 462         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 463                     "Could not connect to CIB: %s", pcmk_rc_str(rc));
 464     }
 465     return rc;
 466 }
 467 
 468 /*!
 469  * \internal
 470  * \brief Query the "real" (non-shadow) CIB and store the result
 471  *
 472  * \param[out]    output    Where to store query output
 473  * \param[out]    error     Where to store error
 474  *
 475  * \return Standard Pacemaker return code
 476  */
 477 static int
 478 query_real_cib(xmlNode **output, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 479 {
 480     cib_t *real_cib = NULL;
 481     int rc = connect_real_cib(&real_cib, error);
 482 
 483     if (rc != pcmk_rc_ok) {
 484         goto done;
 485     }
 486 
 487     rc = real_cib->cmds->query(real_cib, NULL, output, options.cmd_options);
 488     rc = pcmk_legacy2rc(rc);
 489     if (rc != pcmk_rc_ok) {
 490         exit_code = pcmk_rc2exitc(rc);
 491         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 492                     "Could not query the non-shadow CIB: %s", pcmk_rc_str(rc));
 493     }
 494 
 495 done:
 496     cib_delete(real_cib);
 497     return rc;
 498 }
 499 
 500 /*!
 501  * \internal
 502  * \brief Read XML from the given file
 503  *
 504  * \param[in]  filename  Path of input file
 505  * \param[out] output    Where to store XML read from \p filename
 506  * \param[out] error     Where to store error
 507  *
 508  * \return Standard Pacemaker return code
 509  */
 510 static int
 511 read_xml(const char *filename, xmlNode **output, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 512 {
 513     int rc = pcmk_rc_ok;
 514 
 515     *output = pcmk__xml_read(filename);
 516     if (*output == NULL) {
 517         rc = pcmk_rc_no_input;
 518         exit_code = pcmk_rc2exitc(rc);
 519         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 520                     "Could not parse XML from input file '%s'", filename);
 521     }
 522     return rc;
 523 }
 524 
 525 /*!
 526  * \internal
 527  * \brief Write the shadow XML to a file
 528  *
 529  * \param[in]  xml       Shadow XML
 530  * \param[in]  filename  Name of destination file
 531  * \param[in]  reset     Whether the write is a reset (for logging only)
 532  * \param[out] error     Where to store error
 533  */
 534 static int
 535 write_shadow_file(const xmlNode *xml, const char *filename, bool reset,
     /* [previous][next][first][last][top][bottom][index][help] */
 536                   GError **error)
 537 {
 538     int rc = pcmk__xml_write_file(xml, filename, false);
 539 
 540     if (rc != pcmk_rc_ok) {
 541         exit_code = pcmk_rc2exitc(rc);
 542         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 543                     "Could not %s the shadow instance '%s': %s",
 544                     reset? "reset" : "create", options.instance,
 545                     pcmk_rc_str(rc));
 546     }
 547     return rc;
 548 }
 549 
 550 /*!
 551  * \internal
 552  * \brief Create a shell prompt based on the given shadow instance name
 553  *
 554  * \return Newly created prompt
 555  *
 556  * \note The caller is responsible for freeing the return value using \p free().
 557  */
 558 static inline char *
 559 get_shadow_prompt(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 560 {
 561     return crm_strdup_printf("shadow[%.40s] # ", options.instance);
 562 }
 563 
 564 /*!
 565  * \internal
 566  * \brief Set up environment variables for a shadow instance
 567  *
 568  * \param[in,out] out      Output object
 569  * \param[in]     do_switch  If true, switch to an existing instance (logging
 570  *                           only)
 571  * \param[out]    error      Where to store error
 572  */
 573 static void
 574 shadow_setup(pcmk__output_t *out, bool do_switch, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 575 {
 576     const char *active = getenv("CIB_shadow");
 577     const char *prompt = getenv("PS1");
 578     const char *shell = getenv("SHELL");
 579     char *new_prompt = get_shadow_prompt();
 580 
 581     if (pcmk__str_eq(active, options.instance, pcmk__str_none)
 582         && pcmk__str_eq(new_prompt, prompt, pcmk__str_none)) {
 583         // CIB_shadow and prompt environment variables are already set up
 584         goto done;
 585     }
 586 
 587     if (!options.batch && (shell != NULL)) {
 588         out->info(out, "Setting up shadow instance");
 589         setenv("PS1", new_prompt, 1);
 590         setenv("CIB_shadow", options.instance, 1);
 591 
 592         out->message(out, PCMK_XE_INSTRUCTION,
 593                      "Press Ctrl+D to exit the crm_shadow shell");
 594 
 595         if (pcmk__str_eq(shell, "(^|/)bash$", pcmk__str_regex)) {
 596             execl(shell, shell, "--norc", "--noprofile", NULL);
 597         } else {
 598             execl(shell, shell, NULL);
 599         }
 600 
 601         exit_code = pcmk_rc2exitc(errno);
 602         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 603                     "Failed to launch shell '%s': %s",
 604                     shell, pcmk_rc_str(errno));
 605 
 606     } else {
 607         char *msg = NULL;
 608         const char *prefix = "A new shadow instance was created. To begin "
 609                              "using it";
 610 
 611         if (do_switch) {
 612             prefix = "To switch to the named shadow instance";
 613         }
 614 
 615         msg = crm_strdup_printf("%s, enter the following into your shell:\n"
 616                                 "\texport CIB_shadow=%s",
 617                                 prefix, options.instance);
 618         out->message(out, "instruction", msg);
 619         free(msg);
 620     }
 621 
 622 done:
 623     free(new_prompt);
 624 }
 625 
 626 /*!
 627  * \internal
 628  * \brief Remind the user to clean up the shadow environment
 629  *
 630  * \param[in,out] out  Output object
 631  */
 632 static void
 633 shadow_teardown(pcmk__output_t *out)
     /* [previous][next][first][last][top][bottom][index][help] */
 634 {
 635     const char *active = getenv("CIB_shadow");
 636     const char *prompt = getenv("PS1");
 637 
 638     if (pcmk__str_eq(active, options.instance, pcmk__str_none)) {
 639         char *our_prompt = get_shadow_prompt();
 640 
 641         if (pcmk__str_eq(prompt, our_prompt, pcmk__str_none)) {
 642             out->message(out, "instruction",
 643                          "Press Ctrl+D to exit the crm_shadow shell");
 644 
 645         } else {
 646             out->message(out, "instruction",
 647                          "Remember to unset the CIB_shadow variable by "
 648                          "entering the following into your shell:\n"
 649                          "\tunset CIB_shadow");
 650         }
 651         free(our_prompt);
 652     }
 653 }
 654 
 655 /*!
 656  * \internal
 657  * \brief Commit the shadow file contents to the active cluster
 658  *
 659  * \param[out] error  Where to store error
 660  */
 661 static void
 662 commit_shadow_file(GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 663 {
 664     char *filename = NULL;
 665     cib_t *real_cib = NULL;
 666 
 667     xmlNodePtr input = NULL;
 668     xmlNodePtr section_xml = NULL;
 669     const char *section = NULL;
 670 
 671     int rc = pcmk_rc_ok;
 672 
 673     if (!options.force) {
 674         const char *reason = "The commit command overwrites the active cluster "
 675                              "configuration";
 676 
 677         exit_code = CRM_EX_USAGE;
 678         set_danger_error(reason, false, true, error);
 679         return;
 680     }
 681 
 682     filename = get_shadow_file(options.instance);
 683     if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
 684         goto done;
 685     }
 686 
 687     if (connect_real_cib(&real_cib, error) != pcmk_rc_ok) {
 688         goto done;
 689     }
 690 
 691     if (read_xml(filename, &input, error) != pcmk_rc_ok) {
 692         goto done;
 693     }
 694 
 695     section_xml = input;
 696 
 697     if (!options.full_upload) {
 698         section = PCMK_XE_CONFIGURATION;
 699         section_xml = pcmk__xe_first_child(input, section, NULL, NULL);
 700     }
 701 
 702     rc = real_cib->cmds->replace(real_cib, section, section_xml,
 703                                  options.cmd_options);
 704     rc = pcmk_legacy2rc(rc);
 705 
 706     if (rc != pcmk_rc_ok) {
 707         exit_code = pcmk_rc2exitc(rc);
 708         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 709                     "Could not commit shadow instance '%s' to the CIB: %s",
 710                     options.instance, pcmk_rc_str(rc));
 711     }
 712 
 713 done:
 714     free(filename);
 715     cib_delete(real_cib);
 716     pcmk__xml_free(input);
 717 }
 718 
 719 /*!
 720  * \internal
 721  * \brief Create a new empty shadow instance
 722  *
 723  * \param[in,out] out    Output object
 724  * \param[out]    error  Where to store error
 725  *
 726  * \note If \p --force is given, we try to write the file regardless of whether
 727  *       it already exists.
 728  */
 729 static void
 730 create_shadow_empty(pcmk__output_t *out, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 731 {
 732     char *filename = get_shadow_file(options.instance);
 733     xmlNode *output = NULL;
 734 
 735     if (!options.force
 736         && (check_file_exists(filename, false, error) != pcmk_rc_ok)) {
 737         goto done;
 738     }
 739 
 740     output = createEmptyCib(0);
 741     crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with);
 742     out->info(out, "Created new %s configuration",
 743               crm_element_value(output, PCMK_XA_VALIDATE_WITH));
 744 
 745     if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) {
 746         goto done;
 747     }
 748     shadow_setup(out, false, error);
 749 
 750 done:
 751     free(filename);
 752     pcmk__xml_free(output);
 753 }
 754 
 755 /*!
 756  * \internal
 757  * \brief Create a shadow instance based on the active CIB
 758  *
 759  * \param[in,out] out    Output object
 760  * \param[in]     reset  If true, overwrite the given existing shadow instance.
 761  *                       Otherwise, create a new shadow instance with the given
 762  *                       name.
 763  * \param[out]    error  Where to store error
 764  *
 765  * \note If \p --force is given, we try to write the file regardless of whether
 766  *       it already exists.
 767  */
 768 static void
 769 create_shadow_from_cib(pcmk__output_t *out, bool reset, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 770 {
 771     char *filename = get_shadow_file(options.instance);
 772     xmlNode *output = NULL;
 773 
 774     if (!options.force) {
 775         if (reset) {
 776             const char *reason = "The reset command overwrites the active "
 777                                  "shadow configuration";
 778 
 779             exit_code = CRM_EX_USAGE;
 780             set_danger_error(reason, true, true, error);
 781             goto done;
 782         }
 783         if (check_file_exists(filename, reset, error) != pcmk_rc_ok) {
 784             goto done;
 785         }
 786     }
 787 
 788     if (query_real_cib(&output, error) != pcmk_rc_ok) {
 789         goto done;
 790     }
 791 
 792     if (write_shadow_file(output, filename, reset, error) != pcmk_rc_ok) {
 793         goto done;
 794     }
 795     shadow_setup(out, false, error);
 796 
 797 done:
 798     free(filename);
 799     pcmk__xml_free(output);
 800 }
 801 
 802 /*!
 803  * \internal
 804  * \brief Delete the shadow file
 805  *
 806  * \param[in,out] out  Output object
 807  * \param[out]    error  Where to store error
 808  */
 809 static void
 810 delete_shadow_file(pcmk__output_t *out, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 811 {
 812     char *filename = NULL;
 813 
 814     if (!options.force) {
 815         const char *reason = "The delete command removes the specified shadow "
 816                              "file";
 817 
 818         exit_code = CRM_EX_USAGE;
 819         set_danger_error(reason, true, true, error);
 820         return;
 821     }
 822 
 823     filename = get_shadow_file(options.instance);
 824 
 825     if ((unlink(filename) < 0) && (errno != ENOENT)) {
 826         exit_code = pcmk_rc2exitc(errno);
 827         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 828                     "Could not remove shadow instance '%s': %s",
 829                     options.instance, strerror(errno));
 830     } else {
 831         shadow_teardown(out);
 832     }
 833     free(filename);
 834 }
 835 
 836 /*!
 837  * \internal
 838  * \brief Open the shadow file in a text editor
 839  *
 840  * \param[out] error  Where to store error
 841  *
 842  * \note The \p EDITOR environment variable must be set.
 843  */
 844 static void
 845 edit_shadow_file(GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 846 {
 847     char *filename = NULL;
 848     const char *editor = NULL;
 849 
 850     if (get_instance_from_env(error) != pcmk_rc_ok) {
 851         return;
 852     }
 853 
 854     filename = get_shadow_file(options.instance);
 855     if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
 856         goto done;
 857     }
 858 
 859     editor = getenv("EDITOR");
 860     if (editor == NULL) {
 861         exit_code = CRM_EX_NOT_CONFIGURED;
 862         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 863                     "No value for EDITOR defined");
 864         goto done;
 865     }
 866 
 867     execlp(editor, "--", filename, NULL);
 868     exit_code = CRM_EX_OSFILE;
 869     g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 870                 "Could not invoke EDITOR (%s %s): %s",
 871                 editor, filename, strerror(errno));
 872 
 873 done:
 874     free(filename);
 875 }
 876 
 877 /*!
 878  * \internal
 879  * \brief Show the contents of the active shadow instance
 880  *
 881  * \param[in,out] out    Output object
 882  * \param[out]    error  Where to store error
 883  */
 884 static void
 885 show_shadow_contents(pcmk__output_t *out, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 886 {
 887     char *filename = NULL;
 888 
 889     if (get_instance_from_env(error) != pcmk_rc_ok) {
 890         return;
 891     }
 892 
 893     filename = get_shadow_file(options.instance);
 894 
 895     if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
 896         xmlNode *output = NULL;
 897         bool quiet_orig = out->quiet;
 898 
 899         if (read_xml(filename, &output, error) != pcmk_rc_ok) {
 900             goto done;
 901         }
 902 
 903         out->quiet = true;
 904         out->message(out, "shadow",
 905                      options.instance, NULL, output, NULL, shadow_disp_content);
 906         out->quiet = quiet_orig;
 907 
 908         pcmk__xml_free(output);
 909     }
 910 
 911 done:
 912     free(filename);
 913 }
 914 
 915 /*!
 916  * \internal
 917  * \brief Show the changes in the active shadow instance
 918  *
 919  * \param[in,out] out    Output object
 920  * \param[out]    error  Where to store error
 921  */
 922 static void
 923 show_shadow_diff(pcmk__output_t *out, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 924 {
 925     char *filename = NULL;
 926     xmlNodePtr old_config = NULL;
 927     xmlNodePtr new_config = NULL;
 928     xmlNodePtr diff = NULL;
 929     bool quiet_orig = out->quiet;
 930 
 931     if (get_instance_from_env(error) != pcmk_rc_ok) {
 932         return;
 933     }
 934 
 935     filename = get_shadow_file(options.instance);
 936     if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
 937         goto done;
 938     }
 939 
 940     if (query_real_cib(&old_config, error) != pcmk_rc_ok) {
 941         goto done;
 942     }
 943 
 944     if (read_xml(filename, &new_config, error) != pcmk_rc_ok) {
 945         goto done;
 946     }
 947     pcmk__xml_mark_changes(old_config, new_config);
 948     diff = xml_create_patchset(0, old_config, new_config, NULL, false);
 949 
 950     pcmk__log_xml_changes(LOG_INFO, new_config);
 951     pcmk__xml_commit_changes(new_config->doc);
 952 
 953     out->quiet = true;
 954     out->message(out, "shadow",
 955                  options.instance, NULL, NULL, diff, shadow_disp_diff);
 956     out->quiet = quiet_orig;
 957 
 958     if (diff != NULL) {
 959         /* @COMPAT: Exit with CRM_EX_DIGEST? This is not really an error; we
 960          * just want to indicate that there are differences (as the diff command
 961          * does).
 962          */
 963         exit_code = CRM_EX_ERROR;
 964     }
 965 
 966 done:
 967     free(filename);
 968     pcmk__xml_free(old_config);
 969     pcmk__xml_free(new_config);
 970     pcmk__xml_free(diff);
 971 }
 972 
 973 /*!
 974  * \internal
 975  * \brief Show the absolute path of the active shadow instance
 976  *
 977  * \param[in,out] out    Output object
 978  * \param[out]    error  Where to store error
 979  */
 980 static void
 981 show_shadow_filename(pcmk__output_t *out, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 982 {
 983     if (get_instance_from_env(error) == pcmk_rc_ok) {
 984         char *filename = get_shadow_file(options.instance);
 985         bool quiet_orig = out->quiet;
 986 
 987         out->quiet = true;
 988         out->message(out, "shadow",
 989                      options.instance, filename, NULL, NULL, shadow_disp_file);
 990         out->quiet = quiet_orig;
 991 
 992         free(filename);
 993     }
 994 }
 995 
 996 /*!
 997  * \internal
 998  * \brief Show the active shadow instance
 999  *
1000  * \param[in,out] out    Output object
1001  * \param[out]    error  Where to store error
1002  */
1003 static void
1004 show_shadow_instance(pcmk__output_t *out, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
1005 {
1006     if (get_instance_from_env(error) == pcmk_rc_ok) {
1007         bool quiet_orig = out->quiet;
1008 
1009         out->quiet = true;
1010         out->message(out, "shadow",
1011                      options.instance, NULL, NULL, NULL, shadow_disp_instance);
1012         out->quiet = quiet_orig;
1013     }
1014 }
1015 
1016 /*!
1017  * \internal
1018  * \brief Switch to the given shadow instance
1019  *
1020  * \param[in,out] out    Output object
1021  * \param[out]    error  Where to store error
1022  */
1023 static void
1024 switch_shadow_instance(pcmk__output_t *out, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
1025 {
1026     char *filename = NULL;
1027 
1028     filename = get_shadow_file(options.instance);
1029     if (check_file_exists(filename, true, error) == pcmk_rc_ok) {
1030         shadow_setup(out, true, error);
1031     }
1032     free(filename);
1033 }
1034 
1035 static gboolean
1036 command_cb(const gchar *option_name, const gchar *optarg, gpointer data,
     /* [previous][next][first][last][top][bottom][index][help] */
1037            GError **error)
1038 {
1039     if (pcmk__str_any_of(option_name, "-w", "--which", NULL)) {
1040         options.cmd = shadow_cmd_which;
1041 
1042     } else if (pcmk__str_any_of(option_name, "-p", "--display", NULL)) {
1043         options.cmd = shadow_cmd_display;
1044 
1045     } else if (pcmk__str_any_of(option_name, "-d", "--diff", NULL)) {
1046         options.cmd = shadow_cmd_diff;
1047 
1048     } else if (pcmk__str_any_of(option_name, "-F", "--file", NULL)) {
1049         options.cmd = shadow_cmd_file;
1050 
1051     } else if (pcmk__str_any_of(option_name, "-c", "--create", NULL)) {
1052         options.cmd = shadow_cmd_create;
1053 
1054     } else if (pcmk__str_any_of(option_name, "-e", "--create-empty", NULL)) {
1055         options.cmd = shadow_cmd_create_empty;
1056 
1057     } else if (pcmk__str_any_of(option_name, "-C", "--commit", NULL)) {
1058         options.cmd = shadow_cmd_commit;
1059 
1060     } else if (pcmk__str_any_of(option_name, "-D", "--delete", NULL)) {
1061         options.cmd = shadow_cmd_delete;
1062 
1063     } else if (pcmk__str_any_of(option_name, "-E", "--edit", NULL)) {
1064         options.cmd = shadow_cmd_edit;
1065 
1066     } else if (pcmk__str_any_of(option_name, "-r", "--reset", NULL)) {
1067         options.cmd = shadow_cmd_reset;
1068 
1069     } else if (pcmk__str_any_of(option_name, "-s", "--switch", NULL)) {
1070         options.cmd = shadow_cmd_switch;
1071 
1072     } else {
1073         // Should be impossible
1074         return FALSE;
1075     }
1076 
1077     // optarg may be NULL and that's okay
1078     pcmk__str_update(&options.instance, optarg);
1079     return TRUE;
1080 }
1081 
1082 static GOptionEntry query_entries[] = {
1083     { "which", 'w', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
1084       "Indicate the active shadow copy", NULL },
1085 
1086     { "display", 'p', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
1087       "Display the contents of the active shadow copy", NULL },
1088 
1089     { "diff", 'd', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
1090       "Display the changes in the active shadow copy", NULL },
1091 
1092     { "file", 'F', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
1093       "Display the location of the active shadow copy file", NULL },
1094 
1095     { NULL }
1096 };
1097 
1098 static GOptionEntry command_entries[] = {
1099     { "create", 'c', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
1100       "Create the named shadow copy of the active cluster configuration",
1101       "name" },
1102 
1103     { "create-empty", 'e', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK,
1104       command_cb,
1105       "Create the named shadow copy with an empty cluster configuration.\n"
1106       INDENT "Optional: --validate-with", "name" },
1107 
1108     { "commit", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
1109       "Upload the contents of the named shadow copy to the cluster", "name" },
1110 
1111     { "delete", 'D', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
1112       "Delete the contents of the named shadow copy", "name" },
1113 
1114     { "edit", 'E', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, command_cb,
1115       "Edit the contents of the active shadow copy with your favorite $EDITOR",
1116       NULL },
1117 
1118     { "reset", 'r', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
1119       "Recreate named shadow copy from the active cluster configuration",
1120       "name. Required: --force." },
1121 
1122     { "switch", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, command_cb,
1123       "(Advanced) Switch to the named shadow copy", "name" },
1124 
1125     { NULL }
1126 };
1127 
1128 static GOptionEntry addl_entries[] = {
1129     { "force", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.force,
1130       "(Advanced) Force the action to be performed", NULL },
1131 
1132     { "batch", 'b', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.batch,
1133       "(Advanced) Don't spawn a new shell", NULL },
1134 
1135     { "all", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &options.full_upload,
1136       "(Advanced) Upload entire CIB, including status, with --commit", NULL },
1137 
1138     { "validate-with", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
1139       &options.validate_with,
1140       "(Advanced) Create an older configuration version", NULL },
1141 
1142     { NULL }
1143 };
1144 
1145 static GOptionContext *
1146 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group)
     /* [previous][next][first][last][top][bottom][index][help] */
1147 {
1148     const char *desc = NULL;
1149     GOptionContext *context = NULL;
1150 
1151     desc = "Examples:\n\n"
1152            "Create a blank shadow configuration:\n\n"
1153            "\t# crm_shadow --create-empty myShadow\n\n"
1154            "Create a shadow configuration from the running cluster\n\n"
1155            "\t# crm_shadow --create myShadow\n\n"
1156            "Display the current shadow configuration:\n\n"
1157            "\t# crm_shadow --display\n\n"
1158            "Discard the current shadow configuration (named myShadow):\n\n"
1159            "\t# crm_shadow --delete myShadow --force\n\n"
1160            "Upload current shadow configuration (named myShadow) to running "
1161            "cluster:\n\n"
1162            "\t# crm_shadow --commit myShadow\n\n";
1163 
1164     context = pcmk__build_arg_context(args, "text (default), xml", group,
1165                                       "<query>|<command>");
1166     g_option_context_set_description(context, desc);
1167 
1168     pcmk__add_arg_group(context, "queries", "Queries:",
1169                         "Show query help", query_entries);
1170     pcmk__add_arg_group(context, "commands", "Commands:",
1171                         "Show command help", command_entries);
1172     pcmk__add_arg_group(context, "additional", "Additional Options:",
1173                         "Show additional options", addl_entries);
1174     return context;
1175 }
1176 
1177 int
1178 main(int argc, char **argv)
     /* [previous][next][first][last][top][bottom][index][help] */
1179 {
1180     int rc = pcmk_rc_ok;
1181     pcmk__output_t *out = NULL;
1182 
1183     GError *error = NULL;
1184 
1185     GOptionGroup *output_group = NULL;
1186     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
1187     gchar **processed_args = pcmk__cmdline_preproc(argv, "CDcersv");
1188     GOptionContext *context = build_arg_context(args, &output_group);
1189 
1190     crm_log_preinit(NULL, argc, argv);
1191 
1192     pcmk__register_formats(output_group, formats);
1193 
1194     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
1195         exit_code = CRM_EX_USAGE;
1196         goto done;
1197     }
1198 
1199     rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
1200     if (rc != pcmk_rc_ok) {
1201         exit_code = CRM_EX_ERROR;
1202         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1203                     "Error creating output format %s: %s", args->output_ty,
1204                     pcmk_rc_str(rc));
1205         goto done;
1206     }
1207 
1208     if (g_strv_length(processed_args) > 1) {
1209         gchar *help = g_option_context_get_help(context, TRUE, NULL);
1210         GString *extra = g_string_sized_new(128);
1211 
1212         for (int lpc = 1; processed_args[lpc] != NULL; lpc++) {
1213             if (extra->len > 0) {
1214                 g_string_append_c(extra, ' ');
1215             }
1216             g_string_append(extra, processed_args[lpc]);
1217         }
1218 
1219         exit_code = CRM_EX_USAGE;
1220         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1221                     "non-option ARGV-elements: %s\n\n%s", extra->str, help);
1222         g_free(help);
1223         g_string_free(extra, TRUE);
1224         goto done;
1225     }
1226 
1227     if (args->version) {
1228         out->version(out, false);
1229         goto done;
1230     }
1231 
1232     pcmk__register_messages(out, fmt_functions);
1233 
1234     if (options.cmd == shadow_cmd_none) {
1235         // @COMPAT: Create a default command if other tools have one
1236         gchar *help = g_option_context_get_help(context, TRUE, NULL);
1237 
1238         exit_code = CRM_EX_USAGE;
1239         g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1240                     "Must specify a query or command option\n\n%s", help);
1241         g_free(help);
1242         goto done;
1243     }
1244 
1245     pcmk__cli_init_logging("crm_shadow", args->verbosity);
1246 
1247     if (args->verbosity > 0) {
1248         cib__set_call_options(options.cmd_options, crm_system_name,
1249                               cib_verbose);
1250     }
1251 
1252     // Run the command
1253     switch (options.cmd) {
1254         case shadow_cmd_commit:
1255             commit_shadow_file(&error);
1256             break;
1257         case shadow_cmd_create:
1258             create_shadow_from_cib(out, false, &error);
1259             break;
1260         case shadow_cmd_create_empty:
1261             create_shadow_empty(out, &error);
1262             break;
1263         case shadow_cmd_reset:
1264             create_shadow_from_cib(out, true, &error);
1265             break;
1266         case shadow_cmd_delete:
1267             delete_shadow_file(out, &error);
1268             break;
1269         case shadow_cmd_diff:
1270             show_shadow_diff(out, &error);
1271             break;
1272         case shadow_cmd_display:
1273             show_shadow_contents(out, &error);
1274             break;
1275         case shadow_cmd_edit:
1276             edit_shadow_file(&error);
1277             break;
1278         case shadow_cmd_file:
1279             show_shadow_filename(out, &error);
1280             break;
1281         case shadow_cmd_switch:
1282             switch_shadow_instance(out, &error);
1283             break;
1284         case shadow_cmd_which:
1285             show_shadow_instance(out, &error);
1286             break;
1287         default:
1288             // Should never reach this point
1289             break;
1290     }
1291 
1292 done:
1293     g_strfreev(processed_args);
1294     pcmk__free_arg_context(context);
1295 
1296     pcmk__output_and_clear_error(&error, out);
1297 
1298     free(options.instance);
1299     g_free(options.validate_with);
1300 
1301     if (out != NULL) {
1302         out->finish(out, exit_code, true, NULL);
1303         pcmk__output_free(out);
1304     }
1305 
1306     crm_exit(exit_code);
1307 }

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