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