Skip to content

Instantly share code, notes, and snippets.

@SalemHarrache
Created February 13, 2015 18:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SalemHarrache/c38a4425a47e939541e7 to your computer and use it in GitHub Desktop.
Save SalemHarrache/c38a4425a47e939541e7 to your computer and use it in GitHub Desktop.
Create snapshots with btrfs (original script https://github.com/lordsutch/btrfs-backup)
#!/usr/bin/env python
# Backup a btrfs volume to another, incrementally
# Requires Python >= 3.3, btrfs-progs >= 3.12 most likely.
# Copyright (c) 2014 Chris Lawrence <lawrencc@debian.org>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN²
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import subprocess
import sys
import os
import time
import argparse
def log(msg, *args, **kwargs):
"""Logs a message to stderr."""
if args:
msg %= args
sys.stderr.write("%s\n" % msg)
def datestr(timestamp=None):
if timestamp is None:
timestamp = time.localtime()
return time.strftime('%Y%m%d-%H%M%S', timestamp)
def new_snapshot(disk, snapshotdir, readonly=True):
snaploc = os.path.join(snapshotdir, datestr())
command = ['btrfs', 'subvolume', 'snapshot']
if readonly:
command += ['-r']
command += [disk, snaploc]
try:
subprocess.check_call(command)
return snaploc
except subprocess.CalledProcessError:
log("Error on command: %s", str(command))
return None
def find_old_backup(bak_dir_time_objs, recurse_val=0):
""" Find oldest time object in "bak_dir_time_objs" structure.
recurse_val = 0 -> start with top entry "year", default
"""
tmp = []
for timeobj in bak_dir_time_objs:
tmp.append(timeobj[recurse_val])
min_val = min(tmp) # find minimum time value
new_timeobj = []
for timeobj in bak_dir_time_objs:
if(timeobj[recurse_val] == min_val):
new_timeobj.append(timeobj)
if (len(new_timeobj) > 1):
# recursive call from year to minute
return find_old_backup(new_timeobj,recurse_val + 1)
else:
return new_timeobj[0]
def delete_old_backups(backuploc, max_num_backups):
""" Delete old backup directories in backup target folder based on their
date. Warning: This function will delete btrfs snapshots in target
folder based on the parameter max_num_backups!
"""
# recurse target backup folder until "max_num_backups" is reached
cur_num_backups = len(set(os.listdir(backuploc)) - set(('latest',)))
for i in range(cur_num_backups - max_num_backups):
# find all backup snapshots in directory and build time object list
bak_dir_time_objs = []
for directory in os.listdir(backuploc):
if directory != "latest":
dtime = time.strptime(directory, '%Y%m%d-%H%M%S')
bak_dir_time_objs.append(dtime)
# find oldest directory object and mark to remove
bak_dir_to_remove = datestr(find_old_backup(bak_dir_time_objs, 0))
bak_dir_to_remove_path = os.path.join(backuploc, bak_dir_to_remove)
log ("Removing old backup dir " + bak_dir_to_remove_path)
# delete snapshot of oldest backup snapshot
delete_snapshot(bak_dir_to_remove_path)
def delete_snapshot(snaploc):
subprocess.check_output(('btrfs', 'subvolume', 'delete', snaploc))
if __name__ == "__main__":
# Parse command line arguments
parser = argparse.ArgumentParser(description="incremental btrfs backup")
parser.add_argument('--num-snapshots', type=int, default=0,
help="only store given number of snapshots")
parser.add_argument('source', help="filesystem to backup")
parser.add_argument('snapshot', help="destination to send snapshots to")
args = parser.parse_args()
# This does not include a test if the source is a subvolume. It should be
# and this should be tested.
if os.path.exists(args.source):
sourceloc = args.source
else:
log("snapshot subvolume does not exist")
sys.exit(1)
# This does not include a test if the destination is a subvolume. It should
# be and this should be tested.
if os.path.exists(args.snapshot):
snapshotloc = os.path.abspath(args.snapshot)
else:
log("snapshot destination subvolume does not exist")
sys.exit(1)
latest = os.path.join(snapshotloc, 'latest')
if os.path.islink(latest):
os.unlink(latest)
elif os.path.exists(latest):
log('confusion: %s %s', latest, "should be a symlink")
sourcesnap = new_snapshot(sourceloc, snapshotloc)
if not sourcesnap:
log("snapshot failed")
sys.exit(1)
# Make latest point to this snapshot
log('new snapshot at %s', sourcesnap)
os.symlink(sourcesnap, latest)
# Need to sync
subprocess.check_call(['sync'])
NUM_BACKUPS = args.num_snapshots
# cleanup snapshots > NUM_BACKUPS in snapshot target
if (NUM_BACKUPS > 0):
delete_old_backups(snapshotloc, NUM_BACKUPS)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment