summaryrefslogtreecommitdiff
path: root/rybackup.py
blob: 4e734beab180d1b205aca0681a07b7f2019f3895 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
#!/usr/bin/env python

# rybackup -- rsync based backup script with rotating backup-snapshots,
#             based on the idea and implementation found on mikerubel.org
# Copyright (C) 2010 Adrian C. <anrxc sysphere.org>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# crontab
#   0 */4 * * * ~/.backup/rybackup.py hourly  # every 4 hours
#   0 18 * * *  ~/.backup/rybackup.py daily   # once a day at 18h
#   58 3 * * 0  ~/.backup/rybackup.py weekly  # every sunday at 3:58
#   53 3 1 * *  ~/.backup/rybackup.py monthly # first day of every month
#
# crontab eCryptfs (i.e. less frequent)
#   @daily  ID=bak-daily  ~/.backup/rybackup.py hourly # job runs first chance,
#   @weekly ID=bak-weekly ~/.backup/rybackup.py daily  # and retries on EAGAIN


from sys import argv
from shutil import rmtree
from subprocess import call
from os import getuid
from os import path, rename
from os import listdir, makedirs


# Configuration
#
# Local backup mount point and sources to backup
bak = {
    "dst" : "/mnt/nfs/backup",
    "src" : [ "/etc/", "/root/", "/home/user/", "/mnt/storage/Documents/" ],
    "efs" : False,             # eCryptfs support
}

# Remote backup server, or removable storage
rmt = {
    "fst" : "-t nfs",          # file-system type
    "opt" : "-o async,vers=3", # mount options, if any
    "dev" : "192.168.0.10",    # device node or NFS host
    "dst" : "/mnt/backup/clientname" # remote NFS export
}

# Number of backup snapshots to keep
count = {
    "hourly"  : 4,
    "daily"   : 3,
    "weekly"  : 2,
    "monthly" : 2,
}

# Files with a role in the backup procedure
files = {
    "mounts"  : "/proc/mounts",
    "exclude" : "/root/.backup/exclude.lst",
}

# System commands used by this program
bin = {
    "cp"     : "/bin/cp",
    "mount"  : "/bin/mount",
    "umount" : "/bin/umount",
    "grep"   : "/bin/grep",
    "touch"  : "/bin/touch",
    "rsync"  : "/usr/bin/rsync",
}



# Functions
#
# Snapshot rotation
def rotate(arg):
    rotation = {  # Interval relations
        "hourly"  : ["hourly", 1, 2],
        "daily"   : ["hourly", count['hourly'], 1],
        "weekly"  : ["daily",  count['daily'],  1],
        "monthly" : ["weekly", count['weekly'], 1],
    }

    # Delete the oldest snapshot
    if path.isdir("%s/%s.%s" % (bak['dst'], arg, count[arg])):
        rmtree("%s/%s.%s" % (bak['dst'], arg, count[arg]))

    # Shift the middle snapshot(s) back by one
    for i in reversed(range(2, count[arg]+1)):
        if i-1 == 1 and arg == "hourly": break

        if path.isdir("%s/%s.%s" % (bak['dst'], arg, i-1)):
            rename("%s/%s.%s" % (bak['dst'], arg, i-1),
                   "%s/%s.%s" % (bak['dst'], arg, i))

    # Make a hard-link-only copy of the latest snapshot
    if path.isdir("%s/%s.%s" % (bak['dst'], rotation[arg][0], rotation[arg][1])):
        call("%s -al %s/%s.%s %s/%s.%s" % (bin['cp'], bak['dst'], rotation[arg][0],
                                           rotation[arg][1], bak['dst'], arg,
                                           rotation[arg][2]), shell=True)

# Rsync backup synchronization
def sync(arg):
    rsyncopt = (             # Mirroring options:
        "--archive",         #  use the archive mode,
        "--verbose",         #  increase verbosity,
        "--delete",          #  delete extraneous files,
        "--delete-excluded", #  delete excluded files
        "--exclude-from=%s" % files['exclude'],
    )

    for i in bak['src']:
        # Create directories from source names
        if not path.isdir("%s/%s.1/%s" % (bak['dst'], arg, i[1:])):
            makedirs("%s/%s.1/%s" % (bak['dst'], arg, i[1:]))

        # Mirror directories with rsync (auto. unlinks files that changed)
        call("%s %s %s %s/%s.1/%s" % (bin['rsync'], " ".join(rsyncopt),
                                      i, bak['dst'], arg, i[1:]), shell=True)

    # Make the backup reflect the snapshot time
    if path.isdir("%s/%s.1" % (bak['dst'], arg)):
        call("%s %s/%s.1" % (bin['touch'], bak['dst'], arg), shell=True)


# Managing mount points
def mount(action):
    if bak['efs']:
        # Exit with EAGAIN if eCryptfs is mounted
        if call("%s -qs ecryptfs %s"
                % (bin['grep'], files['mounts']), shell=True) == 0:
            raise SystemExit(11)

    if action == "mount":
        # Include the export for NFS
        if rmt['fst'] == "-t nfs":
            rmt['dev'] = "%s:%s" % (rmt['dev'], rmt['dst'])

        # Attempt to mount the destination
        if call("%s %s %s %s %s" % (bin[action], rmt['fst'], rmt['opt'],
                                    rmt['dev'], bak['dst']), shell=True) != 0:
            raise SystemExit("Error: could not mount destination")
    elif action == "umount":
        # Unmount the snapshot mountpoint
        if call("%s %s" % (bin[action], bak['dst']), shell=True) != 0:
            raise SystemExit("Error: could not unmount destination")



# Backup procedure
def main():
    usage = "Usage: %s {hourly|daily|weekly|monthly}" % path.split(argv[0])[1]

    if getuid() != 0:
        raise SystemExit("Error: super user privileges required")
    try:
        if argv[1] in count:
            mount("mount")
            rotate(argv[1])
            if argv[1] == "hourly":
                sync(argv[1])
            mount("umount")
        else:
            raise SystemExit(usage)
    except IndexError:
        raise SystemExit(usage)

if __name__ == "__main__":
    main()