GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_sync.cc
Date: 2026-10-11 02:40:02
Exec Total Coverage
Lines: 0 499 0.0%
Branches: 0 324 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System
3 *
4 * This tool figures out the changes made to a cvmfs repository by means
5 * of a union file system mounted on top of a cvmfs volume.
6 * We take all three volumes (namely union, overlay and repository) into
7 * account to sync the changes back into the repository.
8 *
9 * On the repository side we have a catalogs directory that mimics the
10 * shadow directory structure and stores compressed and uncompressed
11 * versions of all catalogs. The raw data are stored in the data
12 * subdirectory in zlib-compressed form. They are named with their SHA-1
13 * hash of the compressed file (like in CVMFS client cache, but with a
14 * 2-level cache hierarchy). Symlinks from the catalog directory to the
15 * data directory form the connection. If necessary, add a .htaccess file
16 * to allow Apache to follow the symlinks.
17 */
18
19 // NOLINTNEXTLINE
20 #define _FILE_OFFSET_BITS 64
21
22 #include "swissknife_sync.h"
23
24 #include <errno.h>
25 #include <fcntl.h>
26 #include <glob.h>
27 #include <inttypes.h>
28 #include <limits.h>
29
30 #include <cstdio>
31 #include <cstdlib>
32 #include <string>
33 #include <vector>
34
35 #include "catalog_mgr_ro.h"
36 #include "catalog_mgr_rw.h"
37 #include "catalog_virtual.h"
38 #include "manifest.h"
39 #include "monitor.h"
40 #include "path_filters/dirtab.h"
41 #include "reflog.h"
42 #include "sanitizer.h"
43 #include "statistics.h"
44 #include "statistics_database.h"
45 #include "sync_mediator.h"
46 #include "sync_union.h"
47 #include "sync_union_aufs.h"
48 #include "sync_union_overlayfs.h"
49 #include "util/capabilities.h"
50 #include "util/logging.h"
51 #include "util/string.h"
52
53 using namespace std; // NOLINT
54
55 ✗ bool swissknife::CommandSync::CheckParams(const SyncParameters &p) {
56 ✗ if (!DirectoryExists(p.dir_scratch)) {
57 ✗ PrintError("overlay (copy on write) directory does not exist");
58 ✗ return false;
59 }
60 ✗ if (!DirectoryExists(p.dir_union)) {
61 ✗ PrintError("union volume does not exist");
62 ✗ return false;
63 }
64 ✗ if (!DirectoryExists(p.dir_rdonly)) {
65 ✗ PrintError("cvmfs read/only repository does not exist");
66 ✗ return false;
67 }
68 ✗ if (p.stratum0 == "") {
69 ✗ PrintError("Stratum0 url missing");
70 ✗ return false;
71 }
72
73 ✗ if (p.manifest_path == "") {
74 ✗ PrintError("manifest output required");
75 ✗ return false;
76 }
77 ✗ if (!DirectoryExists(p.dir_temp)) {
78 ✗ PrintError("data store directory does not exist");
79 ✗ return false;
80 }
81
82 ✗ if (p.min_file_chunk_size >= p.avg_file_chunk_size
83 ✗ || p.avg_file_chunk_size >= p.max_file_chunk_size) {
84 ✗ PrintError("file chunk size values are not sane");
85 ✗ return false;
86 }
87
88 ✗ if (HasPrefix(p.spooler_definition, "gw", false)) {
89 ✗ if (p.session_token_file.empty()) {
90 ✗ PrintError("Session token file has to be provided "
91 "when upstream type is gw.");
92 ✗ return false;
93 }
94 }
95
96 ✗ return true;
97 }
98
99 ✗ int swissknife::CommandCreate::Main(const swissknife::ArgumentList &args) {
100 ✗ const string manifest_path = *args.find('o')->second;
101 ✗ const string dir_temp = *args.find('t')->second;
102 ✗ const string spooler_definition = *args.find('r')->second;
103 ✗ const string repo_name = *args.find('n')->second;
104 ✗ const string reflog_chksum_path = *args.find('R')->second;
105 ✗ if (args.find('l') != args.end()) {
106 const unsigned log_level = kLogLevel0
107 ✗ << String2Uint64(*args.find('l')->second);
108 ✗ if (log_level > kLogNone) {
109 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "invalid log level");
110 ✗ return 1;
111 }
112 ✗ SetLogVerbosity(static_cast<LogLevels>(log_level));
113 }
114 ✗ shash::Algorithms hash_algorithm = shash::kSha1;
115 ✗ if (args.find('a') != args.end()) {
116 ✗ hash_algorithm = shash::ParseHashAlgorithm(*args.find('a')->second);
117 ✗ if (hash_algorithm == shash::kAny) {
118 ✗ PrintError("unknown hash algorithm");
119 ✗ return 1;
120 }
121 }
122
123 ✗ const bool volatile_content = (args.count('v') > 0);
124 ✗ const bool garbage_collectable = (args.count('z') > 0);
125 ✗ std::string voms_authz;
126 ✗ if (args.find('V') != args.end()) {
127 ✗ voms_authz = *args.find('V')->second;
128 }
129
130 const upload::SpoolerDefinition sd(spooler_definition, hash_algorithm,
131 ✗ zlib::kZlibDefault);
132 const std::unique_ptr<upload::Spooler> spooler(
133 ✗ upload::Spooler::Construct(sd));
134 ✗ assert(spooler.get() != nullptr);
135
136 const std::unique_ptr<manifest::Manifest> manifest(
137 catalog::WritableCatalogManager::CreateRepository(
138 ✗ dir_temp, volatile_content, voms_authz, spooler.get()));
139 ✗ if (manifest.get() == nullptr) {
140 ✗ PrintError("Swissknife Sync: Failed to create new repository");
141 ✗ return 1;
142 }
143
144 std::unique_ptr<manifest::Reflog> reflog(
145 ✗ CreateEmptyReflog(dir_temp, repo_name));
146 ✗ if (reflog.get() == nullptr) {
147 ✗ PrintError("Swissknife Sync: Failed to create fresh Reflog");
148 ✗ return 1;
149 }
150
151 ✗ reflog->DropDatabaseFileOwnership();
152 ✗ const string reflog_path = reflog->database_file();
153 ✗ reflog.reset();
154 ✗ shash::Any reflog_hash(hash_algorithm);
155 ✗ manifest::Reflog::HashDatabase(reflog_path, &reflog_hash);
156 ✗ spooler->UploadReflog(reflog_path);
157 ✗ spooler->WaitForUpload();
158 ✗ unlink(reflog_path.c_str());
159 ✗ if (spooler->GetNumberOfErrors()) {
160 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Swissknife Sync: Failed to upload reflog");
161 ✗ return 4;
162 }
163 ✗ assert(!reflog_chksum_path.empty());
164 ✗ manifest::Reflog::WriteChecksum(reflog_chksum_path, reflog_hash);
165
166 // set optional manifest fields
167 ✗ const bool needs_bootstrap_shortcuts = !voms_authz.empty();
168 ✗ manifest->set_garbage_collectability(garbage_collectable);
169 ✗ manifest->set_has_alt_catalog_path(needs_bootstrap_shortcuts);
170
171 ✗ if (!manifest->Export(manifest_path)) {
172 ✗ PrintError("Swissknife Sync: Failed to create new repository");
173 ✗ return 5;
174 }
175
176 ✗ return 0;
177 }
178
179 ✗ int swissknife::CommandUpload::Main(const swissknife::ArgumentList &args) {
180 ✗ const string source = *args.find('i')->second;
181 ✗ const string dest = *args.find('o')->second;
182 ✗ const string spooler_definition = *args.find('r')->second;
183 ✗ shash::Algorithms hash_algorithm = shash::kSha1;
184 ✗ if (args.find('a') != args.end()) {
185 ✗ hash_algorithm = shash::ParseHashAlgorithm(*args.find('a')->second);
186 ✗ if (hash_algorithm == shash::kAny) {
187 ✗ PrintError("Swissknife Sync: Unknown hash algorithm");
188 ✗ return 1;
189 }
190 }
191
192 ✗ const upload::SpoolerDefinition sd(spooler_definition, hash_algorithm);
193 ✗ upload::Spooler *spooler = upload::Spooler::Construct(sd);
194 ✗ assert(spooler);
195 ✗ spooler->Upload(source, dest);
196 ✗ spooler->WaitForUpload();
197
198 ✗ if (spooler->GetNumberOfErrors() > 0) {
199 ✗ LogCvmfs(kLogCatalog, kLogStderr, "Swissknife Sync: failed to upload %s",
200 source.c_str());
201 ✗ return 1;
202 }
203
204 ✗ delete spooler;
205
206 ✗ return 0;
207 }
208
209 ✗ int swissknife::CommandPeek::Main(const swissknife::ArgumentList &args) {
210 ✗ const string file_to_peek = *args.find('d')->second;
211 ✗ const string spooler_definition = *args.find('r')->second;
212
213 // Hash doesn't matter
214 ✗ const upload::SpoolerDefinition sd(spooler_definition, shash::kAny);
215 ✗ upload::Spooler *spooler = upload::Spooler::Construct(sd);
216 ✗ assert(spooler);
217 ✗ const bool success = spooler->Peek(file_to_peek);
218
219 ✗ if (spooler->GetNumberOfErrors() > 0) {
220 ✗ LogCvmfs(kLogCatalog, kLogStderr, "Swissknife Sync: failed to peek for %s",
221 file_to_peek.c_str());
222 ✗ return 2;
223 }
224 ✗ if (!success) {
225 ✗ LogCvmfs(kLogCatalog, kLogStdout, "Swissknife Sync: %s not found",
226 file_to_peek.c_str());
227 ✗ return 1;
228 }
229 ✗ LogCvmfs(kLogCatalog, kLogStdout, "Swissknife Sync: %s available",
230 file_to_peek.c_str());
231
232 ✗ delete spooler;
233
234 ✗ return 0;
235 }
236
237 ✗ int swissknife::CommandRemove::Main(const ArgumentList &args) {
238 ✗ const string file_to_delete = *args.find('o')->second;
239 ✗ const string spooler_definition = *args.find('r')->second;
240
241 // Hash doesn't matter
242 ✗ const upload::SpoolerDefinition sd(spooler_definition, shash::kAny);
243 ✗ upload::Spooler *spooler = upload::Spooler::Construct(sd);
244 ✗ assert(spooler);
245 ✗ spooler->RemoveAsync(file_to_delete);
246 ✗ spooler->WaitForUpload();
247
248 ✗ if (spooler->GetNumberOfErrors() > 0) {
249 ✗ LogCvmfs(kLogCatalog, kLogStderr, "Swissknife Sync: failed to delete %s",
250 file_to_delete.c_str());
251 ✗ return 1;
252 }
253
254 ✗ delete spooler;
255
256 ✗ return 0;
257 }
258
259 ✗ int swissknife::CommandApplyDirtab::Main(const ArgumentList &args) {
260 ✗ const string dirtab_file = *args.find('d')->second;
261 ✗ union_dir_ = MakeCanonicalPath(*args.find('u')->second);
262 ✗ scratch_dir_ = MakeCanonicalPath(*args.find('s')->second);
263 ✗ const shash::Any base_hash = shash::MkFromHexPtr(
264 ✗ shash::HexPtr(*args.find('b')->second), shash::kSuffixCatalog);
265 ✗ const string stratum0 = *args.find('w')->second;
266 ✗ const string dir_temp = *args.find('t')->second;
267 ✗ verbose_ = (args.find('x') != args.end());
268
269 // check if there is a dirtab file
270 ✗ if (!FileExists(dirtab_file)) {
271 ✗ LogCvmfs(kLogCatalog, kLogVerboseMsg,
272 "Swissknife Sync: Didn't find a dirtab at '%s'. Skipping...",
273 dirtab_file.c_str());
274 ✗ return 0;
275 }
276
277 // parse dirtab file
278 ✗ catalog::Dirtab *dirtab = catalog::Dirtab::Create(dirtab_file);
279 ✗ if (!dirtab->IsValid()) {
280 ✗ LogCvmfs(kLogCatalog, kLogStderr,
281 "Swissknife Sync: Invalid or not readable dirtab '%s'",
282 dirtab_file.c_str());
283 ✗ return 1;
284 }
285 ✗ LogCvmfs(kLogCatalog, kLogVerboseMsg,
286 "Swissknife Sync: Found %lu rules in dirtab '%s'",
287 dirtab->RuleCount(), dirtab_file.c_str());
288
289 // initialize catalog infrastructure
290 ✗ const bool auto_manage_catalog_files = true;
291 ✗ const bool follow_redirects = (args.count('L') > 0);
292 ✗ const string proxy = (args.count('@') > 0) ? *args.find('@')->second : "";
293 ✗ if (!InitDownloadManager(follow_redirects, proxy)) {
294 ✗ return 1;
295 }
296 catalog::SimpleCatalogManager catalog_manager(
297 base_hash, stratum0, dir_temp, download_manager(), statistics(),
298 ✗ auto_manage_catalog_files);
299 ✗ catalog_manager.Init();
300
301 ✗ vector<string> new_nested_catalogs;
302 ✗ DetermineNestedCatalogCandidates(*dirtab, &catalog_manager,
303 &new_nested_catalogs);
304 ✗ const bool success = CreateCatalogMarkers(new_nested_catalogs);
305 ✗ delete dirtab;
306
307 ✗ return (success) ? 0 : 1;
308 }
309
310
311 namespace {
312
313 // Overwrite directory traversal in the globbing in order to avoid breaking out
314 // the repository tree
315
316 std::string *g_glob_uniondir = NULL;
317
318 ✗ bool GlobCheckPath(const char *name) {
319 char resolved_cstr[PATH_MAX];
320 ✗ char *retval = realpath(name, resolved_cstr);
321 ✗ if (retval == NULL)
322 ✗ return false;
323
324 ✗ const std::string resolved(resolved_cstr);
325 ✗ if (resolved == *g_glob_uniondir)
326 ✗ return true;
327 ✗ if (!HasPrefix(resolved, (*g_glob_uniondir) + "/", false /*ignore_case*/)) {
328 ✗ errno = EACCES;
329 ✗ return false;
330 }
331 ✗ return true;
332 }
333
334 ✗ void *GlobOpendir(const char *name) {
335 ✗ if (!GlobCheckPath(name))
336 ✗ return NULL;
337 ✗ return opendir(name);
338 }
339
340 ✗ void GlobClosedir(void *dirp) { closedir(static_cast<DIR *>(dirp)); }
341
342 ✗ struct dirent *GlobReaddir(void *dirp) {
343 ✗ return readdir(static_cast<DIR *>(dirp));
344 }
345
346 ✗ int GlobLstat(const char *name, struct stat *st) {
347 ✗ if (!GlobCheckPath(name))
348 ✗ return -1;
349 ✗ return lstat(name, st);
350 }
351
352 ✗ int GlobStat(const char *name, struct stat *st) {
353 ✗ if (!GlobCheckPath(name))
354 ✗ return -1;
355 ✗ return stat(name, st);
356 }
357
358
359 } // anonymous namespace
360
361 ✗ void swissknife::CommandApplyDirtab::DetermineNestedCatalogCandidates(
362 const catalog::Dirtab &dirtab,
363 catalog::SimpleCatalogManager *catalog_manager,
364 vector<string> *nested_catalog_candidates) {
365 // find possible new nested catalog locations
366 ✗ const catalog::Dirtab::Rules &lookup_rules = dirtab.positive_rules();
367 ✗ catalog::Dirtab::Rules::const_iterator i = lookup_rules.begin();
368 ✗ const catalog::Dirtab::Rules::const_iterator iend = lookup_rules.end();
369 ✗ for (; i != iend; ++i) {
370 ✗ assert(!i->is_negation);
371
372 // run a glob using the current dirtab rule on the current repository
373 // state
374 ✗ const std::string &glob_string = i->pathspec.GetGlobString();
375 ✗ const std::string &glob_string_abs = union_dir_ + glob_string;
376 ✗ const int glob_flags = GLOB_ONLYDIR | GLOB_NOSORT | GLOB_PERIOD
377 | GLOB_ALTDIRFUNC;
378 glob_t glob_res;
379 ✗ g_glob_uniondir = new std::string(union_dir_);
380 ✗ glob_res.gl_opendir = GlobOpendir;
381 ✗ glob_res.gl_readdir = GlobReaddir;
382 ✗ glob_res.gl_closedir = GlobClosedir;
383 ✗ glob_res.gl_lstat = GlobLstat;
384 ✗ glob_res.gl_stat = GlobStat;
385 ✗ const int glob_retval = glob(glob_string_abs.c_str(), glob_flags, NULL,
386 &glob_res);
387 ✗ delete g_glob_uniondir;
388 ✗ g_glob_uniondir = NULL;
389
390 ✗ if (glob_retval == 0) {
391 // found some candidates... filtering by cvmfs catalog structure
392 LogCvmfs(kLogCatalog, kLogDebug,
393 "Swissknife Sync: Found %lu entries for pathspec (%s)",
394 glob_res.gl_pathc, glob_string.c_str());
395 ✗ FilterCandidatesFromGlobResult(dirtab, glob_res.gl_pathv,
396 glob_res.gl_pathc, catalog_manager,
397 nested_catalog_candidates);
398 ✗ } else if (glob_retval == GLOB_NOMATCH) {
399 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
400 "Swissknife Sync: WARNING: cannot apply pathspec %s",
401 glob_string.c_str());
402 } else {
403 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
404 "Swissknife Sync: Failed to run glob matching (%s)",
405 glob_string.c_str());
406 }
407
408 ✗ globfree(&glob_res);
409 }
410 }
411
412 ✗ void swissknife::CommandApplyDirtab::FilterCandidatesFromGlobResult(
413 const catalog::Dirtab &dirtab, char **paths, const size_t npaths,
414 catalog::SimpleCatalogManager *catalog_manager,
415 std::vector<std::string> *nested_catalog_candidates) {
416 // go through the paths produced by glob() and filter them
417 ✗ for (size_t i = 0; i < npaths; ++i) {
418 // process candidate paths
419 ✗ const std::string candidate(paths[i]);
420 ✗ const std::string candidate_rel = candidate.substr(union_dir_.size());
421
422 // check if path points to a directory
423 platform_stat64 candidate_info;
424 ✗ const int lstat_retval = platform_lstat(candidate.c_str(), &candidate_info);
425 ✗ if (lstat_retval != 0) {
426 ✗ LogCvmfs(kLogCatalog, kLogDebug | kLogStderr | kLogSyslogErr,
427 "Swissknife Sync: "
428 "Error in processing .cvmfsdirtab: cannot access %s (%d)",
429 candidate.c_str(), errno);
430 ✗ abort();
431 }
432 ✗ assert(lstat_retval == 0);
433 ✗ if (!S_ISDIR(candidate_info.st_mode)) {
434 // The GLOB_ONLYDIR flag is only a hint, non-directories can still be
435 // returned
436 LogCvmfs(kLogCatalog, kLogDebug,
437 "Swissknife Sync: "
438 "The '%s' dirtab entry does not point to a directory "
439 "but to a file or a symbolic link",
440 candidate_rel.c_str());
441 ✗ continue;
442 }
443
444 // check if the path is a meta-directory (. or ..)
445 ✗ assert(candidate_rel.size() >= 2);
446 ✗ if (candidate_rel.substr(candidate_rel.size() - 2) == "/."
447 ✗ || candidate_rel.substr(candidate_rel.size() - 3) == "/..") {
448 ✗ continue;
449 }
450
451 // check that the path isn't excluded in the dirtab
452 ✗ if (dirtab.IsOpposing(candidate_rel)) {
453 LogCvmfs(kLogCatalog, kLogDebug,
454 "Swissknife Sync: Candidate '%s' is excluded by dirtab",
455 candidate_rel.c_str());
456 ✗ continue;
457 }
458
459 // lookup the path in the catalog structure to find out if it already
460 // points to a nested catalog transition point. Furthermore it could be
461 // a new directory and thus not in any catalog yet.
462 ✗ catalog::DirectoryEntry dirent;
463 ✗ const bool lookup_success = catalog_manager->LookupPath(
464 candidate_rel, catalog::kLookupDefault, &dirent);
465 ✗ if (!lookup_success) {
466 LogCvmfs(kLogCatalog, kLogDebug,
467 "Swissknife Sync: Didn't find '%s' in catalogs, could "
468 "be a new directory and nested catalog.",
469 candidate_rel.c_str());
470 ✗ nested_catalog_candidates->push_back(candidate);
471 ✗ } else if (!dirent.IsNestedCatalogMountpoint()
472 ✗ && !dirent.IsNestedCatalogRoot()) {
473 LogCvmfs(kLogCatalog, kLogDebug,
474 "Swissknife Sync: Found '%s' in catalogs but is not a "
475 "nested catalog yet.",
476 candidate_rel.c_str());
477 ✗ nested_catalog_candidates->push_back(candidate);
478 } else {
479 // check if the nested catalog marker is still there, we might need to
480 // recreate the catalog after manual marker removal
481 // Note: First we check if the parent directory shows up in the scratch
482 // space to verify that it was touched (copy-on-write)
483 // Otherwise we would force the cvmfs client behind the union
484 // file-
485 // system to (potentially) unnecessarily fetch catalogs
486 ✗ if (DirectoryExists(scratch_dir_ + candidate_rel)
487 ✗ && !FileExists(union_dir_ + candidate_rel + "/.cvmfscatalog")) {
488 ✗ LogCvmfs(kLogCatalog, kLogStdout,
489 "Swissknife Sync: WARNING: '%s' should be a nested "
490 "catalog according to the dirtab. "
491 "Recreating...",
492 candidate_rel.c_str());
493 ✗ nested_catalog_candidates->push_back(candidate);
494 } else {
495 LogCvmfs(kLogCatalog, kLogDebug,
496 "Swissknife Sync: "
497 "Found '%s' in catalogs and it already is a nested catalog.",
498 candidate_rel.c_str());
499 }
500 }
501 }
502 }
503
504 ✗ bool swissknife::CommandApplyDirtab::CreateCatalogMarkers(
505 const std::vector<std::string> &new_nested_catalogs) {
506 // go through the new nested catalog paths and create .cvmfscatalog markers
507 // where necessary
508 ✗ bool success = true;
509 ✗ std::vector<std::string>::const_iterator k = new_nested_catalogs.begin();
510 const std::vector<std::string>::const_iterator kend = new_nested_catalogs
511 ✗ .end();
512 ✗ for (; k != kend; ++k) {
513 ✗ assert(!k->empty() && k->size() > union_dir_.size());
514
515 // was the marker already created by hand?
516 ✗ const std::string marker_path = *k + "/.cvmfscatalog";
517 ✗ if (FileExists(marker_path)) {
518 ✗ continue;
519 }
520
521 // create a nested catalog marker
522 ✗ const mode_t mode = kDefaultFileMode;
523 ✗ const int fd = open(marker_path.c_str(), O_CREAT, mode);
524 ✗ if (fd < 0) {
525 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
526 "Swissknife Sync: Failed to create nested catalog marker "
527 "at '%s' (errno: %d)",
528 marker_path.c_str(), errno);
529 ✗ success = false;
530 ✗ continue;
531 }
532 ✗ close(fd);
533
534 // inform the user if requested
535 ✗ if (verbose_) {
536 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
537 "Swissknife Sync: Auto-creating nested catalog in %s",
538 k->c_str());
539 }
540 }
541
542 ✗ return success;
543 }
544
545 struct chunk_arg {
546 ✗ chunk_arg(char param, size_t *save_to) : param(param), save_to(save_to) { }
547 char param;
548 size_t *save_to;
549 };
550
551 ✗ bool swissknife::CommandSync::ReadFileChunkingArgs(
552 const swissknife::ArgumentList &args, SyncParameters *params) {
553 typedef std::vector<chunk_arg> ChunkArgs;
554
555 // define where to store the value of which file chunk argument
556 ✗ ChunkArgs chunk_args;
557 ✗ chunk_args.push_back(chunk_arg('a', &params->avg_file_chunk_size));
558 ✗ chunk_args.push_back(chunk_arg('l', &params->min_file_chunk_size));
559 ✗ chunk_args.push_back(chunk_arg('h', &params->max_file_chunk_size));
560
561 // read the arguments
562 ✗ ChunkArgs::const_iterator i = chunk_args.begin();
563 ✗ const ChunkArgs::const_iterator iend = chunk_args.end();
564 ✗ for (; i != iend; ++i) {
565 ✗ const swissknife::ArgumentList::const_iterator arg = args.find(i->param);
566
567 ✗ if (arg != args.end()) {
568 ✗ const size_t arg_value = static_cast<size_t>(String2Uint64(*arg->second));
569 ✗ if (arg_value > 0) {
570 ✗ *i->save_to = arg_value;
571 } else {
572 ✗ return false;
573 }
574 }
575 }
576
577 // check if argument values are sane
578 ✗ return true;
579 }
580
581 ✗ int swissknife::CommandSync::Main(const swissknife::ArgumentList &args) {
582 ✗ const string start_time = GetGMTimestamp();
583
584 // Spawn monitoring process (watchdog)
585 ✗ const std::string watchdog_dir = "/tmp";
586 char watchdog_path[PATH_MAX];
587 ✗ const std::string timestamp = GetGMTimestamp("%Y.%m.%d-%H.%M.%S");
588 ✗ const int path_size = snprintf(watchdog_path, sizeof(watchdog_path),
589 "%s/cvmfs-swissknife-sync-stacktrace.%s.%d",
590 watchdog_dir.c_str(), timestamp.c_str(),
591 getpid());
592 ✗ assert(path_size > 0);
593 ✗ assert(path_size < PATH_MAX);
594 const std::unique_ptr<Watchdog> watchdog(
595 ✗ Watchdog::Create(NULL, false /* needs_read_environ */));
596 ✗ watchdog->Spawn(std::string(watchdog_path));
597
598 ✗ SyncParameters params;
599
600 // Initialization
601 ✗ params.dir_union = MakeCanonicalPath(*args.find('u')->second);
602 ✗ params.dir_scratch = MakeCanonicalPath(*args.find('s')->second);
603 ✗ params.dir_rdonly = MakeCanonicalPath(*args.find('c')->second);
604 ✗ params.dir_temp = MakeCanonicalPath(*args.find('t')->second);
605 ✗ params.base_hash = shash::MkFromHexPtr(shash::HexPtr(*args.find('b')->second),
606 shash::kSuffixCatalog);
607 ✗ params.stratum0 = *args.find('w')->second;
608 ✗ params.manifest_path = *args.find('o')->second;
609 ✗ params.spooler_definition = *args.find('r')->second;
610
611 ✗ params.public_keys = *args.find('K')->second;
612 ✗ params.repo_name = *args.find('N')->second;
613
614 ✗ params.ttl_seconds = catalog::Catalog::kDefaultTTL;
615
616 ✗ if (args.find('f') != args.end())
617 ✗ params.union_fs_type = *args.find('f')->second;
618 ✗ if (args.find('A') != args.end())
619 ✗ params.is_balanced = true;
620 ✗ if (args.find('x') != args.end())
621 ✗ params.print_changeset = true;
622 ✗ if (args.find('y') != args.end())
623 ✗ params.dry_run = true;
624 ✗ if (args.find('m') != args.end())
625 ✗ params.mucatalogs = true;
626 ✗ if (args.find('i') != args.end())
627 ✗ params.ignore_xdir_hardlinks = true;
628 ✗ if (args.find('d') != args.end())
629 ✗ params.stop_for_catalog_tweaks = true;
630 ✗ if (args.find('V') != args.end())
631 ✗ params.voms_authz = true;
632 ✗ if (args.find('F') != args.end())
633 ✗ params.authz_file = *args.find('F')->second;
634 ✗ if (args.find('k') != args.end())
635 ✗ params.include_xattrs = true;
636 ✗ if (args.find('j') != args.end())
637 ✗ params.enable_mtime_ns = true;
638 ✗ if (args.find('Y') != args.end())
639 ✗ params.external_data = true;
640 ✗ if (args.find('W') != args.end())
641 ✗ params.direct_io = true;
642 ✗ if (args.find('S') != args.end()) {
643 ✗ const bool retval = catalog::VirtualCatalog::ParseActions(
644 ✗ *args.find('S')->second, &params.virtual_dir_actions);
645 ✗ if (!retval) {
646 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
647 "Swissknife Sync: Invalid virtual catalog options: %s",
648 args.find('S')->second->c_str());
649 ✗ return 1;
650 }
651 }
652 ✗ if (args.find('z') != args.end()) {
653 ✗ const unsigned log_level = 1 << (kLogLevel0
654 ✗ + String2Uint64(*args.find('z')->second));
655 ✗ if (log_level > kLogNone) {
656 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Swissknife Sync: invalid log level");
657 ✗ return 1;
658 }
659 ✗ SetLogVerbosity(static_cast<LogLevels>(log_level));
660 }
661
662 ✗ if (args.find('X') != args.end())
663 ✗ params.max_weight = String2Uint64(*args.find('X')->second);
664 ✗ if (args.find('M') != args.end())
665 ✗ params.min_weight = String2Uint64(*args.find('M')->second);
666
667 ✗ if (args.find('p') != args.end()) {
668 ✗ params.use_file_chunking = true;
669 ✗ if (!ReadFileChunkingArgs(args, &params)) {
670 ✗ PrintError("Swissknife Sync: Failed to read file chunk size values");
671 ✗ return 2;
672 }
673 }
674 ✗ if (args.find('O') != args.end()) {
675 ✗ params.generate_legacy_bulk_chunks = true;
676 }
677 ✗ shash::Algorithms hash_algorithm = shash::kSha1;
678 ✗ if (args.find('e') != args.end()) {
679 ✗ hash_algorithm = shash::ParseHashAlgorithm(*args.find('e')->second);
680 ✗ if (hash_algorithm == shash::kAny) {
681 ✗ PrintError("Swissknife Sync: Unknown hash algorithm");
682 ✗ return 1;
683 }
684 }
685 ✗ if (args.find('Z') != args.end()) {
686 ✗ params.compression_alg = zlib::ParseCompressionAlgorithm(
687 ✗ *args.find('Z')->second);
688 }
689
690 ✗ if (args.find('E') != args.end())
691 ✗ params.enforce_limits = true;
692 ✗ if (args.find('Q') != args.end()) {
693 ✗ params.nested_kcatalog_limit = String2Uint64(*args.find('Q')->second);
694 } else {
695 ✗ params.nested_kcatalog_limit = SyncParameters::kDefaultNestedKcatalogLimit;
696 }
697 ✗ if (args.find('R') != args.end()) {
698 ✗ params.root_kcatalog_limit = String2Uint64(*args.find('R')->second);
699 } else {
700 ✗ params.root_kcatalog_limit = SyncParameters::kDefaultRootKcatalogLimit;
701 }
702 ✗ if (args.find('U') != args.end()) {
703 ✗ params.file_mbyte_limit = String2Uint64(*args.find('U')->second);
704 } else {
705 ✗ params.file_mbyte_limit = SyncParameters::kDefaultFileMbyteLimit;
706 }
707
708 ✗ if (args.find('v') != args.end()) {
709 ✗ const sanitizer::IntegerSanitizer sanitizer;
710 ✗ if (!sanitizer.IsValid(*args.find('v')->second)) {
711 ✗ PrintError("Swissknife Sync: Invalid revision number");
712 ✗ return 1;
713 }
714 ✗ params.manual_revision = String2Uint64(*args.find('v')->second);
715 }
716
717 ✗ params.branched_catalog = args.find('B') != args.end();
718
719 ✗ if (args.find('q') != args.end()) {
720 ✗ params.max_concurrent_write_jobs = String2Uint64(*args.find('q')->second);
721 }
722
723 ✗ if (args.find('0') != args.end()) {
724 ✗ params.num_upload_tasks = String2Uint64(*args.find('0')->second);
725 }
726
727 ✗ if (args.find('T') != args.end()) {
728 ✗ params.ttl_seconds = String2Uint64(*args.find('T')->second);
729 }
730
731 ✗ if (args.find('g') != args.end()) {
732 ✗ params.ignore_special_files = true;
733 }
734
735 ✗ if (args.find('P') != args.end()) {
736 ✗ params.session_token_file = *args.find('P')->second;
737 }
738
739 ✗ if (args.find('H') != args.end()) {
740 ✗ params.key_file = *args.find('H')->second;
741 }
742
743 ✗ if (args.find('D') != args.end()) {
744 ✗ params.repo_tag.SetName(*args.find('D')->second);
745 }
746
747 ✗ if (args.find('J') != args.end()) {
748 ✗ params.repo_tag.SetDescription(*args.find('J')->second);
749 }
750
751 ✗ if (args.find('C') != args.end()) {
752 ✗ params.repo_tag.SetAutoTagThreshold(
753 ✗ static_cast<time_t>(String2Int64(*args.find('C')->second)));
754 }
755
756 ✗ if (args.find('G') != args.end()) {
757 ✗ params.cache_dir = "/var/spool/cvmfs/" + params.repo_name + "/cache.server";
758 }
759
760 ✗ const bool upload_statsdb = (args.count('I') > 0);
761
762 ✗ if (!CheckParams(params))
763 ✗ return 2;
764 // This may fail, in which case a warning is printed and the process continues
765 ✗ ObtainDacReadSearchCapability();
766
767 ✗ perf::StatisticsTemplate publish_statistics("publish", this->statistics());
768
769 // Start spooler
770 upload::SpoolerDefinition spooler_definition(
771 params.spooler_definition, hash_algorithm, params.compression_alg,
772 ✗ params.generate_legacy_bulk_chunks, params.use_file_chunking,
773 params.min_file_chunk_size, params.avg_file_chunk_size,
774 ✗ params.max_file_chunk_size, params.session_token_file, params.key_file);
775 ✗ if (params.max_concurrent_write_jobs > 0) {
776 spooler_definition
777 ✗ .number_of_concurrent_uploads = params.max_concurrent_write_jobs;
778 }
779 ✗ spooler_definition.num_upload_tasks = params.num_upload_tasks;
780
781 const upload::SpoolerDefinition spooler_definition_catalogs(
782 ✗ spooler_definition.Dup2DefaultCompression());
783
784 ✗ params.spooler = upload::Spooler::Construct(spooler_definition,
785 &publish_statistics);
786 ✗ if (NULL == params.spooler)
787 ✗ return 3;
788 const std::unique_ptr<upload::Spooler> spooler_catalogs(
789 upload::Spooler::Construct(spooler_definition_catalogs,
790 ✗ &publish_statistics));
791 ✗ if (spooler_catalogs.get() == nullptr)
792 ✗ return 3;
793
794 ✗ const bool follow_redirects = (args.count('L') > 0);
795 ✗ const string proxy = (args.count('@') > 0) ? *args.find('@')->second : "";
796 ✗ if (!InitDownloadManager(follow_redirects, proxy)) {
797 ✗ return 3;
798 }
799
800 ✗ if (!InitSignatureManager(params.public_keys)) {
801 ✗ return 3;
802 }
803
804 /*
805 * Note: If the upstream is of type gateway, due to the possibility of
806 * concurrent release managers, it's possible to have a different local and
807 * remote root hashes. We proceed by loading the remote manifest but we give
808 * an empty base hash.
809 */
810 ✗ std::unique_ptr<manifest::Manifest> manifest;
811 ✗ if (params.branched_catalog) {
812 // Throw-away manifest
813 ✗ manifest.reset(new manifest::Manifest(shash::Any(), 0, ""));
814 ✗ } else if (params.virtual_dir_actions
815 ✗ != catalog::VirtualCatalog::kActionNone) {
816 ✗ manifest.reset(this->OpenLocalManifest(params.manifest_path));
817 ✗ params.base_hash = manifest->catalog_hash();
818 } else {
819 // TODO(jblomer): revert to params.base_hash if spooler driver type is not
820 // upload::SpoolerDefinition::Gateway
821 ✗ manifest.reset(
822 ✗ FetchRemoteManifest(params.stratum0, params.repo_name, shash::Any()));
823 }
824 ✗ if (manifest.get() == nullptr) {
825 ✗ return 3;
826 }
827
828 ✗ StatisticsDatabase *stats_db = StatisticsDatabase::OpenStandardDB(
829 params.repo_name);
830
831 ✗ const std::string old_root_hash = manifest->catalog_hash().ToString(true);
832
833 catalog::WritableCatalogManager catalog_manager(
834 params.base_hash, params.stratum0, params.dir_temp,
835 ✗ spooler_catalogs.get(), download_manager(), params.enforce_limits,
836 params.nested_kcatalog_limit, params.root_kcatalog_limit,
837 ✗ params.file_mbyte_limit, statistics(), params.is_balanced,
838 ✗ params.max_weight, params.min_weight, params.cache_dir);
839 ✗ catalog_manager.Init();
840
841 ✗ publish::SyncMediator mediator(&catalog_manager, &params, publish_statistics);
842 ✗ LogCvmfs(kLogPublish, kLogStdout, "Swissknife Sync: Processing changes...");
843
844 // Should be before the synchronization starts to avoid race of GetTTL with
845 // other sqlite operations
846 ✗ if ((params.ttl_seconds > 0)
847 ✗ && ((params.ttl_seconds != catalog_manager.GetTTL())
848 ✗ || !catalog_manager.HasExplicitTTL())) {
849 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
850 "Swissknife Sync: Setting repository TTL to %" PRIu64 "s",
851 params.ttl_seconds);
852 ✗ catalog_manager.SetTTL(params.ttl_seconds);
853 }
854
855 // Either real catalogs or virtual catalog
856 ✗ if (params.virtual_dir_actions == catalog::VirtualCatalog::kActionNone) {
857 publish::SyncUnion *sync;
858 ✗ if (params.union_fs_type == "overlayfs") {
859 ✗ sync = new publish::SyncUnionOverlayfs(
860 ✗ &mediator, params.dir_rdonly, params.dir_union, params.dir_scratch);
861 ✗ } else if (params.union_fs_type == "aufs") {
862 ✗ sync = new publish::SyncUnionAufs(&mediator, params.dir_rdonly,
863 ✗ params.dir_union, params.dir_scratch);
864 } else {
865 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
866 "Swissknife Sync: unknown union file system: %s",
867 params.union_fs_type.c_str());
868 ✗ return 3;
869 }
870
871 ✗ if (!sync->Initialize()) {
872 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
873 "Swissknife Sync: Initialization of the synchronisation "
874 "engine failed");
875 ✗ return 4;
876 }
877
878 ✗ sync->Traverse();
879 } else {
880 ✗ assert(!manifest->history().IsNull());
881 catalog::VirtualCatalog virtual_catalog(manifest.get(), download_manager(),
882 ✗ &catalog_manager, &params);
883 ✗ virtual_catalog.Generate(params.virtual_dir_actions);
884 }
885
886 ✗ if (!params.authz_file.empty()) {
887 LogCvmfs(kLogCvmfs, kLogDebug,
888 "Swissknife Sync: Adding contents of authz file %s to"
889 " root catalog.",
890 params.authz_file.c_str());
891 ✗ const int fd = open(params.authz_file.c_str(), O_RDONLY);
892 ✗ if (fd == -1) {
893 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
894 "Swissknife Sync: Unable to open authz file (%s)"
895 "from the publication process: %s",
896 params.authz_file.c_str(), strerror(errno));
897 ✗ return 7;
898 }
899
900 ✗ std::string new_authz;
901 ✗ const bool read_successful = SafeReadToString(fd, &new_authz);
902 ✗ close(fd);
903
904 ✗ if (!read_successful) {
905 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
906 "Swissknife Sync: Failed to read authz file (%s): %s",
907 params.authz_file.c_str(), strerror(errno));
908 ✗ return 8;
909 }
910
911 ✗ catalog_manager.SetVOMSAuthz(new_authz);
912 }
913
914 ✗ if (!mediator.Commit(manifest.get())) {
915 ✗ PrintError("Swissknife Sync: Something went wrong during sync");
916 ✗ if (!params.dry_run) {
917 ✗ stats_db->StorePublishStatistics(this->statistics(), start_time, false);
918 ✗ if (upload_statsdb) {
919 ✗ stats_db->UploadStatistics(params.spooler);
920 }
921 }
922 ✗ return 5;
923 }
924
925 ✗ perf::Counter *revision_counter = statistics()->Register(
926 "publish.revision", "Published revision number");
927 ✗ revision_counter->Set(
928 ✗ static_cast<int64_t>(catalog_manager.GetRootCatalog()->revision()));
929
930 // finalize the spooler
931 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
932 "Swissknife Sync: Wait for all uploads to finish");
933 ✗ params.spooler->WaitForUpload();
934 ✗ spooler_catalogs->WaitForUpload();
935 ✗ params.spooler->FinalizeSession(false);
936
937 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
938 "Swissknife Sync: Exporting repository manifest");
939
940 // We call FinalizeSession(true) this time, to also trigger the commit
941 // operation on the gateway machine (if the upstream is of type "gw").
942
943 // Get the path of the new root catalog
944 ✗ const std::string new_root_hash = manifest->catalog_hash().ToString(true);
945
946 ✗ if (!spooler_catalogs->FinalizeSession(true, old_root_hash, new_root_hash,
947 params.repo_tag)) {
948 ✗ PrintError("Swissknife Sync: Failed to commit transaction.");
949 ✗ if (!params.dry_run) {
950 ✗ stats_db->StorePublishStatistics(this->statistics(), start_time, false);
951 ✗ if (upload_statsdb) {
952 ✗ stats_db->UploadStatistics(params.spooler);
953 }
954 }
955 ✗ return 9;
956 }
957
958 ✗ if (!params.dry_run) {
959 ✗ stats_db->StorePublishStatistics(this->statistics(), start_time, true);
960 ✗ if (upload_statsdb) {
961 ✗ stats_db->UploadStatistics(params.spooler);
962 }
963 }
964
965 ✗ delete params.spooler;
966
967 ✗ if (!manifest->Export(params.manifest_path)) {
968 ✗ PrintError("Swissknife Sync: Failed to create new repository");
969 ✗ return 6;
970 }
971
972 ✗ return 0;
973 }
974