xref: /dragonfly/sbin/hammer/cmd_cleanup.c (revision 0ca59c34)
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 
63 #include "hammer.h"
64 
65 struct didpfs {
66 	struct didpfs *next;
67 	uuid_t		uuid;
68 };
69 
70 static void do_cleanup(const char *path);
71 static void config_init(const char *path, struct hammer_ioc_config *config);
72 static void migrate_config(FILE *fp, struct hammer_ioc_config *config);
73 static void migrate_snapshots(int fd, const char *snapshots_path);
74 static void migrate_one_snapshot(int fd, const char *fpath,
75 			struct hammer_ioc_snapshot *snapshot);
76 static int strtosecs(char *ptr);
77 static const char *dividing_slash(const char *path);
78 static int check_period(const char *snapshots_path, const char *cmd, int arg1,
79 			time_t *savep);
80 static void save_period(const char *snapshots_path, const char *cmd,
81 			time_t savet);
82 static int check_softlinks(int fd, int new_config, const char *snapshots_path);
83 static void cleanup_softlinks(int fd, int new_config,
84 			const char *snapshots_path, int arg2, char *arg3);
85 static void delete_snapshots(int fd, struct hammer_ioc_snapshot *dsnapshot);
86 static int check_expired(const char *fpath, int arg2);
87 
88 static int create_snapshot(const char *path, const char *snapshots_path);
89 static int cleanup_rebalance(const char *path, const char *snapshots_path,
90 			int arg1, int arg2);
91 static int cleanup_prune(const char *path, const char *snapshots_path,
92 			int arg1, int arg2, int snapshots_disabled);
93 static int cleanup_reblock(const char *path, const char *snapshots_path,
94 			int arg1, int arg2);
95 static int cleanup_recopy(const char *path, const char *snapshots_path,
96 			int arg1, int arg2);
97 static int cleanup_dedup(const char *path, const char *snapshots_path,
98 			int arg1, int arg2);
99 
100 static void runcmd(int *resp, const char *ctl, ...) __printflike(2, 3);
101 
102 /*
103  * WARNING: Do not make the SNAPSHOTS_BASE "/var/snapshots" because
104  * it will interfere with the older HAMMER VERS < 3 snapshots directory
105  * for the /var PFS.
106  */
107 #define SNAPSHOTS_BASE	"/var/hammer"	/* HAMMER VERS >= 3 */
108 #define WS	" \t\r\n"
109 
110 struct didpfs *FirstPFS;
111 
112 void
113 hammer_cmd_cleanup(char **av, int ac)
114 {
115 	char *fstype, *fs, *path;
116 	struct statfs *stfsbuf;
117 	int mntsize, i;
118 
119 	tzset();
120 	if (ac == 0) {
121 		mntsize = getmntinfo(&stfsbuf, MNT_NOWAIT);
122 		if (mntsize > 0) {
123 			for (i=0; i < mntsize; i++) {
124 				/*
125 				 * We will cleanup in the case fstype is hammer.
126 				 * If we have null-mounted PFS, we check the
127 				 * mount source. If it looks like a PFS, we
128 				 * proceed to cleanup also.
129 				 */
130 				fstype = stfsbuf[i].f_fstypename;
131 				fs = stfsbuf[i].f_mntfromname;
132 				if ((strcmp(fstype, "hammer") == 0) ||
133 				    ((strcmp(fstype, "null") == 0) &&
134 				     (strstr(fs, "/@@0x") != NULL ||
135 				      strstr(fs, "/@@-1") != NULL))) {
136 					path = stfsbuf[i].f_mntonname;
137 					do_cleanup(path);
138 				}
139 			}
140 		}
141 
142 	} else {
143 		while (ac) {
144 			do_cleanup(*av);
145 			--ac;
146 			++av;
147 		}
148 	}
149 }
150 
151 static
152 void
153 do_cleanup(const char *path)
154 {
155 	struct hammer_ioc_pseudofs_rw pfs;
156 	struct hammer_ioc_config config;
157 	struct hammer_ioc_version version;
158 	union hammer_ioc_mrecord_any mrec_tmp;
159 	char *snapshots_path = NULL;
160 	char *config_path;
161 	struct stat st;
162 	char *cmd;
163 	char *ptr;
164 	int arg1;
165 	int arg2;
166 	char *arg3;
167 	time_t savet;
168 	char buf[256];
169 	char *cbase;
170 	char *cptr;
171 	FILE *fp = NULL;
172 	struct didpfs *didpfs;
173 	int snapshots_disabled = 0;
174 	int prune_warning = 0;
175 	int new_config = 0;
176 	int snapshots_from_pfs = 0;
177 	int fd;
178 	int r;
179 	int found_rebal = 0;
180 
181 	bzero(&pfs, sizeof(pfs));
182 	bzero(&mrec_tmp, sizeof(mrec_tmp));
183 	pfs.ondisk = &mrec_tmp.pfs.pfsd;
184 	pfs.bytes = sizeof(mrec_tmp.pfs.pfsd);
185 	pfs.pfs_id = -1;
186 
187 	printf("cleanup %-20s -", path);
188 	fd = open(path, O_RDONLY);
189 	if (fd < 0) {
190 		printf(" unable to access directory: %s\n", strerror(errno));
191 		return;
192 	}
193 	if (ioctl(fd, HAMMERIOC_GET_PSEUDOFS, &pfs) < 0) {
194 		printf(" not a HAMMER filesystem: %s\n", strerror(errno));
195 		close(fd);
196 		return;
197 	}
198 	if (pfs.version != HAMMER_IOC_PSEUDOFS_VERSION) {
199 		printf(" unrecognized HAMMER version\n");
200 		close(fd);
201 		return;
202 	}
203 	bzero(&version, sizeof(version));
204 	if (ioctl(fd, HAMMERIOC_GET_VERSION, &version) < 0) {
205 		printf(" HAMMER filesystem but couldn't retrieve version!\n");
206 		close(fd);
207 		return;
208 	}
209 
210 	bzero(&config, sizeof(config));
211 	if (version.cur_version >= 3) {
212 		if (ioctl(fd, HAMMERIOC_GET_CONFIG, &config) == 0 &&
213 		    config.head.error == 0) {
214 			new_config = 1;
215 		}
216 	}
217 
218 	/*
219 	 * Make sure we have not already handled this PFS.  Several nullfs
220 	 * mounts might alias the same PFS.
221 	 */
222 	for (didpfs = FirstPFS; didpfs; didpfs = didpfs->next) {
223 		if (bcmp(&didpfs->uuid, &mrec_tmp.pfs.pfsd.unique_uuid, sizeof(uuid_t)) == 0) {
224 			printf(" PFS #%d already handled\n", pfs.pfs_id);
225 			close(fd);
226 			return;
227 		}
228 	}
229 	didpfs = malloc(sizeof(*didpfs));
230 	didpfs->next = FirstPFS;
231 	FirstPFS = didpfs;
232 	didpfs->uuid = mrec_tmp.pfs.pfsd.unique_uuid;
233 
234 	/*
235 	 * Calculate the old snapshots directory for HAMMER VERSION < 3
236 	 *
237 	 * If the directory is explicitly specified in the PFS config
238 	 * we flag it and will not migrate it later.
239 	 */
240 	if (mrec_tmp.pfs.pfsd.snapshots[0] == '/') {
241 		asprintf(&snapshots_path, "%s", mrec_tmp.pfs.pfsd.snapshots);
242 		snapshots_from_pfs = 1;
243 	} else if (mrec_tmp.pfs.pfsd.snapshots[0]) {
244 		printf(" WARNING: pfs-slave's snapshots dir is not absolute\n");
245 		close(fd);
246 		return;
247 	} else if (mrec_tmp.pfs.pfsd.mirror_flags & HAMMER_PFSD_SLAVE) {
248 		if (version.cur_version < 3) {
249 			printf(" WARNING: must configure snapshot dir for PFS slave\n");
250 			printf("\tWe suggest <fs>/var/slaves/<name> where "
251 			       "<fs> is the base HAMMER fs\n");
252 			printf("\tcontaining the slave\n");
253 			close(fd);
254 			return;
255 		}
256 	} else {
257 		asprintf(&snapshots_path,
258 			 "%s%ssnapshots", path, dividing_slash(path));
259 	}
260 
261 	/*
262 	 * Check for old-style config file
263 	 */
264 	if (snapshots_path) {
265 		asprintf(&config_path, "%s/config", snapshots_path);
266 		fp = fopen(config_path, "r");
267 	}
268 
269 	/*
270 	 * Handle upgrades to hammer version 3, move the config
271 	 * file into meta-data.
272 	 *
273 	 * For the old config read the file into the config structure,
274 	 * we will parse it out of the config structure regardless.
275 	 */
276 	if (version.cur_version >= 3) {
277 		if (fp) {
278 			printf("(migrating) ");
279 			fflush(stdout);
280 			migrate_config(fp, &config);
281 			migrate_snapshots(fd, snapshots_path);
282 			fclose(fp);
283 			if (ioctl(fd, HAMMERIOC_SET_CONFIG, &config) < 0) {
284 				printf(" cannot init meta-data config!\n");
285 				close(fd);
286 				return;
287 			}
288 			remove(config_path);
289 		} else if (new_config == 0) {
290 			config_init(path, &config);
291 			if (ioctl(fd, HAMMERIOC_SET_CONFIG, &config) < 0) {
292 				printf(" cannot init meta-data config!\n");
293 				close(fd);
294 				return;
295 			}
296 		}
297 		new_config = 1;
298 	} else {
299 		/*
300 		 * Create missing snapshots directory for HAMMER VERSION < 3
301 		 */
302 		if (stat(snapshots_path, &st) < 0) {
303 			if (mkdir(snapshots_path, 0755) != 0) {
304 				free(snapshots_path);
305 				printf(" unable to create snapshot dir \"%s\": %s\n",
306 					snapshots_path, strerror(errno));
307 				close(fd);
308 				return;
309 			}
310 		}
311 
312 		/*
313 		 *  Create missing config file for HAMMER VERSION < 3
314 		 */
315 		if (fp == NULL) {
316 			config_init(path, &config);
317 			fp = fopen(config_path, "w");
318 			if (fp) {
319 				fwrite(config.config.text, 1,
320 					strlen(config.config.text), fp);
321 				fclose(fp);
322 			}
323 		} else {
324 			migrate_config(fp, &config);
325 			fclose(fp);
326 		}
327 	}
328 
329 	/*
330 	 * If snapshots_from_pfs is not set we calculate the new snapshots
331 	 * directory default (in /var) for HAMMER VERSION >= 3 and migrate
332 	 * the old snapshots directory over.
333 	 *
334 	 * People who have set an explicit snapshots directory will have
335 	 * to migrate the data manually into /var/hammer, or not bother at
336 	 * all.  People running slaves may wish to migrate it and then
337 	 * clear the snapshots specification in the PFS config for the
338 	 * slave.
339 	 */
340 	if (new_config && snapshots_from_pfs == 0) {
341 		char *npath;
342 
343 		assert(path[0] == '/');
344 		if (strcmp(path, "/") == 0)
345 			asprintf(&npath, "%s/root", SNAPSHOTS_BASE);
346 		else
347 			asprintf(&npath, "%s/%s", SNAPSHOTS_BASE, path + 1);
348 		if (snapshots_path) {
349 			if (stat(npath, &st) < 0 && errno == ENOENT) {
350 				if (stat(snapshots_path, &st) < 0 && errno == ENOENT) {
351 					printf(" HAMMER UPGRADE: Creating snapshots\n"
352 					       "\tCreating snapshots in %s\n",
353 					       npath);
354 					runcmd(&r, "mkdir -p %s", npath);
355 				} else {
356 					printf(" HAMMER UPGRADE: Moving snapshots\n"
357 					       "\tMoving snapshots from %s to %s\n",
358 					       snapshots_path, npath);
359 					runcmd(&r, "mkdir -p %s", npath);
360 					runcmd(&r, "cpdup %s %s", snapshots_path, npath);
361 					if (r != 0) {
362 				    printf("Unable to move snapshots directory!\n");
363 				    printf("Please fix this critical error.\n");
364 				    printf("Aborting cleanup of %s\n", path);
365 						close(fd);
366 						return;
367 					}
368 					runcmd(&r, "rm -rf %s", snapshots_path);
369 				}
370 			}
371 			free(snapshots_path);
372 		} else if (stat(npath, &st) < 0 && errno == ENOENT) {
373 			runcmd(&r, "mkdir -p %s", npath);
374 		}
375 		snapshots_path = npath;
376 	}
377 
378 	/*
379 	 * Lock the PFS.  fd is the base directory of the mounted PFS.
380 	 */
381 	if (flock(fd, LOCK_EX|LOCK_NB) == -1) {
382 		if (errno == EWOULDBLOCK)
383 			printf(" PFS #%d locked by other process\n", pfs.pfs_id);
384 		else
385 			printf(" can not lock %s: %s\n", config_path, strerror(errno));
386 		close(fd);
387 		return;
388 	}
389 
390 	printf(" handle PFS #%d using %s\n", pfs.pfs_id, snapshots_path);
391 
392 	struct pidfh	*pfh = NULL;
393 	static char	pidfile[PIDFILE_BUFSIZE];
394 
395 	snprintf (pidfile, PIDFILE_BUFSIZE, "%s/hammer.cleanup.%d",
396 		pidfile_loc, getpid());
397 	pfh = pidfile_open(pidfile, 0644, NULL);
398 	if (pfh == NULL) {
399 		warn ("Unable to open or create %s", pidfile);
400 	}
401 	pidfile_write(pfh);
402 
403 	/*
404 	 * Process the config file
405 	 */
406 	cbase = config.config.text;
407 
408 	while ((cptr = strchr(cbase, '\n')) != NULL) {
409 		bcopy(cbase, buf, cptr - cbase);
410 		buf[cptr - cbase] = 0;
411 		cbase = cptr + 1;
412 
413 		cmd = strtok(buf, WS);
414 		if (cmd == NULL || cmd[0] == '#')
415 			continue;
416 
417 		arg1 = 0;
418 		arg2 = 0;
419 		arg3 = NULL;
420 		if ((ptr = strtok(NULL, WS)) != NULL) {
421 			arg1 = strtosecs(ptr);
422 			if ((ptr = strtok(NULL, WS)) != NULL) {
423 				arg2 = strtosecs(ptr);
424 				arg3 = strtok(NULL, WS);
425 			}
426 		}
427 
428 		printf("%20s - ", cmd);
429 		fflush(stdout);
430 
431 		r = 1;
432 		if (strcmp(cmd, "snapshots") == 0) {
433 			if (arg1 == 0) {
434 				if (arg2 &&
435 				    check_softlinks(fd, new_config,
436 						    snapshots_path)) {
437 					printf("only removing old snapshots\n");
438 					prune_warning = 1;
439 					cleanup_softlinks(fd, new_config,
440 							  snapshots_path,
441 							  arg2, arg3);
442 				} else {
443 					printf("disabled\n");
444 					snapshots_disabled = 1;
445 				}
446 			} else
447 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
448 				printf("run\n");
449 				cleanup_softlinks(fd, new_config,
450 						  snapshots_path,
451 						  arg2, arg3);
452 				r = create_snapshot(path, snapshots_path);
453 			} else {
454 				printf("skip\n");
455 			}
456 		} else if (arg1 == 0) {
457 			/*
458 			 * The commands following this check can't handle
459 			 * a period of 0, so call the feature disabled and
460 			 * ignore the directive.
461 			 */
462 			printf("disabled\n");
463 		} else if (strcmp(cmd, "prune") == 0) {
464 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
465 				if (prune_warning) {
466 					printf("run - WARNING snapshot "
467 					       "softlinks present "
468 					       "but snapshots disabled\n");
469 				} else {
470 					printf("run\n");
471 				}
472 				r = cleanup_prune(path, snapshots_path,
473 					      arg1, arg2, snapshots_disabled);
474 			} else {
475 				printf("skip\n");
476 			}
477 		} else if (strcmp(cmd, "rebalance") == 0) {
478 			found_rebal = 1;
479 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
480 				printf("run");
481 				fflush(stdout);
482 				if (VerboseOpt)
483 					printf("\n");
484 				r = cleanup_rebalance(path, snapshots_path,
485 						arg1, arg2);
486 			} else {
487 				printf("skip\n");
488 			}
489 		} else if (strcmp(cmd, "reblock") == 0) {
490 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
491 				printf("run");
492 				fflush(stdout);
493 				if (VerboseOpt)
494 					printf("\n");
495 				r = cleanup_reblock(path, snapshots_path,
496 						arg1, arg2);
497 			} else {
498 				printf("skip\n");
499 			}
500 		} else if (strcmp(cmd, "recopy") == 0) {
501 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
502 				printf("run");
503 				fflush(stdout);
504 				if (VerboseOpt)
505 					printf("\n");
506 				r = cleanup_recopy(path, snapshots_path,
507 					       arg1, arg2);
508 			} else {
509 				printf("skip\n");
510 			}
511 		} else if (strcmp(cmd, "dedup") == 0) {
512 			if (check_period(snapshots_path, cmd, arg1, &savet)) {
513 				printf("run");
514 				fflush(stdout);
515 				if (VerboseOpt)
516 					printf("\n");
517 				r = cleanup_dedup(path, snapshots_path,
518 						arg1, arg2);
519 			} else {
520 				printf("skip\n");
521 			}
522 		} else {
523 			printf("unknown directive\n");
524 			r = 1;
525 		}
526 		if (r == 0)
527 			save_period(snapshots_path, cmd, savet);
528 	}
529 
530 	/*
531 	 * Add new rebalance feature if the config doesn't have it.
532 	 * (old style config only).
533 	 */
534 	if (new_config == 0 && found_rebal == 0) {
535 		if ((fp = fopen(config_path, "r+")) != NULL) {
536 			fseek(fp, 0L, 2);
537 			fprintf(fp, "rebalance 1d 5m\n");
538 			fclose(fp);
539 		}
540 	}
541 
542 	/*
543 	 * Cleanup, and delay a little
544 	 */
545 	close(fd);
546 	usleep(1000);
547 	pidfile_close(pfh);
548 	pidfile_remove(pfh);
549 }
550 
551 /*
552  * Initialize new config data (new or old style)
553  */
554 static void
555 config_init(const char *path, struct hammer_ioc_config *config)
556 {
557 	const char *snapshots;
558 
559 	if (strcmp(path, "/tmp") == 0 ||
560 	    strcmp(path, "/var/tmp") == 0 ||
561 	    strcmp(path, "/usr/obj") == 0) {
562 		snapshots = "snapshots 0d 0d\n";
563 	} else {
564 		snapshots = "snapshots 1d 60d\n";
565 	}
566 	bzero(config->config.text, sizeof(config->config.text));
567 	snprintf(config->config.text, sizeof(config->config.text) - 1, "%s%s",
568 		snapshots,
569 		"prune     1d 5m\n"
570 		"rebalance 1d 5m\n"
571 		"#dedup	   1d 5m\n"
572 		"reblock   1d 5m\n"
573 		"recopy    30d 10m\n");
574 }
575 
576 /*
577  * Migrate configuration data from the old snapshots/config
578  * file to the new meta-data format.
579  */
580 static void
581 migrate_config(FILE *fp, struct hammer_ioc_config *config)
582 {
583 	int n;
584 
585 	n = fread(config->config.text, 1, sizeof(config->config.text) - 1, fp);
586 	if (n >= 0)
587 		bzero(config->config.text + n, sizeof(config->config.text) - n);
588 }
589 
590 /*
591  * Migrate snapshot softlinks in the snapshots directory to the
592  * new meta-data format.  The softlinks are left intact, but
593  * this way the pruning code won't lose track of them if you
594  * happen to blow away the snapshots directory.
595  */
596 static void
597 migrate_snapshots(int fd, const char *snapshots_path)
598 {
599 	struct hammer_ioc_snapshot snapshot;
600 	struct dirent *den;
601 	struct stat st;
602 	DIR *dir;
603 	char *fpath;
604 
605 	bzero(&snapshot, sizeof(snapshot));
606 
607 	if ((dir = opendir(snapshots_path)) != NULL) {
608 		while ((den = readdir(dir)) != NULL) {
609 			if (den->d_name[0] == '.')
610 				continue;
611 			asprintf(&fpath, "%s/%s", snapshots_path, den->d_name);
612 			if (lstat(fpath, &st) == 0 && S_ISLNK(st.st_mode)) {
613 				migrate_one_snapshot(fd, fpath, &snapshot);
614 			}
615 			free(fpath);
616 		}
617 		closedir(dir);
618 	}
619 	migrate_one_snapshot(fd, NULL, &snapshot);
620 
621 }
622 
623 /*
624  * Migrate a single snapshot.  If fpath is NULL the ioctl is flushed,
625  * otherwise it is flushed when it fills up.
626  */
627 static void
628 migrate_one_snapshot(int fd, const char *fpath,
629 		     struct hammer_ioc_snapshot *snapshot)
630 {
631 	if (fpath) {
632 		struct hammer_snapshot_data *snap;
633 		struct tm tm;
634 		time_t t;
635 		int year;
636 		int month;
637 		int day = 0;
638 		int hour = 0;
639 		int minute = 0;
640 		int r;
641 		char linkbuf[1024];
642 		const char *ptr;
643 		hammer_tid_t tid;
644 
645 		t = (time_t)-1;
646 		tid = (hammer_tid_t)(int64_t)-1;
647 
648 		/* fpath may contain directory components */
649 		if ((ptr = strrchr(fpath, '/')) != NULL)
650 			++ptr;
651 		else
652 			ptr = fpath;
653 		while (*ptr && *ptr != '-' && *ptr != '.')
654 			++ptr;
655 		if (*ptr)
656 			++ptr;
657 		r = sscanf(ptr, "%4d%2d%2d-%2d%2d",
658 			   &year, &month, &day, &hour, &minute);
659 
660 		if (r >= 3) {
661 			bzero(&tm, sizeof(tm));
662 			tm.tm_isdst = -1;
663 			tm.tm_min = minute;
664 			tm.tm_hour = hour;
665 			tm.tm_mday = day;
666 			tm.tm_mon = month - 1;
667 			tm.tm_year = year - 1900;
668 			t = mktime(&tm);
669 		}
670 		bzero(linkbuf, sizeof(linkbuf));
671 		if (readlink(fpath, linkbuf, sizeof(linkbuf) - 1) > 0 &&
672 		    (ptr = strrchr(linkbuf, '@')) != NULL &&
673 		    ptr > linkbuf && ptr[-1] == '@') {
674 			tid = strtoull(ptr + 1, NULL, 16);
675 		}
676 		if (t != (time_t)-1 && tid != (hammer_tid_t)(int64_t)-1) {
677 			snap = &snapshot->snaps[snapshot->count];
678 			bzero(snap, sizeof(*snap));
679 			snap->tid = tid;
680 			snap->ts = (u_int64_t)t * 1000000ULL;
681 			snprintf(snap->label, sizeof(snap->label),
682 				 "migrated");
683 			++snapshot->count;
684 		} else {
685 			printf("    non-canonical snapshot softlink: %s->%s\n",
686 			       fpath, linkbuf);
687 		}
688 	}
689 
690 	if ((fpath == NULL && snapshot->count) ||
691 	    snapshot->count == HAMMER_SNAPS_PER_IOCTL) {
692 		printf(" (%d snapshots)", snapshot->count);
693 again:
694 		if (ioctl(fd, HAMMERIOC_ADD_SNAPSHOT, snapshot) < 0) {
695 			printf("    Ioctl to migrate snapshots failed: %s\n",
696 			       strerror(errno));
697 		} else if (snapshot->head.error == EALREADY) {
698 			++snapshot->index;
699 			goto again;
700 		} else if (snapshot->head.error) {
701 			printf("    Ioctl to migrate snapshots failed: %s\n",
702 			       strerror(snapshot->head.error));
703 		}
704 		printf("index %d\n", snapshot->index);
705 		snapshot->index = 0;
706 		snapshot->count = 0;
707 		snapshot->head.error = 0;
708 	}
709 }
710 
711 static
712 int
713 strtosecs(char *ptr)
714 {
715 	int val;
716 
717 	val = strtol(ptr, &ptr, 0);
718 	switch(*ptr) {
719 	case 'd':
720 		val *= 24;
721 		/* fall through */
722 	case 'h':
723 		val *= 60;
724 		/* fall through */
725 	case 'm':
726 		val *= 60;
727 		/* fall through */
728 	case 's':
729 		break;
730 	default:
731 		errx(1, "illegal suffix converting %s\n", ptr);
732 		break;
733 	}
734 	return(val);
735 }
736 
737 static const char *
738 dividing_slash(const char *path)
739 {
740 	int len = strlen(path);
741 	if (len && path[len-1] == '/')
742 		return("");
743 	else
744 		return("/");
745 }
746 
747 /*
748  * Check whether the desired period has elapsed since the last successful
749  * run.  The run may take a while and cross a boundary so we remember the
750  * current time_t so we can save it later on.
751  *
752  * Periods in minutes, hours, or days are assumed to have been crossed
753  * if the local time crosses a minute, hour, or day boundary regardless
754  * of how close the last operation actually was.
755  *
756  * If ForceOpt is set always return true.
757  */
758 static int
759 check_period(const char *snapshots_path, const char *cmd, int arg1,
760 	time_t *savep)
761 {
762 	char *check_path;
763 	struct tm tp1;
764 	struct tm tp2;
765 	FILE *fp;
766 	time_t baset, lastt;
767 	char buf[256];
768 
769 	time(savep);
770 	localtime_r(savep, &tp1);
771 
772 	/*
773 	 * Force run if -F
774 	 */
775 	if (ForceOpt)
776 		return(1);
777 
778 	/*
779 	 * Retrieve the start time of the last successful operation.
780 	 */
781 	asprintf(&check_path, "%s/.%s.period", snapshots_path, cmd);
782 	fp = fopen(check_path, "r");
783 	free(check_path);
784 	if (fp == NULL)
785 		return(1);
786 	if (fgets(buf, sizeof(buf), fp) == NULL) {
787 		fclose(fp);
788 		return(1);
789 	}
790 	fclose(fp);
791 
792 	lastt = strtol(buf, NULL, 0);
793 	localtime_r(&lastt, &tp2);
794 
795 	/*
796 	 * Normalize the times.  e.g. if asked to do something on a 1-day
797 	 * interval the operation will be performed as soon as the day
798 	 * turns over relative to the previous operation, even if the previous
799 	 * operation ran a few seconds ago just before midnight.
800 	 */
801 	if (arg1 % 60 == 0) {
802 		tp1.tm_sec = 0;
803 		tp2.tm_sec = 0;
804 	}
805 	if (arg1 % (60 * 60) == 0) {
806 		tp1.tm_min = 0;
807 		tp2.tm_min = 0;
808 	}
809 	if (arg1 % (24 * 60 * 60) == 0) {
810 		tp1.tm_hour = 0;
811 		tp2.tm_hour = 0;
812 	}
813 
814 	baset = mktime(&tp1);
815 	lastt = mktime(&tp2);
816 
817 #if 0
818 	printf("%lld vs %lld\n", (long long)(baset - lastt), (long long)arg1);
819 #endif
820 
821 	if ((int)(baset - lastt) >= arg1)
822 		return(1);
823 	return(0);
824 }
825 
826 /*
827  * Store the start time of the last successful operation.
828  */
829 static void
830 save_period(const char *snapshots_path, const char *cmd,
831 			time_t savet)
832 {
833 	char *ocheck_path;
834 	char *ncheck_path;
835 	FILE *fp;
836 
837 	asprintf(&ocheck_path, "%s/.%s.period", snapshots_path, cmd);
838 	asprintf(&ncheck_path, "%s/.%s.period.new", snapshots_path, cmd);
839 	fp = fopen(ncheck_path, "w");
840 	if (fp) {
841 		fprintf(fp, "0x%08llx\n", (long long)savet);
842 		if (fclose(fp) == 0)
843 			rename(ncheck_path, ocheck_path);
844 		remove(ncheck_path);
845 	} else {
846 		fprintf(stderr, "hammer: Unable to create period-file %s: %s\n",
847 			ncheck_path, strerror(errno));
848 	}
849 }
850 
851 /*
852  * Simply count the number of softlinks in the snapshots dir
853  */
854 static int
855 check_softlinks(int fd, int new_config, const char *snapshots_path)
856 {
857 	struct dirent *den;
858 	struct stat st;
859 	DIR *dir;
860 	char *fpath;
861 	int res = 0;
862 
863 	/*
864 	 * Old-style softlink-based snapshots
865 	 */
866 	if ((dir = opendir(snapshots_path)) != NULL) {
867 		while ((den = readdir(dir)) != NULL) {
868 			if (den->d_name[0] == '.')
869 				continue;
870 			asprintf(&fpath, "%s/%s", snapshots_path, den->d_name);
871 			if (lstat(fpath, &st) == 0 && S_ISLNK(st.st_mode))
872 				++res;
873 			free(fpath);
874 		}
875 		closedir(dir);
876 	}
877 
878 	/*
879 	 * New-style snapshots are stored as filesystem meta-data,
880 	 * count those too.
881 	 */
882 	if (new_config) {
883 		struct hammer_ioc_snapshot snapshot;
884 
885 		bzero(&snapshot, sizeof(snapshot));
886 		do {
887 			if (ioctl(fd, HAMMERIOC_GET_SNAPSHOT, &snapshot) < 0) {
888 				err(2, "hammer cleanup: check_softlink "
889 					"snapshot error");
890 				/* not reached */
891 			}
892 			res += snapshot.count;
893 		} while (snapshot.head.error == 0 && snapshot.count);
894 	}
895 	return (res);
896 }
897 
898 /*
899  * Clean up expired softlinks in the snapshots dir
900  */
901 static void
902 cleanup_softlinks(int fd, int new_config,
903 		  const char *snapshots_path, int arg2, char *arg3)
904 {
905 	struct dirent *den;
906 	struct stat st;
907 	DIR *dir;
908 	char *fpath;
909 	int anylink = 0;
910 
911 	if (arg3 != NULL && strstr(arg3, "any") != NULL)
912 		anylink = 1;
913 
914 	if ((dir = opendir(snapshots_path)) != NULL) {
915 		while ((den = readdir(dir)) != NULL) {
916 			if (den->d_name[0] == '.')
917 				continue;
918 			asprintf(&fpath, "%s/%s", snapshots_path, den->d_name);
919 			if (lstat(fpath, &st) == 0 && S_ISLNK(st.st_mode) &&
920 			    (anylink || strncmp(den->d_name, "snap-", 5) == 0)) {
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 }
1302