Jakob Borg 13 rokov pred
commit
8f6c11011b
3 zmenil súbory, kde vykonal 188 pridanie a 0 odobranie
  1. 33 0
      package.json
  2. 126 0
      server.js
  3. 29 0
      zsnapper.json.sample

+ 33 - 0
package.json

@@ -0,0 +1,33 @@
+{
+    "name": "zsnapper",
+    "description": "A ZFS snapshot service",
+    "author": "Jakob Borg <jakob@nym.se> (http://nym.se/)",
+    "keywords": ["solaris", "zfs", "illumos", "openindiana", "smartos"],
+    "homepage": "http://nym.se/zsnapper/docs/",
+    "version": "0.0.1",
+    "bin": "./server.js",
+    "scripts": {
+        "test": "NODE_PATH=lib PATH=test:$PATH node_modules/.bin/mocha -R spec",
+        "hint": "jshint *.js lib/*.js",
+        "doc": "docco lib/* example.js 2>/dev/null",
+        "cov": "jscoverage lib lib-cov && EXPRESS_COV=1 NODE_PATH=lib-cov mocha -R html-cov > docs/coverage.html"
+    },
+    "dependencies": {
+        "async": "0.1.x",
+        "cron": "0.3.x",
+        "underscore": "1.3.x",
+        "zfs": "*"
+    },
+    "devDependencies": {
+        "mocha": "*",
+        "should": "*"
+    },
+    "repository": {
+        "type": "git",
+        "url": "git://github.com/calmh/zsnapper.git"
+    },
+    "bugs": {
+        "url": "http://github.com/calmh/zsnapper/issues",
+        "email": "zsnapper@nym.se"
+    }
+}

+ 126 - 0
server.js

@@ -0,0 +1,126 @@
+#!/usr/bin/node
+
+var _ = require('underscore');
+var async = require('async');
+var CronJob = require('cron').CronJob;
+var util = require('util');
+var zfs = require('zfs')
+var fs = require('fs')
+
+// Zero pad an integer to the specified length.
+function padInteger(num, length) {
+    "use strict";
+
+    var r = '' + num;
+    while (r.length < length) {
+        r = '0' + r;
+    }
+    return r;
+}
+
+// Format a date to yyyymmddThhmmssZ (UTC).
+function formatDate(d) {
+    "use strict";
+
+    var year, month, day, hour, minute, second;
+    year = d.getUTCFullYear();
+    month = padInteger(d.getUTCMonth() + 1, 2);
+    day = padInteger(d.getUTCDate(), 2);
+    hour = padInteger(d.getUTCHours(), 2);
+    minute = padInteger(d.getUTCMinutes(), 2);
+    second = padInteger(d.getUTCSeconds(), 2);
+    return year + month + day + 'T' + hour + minute + second + 'Z';
+};
+
+// Generate a snapshot name given a base.
+function snapshotName(base) {
+    "use strict";
+
+    var when = formatDate(new Date());
+    return base + '-' + when;
+}
+
+// Create a snapshot on the specified dataset with the specified name, exluding all datasets in excl.
+function createSnapshot(ds, sn, excl) {
+    "use strict";
+
+    function destroyExcludes(snaps, excl) {
+        var toDestroy = snaps.filter(function (s) {
+            var fields = s.name.split('@');
+            var createdNow = fields[1] === sn;
+            var inExclude = _.contains(excl, fields[0]);
+            return createdNow && inExclude;
+        });
+
+        async.forEachSeries(toDestroy, function (snap, cb) {
+            util.log('Destroy snapshot ' + snap.name + ' (excluded)');
+            zfs.destroy({ name: snap.name, recursive: true }, cb);
+        });
+    }
+
+    util.log('Create snapshot ' + ds + '@' + sn);
+    zfs.snapshot({ dataset: ds, name: sn, recursive: true }, function (err) {
+        if (err) {
+            util.log(err);
+        } else {
+            zfs.list({ type: 'snapshot' }, function (err, snaps) {
+                if (err) {
+                    util.log(err);
+                } else {
+                    destroyExcludes(snaps, excl);
+                }
+            });
+        }
+    });
+}
+
+// Keep only num snapshots with the specified base on the specified dataset
+function cleanSnapshots(ds, base, num) {
+    zfs.list({ type: 'snapshot' }, function (err, snaps) {
+        // Find all snapshots that match the specified dataset and base.
+        var ourSnaps = snaps.filter(function (s) {
+            var fields = s.name.split('@');
+            var parts = fields[1].split('-');
+            return fields[0] === ds && parts[0] === base;
+        });
+
+        if (ourSnaps.length > num) {
+            // Get just the sorted list of names.
+            var snapNames = ourSnaps.map(function (s) { return s.name; });
+            snapNames.sort();
+
+            // Get the ones that exceed the specified number of snapshots.
+            var toDestroy = snapNames.slice(0, snapNames.length - num);
+
+            // Destroy them, one after the other.
+            async.forEachSeries(toDestroy, function (sn, cb) {
+                util.log('Destroy snapshot ' + sn + ' (cleaned)');
+                zfs.destroy({ name: sn, recursive: true }, cb);
+            });
+        }
+    });
+}
+
+function snapshot(dataset, base, excl, num) {
+    var name = snapshotName(base);
+    createSnapshot(dataset, name, excl);
+    cleanSnapshots(dataset, base, num);
+}
+
+function loadConfig() {
+    var data = fs.readFileSync(process.argv[2], 'utf-8');
+    var confObj = JSON.parse(data);
+    return confObj;
+}
+
+var cronJobs = [];
+var conf = loadConfig();
+_.each(conf, function (jobs, dataset) {
+    _.each(jobs, function (config, name) {
+        var job = new CronJob('00 ' + config.when, function () {
+            var excl = config.exclude || [];
+            snapshot(dataset, name, excl, parseInt(config.count, 10));
+        }, null, true, 'UTC');
+        cronJobs.push(job);
+    });
+});

+ 29 - 0
zsnapper.json.sample

@@ -0,0 +1,29 @@
+{
+	"zones": {
+		"minute": {
+			"when": "* * * * *",
+			"count": 15,
+			"exclude": [ "zones/swap", "zones/dump" ]
+		},
+		"hourly": {
+			"when": "0 * * * *",
+			"count": 24,
+			"exclude": [ "zones/swap", "zones/dump" ]
+		},
+		"daily": {
+			"when": "00 05 * * *",
+			"count": 10,
+			"exclude": [ "zones/swap", "zones/dump" ]
+		},
+		"weekly": {
+			"when": "00 05 * * 1",
+			"count": 4,
+			"exclude": [ "zones/swap", "zones/dump" ]
+		},
+		"monthly": {
+			"when": "00 05 1 * *",
+			"count": 12,
+			"exclude": [ "zones/swap", "zones/dump" ]
+		}
+	}
+}