| Directory: | cvmfs/ |
|---|---|
| File: | cvmfs/receiver/commit_processor.cc |
| Date: | 2026-08-30 02:40:36 |
| Exec | Total | Coverage | |
|---|---|---|---|
| Lines: | 52 | 243 | 21.4% |
| Branches: | 58 | 560 | 10.4% |
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * This file is part of the CernVM File System. | ||
| 3 | */ | ||
| 4 | |||
| 5 | #include "commit_processor.h" | ||
| 6 | |||
| 7 | #include <time.h> | ||
| 8 | |||
| 9 | #include <cctype> | ||
| 10 | #include <memory> | ||
| 11 | #include <string> | ||
| 12 | #include <vector> | ||
| 13 | |||
| 14 | #include "catalog_diff_tool.h" | ||
| 15 | #include "catalog_merge_tool.h" | ||
| 16 | #include "catalog_mgr_ro.h" | ||
| 17 | #include "catalog_mgr_rw.h" | ||
| 18 | #include "compression/compression.h" | ||
| 19 | #include "manifest.h" | ||
| 20 | #include "manifest_fetch.h" | ||
| 21 | #include "network/download.h" | ||
| 22 | #include "network/sink_path.h" | ||
| 23 | #include "params.h" | ||
| 24 | #include "signing_tool.h" | ||
| 25 | #include "statistics.h" | ||
| 26 | #include "statistics_database.h" | ||
| 27 | #include "swissknife.h" | ||
| 28 | #include "swissknife_history.h" | ||
| 29 | #include "util/algorithm.h" | ||
| 30 | #include "util/logging.h" | ||
| 31 | #include "util/posix.h" | ||
| 32 | #include "util/raii_temp_dir.h" | ||
| 33 | #include "util/string.h" | ||
| 34 | |||
| 35 | namespace { | ||
| 36 | |||
| 37 | ✗ | PathString RemoveRepoName(const PathString &lease_path) { | |
| 38 | ✗ | std::string abs_path = lease_path.ToString(); | |
| 39 | ✗ | const std::string::const_iterator it = std::find(abs_path.begin(), | |
| 40 | ✗ | abs_path.end(), '/'); | |
| 41 | ✗ | if (it != abs_path.end()) { | |
| 42 | ✗ | const size_t idx = it - abs_path.begin() + 1; | |
| 43 | ✗ | return lease_path.Suffix(idx); | |
| 44 | } else { | ||
| 45 | ✗ | return lease_path; | |
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | ✗ | bool EditTags(const RepositoryTag &repo_tag, const std::string &repo_name, | |
| 50 | const receiver::Params ¶ms, const std::string &temp_dir, | ||
| 51 | const std::string &manifest_path, | ||
| 52 | const std::string &public_key_path, const std::string &proxy, | ||
| 53 | const time_t auto_tag_threshold, const bool maintain_undo_tags) { | ||
| 54 | ✗ | swissknife::ArgumentList args; | |
| 55 | ✗ | args['r'].Reset(new std::string(params.spooler_configuration)); | |
| 56 | ✗ | args['w'].Reset(new std::string(params.stratum0)); | |
| 57 | ✗ | args['t'].Reset(new std::string(temp_dir)); | |
| 58 | ✗ | args['m'].Reset(new std::string(manifest_path)); | |
| 59 | ✗ | args['p'].Reset(new std::string(public_key_path)); | |
| 60 | ✗ | args['f'].Reset(new std::string(repo_name)); | |
| 61 | ✗ | args['e'].Reset(new std::string(params.hash_alg_str)); | |
| 62 | ✗ | args['a'].Reset(new std::string(repo_tag.name())); | |
| 63 | ✗ | args['D'].Reset(new std::string(repo_tag.description())); | |
| 64 | ✗ | if (maintain_undo_tags) { | |
| 65 | ✗ | args['x'].Reset(new std::string()); | |
| 66 | } | ||
| 67 | ✗ | args['@'].Reset(new std::string(proxy)); | |
| 68 | // Remove the tags requested by `cvmfs_server tag -r` in the same history | ||
| 69 | // transaction as the (possibly empty) new tag, so a single new history | ||
| 70 | // database is published and registered in the reflog for this commit. | ||
| 71 | ✗ | if (!repo_tag.delete_tags().empty()) { | |
| 72 | ✗ | args['d'].Reset(new std::string(repo_tag.delete_tags())); | |
| 73 | } | ||
| 74 | // Remove outdated auto-generated tags in the same history transaction as the | ||
| 75 | // tag we are about to add, so that only a single new history database is | ||
| 76 | // published (and registered in the reflog) for this commit. | ||
| 77 | ✗ | if (auto_tag_threshold > 0) { | |
| 78 | ✗ | args['c'].Reset(new std::string(StringifyInt(auto_tag_threshold))); | |
| 79 | } | ||
| 80 | |||
| 81 | const std::unique_ptr<swissknife::CommandEditTag> edit_cmd( | ||
| 82 | ✗ | new swissknife::CommandEditTag()); | |
| 83 | ✗ | const int ret = edit_cmd->Main(args); | |
| 84 | |||
| 85 | ✗ | if (ret) { | |
| 86 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, "Error %d editing tags (add: '%s')", | |
| 87 | ✗ | ret, repo_tag.name().c_str()); | |
| 88 | ✗ | return false; | |
| 89 | } | ||
| 90 | |||
| 91 | ✗ | return true; | |
| 92 | } | ||
| 93 | |||
| 94 | } // namespace | ||
| 95 | |||
| 96 | namespace receiver { | ||
| 97 | |||
| 98 | // See commit_processor.h for the contract. `now` is injected so the parser is | ||
| 99 | // deterministic and unit-testable. | ||
| 100 | 1128 | time_t ParseRelativeTimespan(const std::string ×pan, time_t now) { | |
| 101 | // Tokenize on whitespace, lower-casing as we go. | ||
| 102 | 1128 | std::vector<std::string> tokens; | |
| 103 | 1128 | std::string current; | |
| 104 |
2/2✓ Branch 1 taken 12079 times.
✓ Branch 2 taken 1128 times.
|
13207 | for (size_t i = 0; i < timespan.size(); ++i) { |
| 105 | 12079 | const unsigned char c = static_cast<unsigned char>(timespan[i]); | |
| 106 |
2/2✓ Branch 0 taken 2162 times.
✓ Branch 1 taken 9917 times.
|
12079 | if (isspace(c)) { |
| 107 |
2/2✓ Branch 1 taken 1833 times.
✓ Branch 2 taken 329 times.
|
2162 | if (!current.empty()) { |
| 108 |
1/2✓ Branch 1 taken 1833 times.
✗ Branch 2 not taken.
|
1833 | tokens.push_back(current); |
| 109 | 1833 | current.clear(); | |
| 110 | } | ||
| 111 | } else { | ||
| 112 |
1/2✓ Branch 1 taken 9917 times.
✗ Branch 2 not taken.
|
9917 | current += static_cast<char>(tolower(c)); |
| 113 | } | ||
| 114 | } | ||
| 115 |
2/2✓ Branch 1 taken 1034 times.
✓ Branch 2 taken 94 times.
|
1128 | if (!current.empty()) { |
| 116 |
1/2✓ Branch 1 taken 1034 times.
✗ Branch 2 not taken.
|
1034 | tokens.push_back(current); |
| 117 | } | ||
| 118 | |||
| 119 | // Expect exactly "<number> <unit> ago". | ||
| 120 |
6/6✓ Branch 1 taken 846 times.
✓ Branch 2 taken 282 times.
✓ Branch 5 taken 47 times.
✓ Branch 6 taken 799 times.
✓ Branch 7 taken 329 times.
✓ Branch 8 taken 799 times.
|
1128 | if (tokens.size() != 3 || tokens[2] != "ago") { |
| 121 | 329 | return 0; | |
| 122 | } | ||
| 123 | 799 | const std::string &number = tokens[0]; | |
| 124 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 799 times.
|
799 | if (number.empty()) { |
| 125 | ✗ | return 0; | |
| 126 | } | ||
| 127 |
2/2✓ Branch 1 taken 1175 times.
✓ Branch 2 taken 752 times.
|
1927 | for (size_t i = 0; i < number.size(); ++i) { |
| 128 |
2/2✓ Branch 1 taken 47 times.
✓ Branch 2 taken 1128 times.
|
1175 | if (!isdigit(static_cast<unsigned char>(number[i]))) { |
| 129 | 47 | return 0; | |
| 130 | } | ||
| 131 | } | ||
| 132 |
1/2✓ Branch 1 taken 752 times.
✗ Branch 2 not taken.
|
752 | const int64_t count = String2Int64(number); |
| 133 | |||
| 134 | // De-pluralize the unit. | ||
| 135 |
1/2✓ Branch 2 taken 752 times.
✗ Branch 3 not taken.
|
752 | std::string unit = tokens[1]; |
| 136 |
6/8✓ Branch 1 taken 752 times.
✗ Branch 2 not taken.
✓ Branch 5 taken 752 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 564 times.
✓ Branch 8 taken 188 times.
✓ Branch 9 taken 564 times.
✓ Branch 10 taken 188 times.
|
752 | if (!unit.empty() && unit[unit.size() - 1] == 's') { |
| 137 |
1/2✓ Branch 2 taken 564 times.
✗ Branch 3 not taken.
|
564 | unit.resize(unit.size() - 1); |
| 138 | } | ||
| 139 | |||
| 140 | // Fixed-length units can be subtracted directly. | ||
| 141 | 752 | int64_t factor = 0; | |
| 142 |
6/6✓ Branch 1 taken 705 times.
✓ Branch 2 taken 47 times.
✓ Branch 4 taken 94 times.
✓ Branch 5 taken 611 times.
✓ Branch 6 taken 141 times.
✓ Branch 7 taken 611 times.
|
752 | if (unit == "sec" || unit == "second") { |
| 143 | 141 | factor = 1; | |
| 144 |
6/6✓ Branch 1 taken 564 times.
✓ Branch 2 taken 47 times.
✓ Branch 4 taken 47 times.
✓ Branch 5 taken 517 times.
✓ Branch 6 taken 94 times.
✓ Branch 7 taken 517 times.
|
611 | } else if (unit == "min" || unit == "minute") { |
| 145 | 94 | factor = 60; | |
| 146 |
2/2✓ Branch 1 taken 47 times.
✓ Branch 2 taken 470 times.
|
517 | } else if (unit == "hour") { |
| 147 | 47 | factor = 3600; | |
| 148 |
2/2✓ Branch 1 taken 235 times.
✓ Branch 2 taken 235 times.
|
470 | } else if (unit == "day") { |
| 149 | 235 | factor = 86400; | |
| 150 |
2/2✓ Branch 1 taken 47 times.
✓ Branch 2 taken 188 times.
|
235 | } else if (unit == "week") { |
| 151 | 47 | factor = 604800; | |
| 152 | } | ||
| 153 |
2/2✓ Branch 0 taken 564 times.
✓ Branch 1 taken 188 times.
|
752 | if (factor > 0) { |
| 154 | 564 | return now - static_cast<time_t>(count * factor); | |
| 155 | } | ||
| 156 | |||
| 157 | // Calendar units: let mktime() normalize the broken-down time. | ||
| 158 | struct tm broken_time; | ||
| 159 | 188 | localtime_r(&now, &broken_time); | |
| 160 |
2/2✓ Branch 1 taken 94 times.
✓ Branch 2 taken 94 times.
|
188 | if (unit == "month") { |
| 161 | 94 | broken_time.tm_mon -= static_cast<int>(count); | |
| 162 | 94 | return mktime(&broken_time); | |
| 163 | } | ||
| 164 |
2/2✓ Branch 1 taken 47 times.
✓ Branch 2 taken 47 times.
|
94 | if (unit == "year") { |
| 165 | 47 | broken_time.tm_year -= static_cast<int>(count); | |
| 166 | 47 | return mktime(&broken_time); | |
| 167 | } | ||
| 168 | |||
| 169 | 47 | return 0; | |
| 170 | 1128 | } | |
| 171 | |||
| 172 | 7 | CommitProcessor::CommitProcessor() : num_errors_(0), statistics_(NULL) { } | |
| 173 | |||
| 174 | 14 | CommitProcessor::~CommitProcessor() { } | |
| 175 | |||
| 176 | /** | ||
| 177 | * Applies the changes from the new catalog onto the repository. | ||
| 178 | * | ||
| 179 | * Let: | ||
| 180 | * + C_O = the root catalog of the repository (given by old_root_hash) at | ||
| 181 | * the beginning of the lease, on the release manager machine | ||
| 182 | * + C_N = the root catalog of the repository (given by new_root_hash), on | ||
| 183 | * the release manager machine, with the changes introduced during the | ||
| 184 | * lease | ||
| 185 | * + C_G = the current root catalog of the repository on the gateway machine. | ||
| 186 | * | ||
| 187 | * This method applies all the changes from C_N, with respect to C_O, onto C_G. | ||
| 188 | * The resulting catalog on the gateway machine (C_GN) is then set as root | ||
| 189 | * catalog in the repository manifest. The method also signs the updated | ||
| 190 | * repository manifest. | ||
| 191 | */ | ||
| 192 | ✗ | CommitProcessor::Result CommitProcessor::Process( | |
| 193 | const std::string &lease_path, const shash::Any &old_root_hash, | ||
| 194 | const shash::Any &new_root_hash, const RepositoryTag &tag, | ||
| 195 | int64_t lease_expiration, uint64_t *final_revision, bool direct_graft) { | ||
| 196 | ✗ | RepositoryTag final_tag = tag; | |
| 197 | // If tag_name is a generic tag, update the time stamp | ||
| 198 | ✗ | if (final_tag.HasGenericName()) { | |
| 199 | ✗ | final_tag.SetGenericName(); | |
| 200 | } | ||
| 201 | |||
| 202 | ✗ | LogCvmfs(kLogReceiver, kLogSyslog, | |
| 203 | "CommitProcessor - lease_path: %s, old hash: %s, new hash: %s, " | ||
| 204 | "tag_name: %s, tag_description: %s", | ||
| 205 | ✗ | lease_path.c_str(), old_root_hash.ToString(true).c_str(), | |
| 206 | ✗ | new_root_hash.ToString(true).c_str(), final_tag.name().c_str(), | |
| 207 | ✗ | final_tag.description().c_str()); | |
| 208 | |||
| 209 | const std::vector<std::string> lease_path_tokens = SplitString(lease_path, | ||
| 210 | ✗ | '/'); | |
| 211 | |||
| 212 | ✗ | const std::string repo_name = lease_path_tokens.front(); | |
| 213 | |||
| 214 | ✗ | Params params; | |
| 215 | ✗ | if (!GetParamsFromFile(repo_name, ¶ms)) { | |
| 216 | ✗ | LogCvmfs( | |
| 217 | kLogReceiver, kLogSyslogErr, | ||
| 218 | "CommitProcessor - error: Could not get configuration parameters."); | ||
| 219 | ✗ | return kError; | |
| 220 | } | ||
| 221 | |||
| 222 | ✗ | const std::unique_ptr<ServerTool> server_tool(new ServerTool()); | |
| 223 | |||
| 224 | ✗ | if (!server_tool->InitDownloadManager(true, params.proxy)) { | |
| 225 | ✗ | LogCvmfs( | |
| 226 | kLogReceiver, kLogSyslogErr, | ||
| 227 | "CommitProcessor - error: Could not initialize the download manager"); | ||
| 228 | ✗ | return kError; | |
| 229 | } | ||
| 230 | |||
| 231 | ✗ | const std::string public_key = "/etc/cvmfs/keys/" + repo_name + ".pub"; | |
| 232 | ✗ | const std::string certificate = "/etc/cvmfs/keys/" + repo_name + ".crt"; | |
| 233 | ✗ | const std::string private_key = "/etc/cvmfs/keys/" + repo_name + ".key"; | |
| 234 | ✗ | if (!server_tool->InitSignatureManager(public_key, certificate, | |
| 235 | private_key)) { | ||
| 236 | ✗ | LogCvmfs( | |
| 237 | kLogReceiver, kLogSyslogErr, | ||
| 238 | "CommitProcessor - error: Could not initialize the signature manager"); | ||
| 239 | ✗ | return kError; | |
| 240 | } | ||
| 241 | |||
| 242 | ✗ | const shash::Any manifest_base_hash; | |
| 243 | const std::unique_ptr<manifest::Manifest> manifest_tgt( | ||
| 244 | server_tool->FetchRemoteManifest(params.stratum0, repo_name, | ||
| 245 | ✗ | manifest_base_hash)); | |
| 246 | |||
| 247 | // Current catalog from the gateway machine | ||
| 248 | ✗ | if (manifest_tgt.get() == nullptr) { | |
| 249 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 250 | "CommitProcessor - error: Could not open repository manifest"); | ||
| 251 | ✗ | return kError; | |
| 252 | } | ||
| 253 | |||
| 254 | ✗ | LogCvmfs(kLogReceiver, kLogSyslog, | |
| 255 | "CommitProcessor - lease_path: %s, target root hash: %s", | ||
| 256 | lease_path.c_str(), | ||
| 257 | ✗ | manifest_tgt->catalog_hash().ToString(false).c_str()); | |
| 258 | |||
| 259 | |||
| 260 | ✗ | std::string cache_dir_; | |
| 261 | ✗ | if (params.use_local_cache) { | |
| 262 | ✗ | cache_dir_ = "/var/spool/cvmfs/" + repo_name + "/cache.server"; | |
| 263 | } | ||
| 264 | |||
| 265 | const std::string spooler_temp_dir = GetSpoolerTempDir( | ||
| 266 | ✗ | params.spooler_configuration); | |
| 267 | ✗ | assert(!spooler_temp_dir.empty()); | |
| 268 | ✗ | assert(MkdirDeep(spooler_temp_dir + "/receiver", 0755, true)); | |
| 269 | const std::string temp_dir_root = spooler_temp_dir | ||
| 270 | ✗ | + "/receiver/commit_processor"; | |
| 271 | |||
| 272 | ✗ | const PathString relative_lease_path = RemoveRepoName(PathString(lease_path)); | |
| 273 | |||
| 274 | ✗ | std::string new_manifest_path; | |
| 275 | ✗ | shash::Any new_manifest_hash; | |
| 276 | |||
| 277 | ✗ | if (direct_graft) { | |
| 278 | // -- Experimental DirectGraft fast path ---------------------------------- | ||
| 279 | // Grafts new_root_hash directly into the parent catalog at | ||
| 280 | // relative_lease_path via WritableCatalogManager::TryGraftNestedCatalog, | ||
| 281 | // bypassing DiffRec entirely. Only valid when lease_path points to a | ||
| 282 | // brand-new directory subtree. Reached only via the experimental dedicated | ||
| 283 | // kCommitGraft reactor request. | ||
| 284 | ✗ | LogCvmfs(kLogReceiver, kLogSyslog, | |
| 285 | "CommitProcessor - lease_path: %s, direct-graft path " | ||
| 286 | "(skipping DiffRec)", | ||
| 287 | lease_path.c_str()); | ||
| 288 | |||
| 289 | const std::unique_ptr<RaiiTempDir> graft_temp_dir( | ||
| 290 | ✗ | RaiiTempDir::Create(temp_dir_root)); | |
| 291 | ✗ | const std::string graft_temp = graft_temp_dir->dir(); | |
| 292 | |||
| 293 | ✗ | perf::StatisticsTemplate stats_tmpl("publish", statistics_); | |
| 294 | // Register the FsCounters (n_files_added, n_directories_added, etc.) that | ||
| 295 | // StorePublishStatistics expects. In the DiffRec path these are created by | ||
| 296 | // CatalogMergeTool::Run(); DirectGraft bypasses that, so we register them | ||
| 297 | // here. The values stay 0 -- accurate for a graft that adds a whole | ||
| 298 | // subtree atomically rather than individual file-level diffs. | ||
| 299 | ✗ | const perf::FsCounters fs_counters(stats_tmpl); | |
| 300 | const upload::SpoolerDefinition definition( | ||
| 301 | params.spooler_configuration, params.hash_alg, params.compression_alg, | ||
| 302 | ✗ | params.generate_legacy_bulk_chunks, params.use_file_chunking, | |
| 303 | params.min_chunk_size, params.avg_chunk_size, params.max_chunk_size, | ||
| 304 | ✗ | "dummy_token", "dummy_key"); | |
| 305 | const std::unique_ptr<upload::Spooler> spooler( | ||
| 306 | ✗ | upload::Spooler::Construct(definition, &stats_tmpl)); | |
| 307 | |||
| 308 | const std::unique_ptr<catalog::WritableCatalogManager> output_mgr( | ||
| 309 | new catalog::WritableCatalogManager( | ||
| 310 | ✗ | manifest_tgt->catalog_hash(), params.stratum0, graft_temp, | |
| 311 | ✗ | spooler.get(), server_tool->download_manager(), | |
| 312 | ✗ | params.enforce_limits, params.nested_kcatalog_limit, | |
| 313 | ✗ | params.root_kcatalog_limit, params.file_mbyte_limit, statistics_, | |
| 314 | ✗ | params.use_autocatalogs, params.max_weight, params.min_weight, | |
| 315 | ✗ | cache_dir_)); | |
| 316 | ✗ | if (!output_mgr->Init()) { | |
| 317 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 318 | "CommitProcessor - error: Could not initialize catalog manager " | ||
| 319 | "for direct-graft"); | ||
| 320 | ✗ | return kError; | |
| 321 | } | ||
| 322 | |||
| 323 | ✗ | if (new_root_hash.IsNull() | |
| 324 | ✗ | || new_root_hash.suffix != shash::kSuffixCatalog) { | |
| 325 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 326 | "CommitProcessor - error: DirectGraft requires a catalog hash"); | ||
| 327 | ✗ | return kMergeFailure; | |
| 328 | } | ||
| 329 | |||
| 330 | // Download new_root_hash to a temp file to obtain the size of the catalog | ||
| 331 | // database. TryGraftNestedCatalog downloads the catalog once more | ||
| 332 | // internally via LoadFreeCatalog; the probe writes outside the local cache | ||
| 333 | // directory, so that second fetch does not hit the cache. | ||
| 334 | ✗ | const std::string catalog_url = params.stratum0 + "/data/" | |
| 335 | ✗ | + new_root_hash.MakePath(); | |
| 336 | ✗ | const std::string catalog_tmp = graft_temp + "/catalog_size"; | |
| 337 | { | ||
| 338 | ✗ | cvmfs::PathSink catalog_sink(catalog_tmp); | |
| 339 | ✗ | const shash::Any expected = new_root_hash; | |
| 340 | // Decompress while downloading (the content hash is still verified | ||
| 341 | // against the compressed stream): nested_catalogs.size holds the size of | ||
| 342 | // the catalog database, not of the compressed CAS object. Compare | ||
| 343 | // CommandCheck::FetchCatalog, which validates this column against the | ||
| 344 | // size of the decompressed catalog. | ||
| 345 | download::JobInfo dl_job(&catalog_url, true, false, &expected, | ||
| 346 | ✗ | &catalog_sink); | |
| 347 | ✗ | const download::Failures dl_ret = server_tool->download_manager()->Fetch( | |
| 348 | &dl_job); | ||
| 349 | ✗ | if (dl_ret != download::kFailOk) { | |
| 350 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 351 | "CommitProcessor - error: failed to download catalog %s " | ||
| 352 | "for size probe (%d)", | ||
| 353 | catalog_url.c_str(), static_cast<int>(dl_ret)); | ||
| 354 | ✗ | unlink(catalog_tmp.c_str()); | |
| 355 | ✗ | return kError; | |
| 356 | } | ||
| 357 | ✗ | } // PathSink destructor closes the file here | |
| 358 | ✗ | const int64_t catalog_size = GetFileSize(catalog_tmp); | |
| 359 | ✗ | unlink(catalog_tmp.c_str()); | |
| 360 | // A zero size would be recorded as "unknown" by swissknife check and | ||
| 361 | // silently disable its size validation, so reject it here. | ||
| 362 | ✗ | if (catalog_size <= 0) { | |
| 363 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 364 | "CommitProcessor - error: empty or unstatable catalog %s", | ||
| 365 | catalog_url.c_str()); | ||
| 366 | ✗ | return kError; | |
| 367 | } | ||
| 368 | |||
| 369 | // Graft: inserts the nested catalog reference into the parent catalog | ||
| 370 | // and propagates the directory entry + counters upward. | ||
| 371 | ✗ | if (!output_mgr->TryGraftNestedCatalog( | |
| 372 | ✗ | relative_lease_path.ToString(), new_root_hash, | |
| 373 | static_cast<uint64_t>(catalog_size))) { | ||
| 374 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 375 | "CommitProcessor - error: DirectGraft validation failed for " | ||
| 376 | "lease_path: %s", | ||
| 377 | lease_path.c_str()); | ||
| 378 | ✗ | return kMergeFailure; | |
| 379 | } | ||
| 380 | |||
| 381 | // Commit updates manifest_tgt in-place (new root hash, revision++, etc.) | ||
| 382 | ✗ | if (!output_mgr->Commit(false, 0, manifest_tgt.get())) { | |
| 383 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 384 | "CommitProcessor - error: Could not commit grafted catalog"); | ||
| 385 | ✗ | return kMergeFailure; | |
| 386 | } | ||
| 387 | |||
| 388 | // Export the updated manifest to a temp file for CreateNewTag/SigningTool. | ||
| 389 | ✗ | new_manifest_path = CreateTempPath(temp_dir_root, 0600); | |
| 390 | ✗ | if (!manifest_tgt->Export(new_manifest_path)) { | |
| 391 | ✗ | LogCvmfs( | |
| 392 | kLogReceiver, kLogSyslogErr, | ||
| 393 | "CommitProcessor - error: Could not export manifest after graft"); | ||
| 394 | ✗ | return kError; | |
| 395 | } | ||
| 396 | ✗ | new_manifest_hash = manifest_tgt->catalog_hash(); | |
| 397 | ✗ | *final_revision = manifest_tgt->revision(); | |
| 398 | |||
| 399 | ✗ | } else { | |
| 400 | // -- Standard DiffRec path via CatalogMergeTool -------------------------- | ||
| 401 | ✗ | LogCvmfs(kLogReceiver, kLogSyslog, | |
| 402 | "CommitProcessor - lease_path: %s, merging catalogs", | ||
| 403 | lease_path.c_str()); | ||
| 404 | |||
| 405 | CatalogMergeTool<catalog::WritableCatalogManager, | ||
| 406 | catalog::SimpleCatalogManager> | ||
| 407 | merge_tool(params.stratum0, old_root_hash, new_root_hash, | ||
| 408 | relative_lease_path, temp_dir_root, | ||
| 409 | server_tool->download_manager(), manifest_tgt.get(), | ||
| 410 | ✗ | statistics_, cache_dir_); | |
| 411 | ✗ | if (!merge_tool.Init()) { | |
| 412 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 413 | "Error: Could not initialize the catalog merge tool"); | ||
| 414 | ✗ | return kError; | |
| 415 | } | ||
| 416 | ✗ | if (!merge_tool.Run(params, &new_manifest_path, &new_manifest_hash, | |
| 417 | final_revision)) { | ||
| 418 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 419 | "CommitProcessor - error: Catalog merge failed"); | ||
| 420 | ✗ | return kMergeFailure; | |
| 421 | } | ||
| 422 | } | ||
| 423 | |||
| 424 | const std::unique_ptr<RaiiTempDir> raii_temp_dir( | ||
| 425 | ✗ | RaiiTempDir::Create(temp_dir_root)); | |
| 426 | ✗ | const std::string temp_dir = raii_temp_dir->dir(); | |
| 427 | |||
| 428 | // Determine the cutoff below which outdated auto-generated tags are removed. | ||
| 429 | // A value sent by the publisher (already an absolute timestamp) takes | ||
| 430 | // precedence over the gateway's local CVMFS_AUTO_TAG_TIMESPAN configuration, | ||
| 431 | // which is a relative "<N> <unit> ago" timespan resolved here. 0 disables | ||
| 432 | // cleanup. | ||
| 433 | ✗ | time_t auto_tag_threshold = final_tag.auto_tag_threshold(); | |
| 434 | ✗ | if (auto_tag_threshold <= 0 && !params.auto_tag_timespan.empty()) { | |
| 435 | ✗ | auto_tag_threshold = ParseRelativeTimespan(params.auto_tag_timespan, | |
| 436 | time(NULL)); | ||
| 437 | ✗ | if (auto_tag_threshold <= 0) { | |
| 438 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 439 | "CommitProcessor - warning: could not parse " | ||
| 440 | "CVMFS_AUTO_TAG_TIMESPAN '%s' (expected \"<N> <unit> ago\")", | ||
| 441 | params.auto_tag_timespan.c_str()); | ||
| 442 | } | ||
| 443 | } | ||
| 444 | ✗ | if (auto_tag_threshold > 0) { | |
| 445 | ✗ | LogCvmfs(kLogReceiver, kLogSyslog, | |
| 446 | "CommitProcessor - lease_path: %s, cleaning up auto tags " | ||
| 447 | "older than %ld", | ||
| 448 | lease_path.c_str(), static_cast<long>(auto_tag_threshold)); | ||
| 449 | } | ||
| 450 | |||
| 451 | // EditTags adds the tag for the new revision, removes any tags requested by | ||
| 452 | // `cvmfs_server tag -r`, and, when a cleanup threshold is set, removes the | ||
| 453 | // outdated auto tags -- all in the same history transaction. A failure here | ||
| 454 | // is fatal: leaving the new revision untagged (or silently keeping stale | ||
| 455 | // tags) would be worse than aborting the commit. | ||
| 456 | // | ||
| 457 | // Only real publish commits should rotate the undo tags (`trunk` and | ||
| 458 | // `trunk-previous`). Pure gateway tag edits reuse the current root hash as | ||
| 459 | // both old and new hash, so updating undo tags there would incorrectly make | ||
| 460 | // `trunk-previous` point at the current HEAD. | ||
| 461 | ✗ | const bool maintain_undo_tags = (old_root_hash != new_root_hash); | |
| 462 | ✗ | if (!EditTags(final_tag, repo_name, params, temp_dir, new_manifest_path, | |
| 463 | public_key, params.proxy, auto_tag_threshold, | ||
| 464 | maintain_undo_tags)) { | ||
| 465 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, "Error editing tags (add: '%s')", | |
| 466 | ✗ | final_tag.name().c_str()); | |
| 467 | ✗ | return kError; | |
| 468 | } | ||
| 469 | |||
| 470 | // Re-check the lease right before the final, repository-modifying step. The | ||
| 471 | // catalog merge and object upload above can be slow, during which the lease | ||
| 472 | // may have expired and an overlapping lease may have been granted to another | ||
| 473 | // publisher. If the deadline has passed we must not publish: the objects | ||
| 474 | // uploaded above stay unreferenced and are reclaimed by garbage collection. | ||
| 475 | // lease_expiration already has the gateway's configured safety margin | ||
| 476 | // subtracted, so this is a plain comparison against the current time. | ||
| 477 | ✗ | if (static_cast<int64_t>(time(NULL)) >= lease_expiration) { | |
| 478 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 479 | "CommitProcessor - lease_path: %s, lease expired during commit; " | ||
| 480 | "skipping publication, uploaded objects will be " | ||
| 481 | "garbage-collected", | ||
| 482 | lease_path.c_str()); | ||
| 483 | ✗ | return kLeaseExpired; | |
| 484 | } | ||
| 485 | |||
| 486 | ✗ | LogCvmfs(kLogReceiver, kLogSyslog, | |
| 487 | "CommitProcessor - lease_path: %s, signing manifest", | ||
| 488 | lease_path.c_str()); | ||
| 489 | |||
| 490 | // Add C_N root catalog hash to reflog through SigningTool, | ||
| 491 | // so garbage collector can later delete it. | ||
| 492 | ✗ | std::vector<shash::Any> reflog_catalogs; | |
| 493 | ✗ | reflog_catalogs.push_back(new_root_hash); | |
| 494 | |||
| 495 | ✗ | SigningTool signing_tool(server_tool.get()); | |
| 496 | ✗ | const SigningTool::Result res = signing_tool.Run( | |
| 497 | new_manifest_path, params.stratum0, params.spooler_configuration, | ||
| 498 | temp_dir, certificate, private_key, repo_name, "", "", | ||
| 499 | ✗ | "/var/spool/cvmfs/" + repo_name + "/reflog.chksum", params.proxy, | |
| 500 | ✗ | params.garbage_collection, false, false, reflog_catalogs); | |
| 501 | ✗ | switch (res) { | |
| 502 | ✗ | case SigningTool::kReflogChecksumMissing: | |
| 503 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 504 | "CommitProcessor - error: missing reflog.chksum"); | ||
| 505 | ✗ | return kMissingReflog; | |
| 506 | ✗ | case SigningTool::kReflogMissing: | |
| 507 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 508 | "CommitProcessor - error: missing reflog"); | ||
| 509 | ✗ | return kMissingReflog; | |
| 510 | ✗ | case SigningTool::kError: | |
| 511 | case SigningTool::kInitError: | ||
| 512 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 513 | "CommitProcessor - error: signing manifest"); | ||
| 514 | ✗ | return kError; | |
| 515 | ✗ | case SigningTool::kSuccess: | |
| 516 | ✗ | LogCvmfs(kLogReceiver, kLogSyslog, | |
| 517 | "CommitProcessor - lease_path: %s, success.", | ||
| 518 | lease_path.c_str()); | ||
| 519 | } | ||
| 520 | |||
| 521 | ✗ | LogCvmfs(kLogReceiver, kLogSyslog, | |
| 522 | "CommitProcessor - lease_path: %s, new root hash: %s", | ||
| 523 | ✗ | lease_path.c_str(), new_manifest_hash.ToString(false).c_str()); | |
| 524 | |||
| 525 | // Ensure CVMFS_ROOT_HASH is not set in | ||
| 526 | // /var/spool/cvmfs/<REPO_NAME>/client.local | ||
| 527 | ✗ | const std::string fname = "/var/spool/cvmfs/" + repo_name + "/client.local"; | |
| 528 | ✗ | if (truncate(fname.c_str(), 0) < 0) { | |
| 529 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, "Could not truncate %s\n", | |
| 530 | fname.c_str()); | ||
| 531 | ✗ | return kError; | |
| 532 | } | ||
| 533 | |||
| 534 | ✗ | StatisticsDatabase *stats_db = StatisticsDatabase::OpenStandardDB(repo_name); | |
| 535 | ✗ | if (stats_db != NULL) { | |
| 536 | ✗ | if (!stats_db->StorePublishStatistics(statistics_, start_time_, true)) { | |
| 537 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 538 | "Could not store publish statistics"); | ||
| 539 | } | ||
| 540 | ✗ | if (params.upload_stats_db) { | |
| 541 | const upload::SpoolerDefinition sd(params.spooler_configuration, | ||
| 542 | ✗ | shash::kAny); | |
| 543 | ✗ | upload::Spooler *spooler = upload::Spooler::Construct(sd); | |
| 544 | ✗ | if (!stats_db->UploadStatistics(spooler)) { | |
| 545 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, | |
| 546 | "Could not upload statistics DB to upstream storage"); | ||
| 547 | } | ||
| 548 | ✗ | delete spooler; | |
| 549 | } | ||
| 550 | ✗ | delete stats_db; | |
| 551 | |||
| 552 | } else { | ||
| 553 | ✗ | LogCvmfs(kLogReceiver, kLogSyslogErr, "Could not open statistics DB"); | |
| 554 | } | ||
| 555 | |||
| 556 | ✗ | return kSuccess; | |
| 557 | } | ||
| 558 | |||
| 559 | 7 | void CommitProcessor::SetStatistics(perf::Statistics *st, | |
| 560 | const std::string &start_time) { | ||
| 561 | 7 | statistics_ = st; | |
| 562 |
3/6✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 6 taken 7 times.
✗ Branch 7 not taken.
✓ Branch 9 taken 7 times.
✗ Branch 10 not taken.
|
7 | statistics_->Register("publish.revision", ""); |
| 563 | 7 | start_time_ = start_time; | |
| 564 | 7 | } | |
| 565 | |||
| 566 | } // namespace receiver | ||
| 567 |