gvinum.c revision 130391
1/* 2 * Copyright (c) 2004 Lukas Ertl 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 1. Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 2. Redistributions in binary form must reproduce the above copyright 11 * notice, this list of conditions and the following disclaimer in the 12 * documentation and/or other materials provided with the distribution. 13 * 14 * THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND 15 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 * ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 18 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 20 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 21 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 * SUCH DAMAGE. 25 * 26 * $FreeBSD: head/sbin/gvinum/gvinum.c 130391 2004-06-12 21:22:47Z le $ 27 */ 28 29#include <sys/param.h> 30#include <sys/linker.h> 31#include <sys/lock.h> 32#include <sys/module.h> 33#include <sys/mutex.h> 34#include <sys/queue.h> 35#include <sys/utsname.h> 36 37#include <geom/vinum/geom_vinum_var.h> 38#include <geom/vinum/geom_vinum_share.h> 39 40#include <ctype.h> 41#include <err.h> 42#include <libgeom.h> 43#include <stdint.h> 44#include <stdio.h> 45#include <stdlib.h> 46#include <paths.h> 47#include <readline/readline.h> 48#include <readline/history.h> 49#include <unistd.h> 50 51#include "gvinum.h" 52 53void gvinum_cancelinit(int, char **); 54void gvinum_create(int, char **); 55void gvinum_help(void); 56void gvinum_init(int, char **); 57void gvinum_list(int, char **); 58void gvinum_printconfig(int, char **); 59void gvinum_rm(int, char **); 60void gvinum_saveconfig(void); 61void gvinum_start(int, char **); 62void gvinum_stop(int, char **); 63void parseline(int, char **); 64void printconfig(FILE *, char *); 65 66int 67main(int argc, char **argv) 68{ 69 int line, tokens; 70 char buffer[BUFSIZ], *inputline, *token[GV_MAXARGS]; 71 72 /* Load the module if necessary. */ 73 if (kldfind(GVINUMMOD) < 0 && kldload(GVINUMMOD) < 0) 74 err(1, GVINUMMOD ": Kernel module not available"); 75 76 /* Arguments given on the command line. */ 77 if (argc > 1) { 78 argc--; 79 argv++; 80 parseline(argc, argv); 81 82 /* Interactive mode. */ 83 } else { 84 for (;;) { 85 inputline = readline("gvinum -> "); 86 if (inputline == NULL) { 87 if (ferror(stdin)) { 88 err(1, "can't read input"); 89 } else { 90 printf("\n"); 91 exit(0); 92 } 93 } else if (*inputline) { 94 add_history(inputline); 95 strcpy(buffer, inputline); 96 free(inputline); 97 line++; /* count the lines */ 98 tokens = gv_tokenize(buffer, token, GV_MAXARGS); 99 if (tokens) 100 parseline(tokens, token); 101 } 102 } 103 } 104 exit(0); 105} 106 107void 108gvinum_cancelinit(int argc, char **argv) 109{ 110 struct gctl_req *req; 111 int i; 112 const char *errstr; 113 char buf[20]; 114 115 if (argc == 1) 116 return; 117 118 argc--; 119 argv++; 120 121 req = gctl_get_handle(); 122 gctl_ro_param(req, "class", -1, "VINUM"); 123 gctl_ro_param(req, "verb", -1, "cancelinit"); 124 gctl_ro_param(req, "argc", sizeof(int), &argc); 125 if (argc) { 126 for (i = 0; i < argc; i++) { 127 snprintf(buf, sizeof(buf), "argv%d", i); 128 gctl_ro_param(req, buf, -1, argv[i]); 129 } 130 } 131 errstr = gctl_issue(req); 132 if (errstr != NULL) { 133 warnx("can't init: %s", errstr); 134 gctl_free(req); 135 return; 136 } 137 138 gctl_free(req); 139 gvinum_list(0, NULL); 140} 141 142void 143gvinum_create(int argc, char **argv) 144{ 145 struct gctl_req *req; 146 struct gv_drive *d; 147 struct gv_plex *p; 148 struct gv_sd *s; 149 struct gv_volume *v; 150 FILE *tmp; 151 int drives, errors, fd, line, plexes, plex_in_volume; 152 int sd_in_plex, status, subdisks, tokens, volumes; 153 const char *errstr; 154 char buf[BUFSIZ], buf1[BUFSIZ], commandline[BUFSIZ], *ed; 155 char original[BUFSIZ], tmpfile[20], *token[GV_MAXARGS]; 156 char plex[GV_MAXPLEXNAME], volume[GV_MAXVOLNAME]; 157 158 snprintf(tmpfile, sizeof(tmpfile), "/tmp/gvinum.XXXXXX"); 159 160 if ((fd = mkstemp(tmpfile)) == -1) { 161 warn("temporary file not accessible"); 162 return; 163 } 164 if ((tmp = fdopen(fd, "w")) == NULL) { 165 warn("can't open '%s' for writing", tmpfile); 166 return; 167 } 168 printconfig(tmp, "# "); 169 fclose(tmp); 170 171 ed = getenv("EDITOR"); 172 if (ed == NULL) 173 ed = _PATH_VI; 174 175 snprintf(commandline, sizeof(commandline), "%s %s", ed, tmpfile); 176 status = system(commandline); 177 if (status != 0) { 178 warn("couldn't exec %s; status: %d", ed, status); 179 return; 180 } 181 182 if ((tmp = fopen(tmpfile, "r")) == NULL) { 183 warn("can't open '%s' for reading", tmpfile); 184 } 185 186 req = gctl_get_handle(); 187 gctl_ro_param(req, "class", -1, "VINUM"); 188 gctl_ro_param(req, "verb", -1, "create"); 189 190 drives = volumes = plexes = subdisks = 0; 191 plex_in_volume = sd_in_plex = 0; 192 errors = 0; 193 line = 1; 194 while ((fgets(buf, BUFSIZ, tmp)) != NULL) { 195 196 /* Skip empty lines and comments. */ 197 if (*buf == '\0' || *buf == '#') { 198 line++; 199 continue; 200 } 201 202 /* Kill off the newline. */ 203 buf[strlen(buf) - 1] = '\0'; 204 205 /* 206 * Copy the original input line in case we need it for error 207 * output. 208 */ 209 strncpy(original, buf, sizeof(buf)); 210 211 tokens = gv_tokenize(buf, token, GV_MAXARGS); 212 213 if (tokens > 0) { 214 /* Volume definition. */ 215 if (!strcmp(token[0], "volume")) { 216 v = gv_new_volume(tokens, token); 217 if (v == NULL) { 218 warnx("line %d: invalid volume " 219 "definition", line); 220 warnx("line %d: '%s'", line, original); 221 errors++; 222 } else { 223 /* Reset plex count for this volume. */ 224 plex_in_volume = 0; 225 226 /* 227 * Set default volume name for 228 * following plex definitions. 229 */ 230 strncpy(volume, v->name, 231 sizeof(volume)); 232 233 snprintf(buf1, sizeof(buf1), "volume%d", 234 volumes); 235 gctl_ro_param(req, buf1, sizeof(*v), v); 236 volumes++; 237 } 238 239 /* Plex definition. */ 240 } else if (!strcmp(token[0], "plex")) { 241 p = gv_new_plex(tokens, token); 242 if (p == NULL) { 243 warnx("line %d: invalid plex " 244 "definition", line); 245 warnx("line %d: '%s'", line, original); 246 errors++; 247 } else { 248 /* Reset subdisk count for this plex. */ 249 sd_in_plex = 0; 250 251 /* Default name. */ 252 if (strlen(p->name) == 0) { 253 snprintf(p->name, 254 GV_MAXPLEXNAME, 255 "%s.p%d", volume, 256 plex_in_volume++); 257 } 258 259 /* Default volume. */ 260 if (strlen(p->volume) == 0) { 261 snprintf(p->volume, 262 GV_MAXVOLNAME, "%s", 263 volume); 264 } 265 266 /* 267 * Set default plex name for following 268 * subdisk definitions. 269 */ 270 strncpy(plex, p->name, GV_MAXPLEXNAME); 271 272 snprintf(buf1, sizeof(buf1), "plex%d", 273 plexes); 274 gctl_ro_param(req, buf1, sizeof(*p), p); 275 plexes++; 276 } 277 278 /* Subdisk definition. */ 279 } else if (!strcmp(token[0], "sd")) { 280 s = gv_new_sd(tokens, token); 281 if (s == NULL) { 282 warnx("line %d: invalid subdisk " 283 "definition:", line); 284 warnx("line %d: '%s'", line, original); 285 errors++; 286 } else { 287 /* Default name. */ 288 if (strlen(s->name) == 0) { 289 snprintf(s->name, GV_MAXSDNAME, 290 "%s.s%d", plex, 291 sd_in_plex++); 292 } 293 294 /* Default plex. */ 295 if (strlen(s->plex) == 0) { 296 snprintf(s->plex, 297 GV_MAXPLEXNAME, "%s", plex); 298 } 299 300 snprintf(buf1, sizeof(buf1), "sd%d", 301 subdisks); 302 gctl_ro_param(req, buf1, sizeof(*s), s); 303 subdisks++; 304 } 305 306 /* Subdisk definition. */ 307 } else if (!strcmp(token[0], "drive")) { 308 d = gv_new_drive(tokens, token); 309 if (d == NULL) { 310 warnx("line %d: invalid drive " 311 "definition:", line); 312 warnx("line %d: '%s'", line, original); 313 errors++; 314 } else { 315 snprintf(buf1, sizeof(buf1), "drive%d", 316 drives); 317 gctl_ro_param(req, buf1, sizeof(*d), d); 318 drives++; 319 } 320 321 /* Everything else is bogus. */ 322 } else { 323 warnx("line %d: invalid definition:", line); 324 warnx("line %d: '%s'", line, original); 325 errors++; 326 } 327 } 328 line++; 329 } 330 331 fclose(tmp); 332 unlink(tmpfile); 333 334 if (!errors && (volumes || plexes || subdisks || drives)) { 335 gctl_ro_param(req, "volumes", sizeof(int), &volumes); 336 gctl_ro_param(req, "plexes", sizeof(int), &plexes); 337 gctl_ro_param(req, "subdisks", sizeof(int), &subdisks); 338 gctl_ro_param(req, "drives", sizeof(int), &drives); 339 errstr = gctl_issue(req); 340 if (errstr != NULL) 341 warnx("create failed: %s", errstr); 342 } 343 gctl_free(req); 344 gvinum_list(0, NULL); 345} 346 347void 348gvinum_help(void) 349{ 350 printf("COMMANDS\n" 351 "attach plex volume [rename]\n" 352 "attach subdisk plex [offset] [rename]\n" 353 " Attach a plex to a volume, or a subdisk to a plex.\n" 354 "checkparity plex [-f] [-v]\n" 355 " Check the parity blocks of a RAID-4 or RAID-5 plex.\n" 356 "concat [-f] [-n name] [-v] drives\n" 357 " Create a concatenated volume from the specified drives.\n" 358 "create [-f] description-file\n" 359 " Create a volume as described in description-file.\n" 360 "detach [-f] [plex | subdisk]\n" 361 " Detach a plex or subdisk from the volume or plex to" 362 "which it is\n" 363 " attached.\n" 364 "dumpconfig [drive ...]\n" 365 " List the configuration information stored on the" 366 " specified\n" 367 " drives, or all drives in the system if no drive names" 368 " are speci-\n" 369 " fied.\n" 370 "info [-v] [-V]\n" 371 " List information about volume manager state.\n" 372 "init [-S size] [-w] plex | subdisk\n" 373 " Initialize the contents of a subdisk or all the subdisks" 374 " of a\n" 375 " plex to all zeros.\n" 376 "label volume\n" 377 " Create a volume label.\n" 378 "l | list [-r] [-s] [-v] [-V] [volume | plex | subdisk]\n" 379 " List information about specified objects.\n" 380 "ld [-r] [-s] [-v] [-V] [volume]\n" 381 " List information about drives.\n" 382 "ls [-r] [-s] [-v] [-V] [subdisk]\n" 383 " List information about subdisks.\n" 384 "lp [-r] [-s] [-v] [-V] [plex]\n" 385 " List information about plexes.\n" 386 "lv [-r] [-s] [-v] [-V] [volume]\n" 387 " List information about volumes.\n" 388 "mirror [-f] [-n name] [-s] [-v] drives\n" 389 " Create a mirrored volume from the specified drives.\n" 390 "move | mv -f drive object ...\n" 391 " Move the object(s) to the specified drive.\n" 392 "printconfig [file]\n" 393 " Write a copy of the current configuration to file.\n" 394 "quit Exit the vinum program when running in interactive mode." 395 " Nor-\n" 396 " mally this would be done by entering the EOF character.\n" 397 "rename [-r] [drive | subdisk | plex | volume] newname\n" 398 " Change the name of the specified object.\n" 399 "rebuildparity plex [-f] [-v] [-V]\n" 400 " Rebuild the parity blocks of a RAID-4 or RAID-5 plex.\n" 401 "resetconfig\n" 402 " Reset the complete vinum configuration.\n" 403 "rm [-f] [-r] volume | plex | subdisk\n" 404 " Remove an object.\n" 405 "saveconfig\n" 406 " Save vinum configuration to disk after configuration" 407 " failures.\n" 408 "setstate state [volume | plex | subdisk | drive]\n" 409 " Set state without influencing other objects, for" 410 " diagnostic pur-\n" 411 " poses only.\n" 412 "start [-i interval] [-S size] [-w] volume | plex | subdisk\n" 413 " Allow the system to access the objects.\n" 414 "stop [-f] [volume | plex | subdisk]\n" 415 " Terminate access to the objects, or stop vinum if no" 416 " parameters\n" 417 " are specified.\n" 418 "stripe [-f] [-n name] [-v] drives\n" 419 " Create a striped volume from the specified drives.\n" 420 ); 421 422 return; 423} 424 425void 426gvinum_init(int argc, char **argv) 427{ 428 struct gctl_req *req; 429 int i, initsize, j; 430 const char *errstr; 431 char buf[20]; 432 433 initsize = 0; 434 optreset = 1; 435 optind = 1; 436 while ((j = getopt(argc, argv, "S")) != -1) { 437 switch (j) { 438 case 'S': 439 initsize = atoi(optarg); 440 break; 441 case '?': 442 default: 443 return; 444 } 445 } 446 argc -= optind; 447 argv += optind; 448 449 if (!initsize) 450 initsize = 512; 451 452 req = gctl_get_handle(); 453 gctl_ro_param(req, "class", -1, "VINUM"); 454 gctl_ro_param(req, "verb", -1, "init"); 455 gctl_ro_param(req, "argc", sizeof(int), &argc); 456 gctl_ro_param(req, "initsize", sizeof(int), &initsize); 457 if (argc) { 458 for (i = 0; i < argc; i++) { 459 snprintf(buf, sizeof(buf), "argv%d", i); 460 gctl_ro_param(req, buf, -1, argv[i]); 461 } 462 } 463 errstr = gctl_issue(req); 464 if (errstr != NULL) { 465 warnx("can't init: %s", errstr); 466 gctl_free(req); 467 return; 468 } 469 470 gctl_free(req); 471 gvinum_list(0, NULL); 472} 473 474void 475gvinum_list(int argc, char **argv) 476{ 477 struct gctl_req *req; 478 int flags, i, j; 479 const char *errstr; 480 char buf[20], *cmd, config[GV_CFG_LEN + 1]; 481 482 flags = 0; 483 cmd = "list"; 484 485 if (argc) { 486 optreset = 1; 487 optind = 1; 488 cmd = argv[0]; 489 while ((j = getopt(argc, argv, "rsvV")) != -1) { 490 switch (j) { 491 case 'r': 492 flags |= GV_FLAG_R; 493 break; 494 case 's': 495 flags |= GV_FLAG_S; 496 break; 497 case 'v': 498 flags |= GV_FLAG_V; 499 break; 500 case 'V': 501 flags |= GV_FLAG_V; 502 flags |= GV_FLAG_VV; 503 break; 504 case '?': 505 default: 506 return; 507 } 508 } 509 argc -= optind; 510 argv += optind; 511 512 } 513 514 req = gctl_get_handle(); 515 gctl_ro_param(req, "class", -1, "VINUM"); 516 gctl_ro_param(req, "verb", -1, "list"); 517 gctl_ro_param(req, "cmd", -1, cmd); 518 gctl_ro_param(req, "argc", sizeof(int), &argc); 519 gctl_ro_param(req, "flags", sizeof(int), &flags); 520 gctl_rw_param(req, "config", sizeof(config), config); 521 if (argc) { 522 for (i = 0; i < argc; i++) { 523 snprintf(buf, sizeof(buf), "argv%d", i); 524 gctl_ro_param(req, buf, -1, argv[i]); 525 } 526 } 527 errstr = gctl_issue(req); 528 if (errstr != NULL) { 529 warnx("can't get configuration: %s", errstr); 530 gctl_free(req); 531 return; 532 } 533 534 printf("%s", config); 535 gctl_free(req); 536 return; 537} 538 539void 540gvinum_printconfig(int argc, char **argv) 541{ 542 printconfig(stdout, ""); 543} 544 545void 546gvinum_rm(int argc, char **argv) 547{ 548 struct gctl_req *req; 549 int flags, i, j; 550 const char *errstr; 551 char buf[20], *cmd; 552 553 cmd = argv[0]; 554 flags = 0; 555 optreset = 1; 556 optind = 1; 557 while ((j = getopt(argc, argv, "r")) != -1) { 558 switch (j) { 559 case 'r': 560 flags |= GV_FLAG_R; 561 break; 562 case '?': 563 default: 564 return; 565 } 566 } 567 argc -= optind; 568 argv += optind; 569 570 req = gctl_get_handle(); 571 gctl_ro_param(req, "class", -1, "VINUM"); 572 gctl_ro_param(req, "verb", -1, "remove"); 573 gctl_ro_param(req, "argc", sizeof(int), &argc); 574 gctl_ro_param(req, "flags", sizeof(int), &flags); 575 if (argc) { 576 for (i = 0; i < argc; i++) { 577 snprintf(buf, sizeof(buf), "argv%d", i); 578 gctl_ro_param(req, buf, -1, argv[i]); 579 } 580 } 581 errstr = gctl_issue(req); 582 if (errstr != NULL) { 583 warnx("can't remove: %s", errstr); 584 gctl_free(req); 585 return; 586 } 587 gctl_free(req); 588 gvinum_list(0, NULL); 589} 590 591void 592gvinum_saveconfig(void) 593{ 594 struct gctl_req *req; 595 const char *errstr; 596 597 req = gctl_get_handle(); 598 gctl_ro_param(req, "class", -1, "VINUM"); 599 gctl_ro_param(req, "verb", -1, "saveconfig"); 600 errstr = gctl_issue(req); 601 if (errstr != NULL) 602 warnx("can't save configuration: %s", errstr); 603 gctl_free(req); 604} 605 606void 607gvinum_start(int argc, char **argv) 608{ 609 struct gctl_req *req; 610 int i, initsize, j; 611 const char *errstr; 612 char buf[20]; 613 614 /* 'start' with no arguments is a no-op. */ 615 if (argc == 1) 616 return; 617 618 initsize = 0; 619 620 optreset = 1; 621 optind = 1; 622 while ((j = getopt(argc, argv, "S")) != -1) { 623 switch (j) { 624 case 'S': 625 initsize = atoi(optarg); 626 break; 627 case '?': 628 default: 629 return; 630 } 631 } 632 argc -= optind; 633 argv += optind; 634 635 if (!initsize) 636 initsize = 512; 637 638 req = gctl_get_handle(); 639 gctl_ro_param(req, "class", -1, "VINUM"); 640 gctl_ro_param(req, "verb", -1, "start"); 641 gctl_ro_param(req, "argc", sizeof(int), &argc); 642 gctl_ro_param(req, "initsize", sizeof(int), &initsize); 643 if (argc) { 644 for (i = 0; i < argc; i++) { 645 snprintf(buf, sizeof(buf), "argv%d", i); 646 gctl_ro_param(req, buf, -1, argv[i]); 647 } 648 } 649 errstr = gctl_issue(req); 650 if (errstr != NULL) { 651 warnx("can't start: %s", errstr); 652 gctl_free(req); 653 return; 654 } 655 656 gctl_free(req); 657 gvinum_list(0, NULL); 658} 659 660void 661gvinum_stop(int argc, char **argv) 662{ 663 int fileid; 664 665 fileid = kldfind(GVINUMMOD); 666 if (fileid == -1) { 667 warn("cannot find " GVINUMMOD); 668 return; 669 } 670 if (kldunload(fileid) != 0) { 671 warn("cannot unload " GVINUMMOD); 672 return; 673 } 674 675 warnx(GVINUMMOD " unloaded"); 676 exit(0); 677} 678 679void 680parseline(int argc, char **argv) 681{ 682 if (argc <= 0) 683 return; 684 685 if (!strcmp(argv[0], "cancelinit")) 686 gvinum_cancelinit(argc, argv); 687 else if (!strcmp(argv[0], "create")) 688 gvinum_create(argc, argv); 689 else if (!strcmp(argv[0], "exit") || !strcmp(argv[0], "quit")) 690 exit(0); 691 else if (!strcmp(argv[0], "help")) 692 gvinum_help(); 693 else if (!strcmp(argv[0], "init")) 694 gvinum_init(argc, argv); 695 else if (!strcmp(argv[0], "list") || !strcmp(argv[0], "l")) 696 gvinum_list(argc, argv); 697 else if (!strcmp(argv[0], "ld")) 698 gvinum_list(argc, argv); 699 else if (!strcmp(argv[0], "lp")) 700 gvinum_list(argc, argv); 701 else if (!strcmp(argv[0], "ls")) 702 gvinum_list(argc, argv); 703 else if (!strcmp(argv[0], "lv")) 704 gvinum_list(argc, argv); 705 else if (!strcmp(argv[0], "printconfig")) 706 gvinum_printconfig(argc, argv); 707 else if (!strcmp(argv[0], "rm")) 708 gvinum_rm(argc, argv); 709 else if (!strcmp(argv[0], "saveconfig")) 710 gvinum_saveconfig(); 711 else if (!strcmp(argv[0], "start")) 712 gvinum_start(argc, argv); 713 else if (!strcmp(argv[0], "stop")) 714 gvinum_stop(argc, argv); 715 else 716 printf("unknown command '%s'\n", argv[0]); 717 718 return; 719} 720 721/* 722 * The guts of printconfig. This is called from gvinum_printconfig and from 723 * gvinum_create when called without an argument, in order to give the user 724 * something to edit. 725 */ 726void 727printconfig(FILE *of, char *comment) 728{ 729 struct gctl_req *req; 730 struct utsname uname_s; 731 const char *errstr; 732 time_t now; 733 char buf[GV_CFG_LEN + 1]; 734 735 uname(&uname_s); 736 time(&now); 737 738 req = gctl_get_handle(); 739 gctl_ro_param(req, "class", -1, "VINUM"); 740 gctl_ro_param(req, "verb", -1, "getconfig"); 741 gctl_ro_param(req, "comment", -1, comment); 742 gctl_rw_param(req, "config", sizeof(buf), buf); 743 errstr = gctl_issue(req); 744 if (errstr != NULL) { 745 warnx("can't get configuration: %s", errstr); 746 return; 747 } 748 gctl_free(req); 749 750 fprintf(of, "# Vinum configuration of %s, saved at %s", 751 uname_s.nodename, 752 ctime(&now)); 753 754 if (*comment != '\0') 755 fprintf(of, "# Current configuration:\n"); 756 757 fprintf(of, buf); 758} 759