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

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