GCC Code Coverage Report


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