67 help="Clean out unused snapshots (those not linked to)") |
67 help="Clean out unused snapshots (those not linked to)") |
68 |
68 |
69 parser.add_option('--clean', action='store_true', |
69 parser.add_option('--clean', action='store_true', |
70 help="Clean out both intervals and snapshots") |
70 help="Clean out both intervals and snapshots") |
71 |
71 |
72 parser.add_option('-n', '--dry-run', action='store_true', |
72 parser.add_option('-n', '--noop', action='store_true', |
73 help="Don't actually clean anything") |
73 help="Don't actually clean anything") |
74 |
74 |
75 # |
75 # |
76 parser.add_option('-c', '--config', metavar='FILE/DIR', action='append', # multi |
76 parser.add_option('-c', '--config', metavar='FILE/DIR', action='append', # multi |
77 help="Load configuration file(s)") |
77 help="Load configuration file(s)") |
341 |
341 |
342 # parse source -> rsync.RSyncServer |
342 # parse source -> rsync.RSyncServer |
343 source_path = source |
343 source_path = source |
344 source = rsync.parse_source(source, lvm_opts=lvm_options) |
344 source = rsync.parse_source(source, lvm_opts=lvm_options) |
345 |
345 |
346 log.info("parse source: %r -> %s", source_path, source) |
346 log.debug("parse source: %r -> %s", source_path, source) |
347 |
347 |
348 # global defaults |
348 # global defaults |
349 _rsync_options = dict(options.rsync_options) |
349 _rsync_options = dict(options.rsync_options) |
350 |
350 |
351 if rsync_options : |
351 if rsync_options : |
420 temp_path = os.path.join(self.snapshots_dir, 'tmp') |
420 temp_path = os.path.join(self.snapshots_dir, 'tmp') |
421 |
421 |
422 if os.path.exists(temp_path) : |
422 if os.path.exists(temp_path) : |
423 raise Exception("Old temp snapshot dir remains, please clean up: {path}".format(path=temp_path)) |
423 raise Exception("Old temp snapshot dir remains, please clean up: {path}".format(path=temp_path)) |
424 |
424 |
425 log.info("Perform main snapshot: %s -> %s", self.source, snapshot_path) |
425 # link-dest from current? |
426 |
|
427 # build rsync options |
|
428 opts = dict(self.rsync_options) |
|
429 |
|
430 if os.path.exists(self.current_path) : |
426 if os.path.exists(self.current_path) : |
431 # real path to target |
427 # real path to target |
432 target = os.readlink(self.current_path) |
428 target = os.readlink(self.current_path) |
433 target_path = os.path.join(os.path.dirname(self.current_path), target) |
429 target_path = os.path.join(os.path.dirname(self.current_path), target) |
434 target_abs = os.path.abspath(target_path) |
430 |
435 |
431 log.debug("%s: link-dest: %s", self, target_path) |
436 log.info("Using current -> %s as base", target_path) |
|
437 |
432 |
438 # use as link-dest base; hardlinks unchanged files; target directory must be empty |
433 # use as link-dest base; hardlinks unchanged files; target directory must be empty |
439 # rsync links absolute paths.. |
434 link_dest = target_path |
440 opts['link-dest'] = target_abs |
435 |
441 |
436 else : |
|
437 link_dest = None |
|
438 |
|
439 # log |
|
440 log.info("%s: %s -> %s <- %s", self, self.source, snapshot_path, link_dest) |
|
441 |
|
442 # build rsync options |
|
443 opts = dict(self.rsync_options) |
|
444 |
|
445 # rsync links absolute paths.. |
|
446 opts['link-dest'] = os.path.abspath(link_dest) |
|
447 |
|
448 # to tempdir |
442 log.debug("rsync %s -> %s", self.source, temp_path) |
449 log.debug("rsync %s -> %s", self.source, temp_path) |
443 |
450 |
444 # run the rsync.RSyncServer; None as a placeholder will get replaced with the actual source |
451 # run the rsync.RSyncServer; None as a placeholder will get replaced with the actual source |
445 self.source.execute(invoke.optargs(**opts), srcdst=(None, temp_path)) |
452 self.source.execute(invoke.optargs(**opts), srcdst=(None, temp_path)) |
446 |
453 |
448 log.debug("rename %s -> %s", temp_path, snapshot_path) |
455 log.debug("rename %s -> %s", temp_path, snapshot_path) |
449 os.rename(temp_path, snapshot_path) |
456 os.rename(temp_path, snapshot_path) |
450 |
457 |
451 return snapshot_name |
458 return snapshot_name |
452 |
459 |
453 def update_interval (self, options, interval, now, snapshot_name) : |
460 def interval (self, options, interval, now, snapshot_name) : |
454 """ |
461 """ |
455 Update given <interval>/... links for this target, using the given new snapshot |
462 Update given <interval>/... links for this target, using the given new snapshot |
456 """ |
463 """ |
457 |
464 |
458 dir_path = os.path.join(self.path, interval.name) |
465 dir_path = os.path.join(self.path, interval.name) |
459 |
466 |
460 if not os.path.exists(dir_path) : |
467 if not os.path.exists(dir_path) : |
461 log.warn("Creating interval dir: %s", dir_path) |
468 log.warn("%s/%s: Creating interval dir: %s", self, interval, dir_path) |
462 os.mkdir(dir_path) |
469 os.mkdir(dir_path) |
463 |
470 |
464 |
471 |
465 # name |
472 # name |
466 if interval.format is None : |
473 if interval.format is None : |
483 |
490 |
484 # already there? |
491 # already there? |
485 if os.path.exists(path) : |
492 if os.path.exists(path) : |
486 target = os.readlink(path) |
493 target = os.readlink(path) |
487 |
494 |
488 log.info("%s: Keeping existing: %s -> %s", interval, name, target) |
495 log.debug("%s: Keeping existing: %s -> %s", interval, name, target) |
489 |
496 |
490 else : |
497 else : |
491 # update |
498 # update |
492 target = os.path.join('..', 'snapshots', snapshot_name) |
499 target = os.path.join('..', 'snapshots', snapshot_name) |
493 |
500 |
494 log.info("%s: Updating: %s -> %s", interval, name, target) |
501 log.info("%s/%s: %s -> %s", self, interval, name, target) |
495 log.debug("%s -> %s", path, target) |
502 log.debug("%s -> %s", path, target) |
496 |
503 |
497 os.symlink(target, path) |
504 os.symlink(target, path) |
498 |
505 |
499 |
506 |
504 |
511 |
505 # path |
512 # path |
506 dir_path = os.path.join(self.path, interval.name) |
513 dir_path = os.path.join(self.path, interval.name) |
507 |
514 |
508 if not os.path.exists(dir_path) : |
515 if not os.path.exists(dir_path) : |
509 log.warn("%s: Skipping, no interval dir: %s", interval, dir_path) |
516 log.warn("%s/%s: Skipping, no interval dir: %s", self, interval, dir_path) |
510 return |
517 return |
511 |
518 |
512 # configured |
519 # configured |
513 keep = interval.keep |
520 keep = interval.keep |
514 |
521 |
515 if not keep : |
522 if not keep : |
516 log.info("%s: Zero keep given, not cleaning up anything", interval) |
523 log.info("%s/%s: Zero keep given, not cleaning up anything", self, interval) |
517 return |
524 return |
518 |
525 |
519 # items to clean? |
526 # items to clean? |
520 items = os.listdir(dir_path) |
527 items = os.listdir(dir_path) |
521 |
528 |
522 # sort newest -> oldest |
529 # sort newest -> oldest |
523 items.sort(reverse=True) |
530 items.sort(reverse=True) |
524 |
531 |
525 log.info("%s: Have %d / %d items", interval, len(items), keep) |
532 log.debug("%s/%s: Have %d / %d items", self, interval, len(items), keep) |
526 log.debug("%s: items: %s", interval, ' '.join(items)) |
533 log.debug("%s: items: %s", interval, ' '.join(items)) |
527 |
534 |
528 if len(items) > keep : |
535 if len(items) > keep : |
529 # select oldest ones |
536 # select oldest ones |
530 clean = items[keep:] |
537 clean = items[keep:] |
531 |
538 |
532 log.info("%s: Cleaning out %d items", interval, len(clean)) |
539 log.debug("%s/%s: cleaning out: %s", self, interval, ' '.join(clean)) |
533 log.debug("%s: cleaning out: %s", interval, ' '.join(clean)) |
|
534 |
540 |
535 for item in clean : |
541 for item in clean : |
536 path = os.path.join(dir_path, item) |
542 path = os.path.join(dir_path, item) |
537 |
543 |
538 log.info("%s: Clean: %s", interval, path) |
544 log.info("%s/%s: %s", self, interval, path) |
539 |
545 |
540 if not options.dry_run : |
546 if not options.noop : |
541 log.debug("rmtree: %s", path) |
547 log.debug("rmtree: %s", path) |
542 os.unlink(path) |
548 os.unlink(path) |
543 else : |
549 else : |
544 log.debug("dryrun: %s", path) |
550 log.debug("dryrun: %s", path) |
545 |
551 |
580 ## compare |
586 ## compare |
581 used = snapshots & found |
587 used = snapshots & found |
582 unused = snapshots - found |
588 unused = snapshots - found |
583 broken = found - snapshots |
589 broken = found - snapshots |
584 |
590 |
585 log.info("Found used=%d, unused=%d, broken=%d snapshot symlinks", len(used), len(unused), len(broken)) |
591 log.debug("%s: found used=%d, unused=%d, broken=%d snapshot symlinks", self, len(used), len(unused), len(broken)) |
586 log.debug("used=%s, unused=%s", used, unused) |
592 log.debug("used=%s, unused=%s", used, unused) |
587 |
593 |
588 if broken : |
594 if broken : |
589 log.warn("Found broken symlinks to snapshots: %s", ' '.join(broken)) |
595 log.warn("%s: Found broken symlinks to snapshots: %s", self, ' '.join(broken)) |
590 |
596 |
591 if unused : |
597 if unused : |
592 log.info("Cleaning out %d unused snapshots:", len(unused)) |
598 log.debug("%s: Cleaning out %d unused snapshots:", self, len(unused)) |
593 |
599 |
594 for name in unused : |
600 for name in unused : |
595 path = os.path.join(snapshots_path, name) |
601 path = os.path.join(snapshots_path, name) |
596 |
602 |
597 log.info("Clean: %s", name) |
603 log.info("%s: %s", self, name) |
598 |
604 |
599 if not options.dry_run : |
605 if not options.noop : |
600 log.debug("rmtree: %s", path) |
606 log.debug("rmtree: %s", path) |
601 |
607 |
602 # nuke |
608 # nuke |
603 shutil.rmtree(path) |
609 shutil.rmtree(path) |
604 |
610 |
628 """ |
634 """ |
629 Run our intervals. |
635 Run our intervals. |
630 """ |
636 """ |
631 |
637 |
632 if not self.intervals : |
638 if not self.intervals : |
633 log.info("No intervals given; not running any") |
639 log.warn("No intervals given") |
634 |
640 |
635 else : |
641 else : |
636 # maintain intervals |
642 # maintain intervals |
637 log.info("Updating %d intervals...", len(self.intervals)) |
643 log.debug("Updating %d intervals...", len(self.intervals)) |
638 |
644 |
639 for interval in self.intervals : |
645 for interval in self.intervals : |
640 log.debug("%s", interval) |
646 log.debug("%s", interval) |
641 |
647 |
642 log.info("Updating interval: %s", interval) |
|
643 |
|
644 # update |
648 # update |
645 self.update_interval(options, interval, now, snapshot_name) |
649 self.interval(options, interval, now, snapshot_name) |
646 |
650 |
647 def run (self, options) : |
651 def run (self, options) : |
648 """ |
652 """ |
649 Execute |
653 Execute |
650 """ |
654 """ |
653 self.prepare(options) |
657 self.prepare(options) |
654 |
658 |
655 # clean intervals? |
659 # clean intervals? |
656 if options.clean_intervals: |
660 if options.clean_intervals: |
657 for interval in self.intervals : |
661 for interval in self.intervals : |
658 log.info("Cleaning interval: %s...", interval) |
662 log.debug("%s: cleaning interval: %s", self, interval) |
659 |
|
660 self.clean_interval(options, interval) |
663 self.clean_interval(options, interval) |
661 |
664 |
662 # clean snapshots? |
665 # clean snapshots? |
663 if options.clean_snapshots : |
666 if options.clean_snapshots : |
664 log.info("Cleaning snapshots...") |
667 log.debug("%s: cleaning snapshots...", self) |
665 |
668 |
666 self.clean_snapshots(options) |
669 self.clean_snapshots(options) |
667 |
670 |
668 # snapshot from source? |
671 # snapshot from source? |
669 if self.source : |
672 if self.source : |
670 # timestamp for run |
673 # timestamp for run |
671 now = datetime.datetime.now() |
674 now = datetime.datetime.now() |
672 |
675 |
673 log.info("Started snapshot run at: %s", now) |
676 log.debug("%s: started snapshot run at: %s", self, now) |
674 |
677 |
675 # snapshot + current |
678 # snapshot + current |
676 snapshot_name = self.run_snapshot(options, now) |
679 snapshot_name = self.run_snapshot(options, now) |
677 |
680 |
678 # intervals? |
681 # intervals? |
799 if options.run : |
802 if options.run : |
800 |
803 |
801 # given [run/...] definition.. |
804 # given [run/...] definition.. |
802 run_targets = list(_parse_run_targets(options, config, options.run)) |
805 run_targets = list(_parse_run_targets(options, config, options.run)) |
803 |
806 |
804 log.info("Running %d given [run/%s] targets", len(run_targets), options.run) |
807 log.debug("Running %d given [run/%s] targets: %s", len(run_targets), options.run, run_targets) |
805 log.debug("[run/%s]: %s", options.run, run_targets) |
|
806 |
808 |
807 # run |
809 # run |
808 if run_targets : |
810 if run_targets : |
809 log.info("Running %d given targets...", len(run_targets)) |
811 log.debug("Running %d given targets...", len(run_targets)) |
810 |
812 |
811 # run given ones |
813 # run given ones |
812 for name in run_targets : |
814 for name in run_targets : |
813 try : |
815 try : |
814 # get |
816 # get |
819 log.info("Defined targets: %s", ' '.join(options.targets)) |
821 log.info("Defined targets: %s", ' '.join(options.targets)) |
820 return 2 |
822 return 2 |
821 |
823 |
822 |
824 |
823 # run |
825 # run |
824 log.info("Target: %s", name) |
826 log.info("%s", name) |
825 |
827 |
826 target.run(options) |
828 target.run(options) |
827 |
829 |
828 else : |
830 else : |
829 # all targets |
831 # all targets |
830 log.info("Running all %d targets...", len(options.targets)) |
832 log.debug("Running all %d targets...", len(options.targets)) |
831 |
833 |
832 # targets |
834 # targets |
833 for name, target in options.targets.iteritems() : |
835 for name, target in options.targets.iteritems() : |
834 log.info("Target: %s", name) |
836 log.info("%s", name) |
835 |
837 |
836 # run |
838 # run |
837 target.run(options) |
839 target.run(options) |
838 |
840 |
839 # ok |
841 # ok |