1 /** 2 Copyright: Copyright (c) 2019, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 */ 6 module dsnapshot.backend.rsync; 7 8 import logger = std.experimental.logger; 9 import std.algorithm : map, filter; 10 import std.datetime : SysTime; 11 12 import dsnapshot.backend.crypt; 13 import dsnapshot.backend.rsync; 14 import dsnapshot.backend; 15 import dsnapshot.config; 16 import dsnapshot.exception; 17 import dsnapshot.from; 18 import dsnapshot.layout : Layout; 19 import dsnapshot.process; 20 import dsnapshot.types; 21 22 @safe: 23 24 final class RsyncBackend : SyncBackend { 25 RsyncConfig conf; 26 RemoteCmd remoteCmd_; 27 /// Error codes ignored when Synchronizing. 28 const(int)[] ignoreRsyncErrorCodes; 29 30 this(RsyncConfig conf, RemoteCmd remoteCmd, const(int)[] ignoreRsyncErrorCodes) { 31 this.conf = conf; 32 this.remoteCmd_ = remoteCmd; 33 this.ignoreRsyncErrorCodes = ignoreRsyncErrorCodes; 34 } 35 36 override void remoteCmd(const RemoteHost host, const RemoteSubCmd cmd_, const string path) { 37 import std.path : buildPath; 38 39 auto cmd = remoteCmd_.match!((SshRemoteCmd a) { 40 return a.toCmd(cmd_, host.addr, buildPath(host.path, path)); 41 }); 42 43 // TODO: throw exception on failure? 44 spawnProcessLog(cmd).wait; 45 } 46 47 override Layout update(Layout layout) { 48 import dsnapshot.layout_utils; 49 50 return fillLayout(layout, conf.flow, remoteCmd_); 51 } 52 53 override void publishSnapshot(const string newSnapshot) { 54 static import dsnapshot.cmdgroup.remote; 55 56 conf.flow.match!((None a) {}, (FlowLocal a) { 57 dsnapshot.cmdgroup.remote.publishSnapshot((a.dst.value.Path ~ newSnapshot).toString); 58 }, (FlowRsyncToLocal a) { 59 dsnapshot.cmdgroup.remote.publishSnapshot((a.dst.value.Path ~ newSnapshot).toString); 60 }, (FlowLocalToRsync a) { 61 this.remoteCmd(RemoteHost(a.dst.addr, a.dst.path), 62 RemoteSubCmd.publishSnapshot, newSnapshot); 63 }); 64 } 65 66 override void removeDiscarded(const Layout layout) { 67 void local(const LocalAddr local) @safe { 68 import std.file : rmdirRecurse, exists, isDir; 69 70 foreach (const old; layout.discarded 71 .map!(a => (local.value.Path ~ a.name.value).toString) 72 .filter!(a => exists(a) && a.isDir)) { 73 logger.info("Removing old snapshot ", old); 74 try { 75 () @trusted { rmdirRecurse(old); }(); 76 } catch (Exception e) { 77 logger.warning(e.msg); 78 } 79 } 80 } 81 82 void remote(const RemoteHost host) @safe { 83 foreach (const name; layout.discarded.map!(a => a.name)) { 84 logger.info("Removing old snapshot ", name.value); 85 this.remoteCmd(host, RemoteSubCmd.rmdirRecurse, name.value); 86 } 87 } 88 89 conf.flow.match!((None a) {}, (FlowLocal a) { local(a.dst); }, (FlowRsyncToLocal a) { 90 local(a.dst); 91 }, (FlowLocalToRsync a) { remote(RemoteHost(a.dst.addr, a.dst.path)); }); 92 } 93 94 override void sync(const Layout layout, const SnapshotConfig snapshot, 95 const string nameOfNewSnapshot) { 96 import std.algorithm : canFind; 97 import std.array : replace, array, empty; 98 import std.conv : to; 99 import std.file : remove, exists, mkdirRecurse; 100 import std.format : format; 101 import std.path : buildPath, setExtension; 102 import std.process : spawnShell; 103 import std.stdio : stdin, File; 104 import dsnapshot.console; 105 106 static void setupLocalDest(const Path p) @safe { 107 auto dst = p ~ snapshotData; 108 if (!exists(dst.toString)) 109 mkdirRecurse(dst.toString); 110 } 111 112 static void setupRemoteDest(const RemoteCmd remoteCmd, 113 const RemoteHost addr, const string newSnapshot) @safe { 114 string[] cmd = remoteCmd.match!((const SshRemoteCmd a) { 115 return a.toCmd(RemoteSubCmd.mkdirRecurse, addr.addr, 116 buildPath(addr.path, newSnapshot, snapshotData)); 117 }); 118 if (!cmd.empty) { 119 spawnProcessLog(cmd).wait; 120 } 121 } 122 123 static int executeHooks(const string msg, const string[] hooks, const string[string] env) @safe { 124 foreach (s; hooks) { 125 logger.info(msg, ": ", s); 126 if (spawnShell(s, env).wait != 0) 127 return 1; 128 } 129 return 0; 130 } 131 132 string src, dst; 133 134 string[] buildOpts() @safe { 135 string[] opts = [conf.cmdRsync]; 136 opts ~= conf.backupArgs.dup; 137 138 const latest = layout.firstFullBucket; 139 140 if (!conf.rsh.empty) 141 opts ~= ["-e", conf.rsh]; 142 143 if (isInteractiveShell) 144 opts ~= conf.progress; 145 146 if (conf.oneFs && !latest.isNull) 147 opts ~= ["-x"]; 148 149 if (conf.useLinkDest && !latest.isNull) { 150 conf.flow.match!((None a) {}, (FlowLocal a) { 151 opts ~= [ 152 "--link-dest", 153 (a.dst.value.Path ~ latest.get.name.value ~ snapshotData).toString 154 ]; 155 }, (FlowRsyncToLocal a) { 156 opts ~= [ 157 "--link-dest", 158 (a.dst.value.Path ~ latest.get.name.value ~ snapshotData).toString 159 ]; 160 }, (FlowLocalToRsync a) { 161 // from the rsync documentation: 162 // If DIR is a relative path, it is relative to the destination directory. 163 opts ~= [ 164 "--link-dest", 165 buildPath("..", "..", latest.get.name.value, snapshotData) 166 ]; 167 }); 168 } 169 170 foreach (a; conf.exclude) 171 opts ~= ["--exclude", a]; 172 173 if (conf.useFakeRoot) { 174 conf.flow.match!((None a) {}, (FlowLocal a) { 175 opts = conf.fakerootArgs.map!(b => b.replace(snapshotFakerootSaveEnvId, 176 (a.dst.value.Path ~ nameOfNewSnapshot ~ snapshotFakerootEnv).toString)).array 177 ~ opts; 178 }, (FlowRsyncToLocal a) { 179 opts = conf.fakerootArgs.map!(b => b.replace(snapshotFakerootSaveEnvId, 180 (a.dst.value.Path ~ nameOfNewSnapshot ~ snapshotFakerootEnv).toString)).array 181 ~ opts; 182 }, (FlowLocalToRsync a) { 183 opts ~= conf.rsyncFakerootArgs; 184 opts ~= format!"%-(%s %) %s"(conf.fakerootArgs.map!(b => b.replace(snapshotFakerootSaveEnvId, 185 buildPath(a.dst.path, nameOfNewSnapshot, snapshotFakerootEnv))), 186 conf.cmdRsync); 187 }); 188 } 189 190 conf.flow.match!((None a) {}, (FlowLocal a) { 191 src = fixRemteHostForRsync(a.src.value); 192 dst = (a.dst.value.Path ~ nameOfNewSnapshot ~ snapshotData).toString; 193 opts ~= [src, dst]; 194 }, (FlowRsyncToLocal a) { 195 src = fixRemteHostForRsync(makeRsyncAddr(a.src.addr, a.src.path)); 196 dst = (a.dst.value.Path ~ nameOfNewSnapshot ~ snapshotData).toString; 197 opts ~= [src, dst]; 198 }, (FlowLocalToRsync a) { 199 src = fixRemteHostForRsync(a.src.value); 200 dst = makeRsyncAddr(a.dst.addr, buildPath(a.dst.path, 201 nameOfNewSnapshot, snapshotData)); 202 opts ~= [src, dst]; 203 }); 204 205 return opts; 206 } 207 208 auto opts = buildOpts(); 209 210 if (src.empty || dst.empty) { 211 logger.info("source or destination is not configured. Nothing to do."); 212 return; 213 } 214 215 // Configure local destination 216 conf.flow.match!((None a) {}, (FlowLocal a) => setupLocalDest(a.dst.value.Path ~ nameOfNewSnapshot), 217 (FlowRsyncToLocal a) => setupLocalDest(a.dst.value.Path ~ nameOfNewSnapshot), 218 (FlowLocalToRsync a) => setupRemoteDest(remoteCmd_, a.dst, nameOfNewSnapshot)); 219 220 logger.infof("Synchronizing '%s' to '%s'", src, dst); 221 222 string[string] hookEnv = ["DSNAPSHOT_SRC" : src, "DSNAPSHOT_DST" : dst]; 223 224 if (executeHooks("pre_exec", snapshot.hooks.preExec, hookEnv) != 0) 225 throw new SnapshotException(SnapshotException.PreExecFailed.init.SnapshotError); 226 227 auto syncPid = spawnProcessLog(opts); 228 229 if (conf.lowPrio) { 230 try { 231 logger.infof("Changing IO and CPU priority to low (pid %s)", syncPid.processID); 232 executeLog([ 233 "ionice", "-c", "3", "-p", syncPid.processID.to!string 234 ]); 235 executeLog(["renice", "+12", "-p", syncPid.processID.to!string]); 236 } catch (Exception e) { 237 logger.info(e.msg); 238 } 239 } 240 241 auto syncPidExit = syncPid.wait; 242 logger.trace("rsync exit code: ", syncPidExit); 243 if (syncPidExit != 0 && !canFind(ignoreRsyncErrorCodes, syncPidExit)) 244 throw new SnapshotException(SnapshotException.SyncFailed(src, dst).SnapshotError); 245 246 if (executeHooks("post_exec", snapshot.hooks.postExec, hookEnv) != 0) 247 throw new SnapshotException(SnapshotException.PostExecFailed.init.SnapshotError); 248 } 249 250 override void restore(const Layout layout, const SnapshotConfig snapshot, 251 const SysTime time, const string restoreTo) { 252 import std.array : empty; 253 import std.file : exists, mkdirRecurse; 254 import std.path : buildPath; 255 import dsnapshot.console : isInteractiveShell; 256 257 const bestFitSnapshot = layout.bestFitBucket(time); 258 if (bestFitSnapshot.isNull) { 259 logger.error("Unable to find a snapshot to restore for the time ", time); 260 throw new SnapshotException(SnapshotException.RestoreFailed(null, 261 restoreTo).SnapshotError); 262 } 263 264 string src; 265 266 // TODO: this code is similare to the one in cmdgroup.backup. Consider how 267 // it can be deduplicated. Note, similare not the same. 268 string[] buildOpts() { 269 string[] opts = [conf.cmdRsync]; 270 opts ~= conf.restoreArgs.dup; 271 272 if (!conf.rsh.empty) 273 opts ~= ["-e", conf.rsh]; 274 275 if (isInteractiveShell) 276 opts ~= conf.progress; 277 278 foreach (a; conf.exclude) 279 opts ~= ["--exclude", a]; 280 281 conf.flow.match!((None a) {}, (FlowLocal a) { 282 src = fixRemteHostForRsync((a.dst.value.Path ~ bestFitSnapshot.name.value ~ snapshotData) 283 .toString); 284 }, (FlowRsyncToLocal a) { 285 src = fixRemteHostForRsync((a.dst.value.Path ~ bestFitSnapshot.name.value ~ snapshotData) 286 .toString); 287 }, (FlowLocalToRsync a) { 288 src = makeRsyncAddr(a.dst.addr, fixRemteHostForRsync(buildPath(a.dst.path, 289 bestFitSnapshot.name.value, snapshotData))); 290 }); 291 292 opts ~= src; 293 // dst is always on the local machine as specified by the user 294 opts ~= restoreTo; 295 return opts; 296 } 297 298 const opts = buildOpts(); 299 300 if (!exists(restoreTo)) 301 mkdirRecurse(restoreTo); 302 303 logger.infof("Restoring %s to %s", bestFitSnapshot.name.value, restoreTo); 304 305 if (spawnProcessLog(opts).wait != 0) 306 throw new SnapshotException(SnapshotException.RestoreFailed(src, 307 restoreTo).SnapshotError); 308 309 if (conf.useFakeRoot) { 310 conf.flow.match!((None a) {}, (FlowLocal a) => fakerootLocalRestore( 311 a.dst.value.Path ~ bestFitSnapshot.name.value, restoreTo), (FlowRsyncToLocal a) { 312 logger.warning("Restoring permissions to a remote is not supported (yet)"); 313 }, (FlowLocalToRsync a) => fakerootRemoteRestore(snapshot.remoteCmd, 314 a.dst, bestFitSnapshot.name, restoreTo)); 315 } 316 } 317 318 Flow flow() { 319 return conf.flow; 320 } 321 } 322 323 private: 324 325 void fakerootLocalRestore(const Path root, const string restoreTo) { 326 import dsnapshot.stats; 327 328 auto fkdb = fromFakerootEnv(root ~ snapshotFakerootEnv); 329 auto pstats = fromFakeroot(fkdb, root.toString, (root ~ snapshotData).toString); 330 restorePermissions(pstats, Path(restoreTo)); 331 } 332 333 void fakerootRemoteRestore(const RemoteCmd cmd_, const RemoteHost addr, 334 const Name name, const string restoreTo) { 335 import std.array : appender, empty; 336 import std.path : buildPath; 337 import std.string : lineSplitter, strip; 338 import dsnapshot.stats; 339 340 auto cmd = cmd_.match!((const SshRemoteCmd a) { 341 return a.toCmd(RemoteSubCmd.fakerootStats, addr.addr, buildPath(addr.path, name.value)); 342 }); 343 344 auto res = executeLog(cmd); 345 346 if (res.status != 0) { 347 logger.errorf("Unable to restore permissions to %s from %s", restoreTo, 348 snapshotFakerootEnv); 349 logger.info(res.output); 350 return; 351 } 352 353 auto app = appender!(PathStat[])(); 354 foreach (const l; res.output 355 .lineSplitter 356 .map!(a => a.strip) 357 .filter!(a => !a.empty)) { 358 try { 359 app.put(fromPathStat(l)); 360 } catch (Exception e) { 361 logger.warning("Error when parsing ", l); 362 logger.info(e.msg); 363 } 364 } 365 366 restorePermissions(app.data, Path(restoreTo)); 367 } 368 369 void restorePermissions(const from.dsnapshot.stats.PathStat[] pstats, const Path root) @trusted { 370 import core.sys.posix.sys.stat : S_IFMT, stat_t, stat, lstat, chmod; 371 import core.sys.posix.unistd : chown, lchown; 372 import std.file : isFile, isDir, isSymlink; 373 import std.string : toStringz; 374 375 foreach (const f; pstats) { 376 const curr = (root ~ f.path).toString; 377 const currz = curr.toStringz; 378 379 if (curr.isFile || curr.isDir) { 380 stat_t st = void; 381 stat(currz, &st); 382 if (st.st_mode != f.mode) { 383 // only set the permissions thus masking out other bits. 384 chmod(currz, cast(uint) f.mode & ~S_IFMT); 385 } 386 if (st.st_uid != f.uid || st.st_gid != f.gid) 387 chown(currz, cast(uint) f.uid, cast(uint) f.gid); 388 } else if (curr.isSymlink) { 389 stat_t st = void; 390 lstat(currz, &st); 391 if (st.st_uid != f.uid || st.st_gid != f.gid) 392 lchown(currz, cast(uint) f.uid, cast(uint) f.gid); 393 } 394 } 395 }