add bug closure to changelog
[debian/amanda] / server-src / amadmin.c
1 /*
2  * Amanda, The Advanced Maryland Automatic Network Disk Archiver
3  * Copyright (c) 1991-1998 University of Maryland at College Park
4  * All Rights Reserved.
5  *
6  * Permission to use, copy, modify, distribute, and sell this software and its
7  * documentation for any purpose is hereby granted without fee, provided that
8  * the above copyright notice appear in all copies and that both that
9  * copyright notice and this permission notice appear in supporting
10  * documentation, and that the name of U.M. not be used in advertising or
11  * publicity pertaining to distribution of the software without specific,
12  * written prior permission.  U.M. makes no representations about the
13  * suitability of this software for any purpose.  It is provided "as is"
14  * without express or implied warranty.
15  *
16  * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
18  * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
20  * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
21  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22  *
23  * Author: James da Silva, Systems Design and Analysis Group
24  *                         Computer Science Department
25  *                         University of Maryland at College Park
26  */
27 /*
28  * $Id: amadmin.c,v 1.124 2006/07/26 15:17:37 martinea Exp $
29  *
30  * controlling process for the Amanda backup system
31  */
32 #include "amanda.h"
33 #include "cmdline.h"
34 #include "conffile.h"
35 #include "diskfile.h"
36 #include "tapefile.h"
37 #include "infofile.h"
38 #include "logfile.h"
39 #include "version.h"
40 #include "holding.h"
41 #include "find.h"
42 #include "util.h"
43 #include "timestamp.h"
44 #include "server_util.h"
45 #include <getopt.h>
46
47 disklist_t diskq;
48
49 int main(int argc, char **argv);
50 void usage(void);
51 static void estimate(int argc, char **argv);
52 static void estimate_one(disk_t *dp);
53 void force(int argc, char **argv);
54 void force_one(disk_t *dp);
55 void unforce(int argc, char **argv);
56 void unforce_one(disk_t *dp);
57 void force_bump(int argc, char **argv);
58 void force_bump_one(disk_t *dp);
59 void force_no_bump(int argc, char **argv);
60 void force_no_bump_one(disk_t *dp);
61 void unforce_bump(int argc, char **argv);
62 void unforce_bump_one(disk_t *dp);
63 void reuse(int argc, char **argv);
64 void noreuse(int argc, char **argv);
65 void info(int argc, char **argv);
66 void info_one(disk_t *dp);
67 void due(int argc, char **argv);
68 void due_one(disk_t *dp);
69 void find(int argc, char **argv);
70 void holding(int argc, char **argv);
71 void delete(int argc, char **argv);
72 void delete_one(disk_t *dp);
73 void balance(int argc, char **argv);
74 void tape(int argc, char **argv);
75 void bumpsize(int argc, char **argv);
76 void diskloop(int argc, char **argv, char *cmdname, void (*func)(disk_t *dp));
77 char *seqdatestr(int seq);
78 static int next_level0(disk_t *dp, info_t *info);
79 int bump_thresh(int level);
80 void export_db(int argc, char **argv);
81 void import_db(int argc, char **argv);
82 void hosts(int argc, char **argv);
83 void dles(int argc, char **argv);
84 void disklist(int argc, char **argv);
85 void disklist_one(disk_t *dp);
86 void show_version(int argc, char **argv);
87 static void show_config(int argc, char **argv);
88
89 static char *conf_tapelist = NULL;
90 static char *displayunit;
91 static long int unitdivisor;
92 static gboolean print_default = 1;
93 static gboolean print_source = 0;
94 static int opt_days = -1;
95 static char *opt_sort = NULL;
96 static gboolean opt_long = 0;
97 static gboolean opt_outdated = 0;
98
99 static const struct {
100     const char *name;
101     void (*fn)(int, char **);
102     const char *usage;
103 } cmdtab[] = {
104     { "version", show_version,
105         T_("\t\t\t\t\t# Show version info.") },
106     { "config", show_config,
107         T_("\t\t\t\t\t# Show configuration.") },
108     { "estimate", estimate,
109         T_(" [<hostname> [<disks>]* ]*\t# Print server estimate.") },
110     { "force", force,
111         T_(" [<hostname> [<disks>]* ]+\t\t# Force level 0 at next run.") },
112     { "unforce", unforce,
113         T_(" [<hostname> [<disks>]* ]+\t# Clear force command.") },
114     { "force-bump", force_bump,
115         T_(" [<hostname> [<disks>]* ]+\t# Force bump at next run.") },
116     { "force-no-bump", force_no_bump,
117         T_(" [<hostname> [<disks>]* ]+\t# Force no-bump at next run.") },
118     { "unforce-bump", unforce_bump,
119         T_(" [<hostname> [<disks>]* ]+\t# Clear bump command.") },
120     { "disklist", disklist,
121         T_(" [<hostname> [<disks>]* ]*\t# Debug disklist entries.") },
122     { "hosts", hosts,
123         T_("\t\t\t\t\t# Show all distinct hosts in disklist.") },
124     { "dles", dles,
125         T_("\t\t\t\t\t# Show all dles in disklist, one per line.") },
126     { "reuse", reuse,
127         T_(" <tapelabel> ...\t\t # re-use this tape.") },
128     { "no-reuse", noreuse,
129         T_(" <tapelabel> ...\t # never re-use this tape.") },
130     { "find", find,
131         T_(" [<hostname> [<disks>]* ]*\t # Show which tapes these dumps are on.") },
132     { "holding", holding,
133         T_(" {list [ -l ] |delete} [ <hostname> [ <disk> [ <datestamp> [ .. ] ] ] ]+\t # Show or delete holding disk contents.") },
134     { "delete", delete,
135         T_(" [<hostname> [<disks>]* ]+ # Delete from database.") },
136     { "info", info,
137         T_(" [<hostname> [<disks>]* ]*\t # Show current info records.") },
138     { "due", due,
139         T_(" [<hostname> [<disks>]* ]*\t # Show due date.") },
140     { "balance", balance,
141         T_(" [--days <num>]\t\t # Show nightly dump size balance.") },
142     { "tape", tape,
143         T_(" [--days <num>]\t\t # Show which tape is due next.") },
144     { "bumpsize", bumpsize,
145         T_("\t\t\t # Show current bump thresholds.") },
146     { "export", export_db,
147         T_(" [<hostname> [<disks>]* ]* # Export curinfo database to stdout.") },
148     { "import", import_db,
149         T_("\t\t\t\t # Import curinfo database from stdin.") },
150 };
151 #define NCMDS   (int)(sizeof(cmdtab) / sizeof(cmdtab[0]))
152
153 static struct option long_options[] = {
154     {"version"       , 0, NULL,  1},
155     {"no-default"    , 0, NULL,  2},
156     {"print-source"  , 0, NULL,  3},
157     {"days"          , 1, NULL,  4},
158     {"sort"          , 1, NULL,  5},
159     {NULL, 0, NULL, 0}
160 };
161
162 int
163 main(
164     int         argc,
165     char **     argv)
166 {
167     int i;
168     char *conf_diskfile;
169     char *conf_infofile;
170     config_overrides_t *cfg_ovr = NULL;
171
172     /*
173      * Configure program for internationalization:
174      *   1) Only set the message locale for now.
175      *   2) Set textdomain for all amanda related programs to "amanda"
176      *      We don't want to be forced to support dozens of message catalogs.
177      */  
178     setlocale(LC_MESSAGES, "C");
179     textdomain("amanda"); 
180
181     safe_fd(-1, 0);
182     safe_cd();
183
184     set_pname("amadmin");
185
186     /* Don't die when child closes pipe */
187     signal(SIGPIPE, SIG_IGN);
188
189     dbopen(DBG_SUBDIR_SERVER);
190
191     add_amanda_log_handler(amanda_log_stderr);
192
193     cfg_ovr = extract_commandline_config_overrides(&argc, &argv);
194
195     while (1) {
196         int option_index = 0;
197         int c;
198         c = getopt_long(argc, argv, "ld", long_options, &option_index);
199
200         if (c == -1) {
201             break;
202         }
203
204         switch(c) {
205         case 1: printf("amadmin-%s\n", VERSION);
206                 return 0;
207         case 2: print_default = 0;
208                 break;
209         case 3: print_source = 1;
210                 break;
211         case 4: opt_days = atoi(optarg);
212                 break;
213         case 5: opt_sort = g_strdup(optarg);
214                 break;
215         case 'l': opt_long = TRUE;
216                 break;
217         case 'd': opt_outdated = TRUE;
218                 break;
219         default: usage();
220         }
221     }
222     argc -= optind-1, argv += optind-1;
223
224     if(argc < 3) usage();
225
226     set_config_overrides(cfg_ovr);
227     config_init(CONFIG_INIT_EXPLICIT_NAME, argv[1]);
228
229     if(strcmp(argv[2],"version") == 0) {
230         show_version(argc, argv);
231         goto done;
232     }
233
234     conf_diskfile = config_dir_relative(getconf_str(CNF_DISKFILE));
235     read_diskfile(conf_diskfile, &diskq);
236     amfree(conf_diskfile);
237
238     if (config_errors(NULL) >= CFGERR_WARNINGS) {
239         config_print_errors();
240         if (config_errors(NULL) >= CFGERR_ERRORS) {
241             g_critical(_("errors processing config file"));
242         }
243     }
244
245     dbrename(get_config_name(), DBG_SUBDIR_SERVER);
246
247     check_running_as(RUNNING_AS_DUMPUSER);
248
249     conf_tapelist = config_dir_relative(getconf_str(CNF_TAPELIST));
250     if(read_tapelist(conf_tapelist)) {
251         error(_("could not load tapelist \"%s\""), conf_tapelist);
252         /*NOTREACHED*/
253     }
254     /* conf_tapelist is not freed yet -- it may be used to write the
255      * tapelist later. */
256
257     conf_infofile = config_dir_relative(getconf_str(CNF_INFOFILE));
258     if(open_infofile(conf_infofile)) {
259         error(_("could not open info db \"%s\""), conf_infofile);
260         /*NOTREACHED*/
261     }
262     amfree(conf_infofile);
263
264     displayunit = getconf_str(CNF_DISPLAYUNIT);
265     unitdivisor = getconf_unit_divisor();
266
267     for (i = 0; i < NCMDS; i++)
268         if (strcmp(argv[2], cmdtab[i].name) == 0) {
269             (*cmdtab[i].fn)(argc, argv);
270             break;
271         }
272     if (i == NCMDS) {
273         g_fprintf(stderr, _("%s: unknown command \"%s\"\n"), argv[0], argv[2]);
274         usage();
275     }
276
277     close_infofile();
278     clear_tapelist();
279     amfree(conf_tapelist);
280
281 done:
282
283     free_disklist(&diskq);
284     dbclose();
285     return 0;
286 }
287
288
289 void
290 usage(void)
291 {
292     int i;
293
294     g_fprintf(stderr, _("\nUsage: %s [--version] [--no-default] [--print-source] [-o configoption]*\n               <conf> <command> {<args>} ...\n"),
295             get_pname());
296     g_fprintf(stderr, _("    Valid <command>s are:\n"));
297     for (i = 0; i < NCMDS; i++)
298         g_fprintf(stderr, "\t%s%s\n", cmdtab[i].name, _(cmdtab[i].usage));
299     exit(1);
300 }
301
302
303 /* ----------------------------------------------- */
304
305 #define SECS_PER_DAY (24*60*60)
306 time_t today;
307
308 char *
309 seqdatestr(
310     int         seq)
311 {
312     static char str[16];
313     static char *dow[7] = {
314                         T_("Sun"),
315                         T_("Mon"),
316                         T_("Tue"),
317                         T_("Wed"),
318                         T_("Thu"),
319                         T_("Fri"),
320                         T_("Sat")
321                 };
322     time_t t = today + seq*SECS_PER_DAY;
323     struct tm *tm;
324
325     tm = localtime(&t);
326
327     if (tm)
328         g_snprintf(str, SIZEOF(str),
329                  "%2d/%02d %3s", tm->tm_mon+1, tm->tm_mday, _(dow[tm->tm_wday]));
330     else
331         strcpy(str, _("BAD DATE"));
332
333     return str;
334 }
335
336 #undef days_diff
337 #define days_diff(a, b)        (int)(((b) - (a) + SECS_PER_DAY) / SECS_PER_DAY)
338
339 /* when is next level 0 due? 0 = tonight, 1 = tommorrow, etc*/
340 static int
341 next_level0(
342     disk_t *    dp,
343     info_t *    info)
344 {
345     if(dp->strategy == DS_NOFULL)
346         return 1;       /* fake it */
347     if(info->inf[0].date < (time_t)0)
348         return 0;       /* new disk */
349     else
350         return dp->dumpcycle - days_diff(info->inf[0].date, today);
351 }
352
353 /* ----------------------------------------------- */
354
355 void
356 diskloop(
357     int         argc,
358     char **     argv,
359     char *      cmdname,
360     void        (*func)(disk_t *dp))
361 {
362     disk_t *dp;
363     int count = 0;
364     char *errstr;
365
366     if(argc < 4) {
367         g_fprintf(stderr,_("%s: expecting \"%s [<hostname> [<disks>]* ]+\"\n"),
368                 get_pname(), cmdname);
369         usage();
370     }
371
372     errstr = match_disklist(&diskq, argc-3, argv+3);
373     if (errstr) {
374         g_printf("%s", errstr);
375         amfree(errstr);
376     }
377
378     for(dp = diskq.head; dp != NULL; dp = dp->next) {
379         if(dp->todo) {
380             count++;
381             func(dp);
382         }
383     }
384     if(count==0) {
385         g_fprintf(stderr,_("%s: no disk matched\n"),get_pname());
386     }
387 }
388
389 /* ----------------------------------------------- */
390
391
392 static void
393 estimate_one(
394     disk_t *    dp)
395 {
396     char   *hostname = dp->host->hostname;
397     char   *diskname = dp->name;
398     char   *qhost = quote_string(hostname);
399     char   *qdisk = quote_string(diskname);
400     info_t  info;
401     int     stats;
402     gint64  size;
403
404     get_info(hostname, diskname, &info);
405
406     size = internal_server_estimate(dp, &info, 0, &stats);
407     if (stats) {
408         printf("%s %s %d %jd\n", qhost, qdisk, 0, (intmax_t)size);
409     }
410
411     if (info.last_level > 0) {
412         size = internal_server_estimate(dp, &info, info.last_level, &stats);
413         if (stats) {
414             printf("%s %s %d %jd\n", qhost, qdisk, info.last_level,
415                    (intmax_t)size);
416         }
417     }
418
419     if (info.last_level > -1) {
420         size = internal_server_estimate(dp, &info, info.last_level+1, &stats);
421         if (stats) {
422             printf("%s %s %d %jd\n", qhost, qdisk, info.last_level+1,
423                    (intmax_t)size);
424         }
425     }
426
427     amfree(qhost);
428     amfree(qdisk);
429 }
430
431
432 static void
433 estimate(
434     int         argc,
435     char **     argv)
436 {
437     disk_t *dp;
438
439     if(argc >= 4)
440         diskloop(argc, argv, "estimate", estimate_one);
441     else
442         for(dp = diskq.head; dp != NULL; dp = dp->next)
443             estimate_one(dp);
444 }
445
446
447 /* ----------------------------------------------- */
448
449
450 void
451 force_one(
452     disk_t *    dp)
453 {
454     char *hostname = dp->host->hostname;
455     char *diskname = dp->name;
456     info_t info;
457
458     get_info(hostname, diskname, &info);
459     SET(info.command, FORCE_FULL);
460     if (ISSET(info.command, FORCE_BUMP)) {
461         CLR(info.command, FORCE_BUMP);
462         g_printf(_("%s: WARNING: %s:%s FORCE_BUMP command was cleared.\n"),
463                get_pname(), hostname, diskname);
464     }
465     if(put_info(hostname, diskname, &info) == 0) {
466         if (dp->strategy == DS_INCRONLY) {
467             g_printf(_("%s: %s:%s, full dump done offline, next dump will be at level 1.\n"),
468                      get_pname(), hostname, diskname);
469         } else {
470             g_printf(_("%s: %s:%s is set to a forced level 0 at next run.\n"),
471                      get_pname(), hostname, diskname);
472         }
473     } else {
474         g_fprintf(stderr, _("%s: %s:%s could not be forced.\n"),
475                 get_pname(), hostname, diskname);
476     }
477 }
478
479
480 void
481 force(
482     int         argc,
483     char **     argv)
484 {
485     diskloop(argc, argv, "force", force_one);
486 }
487
488
489 /* ----------------------------------------------- */
490
491
492 void
493 unforce_one(
494     disk_t *    dp)
495 {
496     char *hostname = dp->host->hostname;
497     char *diskname = dp->name;
498     info_t info;
499
500     get_info(hostname, diskname, &info);
501     if (ISSET(info.command, FORCE_FULL)) {
502         CLR(info.command, FORCE_FULL);
503         if(put_info(hostname, diskname, &info) == 0){
504             g_printf(_("%s: force command for %s:%s cleared.\n"),
505                    get_pname(), hostname, diskname);
506         } else {
507             g_fprintf(stderr,
508                     _("%s: force command for %s:%s could not be cleared.\n"),
509                     get_pname(), hostname, diskname);
510         }
511     }
512     else {
513         g_printf(_("%s: no force command outstanding for %s:%s, unchanged.\n"),
514                get_pname(), hostname, diskname);
515     }
516 }
517
518 void
519 unforce(
520     int         argc,
521     char **     argv)
522 {
523     diskloop(argc, argv, "unforce", unforce_one);
524 }
525
526
527 /* ----------------------------------------------- */
528
529
530 void
531 force_bump_one(
532     disk_t *    dp)
533 {
534     char *hostname = dp->host->hostname;
535     char *diskname = dp->name;
536     info_t info;
537
538     get_info(hostname, diskname, &info);
539     SET(info.command, FORCE_BUMP);
540     if (ISSET(info.command, FORCE_NO_BUMP)) {
541         CLR(info.command, FORCE_NO_BUMP);
542         g_printf(_("%s: WARNING: %s:%s FORCE_NO_BUMP command was cleared.\n"),
543                get_pname(), hostname, diskname);
544     }
545     if (ISSET(info.command, FORCE_FULL)) {
546         CLR(info.command, FORCE_FULL);
547         g_printf(_("%s: WARNING: %s:%s FORCE_FULL command was cleared.\n"),
548                get_pname(), hostname, diskname);
549     }
550     if(put_info(hostname, diskname, &info) == 0) {
551         g_printf(_("%s: %s:%s is set to bump at next run.\n"),
552                get_pname(), hostname, diskname);
553     } else {
554         g_fprintf(stderr, _("%s: %s:%s could not be forced to bump.\n"),
555                 get_pname(), hostname, diskname);
556     }
557 }
558
559
560 void
561 force_bump(
562     int         argc,
563     char **     argv)
564 {
565     diskloop(argc, argv, "force-bump", force_bump_one);
566 }
567
568
569 /* ----------------------------------------------- */
570
571
572 void
573 force_no_bump_one(
574     disk_t *    dp)
575 {
576     char *hostname = dp->host->hostname;
577     char *diskname = dp->name;
578     info_t info;
579
580     get_info(hostname, diskname, &info);
581     SET(info.command, FORCE_NO_BUMP);
582     if (ISSET(info.command, FORCE_BUMP)) {
583         CLR(info.command, FORCE_BUMP);
584         g_printf(_("%s: WARNING: %s:%s FORCE_BUMP command was cleared.\n"),
585                get_pname(), hostname, diskname);
586     }
587     if(put_info(hostname, diskname, &info) == 0) {
588         g_printf(_("%s: %s:%s is set to not bump at next run.\n"),
589                get_pname(), hostname, diskname);
590     } else {
591         g_fprintf(stderr, _("%s: %s:%s could not be force to not bump.\n"),
592                 get_pname(), hostname, diskname);
593     }
594 }
595
596
597 void
598 force_no_bump(
599     int         argc,
600     char **     argv)
601 {
602     diskloop(argc, argv, "force-no-bump", force_no_bump_one);
603 }
604
605
606 /* ----------------------------------------------- */
607
608
609 void
610 unforce_bump_one(
611     disk_t *    dp)
612 {
613     char *hostname = dp->host->hostname;
614     char *diskname = dp->name;
615     info_t info;
616
617     get_info(hostname, diskname, &info);
618     if (ISSET(info.command, FORCE_BUMP|FORCE_NO_BUMP)) {
619         CLR(info.command, FORCE_BUMP|FORCE_NO_BUMP);
620         if(put_info(hostname, diskname, &info) == 0) {
621             g_printf(_("%s: bump command for %s:%s cleared.\n"),
622                    get_pname(), hostname, diskname);
623         } else {
624             g_fprintf(stderr, _("%s: %s:%s bump command could not be cleared.\n"),
625                     get_pname(), hostname, diskname);
626         }
627     }
628     else {
629         g_printf(_("%s: no bump command outstanding for %s:%s, unchanged.\n"),
630                get_pname(), hostname, diskname);
631     }
632 }
633
634
635 void
636 unforce_bump(
637     int         argc,
638     char **     argv)
639 {
640     diskloop(argc, argv, "unforce-bump", unforce_bump_one);
641 }
642
643
644 /* ----------------------------------------------- */
645
646 void
647 reuse(
648     int         argc,
649     char **     argv)
650 {
651     tape_t *tp;
652     int count;
653
654     if(argc < 4) {
655         g_fprintf(stderr,_("%s: expecting \"reuse <tapelabel> ...\"\n"),
656                 get_pname());
657         usage();
658     }
659
660     for(count=3; count< argc; count++) {
661         tp = lookup_tapelabel(argv[count]);
662         if ( tp == NULL) {
663             g_fprintf(stderr, _("reuse: tape label %s not found in tapelist.\n"),
664                 argv[count]);
665             continue;
666         }
667         if( tp->reuse == 0 ) {
668             tp->reuse = 1;
669             g_printf(_("%s: marking tape %s as reusable.\n"),
670                    get_pname(), argv[count]);
671         } else {
672             g_fprintf(stderr, _("%s: tape %s already reusable.\n"),
673                     get_pname(), argv[count]);
674         }
675     }
676
677     if(write_tapelist(conf_tapelist)) {
678         error(_("could not write tapelist \"%s\""), conf_tapelist);
679         /*NOTREACHED*/
680     }
681 }
682
683 void
684 noreuse(
685     int         argc,
686     char **     argv)
687 {
688     tape_t *tp;
689     int count;
690
691     if(argc < 4) {
692         g_fprintf(stderr,_("%s: expecting \"no-reuse <tapelabel> ...\"\n"),
693                 get_pname());
694         usage();
695     }
696
697     for(count=3; count< argc; count++) {
698         tp = lookup_tapelabel(argv[count]);
699         if ( tp == NULL) {
700             g_fprintf(stderr, _("no-reuse: tape label %s not found in tapelist.\n"),
701                 argv[count]);
702             continue;
703         }
704         if( tp->reuse == 1 ) {
705             tp->reuse = 0;
706             g_printf(_("%s: marking tape %s as not reusable.\n"),
707                    get_pname(), argv[count]);
708         } else {
709             g_fprintf(stderr, _("%s: tape %s already not reusable.\n"),
710                     get_pname(), argv[count]);
711         }
712     }
713
714     if(write_tapelist(conf_tapelist)) {
715         error(_("could not write tapelist \"%s\""), conf_tapelist);
716         /*NOTREACHED*/
717     }
718 }
719
720
721 /* ----------------------------------------------- */
722
723 static int deleted;
724
725 void
726 delete_one(
727     disk_t *    dp)
728 {
729     char *hostname = dp->host->hostname;
730     char *diskname = dp->name;
731     info_t info;
732
733     if(get_info(hostname, diskname, &info)) {
734         g_printf(_("%s: %s:%s NOT currently in database.\n"),
735                get_pname(), hostname, diskname);
736         return;
737     }
738
739     deleted++;
740     if(del_info(hostname, diskname)) {
741         error(_("couldn't delete %s:%s from database: %s"),
742               hostname, diskname, strerror(errno));
743         /*NOTREACHED*/
744     } else {
745         g_printf(_("%s: %s:%s deleted from curinfo database.\n"),
746                get_pname(), hostname, diskname);
747     }
748 }
749
750 void
751 delete(
752     int         argc,
753     char **     argv)
754 {
755     deleted = 0;
756     diskloop(argc, argv, "delete", delete_one);
757
758    if(deleted)
759         g_printf(
760          _("%s: NOTE: you'll have to remove these from the disklist yourself.\n"),
761          get_pname());
762 }
763
764 /* ----------------------------------------------- */
765
766 void
767 info_one(
768     disk_t *    dp)
769 {
770     info_t info;
771     int lev;
772     struct tm *tm;
773     stats_t *sp;
774
775     get_info(dp->host->hostname, dp->name, &info);
776
777     g_printf(_("\nCurrent info for %s %s:\n"), dp->host->hostname, dp->name);
778     if (ISSET(info.command, FORCE_FULL))
779         g_printf(_("  (Forcing to level 0 dump at next run)\n"));
780     if (ISSET(info.command, FORCE_BUMP))
781         g_printf(_("  (Forcing bump at next run)\n"));
782     if (ISSET(info.command, FORCE_NO_BUMP))
783         g_printf(_("  (Forcing no-bump at next run)\n"));
784     g_printf(_("  Stats: dump rates (kps), Full:  %5.1lf, %5.1lf, %5.1lf\n"),
785            info.full.rate[0], info.full.rate[1], info.full.rate[2]);
786     g_printf(_("                    Incremental:  %5.1lf, %5.1lf, %5.1lf\n"),
787            info.incr.rate[0], info.incr.rate[1], info.incr.rate[2]);
788     g_printf(_("          compressed size, Full: %5.1lf%%,%5.1lf%%,%5.1lf%%\n"),
789            info.full.comp[0]*100, info.full.comp[1]*100, info.full.comp[2]*100);
790     g_printf(_("                    Incremental: %5.1lf%%,%5.1lf%%,%5.1lf%%\n"),
791            info.incr.comp[0]*100, info.incr.comp[1]*100, info.incr.comp[2]*100);
792
793     g_printf(_("  Dumps: lev datestmp  tape             file   origK   compK secs\n"));
794     for(lev = 0, sp = &info.inf[0]; lev < 9; lev++, sp++) {
795         if(sp->date < (time_t)0 && sp->label[0] == '\0') continue;
796         tm = localtime(&sp->date);
797         if (tm) {
798             g_printf(_("          %d  %04d%02d%02d  %-15s  %lld %lld %lld %jd\n"),
799                    lev, tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
800                    sp->label,
801                    (long long)sp->filenum,
802                    (long long)sp->size,
803                    (long long)sp->csize,
804                    (intmax_t)sp->secs);
805         } else {
806             g_printf(_("          %d  BAD-DATE  %-15s  %lld %lld %lld %jd\n"),
807                    lev,
808                    sp->label,
809                    (long long)sp->filenum,
810                    (long long)sp->size,
811                    (long long)sp->csize,
812                    (intmax_t)sp->secs);
813         }
814     }
815 }
816
817
818 void
819 info(
820     int         argc,
821     char **     argv)
822 {
823     disk_t *dp;
824
825     if(argc >= 4)
826         diskloop(argc, argv, "info", info_one);
827     else
828         for(dp = diskq.head; dp != NULL; dp = dp->next)
829             info_one(dp);
830 }
831
832 /* ----------------------------------------------- */
833
834 void
835 due_one(
836     disk_t *    dp)
837 {
838     am_host_t *hp;
839     int days;
840     info_t info;
841
842     hp = dp->host;
843     if(get_info(hp->hostname, dp->name, &info)) {
844         g_printf(_("new disk %s:%s ignored.\n"), hp->hostname, dp->name);
845     }
846     else {
847         days = next_level0(dp, &info);
848         if(days < 0) {
849             g_printf(_("Overdue %2d day%s %s:%s\n"),
850                    -days, (-days == 1) ? ": " : "s:",
851                    hp->hostname, dp->name);
852         }
853         else if(days == 0) {
854             g_printf(_("Due today: %s:%s\n"), hp->hostname, dp->name);
855         }
856         else {
857             g_printf(_("Due in %2d day%s %s:%s\n"), days,
858                    (days == 1) ? ": " : "s:",
859                    hp->hostname, dp->name);
860         }
861     }
862 }
863
864 void
865 due(
866     int         argc,
867     char **     argv)
868 {
869     disk_t *dp;
870
871     time(&today);
872     if(argc >= 4)
873         diskloop(argc, argv, "due", due_one);
874     else
875         for(dp = diskq.head; dp != NULL; dp = dp->next)
876             due_one(dp);
877 }
878
879 /* ----------------------------------------------- */
880
881 void
882 tape(
883     int         argc,
884     char **     argv)
885 {
886     int     nb_days = 1;
887     int     runtapes;
888     tape_t *tp;
889     int     i, j;
890     int     skip;
891     int     nb_new_tape;
892
893     if(argc > 4 && strcmp(argv[3],"--days") == 0) {
894         nb_days = atoi(argv[4]);
895         if(nb_days < 1) {
896             g_printf(_("days must be an integer bigger than 0\n"));
897             return;
898         }
899         if (nb_days > 10000)
900             nb_days = 10000;
901     }
902
903     runtapes = getconf_int(CNF_RUNTAPES);
904     tp = lookup_last_reusable_tape(0);
905     skip = 0;
906
907     for ( j=0 ; j < nb_days ; j++ ) {
908         nb_new_tape=0;
909         for ( i=0 ; i < runtapes ; i++ ) {
910             if(i==0)
911                 g_fprintf(stdout, _("The next Amanda run should go onto "));
912             if(tp != NULL) {
913                 if (nb_new_tape > 0) {
914                     if (nb_new_tape == 1)
915                         g_fprintf(stdout, _("1 new tape.\n"));
916                     else
917                         g_fprintf(stdout, _("%d new tapes.\n"), nb_new_tape);
918                     g_fprintf(stdout, "                                   ");
919                     nb_new_tape = 0;
920                 }
921                 g_fprintf(stdout, _("tape %s or a new tape.\n"), tp->label);
922                 if (i < runtapes-1)
923                     g_fprintf(stdout, "                                   ");
924             } else {
925                 nb_new_tape++;
926             }
927             skip++;
928
929             tp = lookup_last_reusable_tape(skip);
930         }
931         if (nb_new_tape > 0) {
932             if (nb_new_tape == 1)
933                 g_fprintf(stdout, _("1 new tape.\n"));
934             else
935                 g_fprintf(stdout, _("%d new tapes.\n"), nb_new_tape);
936         }
937     }
938
939     print_new_tapes(stdout, nb_days * runtapes);
940 }
941
942 /* ----------------------------------------------- */
943
944 void
945 balance(
946     int         argc G_GNUC_UNUSED,
947     char **     argv G_GNUC_UNUSED)
948 {
949     disk_t *dp;
950     struct balance_stats {
951         int disks;
952         off_t origsize, outsize;
953     } *sp;
954     int conf_runspercycle, conf_dumpcycle;
955     int seq, runs_per_cycle, overdue, max_overdue;
956     int later, total, balance, distinct;
957     double fseq, disk_dumpcycle;
958     info_t info;
959     off_t total_balanced, balanced;
960     int empty_day;
961
962     time(&today);
963     conf_dumpcycle = getconf_int(CNF_DUMPCYCLE);
964     conf_runspercycle = getconf_int(CNF_RUNSPERCYCLE);
965     later = conf_dumpcycle;
966     overdue = 0;
967     max_overdue = 0;
968
969     if (opt_days > 0) {
970         later = opt_days;
971     } else if (opt_days == 0) {
972         later = conf_dumpcycle;
973     }
974     if(later > 10000) later = 10000;
975
976     if(conf_runspercycle == 0) {
977         runs_per_cycle = conf_dumpcycle;
978     } else if(conf_runspercycle == -1 ) {
979         runs_per_cycle = guess_runs_from_tapelist();
980     } else
981         runs_per_cycle = conf_runspercycle;
982
983     if (runs_per_cycle <= 0) {
984         runs_per_cycle = 1;
985     }
986
987     total = later + 1;
988     balance = later + 2;
989     distinct = later + 3;
990
991     sp = (struct balance_stats *)
992         alloc(SIZEOF(struct balance_stats) * (distinct+1));
993
994     for(seq=0; seq <= distinct; seq++) {
995         sp[seq].disks = 0;
996         sp[seq].origsize = sp[seq].outsize = (off_t)0;
997     }
998
999     for(dp = diskq.head; dp != NULL; dp = dp->next) {
1000         if(get_info(dp->host->hostname, dp->name, &info)) {
1001             g_printf(_("new disk %s:%s ignored.\n"), dp->host->hostname, dp->name);
1002             continue;
1003         }
1004         if (dp->strategy == DS_NOFULL) {
1005             continue;
1006         }
1007         sp[distinct].disks++;
1008         sp[distinct].origsize += info.inf[0].size/(off_t)unitdivisor;
1009         sp[distinct].outsize += info.inf[0].csize/(off_t)unitdivisor;
1010
1011         sp[balance].disks++;
1012         if(dp->dumpcycle == 0) {
1013             sp[balance].origsize += (info.inf[0].size/(off_t)unitdivisor) * (off_t)runs_per_cycle;
1014             sp[balance].outsize += (info.inf[0].csize/(off_t)unitdivisor) * (off_t)runs_per_cycle;
1015         }
1016         else {
1017             sp[balance].origsize += (info.inf[0].size/(off_t)unitdivisor) *
1018                                     (off_t)(conf_dumpcycle / dp->dumpcycle);
1019             sp[balance].outsize += (info.inf[0].csize/(off_t)unitdivisor) *
1020                                    (off_t)(conf_dumpcycle / dp->dumpcycle);
1021         }
1022
1023         disk_dumpcycle = (double)dp->dumpcycle;
1024         if(dp->dumpcycle <= 0)
1025             disk_dumpcycle = ((double)conf_dumpcycle) / ((double)runs_per_cycle);
1026
1027         seq = next_level0(dp, &info);
1028         fseq = seq + 0.0001;
1029         do {
1030             if(seq < 0) {
1031                 overdue++;
1032                 if (-seq > max_overdue)
1033                     max_overdue = -seq;
1034                 seq = 0;
1035                 fseq = seq + 0.0001;
1036             }
1037             if(seq > later) {
1038                 seq = later;
1039             }
1040             
1041             sp[seq].disks++;
1042             sp[seq].origsize += info.inf[0].size/(off_t)unitdivisor;
1043             sp[seq].outsize += info.inf[0].csize/(off_t)unitdivisor;
1044
1045             if(seq < later) {
1046                 sp[total].disks++;
1047                 sp[total].origsize += info.inf[0].size/(off_t)unitdivisor;
1048                 sp[total].outsize += info.inf[0].csize/(off_t)unitdivisor;
1049             }
1050             
1051             /* See, if there's another run in this dumpcycle */
1052             fseq += disk_dumpcycle;
1053             seq = (int)fseq;
1054         } while (seq < later);
1055     }
1056
1057     if(sp[total].outsize == (off_t)0 && sp[later].outsize == (off_t)0) {
1058         g_printf(_("\nNo data to report on yet.\n"));
1059         amfree(sp);
1060         return;
1061     }
1062
1063     balanced = sp[balance].outsize / (off_t)runs_per_cycle;
1064     if(conf_dumpcycle == later) {
1065         total_balanced = sp[total].outsize / (off_t)runs_per_cycle;
1066     }
1067     else {
1068         total_balanced = (((sp[total].outsize/(off_t)1024) * (off_t)conf_dumpcycle)
1069                             / (off_t)(runs_per_cycle * later)) * (off_t)1024;
1070     }
1071
1072     empty_day = 0;
1073     g_printf(_("\n due-date  #fs    orig %cB     out %cB   balance\n"),
1074            displayunit[0], displayunit[0]);
1075     g_printf("----------------------------------------------\n");
1076     for(seq = 0; seq < later; seq++) {
1077         if(sp[seq].disks == 0 &&
1078            ((seq > 0 && sp[seq-1].disks == 0) ||
1079             ((seq < later-1) && sp[seq+1].disks == 0))) {
1080             empty_day++;
1081         }
1082         else {
1083             if(empty_day > 0) {
1084                 g_printf("\n");
1085                 empty_day = 0;
1086             }
1087             g_printf(_("%-9.9s  %3d %10lld %10lld "),
1088                    seqdatestr(seq), sp[seq].disks,
1089                    (long long)sp[seq].origsize,
1090                    (long long)sp[seq].outsize);
1091             if(!sp[seq].outsize) g_printf("     --- \n");
1092             else g_printf(_("%+8.1lf%%\n"),
1093                         (((double)sp[seq].outsize - (double)balanced) * 100.0 /
1094                         (double)balanced));
1095         }
1096     }
1097
1098     if(sp[later].disks != 0) {
1099         g_printf(_("later      %3d %10lld %10lld "),
1100                sp[later].disks,
1101                (long long)sp[later].origsize,
1102                (long long)sp[later].outsize);
1103         if(!sp[later].outsize) g_printf("     --- \n");
1104         else g_printf(_("%+8.1lf%%\n"),
1105                     (((double)sp[later].outsize - (double)balanced) * 100.0 /
1106                     (double)balanced));
1107     }
1108     g_printf("----------------------------------------------\n");
1109     g_printf(_("TOTAL      %3d %10lld %10lld %9lld\n"),
1110            sp[total].disks,
1111            (long long)sp[total].origsize,
1112            (long long)sp[total].outsize,
1113            (long long)total_balanced);
1114     if (sp[balance].origsize != sp[total].origsize ||
1115         sp[balance].outsize != sp[total].outsize ||
1116         balanced != total_balanced) {
1117         g_printf(_("BALANCED       %10lld %10lld %9lld\n"),
1118                (long long)sp[balance].origsize,
1119                (long long)sp[balance].outsize,
1120                (long long)balanced);
1121     }
1122     if (sp[distinct].disks != sp[total].disks) {
1123         g_printf(_("DISTINCT   %3d %10lld %10lld\n"),
1124                sp[distinct].disks,
1125                (long long)sp[distinct].origsize,
1126                (long long)sp[distinct].outsize);
1127     }
1128     g_printf(plural(_("  (estimated %d run per dumpcycle)\n"),
1129                   _("  (estimated %d runs per dumpcycle)\n"),
1130                   runs_per_cycle),
1131            runs_per_cycle);
1132     if (overdue) {
1133         g_printf(plural(_(" (%d filesystem overdue."),
1134                       _(" (%d filesystems overdue."), overdue),
1135                overdue);
1136         g_printf(plural(_(" The most being overdue %d day.)\n"),
1137                       _(" The most being overdue %d days.)\n"), max_overdue),
1138                max_overdue);
1139     }
1140     amfree(sp);
1141 }
1142
1143
1144 /* ----------------------------------------------- */
1145
1146 void
1147 find(
1148     int         argc,
1149     char **     argv)
1150 {
1151     int start_argc;
1152     char *sort_order = NULL;
1153     find_result_t *output_find;
1154     char *errstr;
1155     char **output_find_log;
1156     char **name;
1157
1158     if(argc < 3) {
1159         g_fprintf(stderr,
1160                 _("%s: expecting \"find [--sort <hkdlpbfw>] [hostname [<disk>]]*\"\n"),
1161                 get_pname());
1162         usage();
1163     }
1164
1165
1166     sort_order = newstralloc(sort_order, DEFAULT_SORT_ORDER);
1167     if (opt_sort) {
1168         size_t i, valid_sort=1;
1169
1170         for(i = strlen(opt_sort); i > 0; i--) {
1171             switch (opt_sort[i - 1]) {
1172             case 'h':
1173             case 'H':
1174             case 'k':
1175             case 'K':
1176             case 'd':
1177             case 'D':
1178             case 'f':
1179             case 'F':
1180             case 'l':
1181             case 'L':
1182             case 'p':
1183             case 'P':
1184             case 'b':
1185             case 'B':
1186             case 'w':
1187             case 'W':
1188                     break;
1189             default: valid_sort=0;
1190             }
1191         }
1192         if(valid_sort) {
1193             sort_order = newstralloc(sort_order, opt_sort);
1194         } else {
1195             g_printf(_("Invalid sort order: %s\n"), opt_sort);
1196             g_printf(_("Use default sort order: %s\n"), sort_order);
1197         }
1198     }
1199     start_argc=4;
1200     errstr = match_disklist(&diskq, argc-(start_argc-1), argv+(start_argc-1));
1201
1202     /* check all log file exists */
1203     output_find_log = find_log();
1204     for (name = output_find_log; *name != NULL; name++) {
1205         amfree(*name);
1206     }
1207     amfree(output_find_log);
1208
1209     output_find = find_dump(&diskq); /* Add deleted dump to diskq */
1210     if(argc-(start_argc-1) > 0) {
1211         find_result_t *afind = NULL;
1212         find_result_t *afind_next = NULL;
1213         find_result_t *new_output_find = NULL;
1214         disk_t *dp;
1215
1216         amfree(errstr);
1217         errstr = match_disklist(&diskq, argc-(start_argc-1),
1218                                         argv+(start_argc-1));
1219         if (errstr) {
1220             g_printf("%s", errstr);
1221             amfree(errstr);
1222         }
1223         for (afind = output_find; afind; afind = afind_next) {
1224             afind_next = afind->next;
1225             dp = lookup_disk(afind->hostname, afind->diskname);
1226             if (dp->todo) {
1227                 afind->next = new_output_find;
1228                 new_output_find = afind;
1229             } else {
1230                 amfree(afind);
1231             }
1232         }
1233         output_find = new_output_find;
1234     } else if (errstr) {
1235         g_printf("%s", errstr);
1236         amfree(errstr);
1237     }
1238
1239     sort_find_result(sort_order, &output_find);
1240     print_find_result(output_find);
1241     free_find_result(&output_find);
1242
1243     amfree(sort_order);
1244 }
1245
1246
1247 /* ------------------------ */
1248
1249 static GSList *
1250 get_file_list(
1251     int argc,
1252     char **argv,
1253     int allow_empty)
1254 {
1255     GSList * file_list = NULL;
1256     GSList * dumplist;
1257     int flags;
1258
1259     flags = CMDLINE_PARSE_DATESTAMP;
1260     if (allow_empty) flags |= CMDLINE_EMPTY_TO_WILDCARD;
1261     dumplist = cmdline_parse_dumpspecs(argc, argv, flags);
1262
1263     file_list = cmdline_match_holding(dumplist);
1264     dumpspec_list_free(dumplist);
1265
1266     return file_list;
1267 }
1268
1269 /* Given a file header, find the history element in curinfo most likely
1270  * corresponding to that dump (this is not an exact science).
1271  *
1272  * @param info: the info_t element for this DLE
1273  * @param file: the header of the file
1274  * @returns: index of the matching history element, or -1 if not found
1275  */
1276 static int
1277 holding_file_find_history(
1278     info_t *info,
1279     dumpfile_t *file)
1280 {
1281     int matching_hist_idx = -1;
1282     int nhist;
1283     int i;
1284
1285     /* Begin by trying to find the history element matching this dump.
1286      * The datestamp on the dump is for the entire run of amdump, while the
1287      * 'date' in the history element of 'info' is the time the dump itself
1288      * began.  A matching history element, then, is the earliest element
1289      * with a 'date' equal to or later than the date of the dumpfile.
1290      *
1291      * We compare using formatted datestamps; even using seconds since epoch,
1292      * we would still face timezone issues, and have to do a reverse (timezone
1293      * to gmt) translation.
1294      */
1295
1296     /* get to the end of the history list and search backward */
1297     for (nhist = 0; info->history[nhist].level > -1; nhist++) /* empty loop */;
1298     for (i = nhist-1; i > -1; i--) {
1299         char *info_datestamp = get_timestamp_from_time(info->history[i].date);
1300         int order = strcmp(file->datestamp, info_datestamp);
1301         amfree(info_datestamp);
1302
1303         if (order <= 0) {
1304             /* only a match if the levels are equal */
1305             if (info->history[i].level == file->dumplevel) {
1306                 matching_hist_idx = i;
1307             }
1308             break;
1309         }
1310     }
1311
1312     return matching_hist_idx;
1313 }
1314
1315 /* A holding file is 'outdated' if a subsequent dump of the same DLE was made
1316  * at the same level or a lower leve; for example, a level 2 dump is outdated if
1317  * there is a subsequent level 2, or a subsequent level 0.
1318  *
1319  * @param file: the header of the file
1320  * @returns: true if the file is outdated
1321  */
1322 static int
1323 holding_file_is_outdated(
1324     dumpfile_t *file)
1325 {
1326     info_t info;
1327     int matching_hist_idx;
1328
1329     if (get_info(file->name, file->disk, &info) == -1) {
1330         return 0; /* assume it's not outdated */
1331     }
1332
1333     /* if the last level is less than the level of this dump, then
1334      * it's outdated */
1335     if (info.last_level < file->dumplevel)
1336         return 1;
1337
1338     /* otherwise, we need to see if this dump is the last at its level */
1339     matching_hist_idx = holding_file_find_history(&info, file);
1340     if (matching_hist_idx == -1) {
1341         return 0; /* assume it's not outdated */
1342     }
1343
1344     /* compare the date of the history element with the most recent date
1345      * for this level.  If they match, then this is the last dump at this
1346      * level, and we checked above for more recent lower-level dumps, so
1347      * the dump is not outdated. */
1348     if (info.history[matching_hist_idx].date == 
1349         info.inf[info.history[matching_hist_idx].level].date) {
1350         return 0;
1351     } else {
1352         return 1;
1353     }
1354 }
1355
1356 static int
1357 remove_holding_file_from_catalog(
1358     char *filename)
1359 {
1360     static int warnings_printed; /* only print once per invocation */
1361     dumpfile_t file;
1362     info_t info;
1363     int matching_hist_idx = -1;
1364     history_t matching_hist; /* will be a copy */
1365     int i;
1366
1367     if (!holding_file_get_dumpfile(filename, &file)) {
1368         g_printf(_("Could not read holding file %s\n"), filename);
1369         return 0;
1370     }
1371
1372     if (get_info(file.name, file.disk, &info) == -1) {
1373         g_printf(_("WARNING: No curinfo record for %s:%s\n"), file.name, file.disk);
1374         dumpfile_free_data(&file);
1375         return 1; /* not an error */
1376     }
1377
1378     matching_hist_idx = holding_file_find_history(&info, &file);
1379
1380     if (matching_hist_idx == -1) {
1381         g_printf(_("WARNING: No dump matching %s found in curinfo.\n"), filename);
1382         dumpfile_free_data(&file);
1383         return 1; /* not an error */
1384     }
1385
1386     /* make a copy */
1387     matching_hist = info.history[matching_hist_idx];
1388
1389     /* Remove the history element itself before doing the stats */
1390     for (i = matching_hist_idx; i < NB_HISTORY; i++) {
1391         info.history[i] = info.history[i+1];
1392     }
1393     info.history[NB_HISTORY].level = -1;
1394
1395     /* Remove stats for that history element, if necessary.  Doing so
1396      * will result in an inconsistent set of backups, so we warn the
1397      * user and adjust last_level to make the next dump get us a 
1398      * consistent picture. */
1399     if (matching_hist.date == info.inf[matching_hist.level].date) {
1400         /* search for an earlier dump at this level */
1401         for (i = matching_hist_idx; info.history[i].level > -1; i++) {
1402             if (info.history[i].level == matching_hist.level)
1403                 break;
1404         }
1405
1406         if (info.history[i].level < 0) {
1407             /* not found => zero it out */
1408             info.inf[matching_hist.level].date = (time_t)-1; /* flag as not set */
1409             info.inf[matching_hist.level].label[0] = '\0';
1410         } else {
1411             /* found => reconstruct stats as best we can */
1412             info.inf[matching_hist.level].size = info.history[i].size;
1413             info.inf[matching_hist.level].csize = info.history[i].csize;
1414             info.inf[matching_hist.level].secs = info.history[i].secs;
1415             info.inf[matching_hist.level].date = info.history[i].date;
1416             info.inf[matching_hist.level].filenum = 0; /* we don't know */
1417             info.inf[matching_hist.level].label[0] = '\0'; /* we don't know */
1418         }
1419
1420         /* set last_level to the level we just deleted, and set command
1421          * appropriately to make sure planner does a new dump at this level
1422          * or lower */
1423         info.last_level = matching_hist.level;
1424         if (info.last_level == 0) {
1425             g_printf(_("WARNING: Deleting the most recent full dump; forcing a full dump at next run.\n"));
1426             SET(info.command, FORCE_FULL);
1427         } else {
1428             g_printf(_("WARNING: Deleting the most recent level %d dump; forcing a level %d dump or \nWARNING: lower at next run.\n"),
1429                 info.last_level, info.last_level);
1430             SET(info.command, FORCE_NO_BUMP);
1431         }
1432
1433         /* Search for and display any subsequent runs that depended on this one */
1434         warnings_printed = 0;
1435         for (i = matching_hist_idx-1; i >= 0; i--) {
1436             char *datestamp;
1437             if (info.history[i].level <= matching_hist.level) break;
1438
1439             datestamp = get_timestamp_from_time(info.history[i].date);
1440             g_printf(_("WARNING: Level %d dump made %s can no longer be accurately restored.\n"), 
1441                 info.history[i].level, datestamp);
1442             amfree(datestamp);
1443
1444             warnings_printed = 1;
1445         }
1446         if (warnings_printed)
1447             g_printf(_("WARNING: (note, dates shown above are for dumps, and may be later than the\nWARNING: corresponding run date)\n"));
1448     }
1449
1450     /* recalculate consecutive_runs based on the history: find the first run
1451      * at this level, and then count the consecutive runs at that level. This
1452      * number may be zero (if we just deleted the last run at this level) */
1453     info.consecutive_runs = 0;
1454     for (i = 0; info.history[i].level >= 0; i++) {
1455         if (info.history[i].level == info.last_level) break;
1456     }
1457     while (info.history[i+info.consecutive_runs].level == info.last_level)
1458         info.consecutive_runs++;
1459
1460     /* this function doesn't touch the performance stats */
1461
1462     /* write out the changes */
1463     if (put_info(file.name, file.disk, &info) == -1) {
1464         g_printf(_("Could not write curinfo record for %s:%s\n"), file.name, file.disk);
1465         dumpfile_free_data(&file);
1466         return 0;
1467     }
1468
1469     dumpfile_free_data(&file);
1470     return 1;
1471 }
1472
1473 void
1474 holding(
1475     int         argc,
1476     char **     argv)
1477 {
1478     GSList *file_list;
1479     GSList *li;
1480     enum { HOLDING_USAGE, HOLDING_LIST, HOLDING_DELETE } action = HOLDING_USAGE;
1481     int long_list = 0;
1482     int outdated_list = 0;
1483     dumpfile_t file;
1484
1485     if (argc < 4)
1486         action = HOLDING_USAGE;
1487     else if (strcmp(argv[3], "list") == 0 && argc >= 4)
1488         action = HOLDING_LIST;
1489     else if (strcmp(argv[3], "delete") == 0 && argc > 4)
1490         action = HOLDING_DELETE;
1491
1492     switch (action) {
1493         case HOLDING_USAGE:
1494             g_fprintf(stderr,
1495                     _("%s: expecting \"holding list [-l] [-d]\" or \"holding delete <host> [ .. ]\"\n"),
1496                     get_pname());
1497             usage();
1498             return;
1499
1500         case HOLDING_LIST:
1501             long_list = opt_long;
1502             outdated_list = opt_outdated;
1503             argc -= 4; argv += 4;
1504
1505             /* header */
1506             if (long_list) {
1507                 g_printf("%-10s %-2s %-4s %s\n", 
1508                     _("size (kB)"), _("lv"), _("outd"), _("dump specification"));
1509             }
1510
1511             file_list = get_file_list(argc, argv, 1);
1512             for (li = file_list; li != NULL; li = li->next) {
1513                 char *dumpstr;
1514                 int is_outdated;
1515
1516                 if (!holding_file_get_dumpfile((char *)li->data, &file)) {
1517                     g_fprintf(stderr, _("Error reading %s\n"), (char *)li->data);
1518                     continue;
1519                 }
1520
1521                 is_outdated = holding_file_is_outdated(&file);
1522
1523                 dumpstr = cmdline_format_dumpspec_components(file.name, file.disk, file.datestamp, NULL);
1524                 /* only print this entry if we're printing everything, or if it's outdated and
1525                  * we're only printing outdated files (-o) */
1526                 if (!outdated_list || is_outdated) {
1527                     if (long_list) {
1528                         g_printf("%-10lld %-2d %-4s %s\n", 
1529                                (long long)holding_file_size((char *)li->data, 0),
1530                                file.dumplevel,
1531                                is_outdated? " *":"",
1532                                dumpstr);
1533                     } else {
1534                         g_printf("%s\n", dumpstr);
1535                     }
1536                 }
1537                 amfree(dumpstr);
1538                 dumpfile_free_data(&file);
1539             }
1540             slist_free_full(file_list, g_free);
1541             break;
1542
1543         case HOLDING_DELETE:
1544             argc -= 4; argv += 4;
1545
1546             file_list = get_file_list(argc, argv, 0);
1547             for (li = file_list; li != NULL; li = li->next) {
1548                 g_fprintf(stderr, _("Deleting '%s'\n"), (char *)li->data);
1549                 /* remove it from the catalog */
1550                 if (!remove_holding_file_from_catalog((char *)li->data))
1551                     exit(1);
1552
1553                 /* unlink it */
1554                 if (!holding_file_unlink((char *)li->data)) {
1555                     error(_("Could not delete '%s'"), (char *)li->data);
1556                 }
1557             }
1558             slist_free_full(file_list, g_free);
1559             break;
1560     }
1561 }
1562
1563
1564 /* ------------------------ */
1565
1566
1567 /* shared code with planner.c */
1568
1569 int
1570 bump_thresh(
1571     int         level)
1572 {
1573     gint64 bump = getconf_int64(CNF_BUMPSIZE);
1574     double mult = getconf_real(CNF_BUMPMULT);
1575
1576     while(--level)
1577         bump = (int)((double)bump * mult);
1578     return bump;
1579 }
1580
1581 void
1582 bumpsize(
1583     int         argc,
1584     char **     argv)
1585 {
1586     int l;
1587     int conf_bumppercent = getconf_int(CNF_BUMPPERCENT);
1588     double conf_bumpmult = getconf_real(CNF_BUMPMULT);
1589
1590     (void)argc; /* Quiet unused parameter warning */
1591     (void)argv; /* Quiet unused parameter warning */
1592
1593     g_printf(_("Current bump parameters:\n"));
1594     if(conf_bumppercent == 0) {
1595         g_printf(_("  bumpsize %5jd KB\t- minimum savings (threshold) to bump level 1 -> 2\n"),
1596                (intmax_t)getconf_int64(CNF_BUMPSIZE));
1597         g_printf(_("  bumpdays %5d\t- minimum days at each level\n"),
1598                getconf_int(CNF_BUMPDAYS));
1599         g_printf(_("  bumpmult %5.5lg\t- threshold = bumpsize * bumpmult**(level-1)\n\n"),
1600                conf_bumpmult);
1601
1602         g_printf(_("      Bump -> To  Threshold\n"));
1603         for(l = 1; l < 9; l++)
1604             g_printf(_("\t%d  ->  %d  %9d KB\n"), l, l+1, bump_thresh(l));
1605         putchar('\n');
1606     }
1607     else {
1608         double bumppercent = (double)conf_bumppercent;
1609
1610         g_printf(_("  bumppercent %3d %%\t- minimum savings (threshold) to bump level 1 -> 2\n"),
1611                conf_bumppercent);
1612         g_printf(_("  bumpdays %5d\t- minimum days at each level\n"),
1613                getconf_int(CNF_BUMPDAYS));
1614         g_printf(_("  bumpmult %5.5lg\t- threshold = disk_size * bumppercent * bumpmult**(level-1)\n\n"),
1615                conf_bumpmult);
1616         g_printf(_("      Bump -> To  Threshold\n"));
1617         for(l = 1; l < 9; l++) {
1618             g_printf(_("\t%d  ->  %d  %7.2lf %%\n"), l, l+1, bumppercent);
1619             bumppercent *= conf_bumpmult;
1620             if(bumppercent >= 100.000) { bumppercent = 100.0;}
1621         }
1622         putchar('\n');
1623     }
1624 }
1625
1626 /* ----------------------------------------------- */
1627
1628 void export_one(disk_t *dp);
1629
1630 void
1631 export_db(
1632     int         argc,
1633     char **     argv)
1634 {
1635     disk_t *dp;
1636     time_t curtime;
1637     char hostname[MAX_HOSTNAME_LENGTH+1];
1638     int i;
1639
1640     g_printf(_("CURINFO Version %s CONF %s\n"), VERSION, getconf_str(CNF_ORG));
1641
1642     curtime = time(0);
1643     if(gethostname(hostname, SIZEOF(hostname)-1) == -1) {
1644         error(_("could not determine host name: %s\n"), strerror(errno));
1645         /*NOTREACHED*/
1646     }
1647     hostname[SIZEOF(hostname)-1] = '\0';
1648     g_printf(_("# Generated by:\n#    host: %s\n#    date: %s"),
1649            hostname, ctime(&curtime));
1650
1651     g_printf(_("#    command:"));
1652     for(i = 0; i < argc; i++)
1653         g_printf(_(" %s"), argv[i]);
1654
1655     g_printf(_("\n# This file can be merged back in with \"amadmin import\".\n"));
1656     g_printf(_("# Edit only with care.\n"));
1657
1658     if(argc >= 4)
1659         diskloop(argc, argv, "export", export_one);
1660     else for(dp = diskq.head; dp != NULL; dp = dp->next)
1661         export_one(dp);
1662 }
1663
1664 void
1665 export_one(
1666     disk_t *    dp)
1667 {
1668     info_t info;
1669     int i,l;
1670     char *qhost, *qdisk;
1671
1672     if(get_info(dp->host->hostname, dp->name, &info)) {
1673         g_fprintf(stderr, _("Warning: no curinfo record for %s:%s\n"),
1674                 dp->host->hostname, dp->name);
1675         return;
1676     }
1677     qhost = quote_string(dp->host->hostname);
1678     qdisk = quote_string(dp->name);
1679     g_printf(_("host: %s\ndisk: %s\n"), qhost, qdisk);
1680     g_printf(_("command: %u\n"), info.command);
1681     g_printf(_("last_level: %d\n"),info.last_level);
1682     g_printf(_("consecutive_runs: %d\n"),info.consecutive_runs);
1683     g_printf(_("full-rate:"));
1684     for(i=0;i<AVG_COUNT;i++) g_printf(_(" %lf"), info.full.rate[i]);
1685     g_printf(_("\nfull-comp:"));
1686     for(i=0;i<AVG_COUNT;i++) g_printf(_(" %lf"), info.full.comp[i]);
1687
1688     g_printf(_("\nincr-rate:"));
1689     for(i=0;i<AVG_COUNT;i++) g_printf(_(" %lf"), info.incr.rate[i]);
1690     g_printf(_("\nincr-comp:"));
1691     for(i=0;i<AVG_COUNT;i++) g_printf(_(" %lf"), info.incr.comp[i]);
1692     g_printf("\n");
1693     for(l=0;l<DUMP_LEVELS;l++) {
1694         if(info.inf[l].date < (time_t)0 && info.inf[l].label[0] == '\0') continue;
1695         g_printf(_("stats: %d %lld %lld %jd %jd %lld %s\n"), l,
1696                (long long)info.inf[l].size,
1697                (long long)info.inf[l].csize,
1698                (intmax_t)info.inf[l].secs,
1699                (intmax_t)info.inf[l].date,
1700                (long long)info.inf[l].filenum,
1701                info.inf[l].label);
1702     }
1703     for(l=0;info.history[l].level > -1;l++) {
1704         g_printf(_("history: %d %lld %lld %jd\n"),
1705                info.history[l].level,
1706                (long long)info.history[l].size,
1707                (long long)info.history[l].csize,
1708                (intmax_t)info.history[l].date);
1709     }
1710     g_printf("//\n");
1711     amfree(qhost);
1712     amfree(qdisk);
1713 }
1714
1715 /* ----------------------------------------------- */
1716
1717 int import_one(void);
1718 char *impget_line(void);
1719
1720 void
1721 import_db(
1722     int         argc,
1723     char **     argv)
1724 {
1725     int vers_maj;
1726     int vers_min;
1727     int vers_patch;
1728     int newer;
1729     char *org;
1730     char *line = NULL;
1731     char *hdr;
1732     char *s;
1733     int rc;
1734     int ch;
1735
1736     (void)argc; /* Quiet unused parameter warning */
1737     (void)argv; /* Quiet unused parameter warning */
1738
1739     /* process header line */
1740
1741     if((line = agets(stdin)) == NULL) {
1742         g_fprintf(stderr, _("%s: empty input.\n"), get_pname());
1743         return;
1744     }
1745
1746     s = line;
1747     ch = *s++;
1748
1749     hdr = "version";
1750     if(strncmp_const_skip(s - 1, "CURINFO Version", s, ch) != 0) {
1751         goto bad_header;
1752     }
1753     ch = *s++;
1754     skip_whitespace(s, ch);
1755     if(ch == '\0'
1756        || sscanf(s - 1, "%d.%d.%d", &vers_maj, &vers_min, &vers_patch) != 3) {
1757         vers_patch = -1;
1758         if (sscanf(s - 1, "%d.%d", &vers_maj, &vers_min) != 2) {
1759             goto bad_header;
1760         }
1761     }
1762
1763     skip_integer(s, ch);                        /* skip over major */
1764     if(ch != '.') {
1765         goto bad_header;
1766     }
1767     ch = *s++;
1768     skip_integer(s, ch);                        /* skip over minor */
1769     if (vers_patch != -1) {
1770         if (ch != '.') {
1771             goto bad_header;
1772         }
1773         ch = *s++;
1774         skip_integer(s, ch);                    /* skip over patch */
1775     } else {
1776         vers_patch = 0;
1777     }
1778
1779     hdr = "comment";
1780     if(ch == '\0') {
1781         goto bad_header;
1782     }
1783     skip_non_whitespace(s, ch);
1784     s[-1] = '\0';
1785
1786     hdr = "CONF";
1787     skip_whitespace(s, ch);                     /* find the org keyword */
1788     if(ch == '\0' || strncmp_const_skip(s - 1, "CONF", s, ch) != 0) {
1789         goto bad_header;
1790     }
1791     ch = *s++;
1792
1793     hdr = "org";
1794     skip_whitespace(s, ch);                     /* find the org string */
1795     if(ch == '\0') {
1796         goto bad_header;
1797     }
1798     org = s - 1;
1799
1800     /*@ignore@*/
1801     newer = (vers_maj != VERSION_MAJOR)? vers_maj > VERSION_MAJOR :
1802             (vers_min != VERSION_MINOR)? vers_min > VERSION_MINOR :
1803                                          vers_patch > VERSION_PATCH;
1804     if(newer)
1805         g_fprintf(stderr,
1806              _("%s: WARNING: input is from newer Amanda version: %d.%d.%d.\n"),
1807                 get_pname(), vers_maj, vers_min, vers_patch);
1808     /*@end@*/
1809
1810     if(strcmp(org, getconf_str(CNF_ORG)) != 0) {
1811         g_fprintf(stderr, _("%s: WARNING: input is from different org: %s\n"),
1812                 get_pname(), org);
1813     }
1814
1815     do {
1816         rc = import_one();
1817     } while (rc);
1818
1819     amfree(line);
1820     return;
1821
1822  bad_header:
1823
1824     /*@i@*/ amfree(line);
1825     g_fprintf(stderr, _("%s: bad CURINFO header line in input: %s.\n"),
1826             get_pname(), hdr);
1827     g_fprintf(stderr, _("    Was the input in \"amadmin export\" format?\n"));
1828     return;
1829 }
1830
1831
1832 int
1833 import_one(void)
1834 {
1835     info_t info;
1836     stats_t onestat;
1837     int rc, level;
1838     char *line = NULL;
1839     char *s, *fp;
1840     int ch;
1841     int nb_history, i;
1842     char *hostname = NULL;
1843     char *diskname = NULL;
1844     long long off_t_tmp;
1845     long long time_t_tmp;
1846
1847     memset(&info, 0, SIZEOF(info_t));
1848
1849     for(level = 0; level < DUMP_LEVELS; level++) {
1850         info.inf[level].date = (time_t)-1;
1851     }
1852
1853     /* get host: disk: command: lines */
1854
1855     hostname = diskname = NULL;
1856
1857     if((line = impget_line()) == NULL) return 0;        /* nothing there */
1858     s = line;
1859     ch = *s++;
1860
1861     skip_whitespace(s, ch);
1862     if(ch == '\0' || strncmp_const_skip(s - 1, "host:", s, ch) != 0) goto parse_err;
1863     skip_whitespace(s, ch);
1864     if(ch == '\0') goto parse_err;
1865     fp = s-1;
1866     skip_quoted_string(s, ch);
1867     s[-1] = '\0';
1868     hostname = unquote_string(fp);
1869     s[-1] = (char)ch;
1870
1871     skip_whitespace(s, ch);
1872     while (ch == 0) {
1873       amfree(line);
1874       if((line = impget_line()) == NULL) goto shortfile_err;
1875       s = line;
1876       ch = *s++;
1877       skip_whitespace(s, ch);
1878     }
1879     if(strncmp_const_skip(s - 1, "disk:", s, ch) != 0) goto parse_err;
1880     skip_whitespace(s, ch);
1881     if(ch == '\0') goto parse_err;
1882     fp = s-1;
1883     skip_quoted_string(s, ch);
1884     s[-1] = '\0';
1885     diskname = unquote_string(fp);
1886     s[-1] = (char)ch;
1887
1888     amfree(line);
1889     if((line = impget_line()) == NULL) goto shortfile_err;
1890     if(sscanf(line, "command: %u", &info.command) != 1) goto parse_err;
1891
1892     /* get last_level and consecutive_runs */
1893
1894     amfree(line);
1895     if((line = impget_line()) == NULL) goto shortfile_err;
1896     rc = sscanf(line, "last_level: %d", &info.last_level);
1897     if(rc == 1) {
1898         amfree(line);
1899         if((line = impget_line()) == NULL) goto shortfile_err;
1900         if(sscanf(line, "consecutive_runs: %d", &info.consecutive_runs) != 1) goto parse_err;
1901         amfree(line);
1902         if((line = impget_line()) == NULL) goto shortfile_err;
1903     }
1904
1905     /* get rate: and comp: lines for full dumps */
1906
1907     rc = sscanf(line, "full-rate: %lf %lf %lf",
1908                 &info.full.rate[0], &info.full.rate[1], &info.full.rate[2]);
1909     if(rc != 3) goto parse_err;
1910
1911     amfree(line);
1912     if((line = impget_line()) == NULL) goto shortfile_err;
1913     rc = sscanf(line, "full-comp: %lf %lf %lf",
1914                 &info.full.comp[0], &info.full.comp[1], &info.full.comp[2]);
1915     if(rc != 3) goto parse_err;
1916
1917     /* get rate: and comp: lines for incr dumps */
1918
1919     amfree(line);
1920     if((line = impget_line()) == NULL) goto shortfile_err;
1921     rc = sscanf(line, "incr-rate: %lf %lf %lf",
1922                 &info.incr.rate[0], &info.incr.rate[1], &info.incr.rate[2]);
1923     if(rc != 3) goto parse_err;
1924
1925     amfree(line);
1926     if((line = impget_line()) == NULL) goto shortfile_err;
1927     rc = sscanf(line, "incr-comp: %lf %lf %lf",
1928                 &info.incr.comp[0], &info.incr.comp[1], &info.incr.comp[2]);
1929     if(rc != 3) goto parse_err;
1930
1931     /* get stats for dump levels */
1932
1933     while(1) {
1934         amfree(line);
1935         if((line = impget_line()) == NULL) goto shortfile_err;
1936         if(strncmp_const(line, "//") == 0) {
1937             /* end of record */
1938             break;
1939         }
1940         if(strncmp_const(line, "history:") == 0) {
1941             /* end of record */
1942             break;
1943         }
1944         memset(&onestat, 0, SIZEOF(onestat));
1945
1946         s = line;
1947         ch = *s++;
1948
1949         skip_whitespace(s, ch);
1950         if(ch == '\0' || strncmp_const_skip(s - 1, "stats:", s, ch) != 0) {
1951             goto parse_err;
1952         }
1953
1954         skip_whitespace(s, ch);
1955         if(ch == '\0' || sscanf(s - 1, "%d", &level) != 1) {
1956             goto parse_err;
1957         }
1958         skip_integer(s, ch);
1959
1960         skip_whitespace(s, ch);
1961         if(ch == '\0' || sscanf(s - 1, "%lld", &off_t_tmp) != 1) {
1962             goto parse_err;
1963         }
1964         onestat.size = (off_t)off_t_tmp;
1965         skip_integer(s, ch);
1966
1967         skip_whitespace(s, ch);
1968         if(ch == '\0' || sscanf(s - 1, "%lld", &off_t_tmp) != 1) {
1969             goto parse_err;
1970         }
1971         onestat.csize = (off_t)off_t_tmp;
1972         skip_integer(s, ch);
1973
1974         skip_whitespace(s, ch);
1975         if(ch == '\0' || sscanf(s - 1, "%lld", &time_t_tmp) != 1) {
1976             goto parse_err;
1977         }
1978         onestat.secs = (time_t)time_t_tmp;
1979         skip_integer(s, ch);
1980
1981         skip_whitespace(s, ch);
1982         if(ch == '\0' || sscanf(s - 1, "%lld", &time_t_tmp) != 1) {
1983             goto parse_err;
1984         }
1985         /* time_t not guarranteed to be long */
1986         /*@i1@*/ onestat.date = (time_t)time_t_tmp;
1987         skip_integer(s, ch);
1988
1989         skip_whitespace(s, ch);
1990         if(ch != '\0') {
1991             if(sscanf(s - 1, "%lld", &off_t_tmp) != 1) {
1992                 goto parse_err;
1993             }
1994             onestat.filenum = (off_t)off_t_tmp;
1995             skip_integer(s, ch);
1996
1997             skip_whitespace(s, ch);
1998             if(ch == '\0') {
1999                 if (onestat.filenum != 0)
2000                     goto parse_err;
2001                 onestat.label[0] = '\0';
2002             } else {
2003                 strncpy(onestat.label, s - 1, SIZEOF(onestat.label)-1);
2004                 onestat.label[SIZEOF(onestat.label)-1] = '\0';
2005             }
2006         }
2007
2008         if(level < 0 || level > 9) goto parse_err;
2009
2010         info.inf[level] = onestat;
2011     }
2012     nb_history = 0;
2013     for(i=0;i<=NB_HISTORY;i++) {
2014         info.history[i].level = -2;
2015     }
2016     while(1) {
2017         history_t onehistory;
2018
2019         if(line[0] == '/' && line[1] == '/') {
2020             info.history[nb_history].level = -2;
2021             rc = 0;
2022             break;
2023         }
2024         memset(&onehistory, 0, SIZEOF(onehistory));
2025         s = line;
2026         ch = *s++;
2027         if(strncmp_const_skip(line, "history:", s, ch) != 0) {
2028             break;
2029         }
2030
2031         skip_whitespace(s, ch);
2032         if(ch == '\0' || sscanf((s - 1), "%d", &onehistory.level) != 1) {
2033             break;
2034         }
2035         skip_integer(s, ch);
2036
2037         skip_whitespace(s, ch);
2038         if(ch == '\0' || sscanf((s - 1), "%lld", &off_t_tmp) != 1) {
2039             break;
2040         }
2041         onehistory.size = (off_t)off_t_tmp;
2042         skip_integer(s, ch);
2043
2044         skip_whitespace(s, ch);
2045         if(ch == '\0' || sscanf((s - 1), "%lld", &off_t_tmp) != 1) {
2046             break;
2047         }
2048         onehistory.csize = (off_t)off_t_tmp;
2049         skip_integer(s, ch);
2050
2051         skip_whitespace(s, ch);
2052         if((ch == '\0') || sscanf((s - 1), "%lld", &time_t_tmp) != 1) {
2053             break;
2054         }
2055         /* time_t not guarranteed to be long */
2056         /*@i1@*/ onehistory.date = (time_t)time_t_tmp;
2057         skip_integer(s, ch);
2058
2059         info.history[nb_history++] = onehistory;
2060         amfree(line);
2061         if((line = impget_line()) == NULL) goto shortfile_err;
2062     }
2063     /*@i@*/ amfree(line);
2064
2065     /* got a full record, now write it out to the database */
2066
2067     if(put_info(hostname, diskname, &info)) {
2068         g_fprintf(stderr, _("%s: error writing record for %s:%s\n"),
2069                 get_pname(), hostname, diskname);
2070     }
2071     amfree(hostname);
2072     amfree(diskname);
2073     return 1;
2074
2075  parse_err:
2076     /*@i@*/ amfree(line);
2077     amfree(hostname);
2078     amfree(diskname);
2079     g_fprintf(stderr, _("%s: parse error reading import record.\n"), get_pname());
2080     return 0;
2081
2082  shortfile_err:
2083     /*@i@*/ amfree(line);
2084     amfree(hostname);
2085     amfree(diskname);
2086     g_fprintf(stderr, _("%s: short file reading import record.\n"), get_pname());
2087     return 0;
2088 }
2089
2090 char *
2091 impget_line(void)
2092 {
2093     char *line;
2094     char *s;
2095     int ch;
2096
2097     for(; (line = agets(stdin)) != NULL; free(line)) {
2098         s = line;
2099         ch = *s++;
2100
2101         skip_whitespace(s, ch);
2102         if(ch == '#') {
2103             /* ignore comment lines */
2104             continue;
2105         } else if(ch) {
2106             /* found non-blank, return line */
2107             return line;
2108         }
2109         /* otherwise, a blank line, so keep going */
2110     }
2111     if(ferror(stdin)) {
2112         g_fprintf(stderr, _("%s: reading stdin: %s\n"),
2113                 get_pname(), strerror(errno));
2114     }
2115     return NULL;
2116 }
2117
2118 /* ----------------------------------------------- */
2119
2120 void
2121 disklist_one(
2122     disk_t *    dp)
2123 {
2124     am_host_t *hp;
2125     netif_t *ip;
2126     dumptype_t *dtype = lookup_dumptype(dp->dtype_name);
2127
2128     hp = dp->host;
2129     ip = hp->netif;
2130
2131     g_printf("line %d (%s):\n", dp->line, dp->filename);
2132
2133     g_printf("    host %s:\n", hp->hostname);
2134     g_printf("        interface %s\n",
2135            interface_name(ip->config)[0] ? interface_name(ip->config) : "default");
2136     g_printf("    disk %s:\n", dp->name);
2137     if (dp->device) g_printf("        device %s\n", dp->device);
2138
2139     g_printf("        program \"%s\"\n", dp->program);
2140     if (dp->application)
2141         g_printf("        application \"%s\"\n", dp->application);
2142
2143     dump_dumptype(dtype, "  ", print_default, print_source);
2144
2145     g_printf("        spindle %d\n", dp->spindle);
2146
2147     g_printf("\n");
2148 }
2149
2150 void
2151 disklist(
2152     int         argc,
2153     char **     argv)
2154 {
2155     disk_t *dp;
2156
2157     if(argc >= 4)
2158         diskloop(argc, argv, "disklist", disklist_one);
2159     else
2160         for(dp = diskq.head; dp != NULL; dp = dp->next)
2161             disklist_one(dp);
2162 }
2163
2164 /* ----------------------------------------------- */
2165
2166 void
2167 hosts(
2168     int         argc G_GNUC_UNUSED,
2169     char **     argv G_GNUC_UNUSED)
2170 {
2171     disk_t *dp;
2172     gint sentinel = 1;
2173     GHashTable *seen = g_hash_table_new(g_str_hash, g_str_equal);
2174
2175     /* enumerate all hosts, skipping those that have been seen (since
2176      * there may be more than one DLE on a host */
2177     for(dp = diskq.head; dp != NULL; dp = dp->next) {
2178         char *hostname = dp->host->hostname;
2179         if (g_hash_table_lookup(seen, hostname))
2180             continue;
2181         g_printf("%s\n", hostname);
2182         g_hash_table_insert(seen, hostname, &sentinel);
2183     }
2184     g_hash_table_destroy(seen);
2185 }
2186
2187 /* ----------------------------------------------- */
2188
2189 void
2190 dles(
2191     int         argc G_GNUC_UNUSED,
2192     char **     argv G_GNUC_UNUSED)
2193 {
2194     disk_t *dp;
2195
2196     for(dp = diskq.head; dp != NULL; dp = dp->next)
2197         g_printf("%s %s\n", dp->host->hostname, dp->name);
2198 }
2199
2200 /* ----------------------------------------------- */
2201
2202 void
2203 show_version(
2204     int         argc,
2205     char **     argv)
2206 {
2207     int i;
2208
2209     (void)argc; /* Quiet unused parameter warning */
2210     (void)argv; /* Quiet unused parameter warning */
2211
2212     for(i = 0; version_info[i] != NULL; i++)
2213         g_printf("%s", version_info[i]);
2214 }
2215
2216
2217 void show_config(
2218     int argc G_GNUC_UNUSED,
2219     char **argv G_GNUC_UNUSED)
2220 {
2221     dump_configuration(print_default, print_source);
2222 }
2223