HAMMER: Add "hammer abort-cleanup" command
[dragonfly.git] / sbin / hammer / cmd_cleanup.c
1 /*
2  * Copyright (c) 2008 The DragonFly Project.  All rights reserved.
3  *
4  * This code is derived from software contributed to The DragonFly Project
5  * by Matthew Dillon <dillon@backplane.com>
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * 1. Redistributions of source code must retain the above copyright
12  *    notice, this list of conditions and the following disclaimer.
13  * 2. Redistributions in binary form must reproduce the above copyright
14  *    notice, this list of conditions and the following disclaimer in
15  *    the documentation and/or other materials provided with the
16  *    distribution.
17  * 3. Neither the name of The DragonFly Project nor the names of its
18  *    contributors may be used to endorse or promote products derived
19  *    from this software without specific, prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22  * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
25  * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26  * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
27  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
31  * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
32  * SUCH DAMAGE.
33  */
34 /*
35  * Clean up specific HAMMER filesystems or all HAMMER filesystems.
36  *
37  * If no filesystems are specified any HAMMER- or null-mounted hammer PFS's
38  * are cleaned.
39  *
40  * Each HAMMER filesystem may contain a configuration file.  If no
41  * configuration file is present one will be created with the following
42  * defaults:
43  *
44  *      snapshots 1d 60d        (0d 0d for /tmp, /var/tmp, /usr/obj)
45  *      prune     1d 5m
46  *      rebalance 1d 5m
47  *      #dedup    1d 5m         (not enabled by default)
48  *      reblock   1d 5m
49  *      recopy    30d 10m
50  *
51  * All hammer commands create and maintain cycle files in the snapshots
52  * directory.
53  *
54  * For HAMMER version 2- the configuration file is a named 'config' in
55  * the snapshots directory, which defaults to <pfs>/snapshots.
56  * For HAMMER version 3+ the configuration file is saved in filesystem
57  * meta-data. The snapshots directory defaults to /var/hammer/<pfs>
58  * (/var/hammer/root for root mount).
59  */
60
61 #include <libutil.h>
62 #include "hammer.h"
63
64 struct didpfs {
65         struct didpfs *next;
66         uuid_t          uuid;
67 };
68
69 static void do_cleanup(const char *path);
70 static void config_init(const char *path, struct hammer_ioc_config *config);
71 static void migrate_config(FILE *fp, struct hammer_ioc_config *config);
72 static void migrate_snapshots(int fd, const char *snapshots_path);
73 static void migrate_one_snapshot(int fd, const char *fpath,
74                         struct hammer_ioc_snapshot *snapshot);
75 static int strtosecs(char *ptr);
76 static const char *dividing_slash(const char *path);
77 static int check_period(const char *snapshots_path, const char *cmd, int arg1,
78                         time_t *savep);
79 static void save_period(const char *snapshots_path, const char *cmd,
80                         time_t savet);
81 static int check_softlinks(int fd, int new_config, const char *snapshots_path);
82 static void cleanup_softlinks(int fd, int new_config,
83                         const char *snapshots_path, int arg2, char *arg3);
84 static void delete_snapshots(int fd, struct hammer_ioc_snapshot *dsnapshot);
85 static int check_expired(const char *fpath, int arg2);
86
87 static int create_snapshot(const char *path, const char *snapshots_path);
88 static int cleanup_rebalance(const char *path, const char *snapshots_path,
89                         int arg1, int arg2);
90 static int cleanup_prune(const char *path, const char *snapshots_path,
91                         int arg1, int arg2, int snapshots_disabled);
92 static int cleanup_reblock(const char *path, const char *snapshots_path,
93                         int arg1, int arg2);
94 static int cleanup_recopy(const char *path, const char *snapshots_path,
95                         int arg1, int arg2);
96 static int cleanup_dedup(const char *path, const char *snapshots_path,
97                         int arg1, int arg2);
98
99 static void runcmd(int *resp, const char *ctl, ...) __printflike(2, 3);
100
101 /*
102  * WARNING: Do not make the SNAPSHOTS_BASE "/var/snapshots" because
103  * it will interfere with the older HAMMER VERS < 3 snapshots directory
104  * for the /var PFS.
105  */
106 #define SNAPSHOTS_BASE  "/var/hammer"   /* HAMMER VERS >= 3 */
107 #define WS      " \t\r\n"
108
109 struct didpfs *FirstPFS;
110
111 void
112 hammer_cmd_cleanup(char **av, int ac)
113 {
114         char *fstype, *fs, *path;
115         struct statfs *stfsbuf;
116         int mntsize, i;
117
118         tzset();
119         if (ac == 0) {
120                 mntsize = getmntinfo(&stfsbuf, MNT_NOWAIT);
121                 if (mntsize > 0) {
122                         for (i=0; i < mntsize; i++) {
123                                 /*
124                                  * We will cleanup in the case fstype is hammer.
125                                  * If we have null-mounted PFS, we check the
126                                  * mount source. If it looks like a PFS, we
127                                  * proceed to cleanup also.
128                                  */
129                                 fstype = stfsbuf[i].f_fstypename;
130                                 fs = stfsbuf[i].f_mntfromname;
131                                 if ((strcmp(fstype, "hammer") == 0) ||
132                                     ((strcmp(fstype, "null") == 0) &&
133                                      (strstr(fs, "/@@0x") != NULL ||
134                                       strstr(fs, "/@@-1") != NULL))) {
135                                         path = stfsbuf[i].f_mntonname;
136                                         do_cleanup(path);
137                                 }
138                         }
139                 }
140
141         } else {
142                 while (ac) {
143                         do_cleanup(*av);
144                         --ac;
145                         ++av;
146                 }
147         }
148 }
149
150 static
151 void
152 do_cleanup(const char *path)
153 {
154         struct hammer_ioc_pseudofs_rw pfs;
155         struct hammer_ioc_config config;
156         struct hammer_ioc_version version;
157         union hammer_ioc_mrecord_any mrec_tmp;
158         char *snapshots_path = NULL;
159         char *config_path;
160         struct stat st;
161         char *cmd;
162         char *ptr;
163         int arg1;
164         int arg2;
165         char *arg3;
166         time_t savet;
167         char buf[256];
168         char *cbase;
169         char *cptr;
170         FILE *fp = NULL;
171         struct didpfs *didpfs;
172         int snapshots_disabled = 0;
173         int prune_warning = 0;
174         int new_config = 0;
175         int snapshots_from_pfs = 0;
176         int fd;
177         int r;
178         int found_rebal = 0;
179
180         bzero(&pfs, sizeof(pfs));
181         bzero(&mrec_tmp, sizeof(mrec_tmp));
182         pfs.ondisk = &mrec_tmp.pfs.pfsd;
183         pfs.bytes = sizeof(mrec_tmp.pfs.pfsd);
184         pfs.pfs_id = -1;
185
186         printf("cleanup %-20s -", path);
187         fd = open(path, O_RDONLY);
188         if (fd < 0) {
189                 printf(" unable to access directory: %s\n", strerror(errno));
190                 return;
191         }
192         if (ioctl(fd, HAMMERIOC_GET_PSEUDOFS, &pfs) < 0) {
193                 printf(" not a HAMMER filesystem: %s\n", strerror(errno));
194                 close(fd);
195                 return;
196         }
197         if (pfs.version != HAMMER_IOC_PSEUDOFS_VERSION) {
198                 printf(" unrecognized HAMMER version\n");
199                 close(fd);
200                 return;
201         }
202         bzero(&version, sizeof(version));
203         if (ioctl(fd, HAMMERIOC_GET_VERSION, &version) < 0) {
204                 printf(" HAMMER filesystem but couldn't retrieve version!\n");
205                 close(fd);
206                 return;
207         }
208
209         bzero(&config, sizeof(config));
210         if (version.cur_version >= 3) {
211                 if (ioctl(fd, HAMMERIOC_GET_CONFIG, &config) == 0 &&
212                     config.head.error == 0) {
213                         new_config = 1;
214                 }
215         }
216
217         /*
218          * Make sure we have not already handled this PFS.  Several nullfs
219          * mounts might alias the same PFS.
220          */
221         for (didpfs = FirstPFS; didpfs; didpfs = didpfs->next) {
222                 if (bcmp(&didpfs->uuid, &mrec_tmp.pfs.pfsd.unique_uuid, sizeof(uuid_t)) == 0) {
223                         printf(" PFS #%d already handled\n", pfs.pfs_id);
224                         close(fd);
225                         return;
226                 }
227         }
228         didpfs = malloc(sizeof(*didpfs));
229         didpfs->next = FirstPFS;
230         FirstPFS = didpfs;
231         didpfs->uuid = mrec_tmp.pfs.pfsd.unique_uuid;
232
233         /*
234          * Calculate the old snapshots directory for HAMMER VERSION < 3
235          *
236          * If the directory is explicitly specified in the PFS config
237          * we flag it and will not migrate it later.
238          */
239         if (mrec_tmp.pfs.pfsd.snapshots[0] == '/') {
240                 asprintf(&snapshots_path, "%s", mrec_tmp.pfs.pfsd.snapshots);
241                 snapshots_from_pfs = 1;
242         } else if (mrec_tmp.pfs.pfsd.snapshots[0]) {
243                 printf(" WARNING: pfs-slave's snapshots dir is not absolute\n");
244                 close(fd);
245                 return;
246         } else if (mrec_tmp.pfs.pfsd.mirror_flags & HAMMER_PFSD_SLAVE) {
247                 if (version.cur_version < 3) {
248                         printf(" WARNING: must configure snapshot dir for PFS slave\n");
249                         printf("\tWe suggest <fs>/var/slaves/<name> where "
250                                "<fs> is the base HAMMER fs\n");
251                         printf("\tcontaining the slave\n");
252                         close(fd);
253                         return;
254                 }
255         } else {
256                 asprintf(&snapshots_path,
257                          "%s%ssnapshots", path, dividing_slash(path));
258         }
259
260         /*
261          * Check for old-style config file
262          */
263         if (snapshots_path) {
264                 asprintf(&config_path, "%s/config", snapshots_path);
265                 fp = fopen(config_path, "r");
266         }
267
268         /*
269          * Handle upgrades to hammer version 3, move the config
270          * file into meta-data.
271          *
272          * For the old config read the file into the config structure,
273          * we will parse it out of the config structure regardless.
274          */
275         if (version.cur_version >= 3) {
276                 if (fp) {
277                         printf("(migrating) ");
278                         fflush(stdout);
279                         migrate_config(fp, &config);
280                         migrate_snapshots(fd, snapshots_path);
281                         fclose(fp);
282                         if (ioctl(fd, HAMMERIOC_SET_CONFIG, &config) < 0) {
283                                 printf(" cannot init meta-data config!\n");
284                                 close(fd);
285                                 return;
286                         }
287                         remove(config_path);
288                 } else if (new_config == 0) {
289                         config_init(path, &config);
290                         if (ioctl(fd, HAMMERIOC_SET_CONFIG, &config) < 0) {
291                                 printf(" cannot init meta-data config!\n");
292                                 close(fd);
293                                 return;
294                         }
295                 }
296                 new_config = 1;
297         } else {
298                 /*
299                  * Create missing snapshots directory for HAMMER VERSION < 3
300                  */
301                 if (stat(snapshots_path, &st) < 0) {
302                         if (mkdir(snapshots_path, 0755) != 0) {
303                                 free(snapshots_path);
304                                 printf(" unable to create snapshot dir \"%s\": %s\n",
305                                         snapshots_path, strerror(errno));
306                                 close(fd);
307                                 return;
308                         }
309                 }
310
311                 /*
312                  *  Create missing config file for HAMMER VERSION < 3
313                  */
314                 if (fp == NULL) {
315                         config_init(path, &config);
316                         fp = fopen(config_path, "w");
317                         if (fp) {
318                                 fwrite(config.config.text, 1,
319                                         strlen(config.config.text), fp);
320                                 fclose(fp);
321                         }
322                 } else {
323                         migrate_config(fp, &config);
324                         fclose(fp);
325                 }
326         }
327
328         /*
329          * If snapshots_from_pfs is not set we calculate the new snapshots
330          * directory default (in /var) for HAMMER VERSION >= 3 and migrate
331          * the old snapshots directory over.
332          *
333          * People who have set an explicit snapshots directory will have
334          * to migrate the data manually into /var/hammer, or not bother at
335          * all.  People running slaves may wish to migrate it and then
336          * clear the snapshots specification in the PFS config for the
337          * slave.
338          */
339         if (new_config && snapshots_from_pfs == 0) {
340                 char *npath;
341
342                 assert(path[0] == '/');
343                 if (strcmp(path, "/") == 0)
344                         asprintf(&npath, "%s/root", SNAPSHOTS_BASE);
345                 else
346                         asprintf(&npath, "%s/%s", SNAPSHOTS_BASE, path + 1);
347                 if (snapshots_path) {
348                         if (stat(npath, &st) < 0 && errno == ENOENT) {
349                                 if (stat(snapshots_path, &st) < 0 && errno == ENOENT) {
350                                         printf(" HAMMER UPGRADE: Creating snapshots\n"
351                                                "\tCreating snapshots in %s\n",
352                                                npath);
353                                         runcmd(&r, "mkdir -p %s", npath);
354                                 } else {
355                                         printf(" HAMMER UPGRADE: Moving snapshots\n"
356                                                "\tMoving snapshots from %s to %s\n",
357                                                snapshots_path, npath);
358                                         runcmd(&r, "mkdir -p %s", npath);
359                                         runcmd(&r, "cpdup %s %s", snapshots_path, npath);
360                                         if (r != 0) {
361                                     printf("Unable to move snapshots directory!\n");
362                                     printf("Please fix this critical error.\n");
363                                     printf("Aborting cleanup of %s\n", path);
364                                                 close(fd);
365                                                 return;
366                                         }
367                                         runcmd(&r, "rm -rf %s", snapshots_path);
368                                 }
369                         }
370                         free(snapshots_path);
371                 } else if (stat(npath, &st) < 0 && errno == ENOENT) {
372                         runcmd(&r, "mkdir -p %s", npath);
373                 }
374                 snapshots_path = npath;
375         }
376
377         /*
378          * Lock the PFS.  fd is the base directory of the mounted PFS.
379          */
380         if (flock(fd, LOCK_EX|LOCK_NB) == -1) {
381                 if (errno == EWOULDBLOCK)
382                         printf(" PFS #%d locked by other process\n", pfs.pfs_id);
383                 else
384                         printf(" can not lock %s: %s\n", config_path, strerror(errno));
385                 close(fd);
386                 return;
387         }
388
389         printf(" handle PFS #%d using %s\n", pfs.pfs_id, snapshots_path);
390
391         struct pidfh    *pfh = NULL;
392         static char     pidfile[PIDFILE_BUFSIZE];
393
394         snprintf (pidfile, PIDFILE_BUFSIZE, "%s/hammer.cleanup.%d",
395                 pidfile_loc, getpid());
396         pfh = pidfile_open(pidfile, 0644, NULL);
397         if (pfh == NULL) {
398                 warn ("Unable to open or create %s", pidfile);
399         }
400         pidfile_write(pfh);
401
402         /*
403          * Process the config file
404          */
405         cbase = config.config.text;
406
407         while ((cptr = strchr(cbase, '\n')) != NULL) {
408                 bcopy(cbase, buf, cptr - cbase);
409                 buf[cptr - cbase] = 0;
410                 cbase = cptr + 1;
411
412                 cmd = strtok(buf, WS);
413                 if (cmd == NULL || cmd[0] == '#')
414                         continue;
415
416                 arg1 = 0;
417                 arg2 = 0;
418                 arg3 = NULL;
419                 if ((ptr = strtok(NULL, WS)) != NULL) {
420                         arg1 = strtosecs(ptr);
421                         if ((ptr = strtok(NULL, WS)) != NULL) {
422                                 arg2 = strtosecs(ptr);
423                                 arg3 = strtok(NULL, WS);
424                         }
425                 }
426
427                 printf("%20s - ", cmd);
428                 fflush(stdout);
429
430                 r = 1;
431                 if (strcmp(cmd, "snapshots") == 0) {
432                         if (arg1 == 0) {
433                                 if (arg2 &&
434                                     check_softlinks(fd, new_config,
435                                                     snapshots_path)) {
436                                         printf("only removing old snapshots\n");
437                                         prune_warning = 1;
438                                         cleanup_softlinks(fd, new_config,
439                                                           snapshots_path,
440                                                           arg2, arg3);
441                                 } else {
442                                         printf("disabled\n");
443                                         snapshots_disabled = 1;
444                                 }
445                         } else
446                         if (check_period(snapshots_path, cmd, arg1, &savet)) {
447                                 printf("run\n");
448                                 cleanup_softlinks(fd, new_config,
449                                                   snapshots_path,
450                                                   arg2, arg3);
451                                 r = create_snapshot(path, snapshots_path);
452                         } else {
453                                 printf("skip\n");
454                         }
455                 } else if (arg1 == 0) {
456                         /*
457                          * The commands following this check can't handle
458                          * a period of 0, so call the feature disabled and
459                          * ignore the directive.
460                          */
461                         printf("disabled\n");
462                 } else if (strcmp(cmd, "prune") == 0) {
463                         if (check_period(snapshots_path, cmd, arg1, &savet)) {
464                                 if (prune_warning) {
465                                         printf("run - WARNING snapshot "
466                                                "softlinks present "
467                                                "but snapshots disabled\n");
468                                 } else {
469                                         printf("run\n");
470                                 }
471                                 r = cleanup_prune(path, snapshots_path,
472                                               arg1, arg2, snapshots_disabled);
473                         } else {
474                                 printf("skip\n");
475                         }
476                 } else if (strcmp(cmd, "rebalance") == 0) {
477                         found_rebal = 1;
478                         if (check_period(snapshots_path, cmd, arg1, &savet)) {
479                                 printf("run");
480                                 fflush(stdout);
481                                 if (VerboseOpt)
482                                         printf("\n");
483                                 r = cleanup_rebalance(path, snapshots_path,
484                                                 arg1, arg2);
485                         } else {
486                                 printf("skip\n");
487                         }
488                 } else if (strcmp(cmd, "reblock") == 0) {
489                         if (check_period(snapshots_path, cmd, arg1, &savet)) {
490                                 printf("run");
491                                 fflush(stdout);
492                                 if (VerboseOpt)
493                                         printf("\n");
494                                 r = cleanup_reblock(path, snapshots_path,
495                                                 arg1, arg2);
496                         } else {
497                                 printf("skip\n");
498                         }
499                 } else if (strcmp(cmd, "recopy") == 0) {
500                         if (check_period(snapshots_path, cmd, arg1, &savet)) {
501                                 printf("run");
502                                 fflush(stdout);
503                                 if (VerboseOpt)
504                                         printf("\n");
505                                 r = cleanup_recopy(path, snapshots_path,
506                                                arg1, arg2);
507                         } else {
508                                 printf("skip\n");
509                         }
510                 } else if (strcmp(cmd, "dedup") == 0) {
511                         if (check_period(snapshots_path, cmd, arg1, &savet)) {
512                                 printf("run");
513                                 fflush(stdout);
514                                 if (VerboseOpt)
515                                         printf("\n");
516                                 r = cleanup_dedup(path, snapshots_path,
517                                                 arg1, arg2);
518                         } else {
519                                 printf("skip\n");
520                         }
521                 } else {
522                         printf("unknown directive\n");
523                         r = 1;
524                 }
525                 if (r == 0)
526                         save_period(snapshots_path, cmd, savet);
527         }
528
529         /*
530          * Add new rebalance feature if the config doesn't have it.
531          * (old style config only).
532          */
533         if (new_config == 0 && found_rebal == 0) {
534                 if ((fp = fopen(config_path, "r+")) != NULL) {
535                         fseek(fp, 0L, 2);
536                         fprintf(fp, "rebalance 1d 5m\n");
537                         fclose(fp);
538                 }
539         }
540
541         /*
542          * Cleanup, and delay a little
543          */
544         close(fd);
545         usleep(1000);
546         pidfile_close(pfh);
547         pidfile_remove(pfh);
548 }
549
550 /*
551  * Initialize new config data (new or old style)
552  */
553 static void
554 config_init(const char *path, struct hammer_ioc_config *config)
555 {
556         const char *snapshots;
557
558         if (strcmp(path, "/tmp") == 0 ||
559             strcmp(path, "/var/tmp") == 0 ||
560             strcmp(path, "/usr/obj") == 0) {
561                 snapshots = "snapshots 0d 0d\n";
562         } else {
563                 snapshots = "snapshots 1d 60d\n";
564         }
565         bzero(config->config.text, sizeof(config->config.text));
566         snprintf(config->config.text, sizeof(config->config.text) - 1, "%s%s",
567                 snapshots,
568                 "prune     1d 5m\n"
569                 "rebalance 1d 5m\n"
570                 "#dedup    1d 5m\n"
571                 "reblock   1d 5m\n"
572                 "recopy    30d 10m\n");
573 }
574
575 /*
576  * Migrate configuration data from the old snapshots/config
577  * file to the new meta-data format.
578  */
579 static void
580 migrate_config(FILE *fp, struct hammer_ioc_config *config)
581 {
582         int n;
583
584         n = fread(config->config.text, 1, sizeof(config->config.text) - 1, fp);
585         if (n >= 0)
586                 bzero(config->config.text + n, sizeof(config->config.text) - n);
587 }
588
589 /*
590  * Migrate snapshot softlinks in the snapshots directory to the
591  * new meta-data format.  The softlinks are left intact, but
592  * this way the pruning code won't lose track of them if you
593  * happen to blow away the snapshots directory.
594  */
595 static void
596 migrate_snapshots(int fd, const char *snapshots_path)
597 {
598         struct hammer_ioc_snapshot snapshot;
599         struct dirent *den;
600         struct stat st;
601         DIR *dir;
602         char *fpath;
603
604         bzero(&snapshot, sizeof(snapshot));
605
606         if ((dir = opendir(snapshots_path)) != NULL) {
607                 while ((den = readdir(dir)) != NULL) {
608                         if (den->d_name[0] == '.')
609                                 continue;
610                         asprintf(&fpath, "%s/%s", snapshots_path, den->d_name);
611                         if (lstat(fpath, &st) == 0 && S_ISLNK(st.st_mode)) {
612                                 migrate_one_snapshot(fd, fpath, &snapshot);
613                         }
614                         free(fpath);
615                 }
616                 closedir(dir);
617         }
618         migrate_one_snapshot(fd, NULL, &snapshot);
619
620 }
621
622 /*
623  * Migrate a single snapshot.  If fpath is NULL the ioctl is flushed,
624  * otherwise it is flushed when it fills up.
625  */
626 static void
627 migrate_one_snapshot(int fd, const char *fpath,
628                      struct hammer_ioc_snapshot *snapshot)
629 {
630         if (fpath) {
631                 struct hammer_snapshot_data *snap;
632                 struct tm tm;
633                 time_t t;
634                 int year;
635                 int month;
636                 int day = 0;
637                 int hour = 0;
638                 int minute = 0;
639                 int r;
640                 char linkbuf[1024];
641                 const char *ptr;
642                 hammer_tid_t tid;
643
644                 t = (time_t)-1;
645                 tid = (hammer_tid_t)(int64_t)-1;
646
647                 /* fpath may contain directory components */
648                 if ((ptr = strrchr(fpath, '/')) != NULL)
649                         ++ptr;
650                 else
651                         ptr = fpath;
652                 while (*ptr && *ptr != '-' && *ptr != '.')
653                         ++ptr;
654                 if (*ptr)
655                         ++ptr;
656                 r = sscanf(ptr, "%4d%2d%2d-%2d%2d",
657                            &year, &month, &day, &hour, &minute);
658
659                 if (r >= 3) {
660                         bzero(&tm, sizeof(tm));
661                         tm.tm_isdst = -1;
662                         tm.tm_min = minute;
663                         tm.tm_hour = hour;
664                         tm.tm_mday = day;
665                         tm.tm_mon = month - 1;
666                         tm.tm_year = year - 1900;
667                         t = mktime(&tm);
668                 }
669                 bzero(linkbuf, sizeof(linkbuf));
670                 if (readlink(fpath, linkbuf, sizeof(linkbuf) - 1) > 0 &&
671                     (ptr = strrchr(linkbuf, '@')) != NULL &&
672                     ptr > linkbuf && ptr[-1] == '@') {
673                         tid = strtoull(ptr + 1, NULL, 16);
674                 }
675                 if (t != (time_t)-1 && tid != (hammer_tid_t)(int64_t)-1) {
676                         snap = &snapshot->snaps[snapshot->count];
677                         bzero(snap, sizeof(*snap));
678                         snap->tid = tid;
679                         snap->ts = (u_int64_t)t * 1000000ULL;
680                         snprintf(snap->label, sizeof(snap->label),
681                                  "migrated");
682                         ++snapshot->count;
683                 } else {
684                         printf("    non-canonical snapshot softlink: %s->%s\n",
685                                fpath, linkbuf);
686                 }
687         }
688
689         if ((fpath == NULL && snapshot->count) ||
690             snapshot->count == HAMMER_SNAPS_PER_IOCTL) {
691                 printf(" (%d snapshots)", snapshot->count);
692 again:
693                 if (ioctl(fd, HAMMERIOC_ADD_SNAPSHOT, snapshot) < 0) {
694                         printf("    Ioctl to migrate snapshots failed: %s\n",
695                                strerror(errno));
696                 } else if (snapshot->head.error == EALREADY) {
697                         ++snapshot->index;
698                         goto again;
699                 } else if (snapshot->head.error) {
700                         printf("    Ioctl to migrate snapshots failed: %s\n",
701                                strerror(snapshot->head.error));
702                 }
703                 printf("index %d\n", snapshot->index);
704                 snapshot->index = 0;
705                 snapshot->count = 0;
706                 snapshot->head.error = 0;
707         }
708 }
709
710 static
711 int
712 strtosecs(char *ptr)
713 {
714         int val;
715
716         val = strtol(ptr, &ptr, 0);
717         switch(*ptr) {
718         case 'd':
719                 val *= 24;
720                 /* fall through */
721         case 'h':
722                 val *= 60;
723                 /* fall through */
724         case 'm':
725                 val *= 60;
726                 /* fall through */
727         case 's':
728                 break;
729         default:
730                 errx(1, "illegal suffix converting %s\n", ptr);
731                 break;
732         }
733         return(val);
734 }
735
736 static const char *
737 dividing_slash(const char *path)
738 {
739         int len = strlen(path);
740         if (len && path[len-1] == '/')
741                 return("");
742         else
743                 return("/");
744 }
745
746 /*
747  * Check whether the desired period has elapsed since the last successful
748  * run.  The run may take a while and cross a boundary so we remember the
749  * current time_t so we can save it later on.
750  *
751  * Periods in minutes, hours, or days are assumed to have been crossed
752  * if the local time crosses a minute, hour, or day boundary regardless
753  * of how close the last operation actually was.
754  *
755  * If ForceOpt is set always return true.
756  */
757 static int
758 check_period(const char *snapshots_path, const char *cmd, int arg1,
759         time_t *savep)
760 {
761         char *check_path;
762         struct tm tp1;
763         struct tm tp2;
764         FILE *fp;
765         time_t baset, lastt;
766         char buf[256];
767
768         time(savep);
769         localtime_r(savep, &tp1);
770
771         /*
772          * Force run if -F
773          */
774         if (ForceOpt)
775                 return(1);
776
777         /*
778          * Retrieve the start time of the last successful operation.
779          */
780         asprintf(&check_path, "%s/.%s.period", snapshots_path, cmd);
781         fp = fopen(check_path, "r");
782         free(check_path);
783         if (fp == NULL)
784                 return(1);
785         if (fgets(buf, sizeof(buf), fp) == NULL) {
786                 fclose(fp);
787                 return(1);
788         }
789         fclose(fp);
790
791         lastt = strtol(buf, NULL, 0);
792         localtime_r(&lastt, &tp2);
793
794         /*
795          * Normalize the times.  e.g. if asked to do something on a 1-day
796          * interval the operation will be performed as soon as the day
797          * turns over relative to the previous operation, even if the previous
798          * operation ran a few seconds ago just before midnight.
799          */
800         if (arg1 % 60 == 0) {
801                 tp1.tm_sec = 0;
802                 tp2.tm_sec = 0;
803         }
804         if (arg1 % (60 * 60) == 0) {
805                 tp1.tm_min = 0;
806                 tp2.tm_min = 0;
807         }
808         if (arg1 % (24 * 60 * 60) == 0) {
809                 tp1.tm_hour = 0;
810                 tp2.tm_hour = 0;
811         }
812
813         baset = mktime(&tp1);
814         lastt = mktime(&tp2);
815
816 #if 0
817         printf("%lld vs %lld\n", (long long)(baset - lastt), (long long)arg1);
818 #endif
819
820         if ((int)(baset - lastt) >= arg1)
821                 return(1);
822         return(0);
823 }
824
825 /*
826  * Store the start time of the last successful operation.
827  */
828 static void
829 save_period(const char *snapshots_path, const char *cmd,
830                         time_t savet)
831 {
832         char *ocheck_path;
833         char *ncheck_path;
834         FILE *fp;
835
836         asprintf(&ocheck_path, "%s/.%s.period", snapshots_path, cmd);
837         asprintf(&ncheck_path, "%s/.%s.period.new", snapshots_path, cmd);
838         fp = fopen(ncheck_path, "w");
839         if (fp) {
840                 fprintf(fp, "0x%08llx\n", (long long)savet);
841                 if (fclose(fp) == 0)
842                         rename(ncheck_path, ocheck_path);
843                 remove(ncheck_path);
844         } else {
845                 fprintf(stderr, "hammer: Unable to create period-file %s: %s\n",
846                         ncheck_path, strerror(errno));
847         }
848 }
849
850 /*
851  * Simply count the number of softlinks in the snapshots dir
852  */
853 static int
854 check_softlinks(int fd, int new_config, const char *snapshots_path)
855 {
856         struct dirent *den;
857         struct stat st;
858         DIR *dir;
859         char *fpath;
860         int res = 0;
861
862         /*
863          * Old-style softlink-based snapshots
864          */
865         if ((dir = opendir(snapshots_path)) != NULL) {
866                 while ((den = readdir(dir)) != NULL) {
867                         if (den->d_name[0] == '.')
868                                 continue;
869                         asprintf(&fpath, "%s/%s", snapshots_path, den->d_name);
870                         if (lstat(fpath, &st) == 0 && S_ISLNK(st.st_mode))
871                                 ++res;
872                         free(fpath);
873                 }
874                 closedir(dir);
875         }
876
877         /*
878          * New-style snapshots are stored as filesystem meta-data,
879          * count those too.
880          */
881         if (new_config) {
882                 struct hammer_ioc_snapshot snapshot;
883
884                 bzero(&snapshot, sizeof(snapshot));
885                 do {
886                         if (ioctl(fd, HAMMERIOC_GET_SNAPSHOT, &snapshot) < 0) {
887                                 err(2, "hammer cleanup: check_softlink "
888                                         "snapshot error");
889                                 /* not reached */
890                         }
891                         res += snapshot.count;
892                 } while (snapshot.head.error == 0 && snapshot.count);
893         }
894         return (res);
895 }
896
897 /*
898  * Clean up expired softlinks in the snapshots dir
899  */
900 static void
901 cleanup_softlinks(int fd, int new_config,
902                   const char *snapshots_path, int arg2, char *arg3)
903 {
904         struct dirent *den;
905         struct stat st;
906         DIR *dir;
907         char *fpath;
908         int anylink = 0;
909
910         if (arg3 != NULL && strstr(arg3, "any") != NULL)
911                 anylink = 1;
912
913         if ((dir = opendir(snapshots_path)) != NULL) {
914                 while ((den = readdir(dir)) != NULL) {
915                         if (den->d_name[0] == '.')
916                                 continue;
917                         asprintf(&fpath, "%s/%s", snapshots_path, den->d_name);
918                         if (lstat(fpath, &st) == 0 && S_ISLNK(st.st_mode) &&
919                             (anylink || strncmp(den->d_name, "snap-", 5) == 0)
920                         ) {
921                                 if (check_expired(den->d_name, arg2)) {
922                                         if (VerboseOpt) {
923                                                 printf("    expire %s\n",
924                                                         fpath);
925                                         }
926                                         remove(fpath);
927                                 }
928                         }
929                         free(fpath);
930                 }
931                 closedir(dir);
932         }
933
934         /*
935          * New-style snapshots are stored as filesystem meta-data,
936          * count those too.
937          */
938         if (new_config) {
939                 struct hammer_ioc_snapshot snapshot;
940                 struct hammer_ioc_snapshot dsnapshot;
941                 struct hammer_snapshot_data *snap;
942                 struct tm *tp;
943                 time_t t;
944                 time_t dt;
945                 char snapts[32];
946                 u_int32_t i;
947
948                 bzero(&snapshot, sizeof(snapshot));
949                 bzero(&dsnapshot, sizeof(dsnapshot));
950                 do {
951                         if (ioctl(fd, HAMMERIOC_GET_SNAPSHOT, &snapshot) < 0) {
952                                 err(2, "hammer cleanup: check_softlink "
953                                         "snapshot error");
954                                 /* not reached */
955                         }
956                         for (i = 0; i < snapshot.count; ++i) {
957                                 snap = &snapshot.snaps[i];
958                                 t = snap->ts / 1000000ULL;
959                                 dt = time(NULL) - t;
960                                 if ((int)dt > arg2 || snap->tid == 0) {
961                                         dsnapshot.snaps[dsnapshot.count++] =
962                                                 *snap;
963                                 }
964                                 if ((int)dt > arg2 && VerboseOpt) {
965                                         tp = localtime(&t);
966                                         strftime(snapts, sizeof(snapts),
967                                                  "%Y-%m-%d %H:%M:%S %Z", tp);
968                                         printf("    expire 0x%016jx %s %s\n",
969                                                (uintmax_t)snap->tid,
970                                                snapts,
971                                                snap->label);
972                                 }
973                                 if (dsnapshot.count == HAMMER_SNAPS_PER_IOCTL)
974                                         delete_snapshots(fd, &dsnapshot);
975                         }
976                 } while (snapshot.head.error == 0 && snapshot.count);
977
978                 if (dsnapshot.count)
979                         delete_snapshots(fd, &dsnapshot);
980         }
981 }
982
983 static void
984 delete_snapshots(int fd, struct hammer_ioc_snapshot *dsnapshot)
985 {
986         for (;;) {
987                 if (ioctl(fd, HAMMERIOC_DEL_SNAPSHOT, dsnapshot) < 0) {
988                         printf("    Ioctl to delete snapshots failed: %s\n",
989                                strerror(errno));
990                         break;
991                 }
992                 if (dsnapshot->head.error) {
993                         printf("    Ioctl to delete snapshots failed at "
994                                "snap=%016jx: %s\n",
995                                dsnapshot->snaps[dsnapshot->index].tid,
996                                strerror(dsnapshot->head.error));
997                         if (++dsnapshot->index < dsnapshot->count)
998                                 continue;
999                 }
1000                 break;
1001         }
1002         dsnapshot->index = 0;
1003         dsnapshot->count = 0;
1004         dsnapshot->head.error = 0;
1005 }
1006
1007 /*
1008  * Take a softlink path in the form snap-yyyymmdd-hhmm and the
1009  * expiration in seconds (arg2) and return non-zero if the softlink
1010  * has expired.
1011  */
1012 static int
1013 check_expired(const char *fpath, int arg2)
1014 {
1015         struct tm tm;
1016         time_t t;
1017         int year;
1018         int month;
1019         int day = 0;
1020         int hour = 0;
1021         int minute = 0;
1022         int r;
1023
1024         while (*fpath && *fpath != '-' && *fpath != '.')
1025                 ++fpath;
1026         if (*fpath)
1027                 ++fpath;
1028
1029         r = sscanf(fpath, "%4d%2d%2d-%2d%2d",
1030                    &year, &month, &day, &hour, &minute);
1031
1032         if (r >= 3) {
1033                 bzero(&tm, sizeof(tm));
1034                 tm.tm_isdst = -1;
1035                 tm.tm_min = minute;
1036                 tm.tm_hour = hour;
1037                 tm.tm_mday = day;
1038                 tm.tm_mon = month - 1;
1039                 tm.tm_year = year - 1900;
1040                 t = mktime(&tm);
1041                 if (t == (time_t)-1)
1042                         return(0);
1043                 t = time(NULL) - t;
1044                 if ((int)t > arg2)
1045                         return(1);
1046         }
1047         return(0);
1048 }
1049
1050 /*
1051  * Issue a snapshot.
1052  */
1053 static int
1054 create_snapshot(const char *path, const char *snapshots_path)
1055 {
1056         int r;
1057
1058         runcmd(&r, "hammer snapshot %s %s", path, snapshots_path);
1059         return(r);
1060 }
1061
1062 static int
1063 cleanup_prune(const char *path, const char *snapshots_path,
1064                   int arg1 __unused, int arg2, int snapshots_disabled)
1065 {
1066         const char *path_or_snapshots_path;
1067         struct softprune *base = NULL;
1068         struct hammer_ioc_prune dummy_template;
1069
1070         bzero(&dummy_template, sizeof(dummy_template));
1071         hammer_softprune_scandir(&base, &dummy_template, snapshots_path);
1072
1073         /*
1074          * If the snapshots_path (e.g. /var/hammer/...) has no snapshots
1075          * in it then prune will get confused and prune the filesystem
1076          * containing the snapshots_path instead of the requested
1077          * filesystem.  De-confuse prune.  We need a better way.
1078          */
1079         path_or_snapshots_path = base ? snapshots_path : path;
1080
1081         /*
1082          * If snapshots have been disabled run prune-everything instead
1083          * of prune.
1084          */
1085         if (snapshots_disabled && arg2) {
1086                 runcmd(NULL,
1087                        "hammer -c %s/.prune.cycle -t %d prune-everything %s",
1088                        snapshots_path, arg2, path);
1089         } else if (snapshots_disabled) {
1090                 runcmd(NULL, "hammer prune-everything %s", path);
1091         } else if (arg2) {
1092                 runcmd(NULL, "hammer -c %s/.prune.cycle -t %d prune %s",
1093                         snapshots_path, arg2, path_or_snapshots_path);
1094         } else {
1095                 runcmd(NULL, "hammer prune %s", path_or_snapshots_path);
1096         }
1097         return(0);
1098 }
1099
1100 static int
1101 cleanup_rebalance(const char *path, const char *snapshots_path,
1102                   int arg1 __unused, int arg2)
1103 {
1104         if (VerboseOpt == 0) {
1105                 printf(".");
1106                 fflush(stdout);
1107         }
1108
1109         runcmd(NULL,
1110                "hammer -c %s/.rebalance.cycle -t %d rebalance %s",
1111                snapshots_path, arg2, path);
1112         if (VerboseOpt == 0) {
1113                 printf(".");
1114                 fflush(stdout);
1115         }
1116         if (VerboseOpt == 0)
1117                 printf("\n");
1118         return(0);
1119 }
1120
1121 static int
1122 cleanup_reblock(const char *path, const char *snapshots_path,
1123                   int arg1 __unused, int arg2)
1124 {
1125         if (VerboseOpt == 0) {
1126                 printf(".");
1127                 fflush(stdout);
1128         }
1129
1130         /*
1131          * When reblocking the B-Tree always reblock everything in normal
1132          * mode.
1133          */
1134         runcmd(NULL,
1135                "hammer -c %s/.reblock-1.cycle -t %d reblock-btree %s",
1136                snapshots_path, arg2, path);
1137         if (VerboseOpt == 0) {
1138                 printf(".");
1139                 fflush(stdout);
1140         }
1141
1142         /*
1143          * When reblocking the inodes always reblock everything in normal
1144          * mode.
1145          */
1146         runcmd(NULL,
1147                "hammer -c %s/.reblock-2.cycle -t %d reblock-inodes %s",
1148                snapshots_path, arg2, path);
1149         if (VerboseOpt == 0) {
1150                 printf(".");
1151                 fflush(stdout);
1152         }
1153
1154         /*
1155          * When reblocking the directories always reblock everything in normal
1156          * mode.
1157          */
1158         runcmd(NULL,
1159                "hammer -c %s/.reblock-4.cycle -t %d reblock-dirs %s",
1160                snapshots_path, arg2, path);
1161         if (VerboseOpt == 0) {
1162                 printf(".");
1163                 fflush(stdout);
1164         }
1165
1166         /*
1167          * Do not reblock all the data in normal mode.
1168          */
1169         runcmd(NULL,
1170                "hammer -c %s/.reblock-3.cycle -t %d reblock-data %s 95",
1171                snapshots_path, arg2, path);
1172         if (VerboseOpt == 0)
1173                 printf("\n");
1174         return(0);
1175 }
1176
1177 static int
1178 cleanup_recopy(const char *path, const char *snapshots_path,
1179                   int arg1 __unused, int arg2)
1180 {
1181         if (VerboseOpt == 0) {
1182                 printf(".");
1183                 fflush(stdout);
1184         }
1185         runcmd(NULL,
1186                "hammer -c %s/.recopy-1.cycle -t %d reblock-btree %s",
1187                snapshots_path, arg2, path);
1188         if (VerboseOpt == 0) {
1189                 printf(".");
1190                 fflush(stdout);
1191         }
1192         runcmd(NULL,
1193                "hammer -c %s/.recopy-2.cycle -t %d reblock-inodes %s",
1194                snapshots_path, arg2, path);
1195         if (VerboseOpt == 0) {
1196                 printf(".");
1197                 fflush(stdout);
1198         }
1199         runcmd(NULL,
1200                "hammer -c %s/.recopy-4.cycle -t %d reblock-dirs %s",
1201                snapshots_path, arg2, path);
1202         if (VerboseOpt == 0) {
1203                 printf(".");
1204                 fflush(stdout);
1205         }
1206         runcmd(NULL,
1207                "hammer -c %s/.recopy-3.cycle -t %d reblock-data %s",
1208                snapshots_path, arg2, path);
1209         if (VerboseOpt == 0)
1210                 printf("\n");
1211         return(0);
1212 }
1213
1214 static int
1215 cleanup_dedup(const char *path, const char *snapshots_path __unused,
1216                   int arg1 __unused, int arg2)
1217 {
1218         if (VerboseOpt == 0) {
1219                 printf(".");
1220                 fflush(stdout);
1221         }
1222
1223         runcmd(NULL, "hammer -t %d dedup %s", arg2, path);
1224         if (VerboseOpt == 0) {
1225                 printf(".");
1226                 fflush(stdout);
1227         }
1228         if (VerboseOpt == 0)
1229                 printf("\n");
1230         return(0);
1231 }
1232
1233 static
1234 void
1235 runcmd(int *resp, const char *ctl, ...)
1236 {
1237         va_list va;
1238         char *cmd;
1239         char *arg;
1240         char **av;
1241         int n;
1242         int nmax;
1243         int res;
1244         pid_t pid;
1245
1246         /*
1247          * Generate the command
1248          */
1249         va_start(va, ctl);
1250         vasprintf(&cmd, ctl, va);
1251         va_end(va);
1252         if (VerboseOpt)
1253                 printf("    %s\n", cmd);
1254
1255         /*
1256          * Break us down into arguments.  We do not just use system() here
1257          * because it blocks SIGINT and friends.
1258          */
1259         n = 0;
1260         nmax = 16;
1261         av = malloc(sizeof(char *) * nmax);
1262
1263         for (arg = strtok(cmd, WS); arg; arg = strtok(NULL, WS)) {
1264                 if (n == nmax - 1) {
1265                         nmax += 16;
1266                         av = realloc(av, sizeof(char *) * nmax);
1267                 }
1268                 av[n++] = arg;
1269         }
1270         av[n++] = NULL;
1271
1272         /*
1273          * Run the command.
1274          */
1275         RunningIoctl = 1;
1276         if ((pid = fork()) == 0) {
1277                 if (VerboseOpt < 2) {
1278                         int fd = open("/dev/null", O_RDWR);
1279                         dup2(fd, 1);
1280                         close(fd);
1281                 }
1282                 execvp(av[0], av);
1283                 _exit(127);
1284         } else if (pid < 0) {
1285                 res = 127;
1286         } else {
1287                 int status;
1288
1289                 while (waitpid(pid, &status, 0) != pid)
1290                         ;
1291                 res = WEXITSTATUS(status);
1292         }
1293         RunningIoctl = 0;
1294         if (DidInterrupt)
1295                 _exit(1);
1296
1297         free(cmd);
1298         free(av);
1299         if (resp)
1300                 *resp = res;
1301 }