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-2024 the Pacemaker project contributors
   3  *
   4  * The version control history for this file may have further details.
   5  *
   6  * This source code is licensed under the GNU 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  * \enum shadow_disp_flags
  59  * \brief Bit flags to control which fields of shadow CIB info are displayed
  60  *
  61  * \note Ignored for XML output.
  62  */
  63 enum shadow_disp_flags {
  64     shadow_disp_instance = (1 << 0),
  65     shadow_disp_file     = (1 << 1),
  66     shadow_disp_content  = (1 << 2),
  67     shadow_disp_diff     = (1 << 3),
  68 };
  69 
  70 static crm_exit_t exit_code = CRM_EX_OK;
  71 
  72 static struct {
  73     enum shadow_command cmd;
  74     int cmd_options;
  75     char *instance;
  76     gboolean force;
  77     gboolean batch;
  78     gboolean full_upload;
  79     gchar *validate_with;
  80 } options = {
  81     .cmd_options = cib_sync_call,
  82 };
  83 
  84 /*!
  85  * \internal
  86  * \brief Display an instruction to the user
  87  *
  88  * \param[in,out] out   Output object
  89  * \param[in]     args  Message-specific arguments
  90  *
  91  * \return Standard Pacemaker return code
  92  *
  93  * \note \p args should contain the following:
  94  *       -# Instructional message
  95  */
  96 PCMK__OUTPUT_ARGS("instruction", "const char *")
     /* [previous][next][first][last][top][bottom][index][help] */
  97 static int
  98 instruction_default(pcmk__output_t *out, va_list args)
  99 {
 100     const char *msg = va_arg(args, const char *);
 101 
 102     if (msg == NULL) {
 103         return pcmk_rc_no_output;
 104     }
 105     return out->info(out, "%s", msg);
 106 }
 107 
 108 /*!
 109  * \internal
 110  * \brief Display an instruction to the user
 111  *
 112  * \param[in,out] out   Output object
 113  * \param[in]     args  Message-specific arguments
 114  *
 115  * \return Standard Pacemaker return code
 116  *
 117  * \note \p args should contain the following:
 118  *       -# Instructional message
 119  */
 120 PCMK__OUTPUT_ARGS("instruction", "const char *")
     /* [previous][next][first][last][top][bottom][index][help] */
 121 static int
 122 instruction_xml(pcmk__output_t *out, va_list args)
 123 {
 124     const char *msg = va_arg(args, const char *);
 125 
 126     if (msg == NULL) {
 127         return pcmk_rc_no_output;
 128     }
 129     pcmk__output_create_xml_text_node(out, "instruction", msg);
 130     return pcmk_rc_ok;
 131 }
 132 
 133 /*!
 134  * \internal
 135  * \brief Display information about a shadow CIB instance
 136  *
 137  * \param[in,out] out   Output object
 138  * \param[in]     args  Message-specific arguments
 139  *
 140  * \return Standard Pacemaker return code
 141  *
 142  * \note \p args should contain the following:
 143  *       -# Instance name (can be \p NULL)
 144  *       -# Shadow file name (can be \p NULL)
 145  *       -# Shadow file content (can be \p NULL)
 146  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
 147  *       -# Group of \p shadow_disp_flags indicating which fields to display
 148  */
 149 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
     /* [previous][next][first][last][top][bottom][index][help] */
 150                   "const xmlNode *", "enum shadow_disp_flags")
 151 static int
 152 shadow_default(pcmk__output_t *out, va_list args)
 153 {
 154     const char *instance = va_arg(args, const char *);
 155     const char *filename = va_arg(args, const char *);
 156     const xmlNode *content = va_arg(args, const xmlNode *);
 157     const xmlNode *diff = va_arg(args, const xmlNode *);
 158     enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
 159 
 160     int rc = pcmk_rc_no_output;
 161 
 162     if (pcmk_is_set(flags, shadow_disp_instance)) {
 163         rc = out->info(out, "Instance: %s", pcmk__s(instance, "<unknown>"));
 164     }
 165     if (pcmk_is_set(flags, shadow_disp_file)) {
 166         rc = out->info(out, "File name: %s", pcmk__s(filename, "<unknown>"));
 167     }
 168     if (pcmk_is_set(flags, shadow_disp_content)) {
 169         rc = out->info(out, "Content:");
 170 
 171         if (content != NULL) {
 172             GString *buf = g_string_sized_new(1024);
 173             gchar *str = NULL;
 174 
 175             pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
 176                              buf, 0);
 177 
 178             str = g_string_free(buf, FALSE);
 179             str = pcmk__trim(str);
 180             if (!pcmk__str_empty(str)) {
 181                 out->info(out, "%s", str);
 182             }
 183             g_free(str);
 184 
 185         } else {
 186             out->info(out, "<unknown>");
 187         }
 188     }
 189     if (pcmk_is_set(flags, shadow_disp_diff)) {
 190         rc = out->info(out, "Diff:");
 191 
 192         if (diff != NULL) {
 193             out->message(out, "xml-patchset", diff);
 194         } else {
 195             out->info(out, "<empty>");
 196         }
 197     }
 198 
 199     return rc;
 200 }
 201 
 202 /*!
 203  * \internal
 204  * \brief Display information about a shadow CIB instance
 205  *
 206  * \param[in,out] out   Output object
 207  * \param[in]     args  Message-specific arguments
 208  *
 209  * \return Standard Pacemaker return code
 210  *
 211  * \note \p args should contain the following:
 212  *       -# Instance name (can be \p NULL)
 213  *       -# Shadow file name (can be \p NULL)
 214  *       -# Shadow file content (can be \p NULL)
 215  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
 216  *       -# Group of \p shadow_disp_flags indicating which fields to display
 217  */
 218 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
     /* [previous][next][first][last][top][bottom][index][help] */
 219                   "const xmlNode *", "enum shadow_disp_flags")
 220 static int
 221 shadow_text(pcmk__output_t *out, va_list args)
 222 {
 223     if (!out->is_quiet(out)) {
 224         return shadow_default(out, args);
 225 
 226     } else {
 227         const char *instance = va_arg(args, const char *);
 228         const char *filename = va_arg(args, const char *);
 229         const xmlNode *content = va_arg(args, const xmlNode *);
 230         const xmlNode *diff = va_arg(args, const xmlNode *);
 231         enum shadow_disp_flags flags = (enum shadow_disp_flags) va_arg(args, int);
 232 
 233         int rc = pcmk_rc_no_output;
 234         bool quiet_orig = out->quiet;
 235 
 236         /* We have to disable quiet mode for the "xml-patchset" message if we
 237          * call it, so we might as well do so for this whole section.
 238          */
 239         out->quiet = false;
 240 
 241         if (pcmk_is_set(flags, shadow_disp_instance) && (instance != NULL)) {
 242             rc = out->info(out, "%s", instance);
 243         }
 244         if (pcmk_is_set(flags, shadow_disp_file) && (filename != NULL)) {
 245             rc = out->info(out, "%s", filename);
 246         }
 247         if (pcmk_is_set(flags, shadow_disp_content) && (content != NULL)) {
 248             GString *buf = g_string_sized_new(1024);
 249             gchar *str = NULL;
 250 
 251             pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text,
 252                              buf, 0);
 253 
 254             str = g_string_free(buf, FALSE);
 255             str = pcmk__trim(str);
 256             rc = out->info(out, "%s", str);
 257             g_free(str);
 258         }
 259         if (pcmk_is_set(flags, shadow_disp_diff) && (diff != NULL)) {
 260             rc = out->message(out, "xml-patchset", diff);
 261         }
 262 
 263         out->quiet = quiet_orig;
 264         return rc;
 265     }
 266 }
 267 
 268 /*!
 269  * \internal
 270  * \brief Display information about a shadow CIB instance
 271  *
 272  * \param[in,out] out   Output object
 273  * \param[in]     args  Message-specific arguments
 274  *
 275  * \return Standard Pacemaker return code
 276  *
 277  * \note \p args should contain the following:
 278  *       -# Instance name (can be \p NULL)
 279  *       -# Shadow file name (can be \p NULL)
 280  *       -# Shadow file content (can be \p NULL)
 281  *       -# Patchset containing the changes in the shadow CIB (can be \p NULL)
 282  *       -# Group of \p shadow_disp_flags indicating which fields to display
 283  *          (ignored)
 284  */
 285 PCMK__OUTPUT_ARGS("shadow", "const char *", "const char *", "const xmlNode *",
     /* [previous][next][first][last][top][bottom][index][help] */
 286                   "const xmlNode *", "enum shadow_disp_flags")
 287 static int
 288 shadow_xml(pcmk__output_t *out, va_list args)
 289 {
 290     const char *instance = va_arg(args, const char *);
 291     const char *filename = va_arg(args, const char *);
 292     const xmlNode *content = va_arg(args, const xmlNode *);
 293     const xmlNode *diff = va_arg(args, const xmlNode *);
 294     enum shadow_disp_flags flags G_GNUC_UNUSED =
 295         (enum shadow_disp_flags) va_arg(args, int);
 296 
 297     pcmk__output_xml_create_parent(out, PCMK_XE_SHADOW,
 298                                    PCMK_XA_INSTANCE, instance,
 299                                    PCMK_XA_FILE, filename,
 300                                    NULL);
 301 
 302     if (content != NULL) {
 303         GString *buf = g_string_sized_new(1024);
 304 
 305         pcmk__xml_string(content, pcmk__xml_fmt_pretty|pcmk__xml_fmt_text, buf,
 306                          0);
 307 
 308         out->output_xml(out, PCMK_XE_CONTENT, buf->str);
 309         g_string_free(buf, TRUE);
 310     }
 311 
 312     if (diff != NULL) {
 313         out->message(out, "xml-patchset", diff);
 314     }
 315 
 316     pcmk__output_xml_pop_parent(out);
 317     return pcmk_rc_ok;
 318 }
 319 
 320 static const pcmk__supported_format_t formats[] = {
 321     PCMK__SUPPORTED_FORMAT_NONE,
 322     PCMK__SUPPORTED_FORMAT_TEXT,
 323     PCMK__SUPPORTED_FORMAT_XML,
 324     { NULL, NULL, NULL }
 325 };
 326 
 327 static const pcmk__message_entry_t fmt_functions[] = {
 328     { "instruction", "default", instruction_default },
 329     { "instruction", "xml", instruction_xml },
 330     { "shadow", "default", shadow_default },
 331     { "shadow", "text", shadow_text },
 332     { "shadow", "xml", shadow_xml },
 333 
 334     { NULL, NULL, NULL }
 335 };
 336 
 337 /*!
 338  * \internal
 339  * \brief Set the error when \p --force is not passed with a dangerous command
 340  *
 341  * \param[in]  reason         Why command is dangerous
 342  * \param[in]  for_shadow     If true, command is dangerous to the shadow file.
 343  *                            Otherwise, command is dangerous to the active
 344  *                            cluster.
 345  * \param[in]  show_mismatch  If true and the supplied shadow instance is not
 346  *                            the same as the active shadow instance, report
 347  *                            this
 348  * \param[out] error          Where to store error
 349  */
 350 static void
 351 set_danger_error(const char *reason, bool for_shadow, bool show_mismatch,
     /* [previous][next][first][last][top][bottom][index][help] */
 352                  GError **error)
 353 {
 354     const char *active = getenv("CIB_shadow");
 355     char *full = NULL;
 356 
 357     if (show_mismatch
 358         && !pcmk__str_eq(active, options.instance, pcmk__str_null_matches)) {
 359 
 360         full = crm_strdup_printf("%s.\nAdditionally, the supplied shadow "
 361                                  "instance (%s) is not the same as the active "
 362                                  "one (%s)",
 363                                 reason, options.instance, active);
 364         reason = full;
 365     }
 366 
 367     g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 368                 "%s%sTo prevent accidental destruction of the %s, the --force "
 369                 "flag is required in order to proceed.",
 370                 pcmk__s(reason, ""), ((reason != NULL)? ".\n" : ""),
 371                 (for_shadow? "shadow file" : "cluster"));
 372     free(full);
 373 }
 374 
 375 /*!
 376  * \internal
 377  * \brief Get the active shadow instance from the environment
 378  *
 379  * This sets \p options.instance to the value of the \p CIB_shadow env variable.
 380  *
 381  * \param[out] error  Where to store error
 382  */
 383 static int
 384 get_instance_from_env(GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 385 {
 386     int rc = pcmk_rc_ok;
 387 
 388     pcmk__str_update(&options.instance, getenv("CIB_shadow"));
 389     if (options.instance == NULL) {
 390         rc = ENXIO;
 391         exit_code = pcmk_rc2exitc(rc);
 392         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 393                     "No active shadow configuration defined");
 394     }
 395     return rc;
 396 }
 397 
 398 /*!
 399  * \internal
 400  * \brief Validate that the shadow file does or does not exist, as appropriate
 401  *
 402  * \param[in]  filename      Absolute path of shadow file
 403  * \param[in]  should_exist  Whether the shadow file is expected to exist
 404  * \param[out] error         Where to store error
 405  *
 406  * \return Standard Pacemaker return code
 407  */
 408 static int
 409 check_file_exists(const char *filename, bool should_exist, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 410 {
 411     struct stat buf;
 412 
 413     if (!should_exist && (stat(filename, &buf) == 0)) {
 414         char *reason = crm_strdup_printf("A shadow instance '%s' already "
 415                                          "exists", options.instance);
 416 
 417         exit_code = CRM_EX_CANTCREAT;
 418         set_danger_error(reason, true, false, error);
 419         free(reason);
 420         return EEXIST;
 421     }
 422 
 423     if (should_exist && (stat(filename, &buf) < 0)) {
 424         int rc = errno;
 425 
 426         exit_code = pcmk_rc2exitc(rc);
 427         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 428                     "Could not access shadow instance '%s': %s",
 429                     options.instance, strerror(rc));
 430         return errno;
 431     }
 432 
 433     return pcmk_rc_ok;
 434 }
 435 
 436 /*!
 437  * \internal
 438  * \brief Connect to the "real" (non-shadow) CIB
 439  *
 440  * \param[out] real_cib  Where to store CIB connection
 441  * \param[out] error     Where to store error
 442  *
 443  * \return Standard Pacemaker return code
 444  */
 445 static int
 446 connect_real_cib(cib_t **real_cib, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 447 {
 448     int rc = pcmk_rc_ok;
 449 
 450     *real_cib = cib_new_no_shadow();
 451     if (*real_cib == NULL) {
 452         rc = ENOMEM;
 453         exit_code = pcmk_rc2exitc(rc);
 454         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 455                     "Could not create a CIB connection object");
 456         return rc;
 457     }
 458 
 459     rc = cib__signon_attempts(*real_cib, cib_command, 5);
 460     rc = pcmk_legacy2rc(rc);
 461     if (rc != pcmk_rc_ok) {
 462         exit_code = pcmk_rc2exitc(rc);
 463         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 464                     "Could not connect to CIB: %s", pcmk_rc_str(rc));
 465     }
 466     return rc;
 467 }
 468 
 469 /*!
 470  * \internal
 471  * \brief Query the "real" (non-shadow) CIB and store the result
 472  *
 473  * \param[out]    output    Where to store query output
 474  * \param[out]    error     Where to store error
 475  *
 476  * \return Standard Pacemaker return code
 477  */
 478 static int
 479 query_real_cib(xmlNode **output, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 480 {
 481     cib_t *real_cib = NULL;
 482     int rc = connect_real_cib(&real_cib, error);
 483 
 484     if (rc != pcmk_rc_ok) {
 485         goto done;
 486     }
 487 
 488     rc = real_cib->cmds->query(real_cib, NULL, output, options.cmd_options);
 489     rc = pcmk_legacy2rc(rc);
 490     if (rc != pcmk_rc_ok) {
 491         exit_code = pcmk_rc2exitc(rc);
 492         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 493                     "Could not query the non-shadow CIB: %s", pcmk_rc_str(rc));
 494     }
 495 
 496 done:
 497     cib_delete(real_cib);
 498     return rc;
 499 }
 500 
 501 /*!
 502  * \internal
 503  * \brief Read XML from the given file
 504  *
 505  * \param[in]  filename  Path of input file
 506  * \param[out] output    Where to store XML read from \p filename
 507  * \param[out] error     Where to store error
 508  *
 509  * \return Standard Pacemaker return code
 510  */
 511 static int
 512 read_xml(const char *filename, xmlNode **output, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 513 {
 514     int rc = pcmk_rc_ok;
 515 
 516     *output = pcmk__xml_read(filename);
 517     if (*output == NULL) {
 518         rc = pcmk_rc_no_input;
 519         exit_code = pcmk_rc2exitc(rc);
 520         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 521                     "Could not parse XML from input file '%s'", filename);
 522     }
 523     return rc;
 524 }
 525 
 526 /*!
 527  * \internal
 528  * \brief Write the shadow XML to a file
 529  *
 530  * \param[in]  xml       Shadow XML
 531  * \param[in]  filename  Name of destination file
 532  * \param[in]  reset     Whether the write is a reset (for logging only)
 533  * \param[out] error     Where to store error
 534  */
 535 static int
 536 write_shadow_file(const xmlNode *xml, const char *filename, bool reset,
     /* [previous][next][first][last][top][bottom][index][help] */
 537                   GError **error)
 538 {
 539     int rc = pcmk__xml_write_file(xml, filename, false);
 540 
 541     if (rc != pcmk_rc_ok) {
 542         exit_code = pcmk_rc2exitc(rc);
 543         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 544                     "Could not %s the shadow instance '%s': %s",
 545                     reset? "reset" : "create", options.instance,
 546                     pcmk_rc_str(rc));
 547     }
 548     return rc;
 549 }
 550 
 551 /*!
 552  * \internal
 553  * \brief Create a shell prompt based on the given shadow instance name
 554  *
 555  * \return Newly created prompt
 556  *
 557  * \note The caller is responsible for freeing the return value using \p free().
 558  */
 559 static inline char *
 560 get_shadow_prompt(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 561 {
 562     return crm_strdup_printf("shadow[%.40s] # ", options.instance);
 563 }
 564 
 565 /*!
 566  * \internal
 567  * \brief Set up environment variables for a shadow instance
 568  *
 569  * \param[in,out] out      Output object
 570  * \param[in]     do_switch  If true, switch to an existing instance (logging
 571  *                           only)
 572  * \param[out]    error      Where to store error
 573  */
 574 static void
 575 shadow_setup(pcmk__output_t *out, bool do_switch, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 576 {
 577     const char *active = getenv("CIB_shadow");
 578     const char *prompt = getenv("PS1");
 579     const char *shell = getenv("SHELL");
 580     char *new_prompt = get_shadow_prompt();
 581 
 582     if (pcmk__str_eq(active, options.instance, pcmk__str_none)
 583         && pcmk__str_eq(new_prompt, prompt, pcmk__str_none)) {
 584         // CIB_shadow and prompt environment variables are already set up
 585         goto done;
 586     }
 587 
 588     if (!options.batch && (shell != NULL)) {
 589         out->info(out, "Setting up shadow instance");
 590         setenv("PS1", new_prompt, 1);
 591         setenv("CIB_shadow", options.instance, 1);
 592 
 593         out->message(out, PCMK_XE_INSTRUCTION,
 594                      "Press Ctrl+D to exit the crm_shadow shell");
 595 
 596         if (pcmk__str_eq(shell, "(^|/)bash$", pcmk__str_regex)) {
 597             execl(shell, shell, "--norc", "--noprofile", NULL);
 598         } else {
 599             execl(shell, shell, NULL);
 600         }
 601 
 602         exit_code = pcmk_rc2exitc(errno);
 603         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 604                     "Failed to launch shell '%s': %s",
 605                     shell, pcmk_rc_str(errno));
 606 
 607     } else {
 608         char *msg = NULL;
 609         const char *prefix = "A new shadow instance was created. To begin "
 610                              "using it";
 611 
 612         if (do_switch) {
 613             prefix = "To switch to the named shadow instance";
 614         }
 615 
 616         msg = crm_strdup_printf("%s, enter the following into your shell:\n"
 617                                 "\texport CIB_shadow=%s",
 618                                 prefix, options.instance);
 619         out->message(out, "instruction", msg);
 620         free(msg);
 621     }
 622 
 623 done:
 624     free(new_prompt);
 625 }
 626 
 627 /*!
 628  * \internal
 629  * \brief Remind the user to clean up the shadow environment
 630  *
 631  * \param[in,out] out  Output object
 632  */
 633 static void
 634 shadow_teardown(pcmk__output_t *out)
     /* [previous][next][first][last][top][bottom][index][help] */
 635 {
 636     const char *active = getenv("CIB_shadow");
 637     const char *prompt = getenv("PS1");
 638 
 639     if (pcmk__str_eq(active, options.instance, pcmk__str_none)) {
 640         char *our_prompt = get_shadow_prompt();
 641 
 642         if (pcmk__str_eq(prompt, our_prompt, pcmk__str_none)) {
 643             out->message(out, "instruction",
 644                          "Press Ctrl+D to exit the crm_shadow shell");
 645 
 646         } else {
 647             out->message(out, "instruction",
 648                          "Remember to unset the CIB_shadow variable by "
 649                          "entering the following into your shell:\n"
 650                          "\tunset CIB_shadow");
 651         }
 652         free(our_prompt);
 653     }
 654 }
 655 
 656 /*!
 657  * \internal
 658  * \brief Commit the shadow file contents to the active cluster
 659  *
 660  * \param[out] error  Where to store error
 661  */
 662 static void
 663 commit_shadow_file(GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 664 {
 665     char *filename = NULL;
 666     cib_t *real_cib = NULL;
 667 
 668     xmlNodePtr input = NULL;
 669     xmlNodePtr section_xml = NULL;
 670     const char *section = NULL;
 671 
 672     int rc = pcmk_rc_ok;
 673 
 674     if (!options.force) {
 675         const char *reason = "The commit command overwrites the active cluster "
 676                              "configuration";
 677 
 678         exit_code = CRM_EX_USAGE;
 679         set_danger_error(reason, false, true, error);
 680         return;
 681     }
 682 
 683     filename = get_shadow_file(options.instance);
 684     if (check_file_exists(filename, true, error) != pcmk_rc_ok) {
 685         goto done;
 686     }
 687 
 688     if (connect_real_cib(&real_cib, error) != pcmk_rc_ok) {
 689         goto done;
 690     }
 691 
 692     if (read_xml(filename, &input, error) != pcmk_rc_ok) {
 693         goto done;
 694     }
 695 
 696     section_xml = input;
 697 
 698     if (!options.full_upload) {
 699         section = PCMK_XE_CONFIGURATION;
 700         section_xml = pcmk__xe_first_child(input, section, NULL, NULL);
 701     }
 702 
 703     rc = real_cib->cmds->replace(real_cib, section, section_xml,
 704                                  options.cmd_options);
 705     rc = pcmk_legacy2rc(rc);
 706 
 707     if (rc != pcmk_rc_ok) {
 708         exit_code = pcmk_rc2exitc(rc);
 709         g_set_error(error, PCMK__EXITC_ERROR, exit_code,
 710                     "Could not commit shadow instance '%s' to the CIB: %s",
 711                     options.instance, pcmk_rc_str(rc));
 712     }
 713 
 714 done:
 715     free(filename);
 716     cib_delete(real_cib);
 717     pcmk__xml_free(input);
 718 }
 719 
 720 /*!
 721  * \internal
 722  * \brief Create a new empty shadow instance
 723  *
 724  * \param[in,out] out    Output object
 725  * \param[out]    error  Where to store error
 726  *
 727  * \note If \p --force is given, we try to write the file regardless of whether
 728  *       it already exists.
 729  */
 730 static void
 731 create_shadow_empty(pcmk__output_t *out, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 732 {
 733     char *filename = get_shadow_file(options.instance);
 734     xmlNode *output = NULL;
 735 
 736     if (!options.force
 737         && (check_file_exists(filename, false, error) != pcmk_rc_ok)) {
 738         goto done;
 739     }
 740 
 741     output = createEmptyCib(0);
 742     crm_xml_add(output, PCMK_XA_VALIDATE_WITH, options.validate_with);
 743     out->info(out, "Created new %s configuration",
 744               crm_element_value(output, PCMK_XA_VALIDATE_WITH));
 745 
 746     if (write_shadow_file(output, filename, false, error) != pcmk_rc_ok) {
 747         goto done;
 748     }
 749     shadow_setup(out, false, error);
 750 
 751 done:
 752     free(filename);
 753     pcmk__xml_free(output);
 754 }
 755 
 756 /*!
 757  * \internal
 758  * \brief Create a shadow instance based on the active CIB
 759  *
 760  * \param[in,out] out    Output object
 761  * \param[in]     reset  If true, overwrite the given existing shadow instance.
 762  *                       Otherwise, create a new shadow instance with the given
 763  *                       name.
 764  * \param[out]    error  Where to store error
 765  *
 766  * \note If \p --force is given, we try to write the file regardless of whether
 767  *       it already exists.
 768  */
 769 static void
 770 create_shadow_from_cib(pcmk__output_t *out, bool reset, GError **error)
     /* [previous][next][first][last][top][bottom][index][help] */
 771 {
 772     char *filename = get_shadow_file(options.instance);
 773     xmlNode *output = NULL;
 774 
 775     if (!options.force) {
 776         if (reset) {
 777             const char *reason = "The reset command overwrites the active "
 778                                  "shadow configuration";
 779 
 780             exit_code = CRM_EX_USAGE;
 781             set_danger_error(reason, true, true, error);
 782             goto done;
 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     pcmk__xml_free(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         pcmk__xml_free(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     pcmk__xml_free(old_config);
 971     pcmk__xml_free(new_config);
 972     pcmk__xml_free(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. Required: --force." },
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] */