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.cmdgroup.backup; 7 8 import logger = std.experimental.logger; 9 import std.array : empty; 10 import std.exception : collectException; 11 12 import sumtype; 13 14 import dsnapshot.config : Config; 15 import dsnapshot.types; 16 17 import dsnapshot.exception; 18 import dsnapshot.layout : Name, Layout; 19 import dsnapshot.layout_utils; 20 import dsnapshot.console; 21 22 version (unittest) { 23 import unit_threaded.assertions; 24 } 25 26 int cmdBackup(Config.Global global, Config.Backup backup, Snapshot[] snapshots) { 27 import std.algorithm : filter; 28 29 int exitStatus; 30 31 foreach (s; snapshots.filter!(a => backup.name.value.empty || backup.name.value == a.name)) { 32 int snapshotStatus = 1; 33 try { 34 snapshot(s, backup.ignoreRsyncErrorCodes); 35 snapshotStatus = 0; 36 } catch (SnapshotException e) { 37 e.errMsg.match!(a => a.print); 38 logger.error(e.msg); 39 } catch (Exception e) { 40 logger.error(e.msg); 41 } 42 43 exitStatus = (snapshotStatus + exitStatus) == 0 ? 0 : 1; 44 } 45 46 return exitStatus; 47 } 48 49 private: 50 51 void snapshot(Snapshot snapshot, const int[] ignoreRsyncErrorCodes) { 52 import std.file : exists, mkdirRecurse, rename; 53 import std.path : buildPath, setExtension; 54 import std.datetime : UTC, SysTime, Clock; 55 56 // Extract an updated layout of the snapshots at the destination. 57 auto layout = snapshot.syncCmd.match!((None a) => snapshot.layout, 58 (RsyncConfig a) => fillLayout(snapshot.layout, a.flow, snapshot.remoteCmd)); 59 60 // this ensure that dsnapshot is only executed when there are actual work 61 // to do. If multiple snapshots are taken close to each other in time then 62 // it means that the "last" one of them is actually the only one that is 63 // kept because it is closest to the bucket. 64 if (!layout.isFirstBucketEmpty) { 65 logger.infof("Nothing to do because a snapshot where recently taken"); 66 auto first = layout.snapshotTimeInBucket(0); 67 if (!first.isNull) 68 logger.info(first.get); 69 return; 70 } 71 72 auto flow = snapshot.syncCmd.match!((None a) => None.init.Flow, (RsyncConfig a) => a.flow); 73 74 logger.trace("Updated layout with information from destination: ", layout); 75 76 const newSnapshot = () { 77 auto c = Clock.currTime; 78 c.timezone = UTC(); 79 return c.toISOExtString ~ snapshotInProgressSuffix; 80 }(); 81 82 snapshot.syncCmd.match!((None a) { 83 logger.info("No sync done for ", snapshot.name, " (missing command configuration)"); 84 }, (RsyncConfig a) { 85 sync(a, layout, flow, snapshot.hooks, snapshot.remoteCmd, newSnapshot, 86 ignoreRsyncErrorCodes); 87 }); 88 89 flow.match!((None a) {}, (FlowLocal a) { 90 finishLocalSnapshot(a.dst, layout, newSnapshot); 91 }, (FlowRsyncToLocal a) { finishLocalSnapshot(a.dst, layout, newSnapshot); }, 92 (FlowLocalToRsync a) { 93 finishRemoteSnapshot(a.dst, layout, snapshot.remoteCmd, newSnapshot); 94 }); 95 } 96 97 void sync(const RsyncConfig conf, const Layout layout, const Flow flow, const Hooks hooks, 98 const RemoteCmd remoteCmd, const string newSnapshot, const int[] ignoreRsyncErrorCodes) { 99 import std.algorithm : canFind; 100 import std.array : empty; 101 import std.conv : to; 102 import std.file : remove, exists, mkdirRecurse; 103 import std.path : buildPath, setExtension; 104 import std.process : spawnProcess, wait, execute, spawnShell, executeShell; 105 import std.stdio : stdin, File; 106 107 static void setupLocalDest(const Path p) { 108 if (!exists(p.toString)) 109 mkdirRecurse(p.toString); 110 } 111 112 static void setupRemoteDest(const RemoteCmd remoteCmd, const RsyncAddr addr, 113 const string newSnapshot) { 114 string[] cmd = remoteCmd.match!((const SshRemoteCmd a) { 115 return a.toCmd(RemoteSubCmd.mkdirRecurse, addr.addr, 116 buildPath(addr.path, newSnapshot)); 117 }); 118 if (!cmd.empty) { 119 logger.infof("%-(%s %)", cmd); 120 spawnProcess(cmd).wait; 121 } 122 } 123 124 static int executeHooks(const string msg, const string[] hooks, const string[string] env) { 125 foreach (s; hooks) { 126 logger.info(msg, ": ", s); 127 if (spawnShell(s, env).wait != 0) 128 return 1; 129 } 130 return 0; 131 } 132 133 string src, dst; 134 135 string[] buildOpts() { 136 string[] opts = [conf.cmdRsync]; 137 opts ~= conf.args.dup; 138 139 const latest = layout.firstFullBucket; 140 141 if (!conf.rsh.empty) 142 opts ~= ["-e", conf.rsh]; 143 144 if (isInteractiveShell) 145 opts ~= conf.progress; 146 147 if (conf.oneFs && !latest.isNull) 148 opts ~= ["-x"]; 149 150 if (conf.useLinkDest && !latest.isNull) { 151 flow.match!((None a) {}, (FlowLocal a) { 152 opts ~= [ 153 "--link-dest", (a.dst.value.Path ~ latest.get.name.value).toString 154 ]; 155 }, (FlowRsyncToLocal a) { 156 opts ~= [ 157 "--link-dest", (a.dst.value.Path ~ latest.get.name.value).toString 158 ]; 159 }, (FlowLocalToRsync a) { 160 // from the rsync documentation: 161 // If DIR is a relative path, it is relative to the destination directory. 162 opts ~= ["--link-dest", buildPath("..", latest.get.name.value)]; 163 }); 164 } 165 166 foreach (a; conf.exclude) 167 opts ~= ["--exclude", a]; 168 169 flow.match!((None a) {}, (FlowLocal a) { 170 src = fixRsyncAddr(a.src.value); 171 dst = (a.dst.value.Path ~ newSnapshot).toString; 172 opts ~= [src, dst]; 173 }, (FlowRsyncToLocal a) { 174 src = fixRsyncAddr(makeRsyncAddr(a.src.addr, a.src.path)); 175 dst = (a.dst.value.Path ~ newSnapshot).toString; 176 opts ~= [src, dst]; 177 }, (FlowLocalToRsync a) { 178 src = fixRsyncAddr(a.src.value); 179 dst = makeRsyncAddr(a.dst.addr, buildPath(a.dst.path, newSnapshot)); 180 opts ~= [src, dst]; 181 }); 182 return opts; 183 } 184 185 auto opts = buildOpts(); 186 187 // Configure local destination 188 flow.match!((None a) {}, (FlowLocal a) => setupLocalDest(a.dst.value.Path ~ newSnapshot), 189 (FlowRsyncToLocal a) => setupLocalDest(a.dst.value.Path ~ newSnapshot), 190 (FlowLocalToRsync a) => setupRemoteDest(remoteCmd, a.dst, newSnapshot)); 191 192 if (src.empty || dst.empty) { 193 logger.info("source or destination is empty. Nothing to do."); 194 return; 195 } 196 197 logger.infof("Synchronizing '%s' to '%s'", src, dst); 198 199 string[string] hookEnv = ["DSNAPSHOT_SRC" : src, "DSNAPSHOT_DST" : dst]; 200 201 if (executeHooks("pre_exec", hooks.preExec, hookEnv) != 0) 202 throw new SnapshotException(SnapshotException.PreExecFailed.init.SnapshotError); 203 204 logger.infof("%-(%s %)", opts); 205 auto syncPid = spawnProcess(opts); 206 207 if (conf.lowPrio) { 208 try { 209 logger.infof("Changing IO and CPU priority to low (pid %s)", syncPid.processID); 210 execute(["ionice", "-c", "3", "-p", syncPid.processID.to!string]); 211 execute(["renice", "+12", "-p", syncPid.processID.to!string]); 212 } catch (Exception e) { 213 logger.info(e.msg); 214 } 215 } 216 217 auto syncPidExit = syncPid.wait; 218 logger.trace("rsync exit code: ", syncPidExit); 219 if (syncPidExit != 0 && !canFind(ignoreRsyncErrorCodes, syncPidExit)) 220 throw new SnapshotException(SnapshotException.SyncFailed(src, dst).SnapshotError); 221 222 if (executeHooks("post_exec", hooks.postExec, hookEnv) != 0) 223 throw new SnapshotException(SnapshotException.PostExecFailed.init.SnapshotError); 224 } 225 226 void finishLocalSnapshot(const LocalAddr local, const Layout layout, const string newSnapshot) { 227 import std.algorithm : map; 228 import std.file : rmdirRecurse, exists, isDir; 229 import dsnapshot.cmdgroup.remote : publishSnapshot; 230 231 publishSnapshot((local.value.Path ~ newSnapshot).toString); 232 233 foreach (const name; layout.discarded.map!(a => a.name)) { 234 const old = (local.value.Path ~ name.value).toString; 235 if (exists(old) && old.isDir) { 236 logger.info("Removing old snapshot ", old); 237 try { 238 rmdirRecurse(old); 239 } catch (Exception e) { 240 logger.warning(e.msg); 241 } 242 } 243 } 244 } 245 246 void finishRemoteSnapshot(const RsyncAddr addr, const Layout layout, 247 const RemoteCmd cmd_, const string newSnapshot) { 248 import std.algorithm : map; 249 import std.path : buildPath; 250 import std.process : spawnProcess, wait; 251 252 { //TODO: create a helper function that execute commands 253 auto cmd = cmd_.match!((const SshRemoteCmd a) { 254 return a.toCmd(RemoteSubCmd.publishSnapshot, addr.addr, 255 buildPath(addr.path, newSnapshot)); 256 }); 257 logger.infof("%-(%s %)", cmd); 258 spawnProcess(cmd).wait; 259 } 260 261 foreach (const name; layout.discarded.map!(a => a.name)) { 262 auto cmd = cmd_.match!((const SshRemoteCmd a) { 263 return a.toCmd(RemoteSubCmd.rmdirRecurse, addr.addr, buildPath(addr.path, name.value)); 264 }); 265 266 logger.info("Removing old snapshot ", name.value); 267 try { 268 logger.infof("%-(%s %)", cmd); 269 spawnProcess(cmd).wait; 270 } catch (Exception e) { 271 logger.warning(e.msg); 272 } 273 } 274 }