GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_pull.cc
Date: 2026-10-25 02:40:14
Exec Total Coverage
Lines: 0 541 0.0%
Branches: 0 434 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 *
4 * Replicates a cvmfs repository. Uses the cvmfs intrinsic Merkle trees
5 * to calculate the difference set.
6 */
7
8 // NOLINTNEXTLINE
9 #define _FILE_OFFSET_BITS 64
10
11
12 #include "swissknife_pull.h"
13
14 #include <inttypes.h>
15 #include <pthread.h>
16 #include <sys/stat.h>
17 #include <unistd.h>
18
19 #include <atomic>
20 #include <cstdlib>
21 #include <cstring>
22 #include <string>
23 #include <vector>
24
25 #include "catalog.h"
26 #include "compression/compression.h"
27 #include "crypto/hash.h"
28 #include "history_sqlite.h"
29 #include "manifest.h"
30 #include "manifest_fetch.h"
31 #include "network/download.h"
32 #include "network/sink_mem.h"
33 #include "network/sink_path.h"
34 #include "object_fetcher.h"
35 #include "path_filters/inclusion_spec.h"
36 #include "path_filters/relaxed_path_filter.h"
37 #include "reflog.h"
38 #include "upload.h"
39 #include "util/exception.h"
40 #include "util/logging.h"
41 #include "util/posix.h"
42 #include "util/shared_ptr.h"
43 #include "util/smalloc.h"
44 #include "util/string.h"
45
46 using namespace std; // NOLINT
47
48 namespace swissknife {
49
50 namespace {
51
52 typedef HttpObjectFetcher<> ObjectFetcher;
53
54 /**
55 * This just stores an shash::Any in a predictable way to send it through a
56 * POSIX pipe.
57 */
58 class ChunkJob {
59 public:
60 ✗ ChunkJob()
61 ✗ : suffix(shash::kSuffixNone)
62 ✗ , hash_algorithm(shash::kAny)
63 ✗ , compression_alg(zlib::kZlibDefault) { }
64
65 ✗ ChunkJob(const shash::Any &hash, zlib::Algorithms compression_alg)
66 ✗ : suffix(hash.suffix)
67 ✗ , hash_algorithm(hash.algorithm)
68 ✗ , compression_alg(compression_alg) {
69 ✗ memcpy(digest, hash.digest, hash.GetDigestSize());
70 }
71
72 ✗ bool IsTerminateJob() const { return (hash_algorithm == shash::kAny); }
73
74 ✗ shash::Any hash() const {
75 ✗ assert(!IsTerminateJob());
76 ✗ return shash::Any(hash_algorithm, digest, suffix);
77 }
78
79 const shash::Suffix suffix;
80 const shash::Algorithms hash_algorithm;
81 const zlib::Algorithms compression_alg;
82 unsigned char digest[shash::kMaxDigestSize];
83 };
84
85 ✗ static void SpoolerOnUpload(const upload::SpoolerResult &result) {
86 ✗ unlink(result.local_path.c_str());
87 ✗ if (result.return_code != 0) {
88 ✗ PANIC(kLogStderr, "spooler failure %d (%s, hash: %s)", result.return_code,
89 result.local_path.c_str(), result.content_hash.ToString().c_str());
90 }
91 }
92
93 SharedPtr<string> stratum0_url;
94 SharedPtr<string> stratum1_url;
95 SharedPtr<string> temp_dir;
96 unsigned num_parallel = 1;
97 bool pull_history = false;
98 bool apply_timestamp_threshold = false;
99 uint64_t timestamp_threshold = 0;
100 bool is_garbage_collectable = false;
101 bool initial_snapshot = false;
102 upload::Spooler *spooler = NULL;
103 int pipe_chunks[2];
104 // required for concurrent reading
105 pthread_mutex_t lock_pipe = PTHREAD_MUTEX_INITIALIZER;
106 unsigned retries = 3;
107 catalog::RelaxedPathFilter *pathfilter = NULL;
108 catalog::InclusionSpec *inclusion_spec = NULL;
109 std::atomic<int64_t> overall_chunks;
110 std::atomic<int64_t> overall_new;
111 std::atomic<int64_t> chunk_queue;
112 bool preload_cache = false;
113 string *preload_cachedir = NULL;
114 bool inspect_existing_catalogs = false;
115 // Set once a revisited history chain reaches a catalog that is already
116 // present. Older revisions missing locally were removed by the replica's gc.
117 bool reached_existing_history = false;
118 // Upstream path of a local backend; empty for other backends
119 string local_upstream_path;
120 manifest::Reflog *reflog = NULL;
121
122 } // anonymous namespace
123
124
125 ✗ static std::string MakePath(const shash::Any &hash) {
126 return (preload_cache)
127 ✗ ? *preload_cachedir + "/" + hash.MakePathWithoutSuffix()
128 ✗ : "data/" + hash.MakePath();
129 }
130
131
132 ✗ static bool Peek(const string &remote_path) {
133 ✗ return (preload_cache) ? FileExists(remote_path) : spooler->Peek(remote_path);
134 }
135
136 ✗ static bool Peek(const shash::Any &remote_hash) {
137 ✗ return Peek(MakePath(remote_hash));
138 }
139
140 ✗ static void ReportDownloadError(const download::JobInfo &download_job) {
141 ✗ const download::Failures error_code = download_job.error_code();
142 ✗ const int http_code = download_job.http_code();
143 ✗ const std::string url = *download_job.url();
144
145 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to download %s (%d - %s)",
146 url.c_str(), error_code, download::Code2Ascii(error_code));
147
148 ✗ switch (error_code) {
149 ✗ case download::kFailProxyResolve:
150 case download::kFailHostResolve:
151 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
152 "DNS lookup for Stratum 0 failed - "
153 "please check the network connection");
154 ✗ break;
155
156 ✗ case download::kFailHostHttp:
157 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
158 "unexpected HTTP error code %d - "
159 "please check the stratum 0 health",
160 http_code);
161 ✗ break;
162
163 ✗ case download::kFailBadData:
164 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
165 "downloaded corrupted data - "
166 "please check the stratum 0 health");
167 ✗ break;
168
169 ✗ default:
170 ✗ if (download::IsProxyTransferError(error_code)
171 ✗ || download::IsHostTransferError(error_code)) {
172 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
173 "couldn't reach Stratum 0 - "
174 "please check the network connection");
175 } else {
176 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
177 "unexpected error - feel free to file "
178 "a bug report");
179 }
180 }
181 }
182
183
184 ✗ static void Store(const string &local_path,
185 const string &remote_path,
186 const bool compressed_src) {
187 ✗ if (preload_cache) {
188 ✗ if (!compressed_src) {
189 ✗ const int retval = rename(local_path.c_str(), remote_path.c_str());
190 ✗ if (retval != 0) {
191 ✗ PANIC(kLogStderr, "Failed to move '%s' to '%s'", local_path.c_str(),
192 remote_path.c_str());
193 }
194 } else {
195 // compressed input
196 ✗ string tmp_dest;
197 ✗ FILE *fdest = CreateTempFile(remote_path, 0660, "w", &tmp_dest);
198 ✗ if (fdest == NULL) {
199 ✗ PANIC(kLogStderr, "Failed to create temporary file '%s'",
200 remote_path.c_str());
201 }
202 ✗ int retval = zlib::DecompressPath2File(local_path, fdest);
203 ✗ if (!retval) {
204 ✗ PANIC(kLogStderr, "Failed to preload %s to %s", local_path.c_str(),
205 remote_path.c_str());
206 }
207 ✗ fclose(fdest);
208 ✗ retval = rename(tmp_dest.c_str(), remote_path.c_str());
209 ✗ assert(retval == 0);
210 ✗ unlink(local_path.c_str());
211 }
212 } else {
213 ✗ spooler->Upload(local_path, remote_path);
214 }
215 }
216
217 ✗ static void Store(const string &local_path,
218 const shash::Any &remote_hash,
219 const bool compressed_src = true) {
220 ✗ Store(local_path, MakePath(remote_hash), compressed_src);
221 }
222
223
224 ✗ static void StoreBuffer(const unsigned char *buffer, const unsigned size,
225 const std::string &dest_path, const bool compress) {
226 ✗ string tmp_file;
227 ✗ FILE *ftmp = CreateTempFile(*temp_dir + "/cvmfs", 0600, "w", &tmp_file);
228 ✗ assert(ftmp);
229 int retval;
230 ✗ if (compress) {
231 ✗ shash::Any dummy(shash::kSha1); // hardcoded hash no problem, unused
232 ✗ retval = zlib::CompressMem2File(buffer, size, ftmp, &dummy);
233 } else {
234 ✗ retval = CopyMem2File(buffer, size, ftmp);
235 }
236 ✗ assert(retval);
237 ✗ fclose(ftmp);
238 ✗ Store(tmp_file, dest_path, true);
239 }
240
241 ✗ static void StoreBuffer(const unsigned char *buffer, const unsigned size,
242 const shash::Any &dest_hash, const bool compress) {
243 ✗ StoreBuffer(buffer, size, MakePath(dest_hash), compress);
244 }
245
246
247 ✗ static void WaitForStorage() {
248 ✗ if (!preload_cache)
249 ✗ spooler->WaitForUpload();
250 }
251
252
253 struct MainWorkerContext {
254 download::DownloadManager *download_manager;
255 };
256
257 ✗ static void *MainWorker(void *data) {
258 ✗ MainWorkerContext *mwc = static_cast<MainWorkerContext *>(data);
259 ✗ download::DownloadManager *download_manager = mwc->download_manager;
260
261 while (1) {
262 ✗ ChunkJob next_chunk;
263 {
264 ✗ const MutexLockGuard m(&lock_pipe);
265 ✗ ReadPipe(pipe_chunks[0], &next_chunk, sizeof(next_chunk));
266 }
267 ✗ if (next_chunk.IsTerminateJob())
268 ✗ break;
269
270 ✗ const shash::Any chunk_hash = next_chunk.hash();
271 ✗ const zlib::Algorithms compression_alg = next_chunk.compression_alg;
272 ✗ LogCvmfs(kLogCvmfs, kLogVerboseMsg, "processing chunk %s",
273 chunk_hash.ToString().c_str());
274
275 ✗ if (!Peek(chunk_hash)) {
276 ✗ string tmp_file;
277 ✗ FILE *fchunk = CreateTempFile(*temp_dir + "/cvmfs", 0600, "w", &tmp_file);
278 ✗ const string url_chunk = *stratum0_url + "/data/" + chunk_hash.MakePath();
279 ✗ cvmfs::FileSink filesink(fchunk);
280 download::JobInfo download_chunk(&url_chunk, false, false, &chunk_hash,
281 ✗ &filesink);
282
283 ✗ const download::Failures download_result = download_manager->Fetch(
284 &download_chunk);
285 ✗ if (download_result != download::kFailOk) {
286 ✗ ReportDownloadError(download_chunk);
287 ✗ PANIC(kLogStderr, "Download error");
288 }
289 ✗ fclose(fchunk);
290 ✗ Store(tmp_file, chunk_hash,
291 (compression_alg == zlib::kZlibDefault) ? true : false);
292 overall_new.fetch_add(1);
293 }
294 ✗ if (overall_chunks.fetch_add(1) % 1000 == 0)
295 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, ".");
296 chunk_queue.fetch_sub(1);
297 }
298 ✗ return NULL;
299 }
300
301
302 ✗ bool CommandPull::PullRecursion(catalog::Catalog *catalog,
303 const std::string &path) {
304 ✗ assert(catalog);
305
306
307 // Nested catalogs (in a nested code block because goto fail...)
308 {
309 const catalog::Catalog::NestedCatalogList
310 ✗ nested_catalogs = catalog->ListOwnNestedCatalogs();
311 ✗ for (catalog::Catalog::NestedCatalogList::const_iterator
312 ✗ i = nested_catalogs.begin(),
313 ✗ iEnd = nested_catalogs.end();
314 ✗ i != iEnd;
315 ✗ ++i) {
316 // Partial replication: prune entire subtrees that contain no included
317 // path. IsExcluded() is true exactly when the subtree rooted at this
318 // mountpoint is disjoint from the inclusion set, so neither this nested
319 // catalog nor any of its descendants need to be replicated. Ancestor
320 // catalogs on the way to an included subtree are *not* excluded and are
321 // therefore still pulled and recursed into.
322 ✗ if (inclusion_spec
323 ✗ && inclusion_spec->IsExcluded(i->mountpoint.ToString())) {
324 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
325 "Pruning excluded subtree (catalog + objects) at %s",
326 i->mountpoint.c_str());
327 ✗ continue;
328 }
329 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Replicating from catalog at %s",
330 i->mountpoint.c_str());
331 ✗ shash::Any previous_catalog_hash; // expected to be null for subcatalog
332 ✗ const bool retval = Pull(i->hash, i->mountpoint.ToString(),
333 previous_catalog_hash);
334 ✗ if (!retval)
335 ✗ return false;
336 }
337 }
338
339 ✗ return true;
340 }
341
342 ✗ bool CommandPull::Pull(const shash::Any &catalog_hash,
343 const std::string &path,
344 shash::Any &previous_catalog,
345 bool is_historic_catalog) {
346 int retval;
347 download::Failures dl_retval;
348 ✗ assert(shash::kSuffixCatalog == catalog_hash.suffix);
349 ✗ previous_catalog.SetNull();
350
351 // With a changed inclusion spec, existing catalogs may contain objects
352 // that were previously excluded. Revisit them and fetch missing chunks.
353 ✗ const bool catalog_exists = Peek(catalog_hash);
354 ✗ const bool revisit = inspect_existing_catalogs && !preload_cache;
355 ✗ if (revisit && path.empty()) {
356 ✗ if (catalog_exists) {
357 ✗ reached_existing_history = true;
358 ✗ } else if (is_historic_catalog && reached_existing_history) {
359 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
360 " Not restoring collected historic catalog %s",
361 catalog_hash.ToString().c_str());
362 ✗ return true;
363 }
364 }
365 ✗ if (catalog_exists && !revisit) {
366 ✗ if (inspect_existing_catalogs) {
367 ✗ catalog::Catalog *catalog = catalog::Catalog::AttachFreely(
368 ✗ path, MakePath(catalog_hash), catalog_hash);
369 ✗ if (catalog == NULL) {
370 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to attach catalog %s",
371 catalog_hash.ToString().c_str());
372 ✗ return false;
373 }
374 ✗ const bool retval = PullRecursion(catalog, path);
375 ✗ delete catalog;
376 ✗ return retval;
377 }
378
379 ✗ LogCvmfs(kLogCvmfs, kLogStdout, " Catalog up to date");
380 ✗ return true;
381 }
382
383 // Check if the catalog matches the pathfilter
384 ✗ if (path != "" && // necessary to load the root catalog
385 ✗ pathfilter && !pathfilter->IsMatching(path)) {
386 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
387 " Catalog in '%s' does not match"
388 " the path specification",
389 path.c_str());
390 ✗ return true;
391 }
392
393 ✗ const int64_t gauge_chunks = overall_chunks.load();
394 ✗ const int64_t gauge_new = overall_new.load();
395
396 // Download and uncompress catalog
397 ✗ shash::Any chunk_hash;
398 zlib::Algorithms compression_alg;
399 ✗ catalog::Catalog *catalog = NULL;
400 ✗ string file_catalog;
401 ✗ string file_catalog_vanilla;
402 ✗ FILE *fcatalog = CreateTempFile(*temp_dir + "/cvmfs", 0600, "w",
403 &file_catalog);
404 ✗ if (!fcatalog) {
405 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "I/O error");
406 ✗ return false;
407 }
408 ✗ fclose(fcatalog);
409 ✗ FILE *fcatalog_vanilla = CreateTempFile(*temp_dir + "/cvmfs", 0600, "w",
410 &file_catalog_vanilla);
411 ✗ if (!fcatalog_vanilla) {
412 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "I/O error");
413 ✗ unlink(file_catalog.c_str());
414 ✗ return false;
415 }
416 // Historic catalogs retained locally may already have been collected on
417 // Stratum 0, so revisited catalogs are read from the replica's backend.
418 // Without direct access (S3), fall back to the replica's own URL.
419 ✗ const string &catalog_url = catalog_exists ? *stratum1_url : *stratum0_url;
420 ✗ const string url_catalog = catalog_url + "/data/" + catalog_hash.MakePath();
421 ✗ cvmfs::FileSink filesink(fcatalog_vanilla);
422 download::JobInfo download_catalog(&url_catalog, false, false, &catalog_hash,
423 ✗ &filesink);
424 ✗ if (catalog_exists && !local_upstream_path.empty()) {
425 ✗ fclose(fcatalog_vanilla);
426 ✗ if (!CopyPath2Path(local_upstream_path + "/data/" + catalog_hash.MakePath(),
427 file_catalog_vanilla)) {
428 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to read local catalog %s",
429 catalog_hash.ToString().c_str());
430 ✗ goto pull_cleanup;
431 }
432 ✗ dl_retval = download::kFailOk;
433 } else {
434 ✗ dl_retval = download_manager()->Fetch(&download_catalog);
435 ✗ fclose(fcatalog_vanilla);
436 }
437 ✗ if (dl_retval != download::kFailOk) {
438 const bool genuinely_missing = (dl_retval == download::kFailHostHttp)
439 ✗ && (download_catalog.http_code() == 404);
440 ✗ if (is_historic_catalog && is_garbage_collectable && genuinely_missing
441 ✗ && !catalog_exists) {
442 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
443 "skipping missing catalog %s - "
444 "probably swept by garbage collection",
445 catalog_hash.ToString().c_str());
446 ✗ goto pull_skip;
447 } else {
448 ✗ ReportDownloadError(download_catalog);
449 ✗ goto pull_cleanup;
450 }
451 }
452 ✗ retval = zlib::DecompressPath2Path(file_catalog_vanilla, file_catalog);
453 ✗ if (!retval) {
454 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "decompression failure (file %s, hash %s)",
455 file_catalog_vanilla.c_str(), catalog_hash.ToString().c_str());
456 ✗ goto pull_cleanup;
457 }
458 ✗ if (path.empty() && reflog != NULL) {
459 ✗ if (!reflog->AddCatalog(catalog_hash)) {
460 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to add catalog to Reflog.");
461 ✗ goto pull_cleanup;
462 }
463 }
464
465 ✗ catalog = catalog::Catalog::AttachFreely(path, file_catalog, catalog_hash);
466 ✗ if (catalog == NULL) {
467 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to attach catalog %s",
468 catalog_hash.ToString().c_str());
469 ✗ goto pull_cleanup;
470 }
471
472 // Always pull the HEAD root catalog and nested catalogs
473 ✗ if (apply_timestamp_threshold && (path == "")
474 ✗ && (catalog->GetLastModified() < timestamp_threshold)) {
475 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
476 " Pruning at root catalog from %s due to threshold at %s",
477 StringifyTime(catalog->GetLastModified(), false).c_str(),
478 StringifyTime(timestamp_threshold, false).c_str());
479 ✗ delete catalog;
480 ✗ goto pull_skip;
481 }
482 ✗ apply_timestamp_threshold = true;
483
484 // Traverse the chunks. In partial replication mode, excluded subtrees are
485 // pruned before reaching this point (see PullRecursion), so any catalog we
486 // pull here has its objects replicated. Ancestor catalogs on the way to an
487 // included subtree are replicated in full, which keeps navigation intact.
488 {
489 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak,
490 " Processing chunks [%" PRIu64 " registered chunks]: ",
491 catalog->GetNumChunks());
492 ✗ retval = catalog->AllChunksBegin();
493 ✗ if (!retval) {
494 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to gather chunks");
495 ✗ goto pull_cleanup;
496 }
497 ✗ while (catalog->AllChunksNext(&chunk_hash, &compression_alg)) {
498 ✗ ChunkJob next_chunk(chunk_hash, compression_alg);
499 ✗ WritePipe(pipe_chunks[1], &next_chunk, sizeof(next_chunk));
500 chunk_queue.fetch_add(1);
501 }
502 ✗ catalog->AllChunksEnd();
503 ✗ while (chunk_queue.load() != 0) {
504 ✗ SafeSleepMs(100);
505 }
506 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
507 " fetched %" PRId64 " new chunks out of "
508 "%" PRId64 " unique chunks",
509 overall_new.load() - gauge_new,
510 overall_chunks.load() - gauge_chunks);
511 }
512
513 ✗ retval = PullRecursion(catalog, path);
514 ✗ previous_catalog = catalog->GetPreviousRevision();
515 ✗ delete catalog;
516 ✗ unlink(file_catalog.c_str());
517
518
519 ✗ WaitForStorage();
520 ✗ if (!retval)
521 ✗ return false;
522 ✗ if (catalog_exists) {
523 ✗ unlink(file_catalog_vanilla.c_str());
524 } else {
525 ✗ Store(file_catalog_vanilla, catalog_hash);
526 }
527 ✗ return true;
528
529 ✗ pull_cleanup:
530 ✗ delete catalog;
531 ✗ unlink(file_catalog.c_str());
532 ✗ unlink(file_catalog_vanilla.c_str());
533 ✗ return false;
534
535 ✗ pull_skip:
536 ✗ unlink(file_catalog.c_str());
537 ✗ unlink(file_catalog_vanilla.c_str());
538 ✗ return true;
539 }
540
541
542 ✗ int swissknife::CommandPull::Main(const swissknife::ArgumentList &args) {
543 int retval;
544 manifest::Failures m_retval;
545 download::Failures dl_retval;
546 ✗ unsigned timeout = 60;
547 ✗ const int fd_lockfile = -1;
548 ✗ string spooler_definition_str;
549 ✗ manifest::ManifestEnsemble ensemble;
550 ✗ shash::Any meta_info_hash;
551 ✗ string meta_info;
552 ✗ shash::Any previous_catalog_hash;
553 ✗ shash::Any current_catalog_hash;
554
555 // Option parsing
556 ✗ if (args.find('c') != args.end())
557 ✗ preload_cache = true;
558 ✗ if (args.find('l') != args.end()) {
559 const unsigned log_level = kLogLevel0
560 ✗ << String2Uint64(*args.find('l')->second);
561 ✗ if (log_level > kLogNone) {
562 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "invalid log level");
563 ✗ return 1;
564 }
565 ✗ SetLogVerbosity(static_cast<LogLevels>(log_level));
566 }
567 ✗ stratum0_url = args.find('u')->second;
568 ✗ temp_dir = args.find('x')->second;
569 ✗ if (preload_cache) {
570 ✗ preload_cachedir = new string(*args.find('r')->second);
571 } else {
572 ✗ spooler_definition_str = *args.find('r')->second;
573 }
574 ✗ string master_keys = *args.find('k')->second;
575 ✗ if (DirectoryExists(master_keys))
576 ✗ master_keys = JoinStrings(FindFilesBySuffix(master_keys, ".pub"), ":");
577 ✗ const string repository_name = *args.find('m')->second;
578 ✗ if (args.find('n') != args.end())
579 ✗ num_parallel = String2Uint64(*args.find('n')->second);
580 ✗ if (args.find('t') != args.end())
581 ✗ timeout = String2Uint64(*args.find('t')->second);
582 ✗ if (args.find('a') != args.end())
583 ✗ retries = String2Uint64(*args.find('a')->second);
584 ✗ if (args.find('d') != args.end()) {
585 ✗ pathfilter = catalog::RelaxedPathFilter::Create(*args.find('d')->second);
586 ✗ assert(pathfilter->IsValid());
587 }
588 ✗ if (args.find('E') != args.end()) {
589 ✗ if (pathfilter != NULL) {
590 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
591 "Options -d and -E are mutually exclusive");
592 ✗ return 1;
593 }
594 ✗ inclusion_spec = catalog::InclusionSpec::Create(*args.find('E')->second);
595 ✗ if (inclusion_spec == NULL || !inclusion_spec->IsValid()) {
596 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
597 "Failed to parse inclusion spec from '%s'",
598 args.find('E')->second->c_str());
599 ✗ return 1;
600 }
601 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
602 "CernVM-FS: partial replication with inclusion spec (version %d)",
603 inclusion_spec->version());
604 }
605 ✗ if (args.find('p') != args.end())
606 ✗ pull_history = true;
607 ✗ if (args.find('z') != args.end())
608 ✗ inspect_existing_catalogs = true;
609 ✗ if (args.find('w') != args.end())
610 ✗ stratum1_url = args.find('w')->second;
611 ✗ if (args.find('i') != args.end())
612 ✗ initial_snapshot = true;
613 ✗ shash::Any reflog_hash;
614 ✗ string reflog_chksum_path;
615 ✗ if (args.find('R') != args.end()) {
616 ✗ reflog_chksum_path = *args.find('R')->second;
617 ✗ if (!initial_snapshot) {
618 ✗ if (!manifest::Reflog::ReadChecksum(reflog_chksum_path, &reflog_hash)) {
619 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Could not read reflog checksum");
620 ✗ return 1;
621 }
622 }
623 }
624 ✗ if (args.find('Z') != args.end()) {
625 ✗ timestamp_threshold = String2Int64(*args.find('Z')->second);
626 }
627
628 ✗ if (!preload_cache && stratum1_url.Get() == NULL) {
629 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "need -w <stratum 1 URL>");
630 ✗ return 1;
631 }
632
633 typedef std::vector<history::History::Tag> TagVector;
634 ✗ TagVector historic_tags;
635
636 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "CernVM-FS: replicating from %s",
637 stratum0_url->c_str());
638
639 ✗ int result = 1;
640
641 // Initialization
642 overall_chunks.store(0);
643 overall_new.store(0);
644 chunk_queue.store(0);
645
646 ✗ const bool follow_redirects = false;
647 ✗ const unsigned max_pool_handles = num_parallel + 1;
648 ✗ const string proxy = (args.find('@') != args.end()) ? *args.find('@')->second
649 ✗ : "";
650
651 ✗ if (!this->InitDownloadManager(follow_redirects, proxy, max_pool_handles)) {
652 ✗ return 1;
653 }
654
655 ✗ if (!this->InitSignatureManager(master_keys)) {
656 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to initialize CVMFS signatures");
657 ✗ return 1;
658 } else {
659 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "CernVM-FS: using public key(s) %s",
660 JoinStrings(SplitString(master_keys, ':'), ", ").c_str());
661 }
662
663 unsigned current_group;
664 ✗ vector<vector<download::DownloadManager::ProxyInfo> > proxies;
665 ✗ download_manager()->GetProxyInfo(&proxies, &current_group, NULL);
666 ✗ if (proxies.size() > 0) {
667 ✗ string proxy_str = "\nWarning, replicating through proxies\n";
668 ✗ proxy_str += " Load-balance groups:\n";
669 ✗ for (unsigned i = 0; i < proxies.size(); ++i) {
670 ✗ vector<string> urls;
671 ✗ for (unsigned j = 0; j < proxies[i].size(); ++j) {
672 ✗ urls.push_back(proxies[i][j].url);
673 }
674 ✗ proxy_str += " [" + StringifyInt(i) + "] " + JoinStrings(urls, ", ")
675 ✗ + "\n";
676 }
677 ✗ proxy_str += " Active proxy: [" + StringifyInt(current_group) + "] "
678 ✗ + proxies[current_group][0].url;
679 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "%s\n", proxy_str.c_str());
680 }
681 ✗ download_manager()->SetTimeout(timeout, timeout);
682 ✗ download_manager()->SetRetryParameters(retries, 500, 2000);
683 ✗ download_manager()->Spawn();
684
685 // init the download helper
686 ✗ const ObjectFetcher object_fetcher(repository_name, *stratum0_url, *temp_dir,
687 ✗ download_manager(), signature_manager());
688
689 pthread_t *workers = reinterpret_cast<pthread_t *>(
690 ✗ smalloc(sizeof(pthread_t) * num_parallel));
691
692 // Check if we have a replica-ready server
693 ✗ const string url_sentinel = *stratum0_url + "/.cvmfs_master_replica";
694 ✗ download::JobInfo download_sentinel(&url_sentinel, false);
695 ✗ retval = download_manager()->Fetch(&download_sentinel);
696 ✗ if (retval != download::kFailOk) {
697 ✗ if (download_sentinel.http_code() == 404) {
698 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
699 "This is not a CernVM-FS server for replication");
700 } else {
701 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
702 "Failed to contact stratum 0 server (%d - %s)", retval,
703 download::Code2Ascii(download_sentinel.error_code()));
704 }
705 ✗ goto fini;
706 }
707
708 ✗ m_retval = FetchRemoteManifestEnsemble(*stratum0_url, repository_name,
709 &ensemble);
710 ✗ if (m_retval != manifest::kFailOk) {
711 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to fetch manifest (%d - %s)",
712 m_retval, manifest::Code2Ascii(m_retval));
713 ✗ goto fini;
714 }
715
716 // Get meta info
717 ✗ meta_info_hash = ensemble.manifest->meta_info();
718 ✗ if (!meta_info_hash.IsNull()) {
719 ✗ meta_info_hash = ensemble.manifest->meta_info();
720 ✗ const string url = *stratum0_url + "/data/" + meta_info_hash.MakePath();
721 ✗ cvmfs::MemSink metainfo_memsink;
722 download::JobInfo download_metainfo(&url, true, false, &meta_info_hash,
723 ✗ &metainfo_memsink);
724 ✗ dl_retval = download_manager()->Fetch(&download_metainfo);
725 ✗ if (dl_retval != download::kFailOk) {
726 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to fetch meta info (%d - %s)",
727 dl_retval, download::Code2Ascii(dl_retval));
728 ✗ goto fini;
729 }
730 ✗ meta_info = string(reinterpret_cast<char *>(metainfo_memsink.data()),
731 ✗ metainfo_memsink.pos());
732 }
733
734 ✗ is_garbage_collectable = ensemble.manifest->garbage_collectable();
735
736 // Manifest available, now the spooler's hash algorithm can be determined
737 // That doesn't actually matter because the replication does no re-hashing
738 ✗ if (!preload_cache) {
739 const upload::SpoolerDefinition spooler_definition(
740 ✗ spooler_definition_str, ensemble.manifest->GetHashAlgorithm());
741 ✗ spooler = upload::Spooler::Construct(spooler_definition);
742 ✗ assert(spooler);
743 ✗ if (spooler_definition.driver_type == upload::SpoolerDefinition::Local)
744 ✗ local_upstream_path = spooler_definition.spooler_configuration;
745 ✗ spooler->RegisterListener(&SpoolerOnUpload);
746 }
747
748 // Open the reflog for modification
749 ✗ if (!preload_cache) {
750 ✗ if (initial_snapshot) {
751 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Creating an empty Reflog for '%s'",
752 repository_name.c_str());
753 ✗ reflog = CreateEmptyReflog(*temp_dir, repository_name);
754 ✗ if (reflog == NULL) {
755 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to create initial Reflog");
756 ✗ goto fini;
757 }
758 } else {
759 ObjectFetcher object_fetcher_stratum1(repository_name,
760 ✗ *stratum1_url,
761 ✗ *temp_dir,
762 download_manager(),
763 ✗ signature_manager());
764
765 ✗ if (!reflog_hash.IsNull()) {
766 ✗ reflog = FetchReflog(&object_fetcher_stratum1, repository_name,
767 reflog_hash);
768 ✗ assert(reflog != NULL);
769 } else {
770 ✗ LogCvmfs(kLogCvmfs, kLogVerboseMsg, "no reflog (ignoring)");
771 ✗ if (spooler->Peek(".cvmfsreflog")) {
772 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
773 "no reflog hash specified but reflog is present");
774 ✗ goto fini;
775 }
776 }
777 }
778
779 ✗ if (reflog != NULL) {
780 ✗ reflog->BeginTransaction();
781 // On commit: use manifest's hash algorithm
782 ✗ reflog_hash.algorithm = ensemble.manifest->GetHashAlgorithm();
783 }
784 }
785
786 // Fetch tag list.
787 // If we are just preloading the cache it is not strictly necessarily to
788 // download the entire tag list
789 // TODO(molina): add user option to download tags when preloading the cache
790 ✗ if (!ensemble.manifest->history().IsNull() && !preload_cache) {
791 ✗ const shash::Any history_hash = ensemble.manifest->history();
792 ✗ const string history_url = *stratum0_url + "/data/"
793 ✗ + history_hash.MakePath();
794 ✗ const string history_path = *temp_dir + "/" + history_hash.ToString();
795
796 ✗ cvmfs::PathSink pathsink(history_path);
797 download::JobInfo download_history(&history_url, false, false,
798 ✗ &history_hash, &pathsink);
799 ✗ dl_retval = download_manager()->Fetch(&download_history);
800 ✗ if (dl_retval != download::kFailOk) {
801 ✗ ReportDownloadError(download_history);
802 ✗ goto fini;
803 }
804 ✗ const std::string history_db_path = history_path + ".uncompressed";
805 ✗ retval = zlib::DecompressPath2Path(history_path, history_db_path);
806 ✗ assert(retval);
807 ✗ history::History *tag_db = history::SqliteHistory::Open(history_db_path);
808 ✗ if (NULL == tag_db) {
809 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to open history database (%s)",
810 history_db_path.c_str());
811 ✗ unlink(history_db_path.c_str());
812 ✗ goto fini;
813 }
814 ✗ retval = tag_db->List(&historic_tags);
815 ✗ delete tag_db;
816 ✗ unlink(history_db_path.c_str());
817 ✗ if (!retval) {
818 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "failed to read history database (%s)",
819 history_db_path.c_str());
820 ✗ goto fini;
821 }
822
823 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Found %lu named snapshots",
824 historic_tags.size());
825 // TODO(jblomer): We should repliacte the previous history dbs, too,
826 // in order to avoid races on fail-over between non-synchronized stratum 1s
827 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Uploading history database");
828 ✗ Store(history_path, history_hash);
829 ✗ WaitForStorage();
830 ✗ unlink(history_path.c_str());
831 ✗ if (reflog != NULL && !reflog->AddHistory(history_hash)) {
832 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Failed to add history to Reflog.");
833 ✗ goto fini;
834 }
835 }
836
837 // Starting threads
838 ✗ MakePipe(pipe_chunks);
839 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Starting %u workers", num_parallel);
840 MainWorkerContext mwc;
841 ✗ mwc.download_manager = download_manager();
842 ✗ for (unsigned i = 0; i < num_parallel; ++i) {
843 ✗ const int retval = pthread_create(&workers[i], NULL, MainWorker,
844 static_cast<void *>(&mwc));
845 ✗ assert(retval == 0);
846 }
847
848 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Replicating from trunk catalog at /");
849 ✗ current_catalog_hash = ensemble.manifest->catalog_hash();
850 ✗ retval = 1;
851 {
852 ✗ bool is_historic = false;
853 ✗ reached_existing_history = false;
854 do {
855 ✗ retval = static_cast<int>(
856 ✗ Pull(current_catalog_hash, "", previous_catalog_hash, is_historic)
857 ✗ && (retval != 0));
858 ✗ if (pull_history && !previous_catalog_hash.IsNull()) {
859 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Replicating from historic catalog %s",
860 previous_catalog_hash.ToString().c_str());
861 }
862 ✗ current_catalog_hash = previous_catalog_hash;
863 ✗ is_historic = true;
864 ✗ } while (pull_history && !previous_catalog_hash.IsNull());
865 }
866
867 ✗ if (!historic_tags.empty()) {
868 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Checking tagged snapshots...");
869 }
870 ✗ for (TagVector::const_iterator i = historic_tags.begin(),
871 ✗ iend = historic_tags.end();
872 ✗ i != iend;
873 ✗ ++i) {
874 ✗ if (Peek(i->root_hash) && !inspect_existing_catalogs)
875 ✗ continue;
876 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Replicating from %s repository tag",
877 i->name.c_str());
878 ✗ apply_timestamp_threshold = false;
879
880 ✗ bool retval2 = true;
881 ✗ current_catalog_hash = i->root_hash;
882 ✗ reached_existing_history = false;
883 do {
884 ✗ retval2 = Pull(current_catalog_hash, "", previous_catalog_hash,
885 true /* is_historic_catalog */)
886 ✗ && retval2;
887 ✗ if (pull_history && !previous_catalog_hash.IsNull()) {
888 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Replicating from historic catalog %s",
889 previous_catalog_hash.ToString().c_str());
890 }
891 ✗ current_catalog_hash = previous_catalog_hash;
892 ✗ } while (pull_history && !previous_catalog_hash.IsNull());
893 ✗ retval = retval && retval2;
894 }
895
896 // Stopping threads
897 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Stopping %u workers", num_parallel);
898 ✗ for (unsigned i = 0; i < num_parallel; ++i) {
899 ✗ ChunkJob terminate_workers;
900 ✗ WritePipe(pipe_chunks[1], &terminate_workers, sizeof(terminate_workers));
901 }
902 ✗ for (unsigned i = 0; i < num_parallel; ++i) {
903 ✗ int retval = pthread_join(workers[i], NULL); // NOLINT (false positive)
904 ✗ assert(retval == 0);
905 }
906 ✗ ClosePipe(pipe_chunks);
907
908 ✗ if (!retval)
909 ✗ goto fini;
910
911 ✗ WaitForStorage();
912 ✗ if (!Peek(ensemble.manifest->catalog_hash())) {
913 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
914 "root catalog %s is missing from the destination - refusing to "
915 "publish an incomplete revision",
916 ensemble.manifest->catalog_hash().ToString().c_str());
917 ✗ goto fini;
918 }
919
920 // Upload manifest ensemble
921 {
922 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Uploading manifest ensemble");
923 ✗ WaitForStorage();
924
925 ✗ if (!Peek(ensemble.manifest->certificate())) {
926 ✗ StoreBuffer(ensemble.cert_buf, ensemble.cert_size,
927 ✗ ensemble.manifest->certificate(), true);
928 }
929 ✗ if (reflog != NULL
930 ✗ && !reflog->AddCertificate(ensemble.manifest->certificate())) {
931 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Failed to add certificate to Reflog.");
932 ✗ goto fini;
933 }
934 ✗ if (!meta_info_hash.IsNull()) {
935 const unsigned char *info = reinterpret_cast<const unsigned char *>(
936 ✗ meta_info.data());
937 ✗ StoreBuffer(info, meta_info.size(), meta_info_hash, true);
938 ✗ if (reflog != NULL && !reflog->AddMetainfo(meta_info_hash)) {
939 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Failed to add metainfo to Reflog.");
940 ✗ goto fini;
941 }
942 }
943
944 // Create alternative bootstrapping symlinks for VOMS secured repos
945 ✗ if (ensemble.manifest->has_alt_catalog_path()) {
946 ✗ const bool success = spooler->PlaceBootstrappingShortcut(
947 ✗ ensemble.manifest->certificate())
948 ✗ && spooler->PlaceBootstrappingShortcut(
949 ✗ ensemble.manifest->catalog_hash())
950 ✗ && (ensemble.manifest->history().IsNull()
951 ✗ || spooler->PlaceBootstrappingShortcut(
952 ✗ ensemble.manifest->history()))
953 ✗ && (meta_info_hash.IsNull()
954 ✗ || spooler->PlaceBootstrappingShortcut(
955 ✗ meta_info_hash));
956
957 ✗ if (!success) {
958 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
959 "failed to place root catalog bootstrapping symlinks");
960 ✗ goto fini;
961 }
962 }
963
964 // upload Reflog database
965 ✗ if (!preload_cache && reflog != NULL) {
966 ✗ reflog->CommitTransaction();
967 ✗ reflog->DropDatabaseFileOwnership();
968 ✗ const string reflog_path = reflog->database_file();
969 ✗ delete reflog;
970 ✗ manifest::Reflog::HashDatabase(reflog_path, &reflog_hash);
971 ✗ WaitForStorage(); // Reduce the duration of reflog /wo checksum
972 ✗ spooler->UploadReflog(reflog_path);
973 ✗ spooler->WaitForUpload();
974 ✗ unlink(reflog_path.c_str());
975 ✗ if (spooler->GetNumberOfErrors()) {
976 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Failed to upload Reflog (errors: %d)",
977 spooler->GetNumberOfErrors());
978 ✗ goto fini;
979 }
980 ✗ assert(!reflog_chksum_path.empty());
981 ✗ manifest::Reflog::WriteChecksum(reflog_chksum_path, reflog_hash);
982 }
983
984 ✗ if (preload_cache) {
985 ✗ const bool retval = ensemble.manifest->ExportBreadcrumb(*preload_cachedir,
986 0660);
987 ✗ assert(retval);
988 } else {
989 // pkcs#7 structure contains content + certificate + signature
990 // So there is no race with whitelist and pkcs7 signature being out of
991 // sync
992 ✗ if (ensemble.whitelist_pkcs7_buf) {
993 ✗ StoreBuffer(ensemble.whitelist_pkcs7_buf, ensemble.whitelist_pkcs7_size,
994 ".cvmfswhitelist.pkcs7", false);
995 }
996 ✗ StoreBuffer(ensemble.whitelist_buf, ensemble.whitelist_size,
997 ".cvmfswhitelist", false);
998 ✗ StoreBuffer(ensemble.raw_manifest_buf, ensemble.raw_manifest_size,
999 ".cvmfspublished", false);
1000
1001 // Upload the partial replication spec to the backend
1002 ✗ if (inclusion_spec != NULL) {
1003 ✗ const std::string &spec_content = inclusion_spec->content();
1004 ✗ StoreBuffer(
1005 ✗ reinterpret_cast<const unsigned char *>(spec_content.data()),
1006 ✗ spec_content.size(), ".cvmfs_partial_replication", false);
1007 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
1008 "Uploaded partial replication spec to backend");
1009 }
1010 }
1011 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Serving revision %" PRIu64,
1012 ensemble.manifest->revision());
1013 }
1014
1015 ✗ WaitForStorage();
1016 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
1017 "Fetched %" PRId64 " new chunks out of %" PRId64 " processed chunks",
1018 (overall_new).load(), overall_chunks.load());
1019 ✗ result = 0;
1020
1021 ✗ fini:
1022 if (fd_lockfile >= 0)
1023 UnlockFile(fd_lockfile);
1024 ✗ free(workers);
1025 ✗ delete spooler;
1026 ✗ delete pathfilter;
1027 ✗ delete inclusion_spec;
1028 ✗ inclusion_spec = NULL;
1029 ✗ return result;
1030 }
1031
1032 } // namespace swissknife
1033