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