Browse code

Specify age for systems that are not always on

In addition to just always creating a new snapshot, one can now also
specify an age in minutes in the ZFS property. The tool then only
creates a new snapshot if the last one is older than that age.

Lorenz Hüdepohl authored on12/10/2015 12:39:15
Showing1 changed files
... ...
@@ -24,7 +24,7 @@
24 24
 # THE SOFTWARE.
25 25
 
26 26
 import sys
27
-import datetime
27
+import time
28 28
 import argparse
29 29
 import locale
30 30
 from subprocess import Popen, PIPE, check_call
... ...
@@ -41,10 +41,15 @@ parser = argparse.ArgumentParser(
41 41
  "keep-all", which is used as the number of old snapshots to keep. If there
42 42
  are more, the oldest are destroyed.
43 43
 
44
+ Additionally, a number of minutes can be appended with a comma, a new
45
+ snapshot is then only created if the newest existing snapshot is older
46
+ than this duration
47
+
44 48
  If the value is "none" the filesystem is ignored (useful to prohibit property
45 49
  inheritance).
46 50
 
47
- Example:
51
+
52
+ Example usage for systems that are typically running (e.g. servers):
48 53
 
49 54
    Setup properties:
50 55
 
... ...
@@ -59,10 +64,32 @@ parser = argparse.ArgumentParser(
59 64
     @daily      zfs_auto_snapshot daily
60 65
     @hourly     zfs_auto_snapshot hourly
61 66
     */5 * * * * zfs_auto_snapshot 5min
67
+
68
+ Example usage for systems that are only running some of the time (e.g. laptops):
69
+
70
+   Properties:
71
+
72
+    #> zfs set autosnapshot:5min=12,5             dpool/fs
73
+    #> zfs set autosnapshot:hourly=24,60          dpool/fs
74
+    #> zfs set autosnapshot:daily=7,1440          dpool/fs
75
+    #> zfs set autosnapshot:weekly=keep-all,10080 dpool/fs
76
+
77
+   Use e.g. with this crontab (to check every few minutes whether to update
78
+   any of the snapshots)
79
+
80
+    */5  * * * * zfs_auto_snapshot 5min
81
+    */15 * * * * zfs_auto_snapshot hourly
82
+    */30 * * * * zfs_auto_snapshot daily
83
+    */30 * * * * zfs_auto_snapshot weekly
62 84
 ''')
63 85
 
64 86
 parser.add_argument('tagname', metavar='TAGNAME', type=str,
65
-                help='Consider all filesystems with an \'autosnapshot:TAGNAME\'\nproperty')
87
+                help='Consider all filesystems with an \'autosnapshot:TAGNAME\'\nproperty.\n'
88
+                     'If TAGNAME is of the form TAG=VALUE, the value of the ZFS\n'
89
+                     'property will be ignored and the command line value used\n'
90
+                     'instead. VALUE can be 0, in which case all existing\n'
91
+                     'snapshots will be deleted and no new snapshot will be\n'
92
+                     'created.')
66 93
 
67 94
 parser.add_argument('-v', '--verbose', action="store_true",
68 95
 		help='Echo the commands that are issued.')
... ...
@@ -75,13 +102,14 @@ parser.add_argument('-n', '--dry-run', action="store_true",
75 102
 
76 103
 args = parser.parse_args()
77 104
 
105
+TIME_FORMAT="%Y-%m-%d-%H:%M"
78 106
 
79 107
 zfs = which("zfs")
80 108
 
81 109
 if not zfs:
82 110
     raise Exception("No 'zfs' executable found in your PATH")
83 111
 
84
-def call(cmdargs):
112
+def mutative_cmd(cmdargs):
85 113
     if args.verbose or args.dry_run:
86 114
         print(" ".join(cmdargs))
87 115
     if not args.dry_run:
... ...
@@ -93,13 +121,40 @@ zfs_props = Popen([zfs, "get", "-t", "filesystem", "autosnapshot:{0}".format(arg
93 121
 encoding = locale.getdefaultlocale()[1]
94 122
 
95 123
 for line in zfs_props.stdout:
96
-    fs, prop, number, source = line.decode(encoding).split("\t")
124
+    fs, prop, value, source = line.decode(encoding).split("\t")
125
+    try:
126
+        number, minutes = value.split(",")
127
+    except:
128
+        number = value
129
+        minutes = None
97 130
     if number != "-" and number != "none":
131
+        snapshots = []
132
+
133
+        zfs_snapshots = Popen([zfs, "list", "-t", "snapshot", "-d", "1", fs, "-H"], stdout=PIPE)
134
+
135
+        for line_snapshots in zfs_snapshots.stdout:
136
+            snapname, rest = line_snapshots.decode(encoding).split("\t", 1)
137
+            if snapname.startswith("{0}@autosnapshot-{1}-".format(fs, args.tagname)):
138
+                snapshots.append(snapname)
139
+
98 140
         if not args.delete_only:
141
+            if minutes is not None and len(snapshots) > 0:
142
+                # Round time now to format used
143
+                now = time.strptime(time.strftime(TIME_FORMAT, time.gmtime()), TIME_FORMAT)
144
+
145
+                newest_time = time.strptime(snapshots[-1][len("{0}@autosnapshot-{1}-".format(fs, args.tagname)):], TIME_FORMAT)
146
+                age = (time.mktime(now) - time.mktime(newest_time))
147
+
148
+                if int(minutes) > age/60:
149
+                    if args.verbose or args.dry_run:
150
+                        print("No snapshot for {0}, last snapshot is only {1:.0f} minutes old, less than the configured {2}".format(
151
+                                fs, age/60, minutes))
152
+                    continue
153
+
99 154
             # Create new snapshot
100 155
             new_snapshot = "{0}@autosnapshot-{1}-{2}".format(fs, args.tagname,
101
-                                datetime.datetime.now().strftime("%Y-%m-%d-%H:%M"))
102
-            call([zfs, "snapshot", new_snapshot])
156
+                                time.strftime(TIME_FORMAT, time.gmtime()))
157
+            mutative_cmd([zfs, "snapshot", new_snapshot])
103 158
 
104 159
         # Delete oldest snapshots
105 160
         if number == "keep-all":
... ...
@@ -110,24 +165,11 @@ for line in zfs_props.stdout:
110 165
         if number <= 0:
111 166
             raise Exception("Invalid number for property 'autosnapshot:{0}' on filesystem {1}: {2}".format(args.tagname, fs, number))
112 167
 
113
-        snapshots = []
114
-
115
-        zfs_snapshots = Popen([zfs, "list", "-t", "snapshot", "-d", "1", fs, "-H"], stdout=PIPE)
116
-
117
-        for line_snapshots in zfs_snapshots.stdout:
118
-            snapname, rest = line_snapshots.decode(encoding).split("\t", 1)
119
-            if snapname.startswith("{0}@autosnapshot-{1}-".format(fs, args.tagname)):
120
-                snapshots.append(snapname)
121
-
122
-        # in dry-run mode, the not-created snapshot does not appear
123
-        # in the output of zfs, append it manually
124
-        if args.dry_run and not args.delete_only:
125
-            snapshots.append(new_snapshot)
126
-
168
+        snapshots.append(new_snapshot)
127 169
         to_delete = snapshots[:-number]
128 170
 
129 171
         for d in to_delete:
130 172
             # Delete this old snapshot
131 173
             if not d.startswith("{0}@autosnapshot-{1}-".format(fs, args.tagname)):
132 174
                 raise Exception("Invalid snapshot name '{0}', does not start with '{1}@autosnapshot-{2}-'".format(d, fs, args.tagname))
133
-            call([zfs, "destroy", d])
175
+            mutative_cmd([zfs, "destroy", d])