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 }