GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/swissknife_history.cc
Date: 2024-04-21 02:33:16
Exec Total Coverage
Lines: 0 638 0.0%
Branches: 0 1557 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System
3 */
4
5 #include "swissknife_history.h"
6 #include "cvmfs_config.h"
7
8 #include <algorithm>
9 #include <cassert>
10 #include <ctime>
11
12 #include "catalog_rw.h"
13 #include "crypto/hash.h"
14 #include "crypto/signature.h"
15 #include "manifest_fetch.h"
16 #include "network/download.h"
17 #include "upload.h"
18
19 using namespace std; // NOLINT
20 using namespace swissknife; // NOLINT
21
22 const std::string CommandTag::kHeadTag = "trunk";
23 const std::string CommandTag::kPreviousHeadTag = "trunk-previous";
24
25 const std::string CommandTag::kHeadTagDescription = "current HEAD";
26 const std::string CommandTag::kPreviousHeadTagDescription =
27 "default undo target";
28
29 static void InsertCommonParameters(ParameterList *r) {
30 r->push_back(Parameter::Mandatory('w', "repository directory / url"));
31 r->push_back(Parameter::Mandatory('t', "temporary scratch directory"));
32 r->push_back(Parameter::Optional('p', "public key of the repository"));
33 r->push_back(Parameter::Optional('f', "fully qualified repository name"));
34 r->push_back(Parameter::Optional('r', "spooler definition string"));
35 r->push_back(Parameter::Optional('m', "(unsigned) manifest file to edit"));
36 r->push_back(Parameter::Optional('b', "mounted repository base hash"));
37 r->push_back(
38 Parameter::Optional('e', "hash algorithm to use (default SHA1)"));
39 r->push_back(Parameter::Switch('L', "follow HTTP redirects"));
40 r->push_back(Parameter::Optional('P', "session_token_file"));
41 r->push_back(Parameter::Optional('@', "proxy url"));
42 }
43
44 CommandTag::Environment *CommandTag::InitializeEnvironment(
45 const ArgumentList &args, const bool read_write) {
46 const string repository_url = MakeCanonicalPath(*args.find('w')->second);
47 const string tmp_path = MakeCanonicalPath(*args.find('t')->second);
48 const string spl_definition =
49 (args.find('r') == args.end())
50 ? ""
51 : MakeCanonicalPath(*args.find('r')->second);
52 const string manifest_path = (args.find('m') == args.end())
53 ? ""
54 : MakeCanonicalPath(*args.find('m')->second);
55 const shash::Algorithms hash_algo =
56 (args.find('e') == args.end())
57 ? shash::kSha1
58 : shash::ParseHashAlgorithm(*args.find('e')->second);
59 const string pubkey_path = (args.find('p') == args.end())
60 ? ""
61 : MakeCanonicalPath(*args.find('p')->second);
62 const shash::Any base_hash =
63 (args.find('b') == args.end())
64 ? shash::Any()
65 : shash::MkFromHexPtr(shash::HexPtr(*args.find('b')->second),
66 shash::kSuffixCatalog);
67 const string repo_name =
68 (args.find('f') == args.end()) ? "" : *args.find('f')->second;
69
70 string session_token_file;
71 if (args.find('P') != args.end()) {
72 session_token_file = *args.find('P')->second;
73 }
74
75 // Sanity checks
76 if (hash_algo == shash::kAny) {
77 LogCvmfs(kLogCvmfs, kLogStderr, "failed to parse hash algorithm to use");
78 return NULL;
79 }
80
81 if (read_write && spl_definition.empty()) {
82 LogCvmfs(kLogCvmfs, kLogStderr, "no upstream storage provided (-r)");
83 return NULL;
84 }
85
86 if (read_write && manifest_path.empty()) {
87 LogCvmfs(kLogCvmfs, kLogStderr, "no (unsigned) manifest provided (-m)");
88 return NULL;
89 }
90
91 if (!read_write && pubkey_path.empty()) {
92 LogCvmfs(kLogCvmfs, kLogStderr, "no public key provided (-p)");
93 return NULL;
94 }
95
96 if (!read_write && repo_name.empty()) {
97 LogCvmfs(kLogCvmfs, kLogStderr, "no repository name provided (-f)");
98 return NULL;
99 }
100
101 if (HasPrefix(spl_definition, "gw", false)) {
102 if (session_token_file.empty()) {
103 PrintError(
104 "Session token file has to be provided "
105 "when upstream type is gw.");
106 return NULL;
107 }
108 }
109
110 // create new environment
111 // Note: We use this encapsulation because we cannot be sure that the
112 // Command object gets deleted properly. With the Environment object at
113 // hand we have full control and can make heavy and safe use of RAII
114 UniquePtr<Environment> env(new Environment(repository_url, tmp_path));
115 env->manifest_path.Set(manifest_path);
116 env->history_path.Set(CreateTempPath(tmp_path + "/history", 0600));
117
118 // initialize the (swissknife global) download manager
119 const bool follow_redirects = (args.count('L') > 0);
120 const std::string &proxy = (args.count('@') > 0) ?
121 *args.find('@')->second : "";
122 if (!this->InitDownloadManager(follow_redirects, proxy)) {
123 return NULL;
124 }
125
126 // initialize the (swissknife global) signature manager (if possible)
127 if (!pubkey_path.empty() &&
128 !this->InitVerifyingSignatureManager(pubkey_path)) {
129 return NULL;
130 }
131
132 // open the (yet unsigned) manifest file if it is there, otherwise load the
133 // latest manifest from the server
134 env->manifest =
135 (FileExists(env->manifest_path.path()))
136 ? OpenLocalManifest(env->manifest_path.path())
137 : FetchRemoteManifest(env->repository_url, repo_name, base_hash);
138
139 if (!env->manifest.IsValid()) {
140 LogCvmfs(kLogCvmfs, kLogStderr, "failed to load manifest file");
141 return NULL;
142 }
143
144 // figure out the hash of the history from the previous revision if needed
145 if (read_write && env->manifest->history().IsNull() && !base_hash.IsNull()) {
146 env->previous_manifest =
147 FetchRemoteManifest(env->repository_url, repo_name, base_hash);
148 if (!env->previous_manifest.IsValid()) {
149 LogCvmfs(kLogCvmfs, kLogStderr, "failed to load previous manifest");
150 return NULL;
151 }
152
153 LogCvmfs(kLogCvmfs, kLogDebug,
154 "using history database '%s' from previous "
155 "manifest (%s) as basis",
156 env->previous_manifest->history().ToString().c_str(),
157 env->previous_manifest->repository_name().c_str());
158 env->manifest->set_history(env->previous_manifest->history());
159 env->manifest->set_repository_name(
160 env->previous_manifest->repository_name());
161 }
162
163 // download the history database referenced in the manifest
164 env->history = GetHistory(env->manifest.weak_ref(), env->repository_url,
165 env->history_path.path(), read_write);
166 if (!env->history.IsValid()) {
167 return NULL;
168 }
169
170 // if the using Command is expected to change the history database, we
171 // need
172 // to initialize the upload spooler for potential later history upload
173 if (read_write) {
174 const bool use_file_chunking = false;
175 const bool generate_legacy_bulk_chunks = false;
176 const upload::SpoolerDefinition sd(spl_definition, hash_algo,
177 zlib::kZlibDefault,
178 generate_legacy_bulk_chunks,
179 use_file_chunking, 0, 0, 0,
180 session_token_file);
181 env->spooler = upload::Spooler::Construct(sd);
182 if (!env->spooler.IsValid()) {
183 LogCvmfs(kLogCvmfs, kLogStderr, "failed to initialize upload spooler");
184 return NULL;
185 }
186 }
187
188 // return the pointer of the Environment (passing the ownership along)
189 return env.Release();
190 }
191
192 bool CommandTag::CloseAndPublishHistory(Environment *env) {
193 assert(env->spooler.IsValid());
194
195 // set the previous revision pointer of the history database
196 env->history->SetPreviousRevision(env->manifest->history());
197
198 // close the history database
199 history::History *weak_history = env->history.Release();
200 delete weak_history;
201
202 // compress and upload the new history database
203 Future<shash::Any> history_hash;
204 upload::Spooler::CallbackPtr callback = env->spooler->RegisterListener(
205 &CommandTag::UploadClosure, this, &history_hash);
206 env->spooler->ProcessHistory(env->history_path.path());
207 env->spooler->WaitForUpload();
208 const shash::Any new_history_hash = history_hash.Get();
209 env->spooler->UnregisterListener(callback);
210
211 // retrieve the (async) uploader result
212 if (new_history_hash.IsNull()) {
213 return false;
214 }
215
216 // update the (yet unsigned) manifest file
217 env->manifest->set_history(new_history_hash);
218 if (!env->manifest->Export(env->manifest_path.path())) {
219 LogCvmfs(kLogCvmfs, kLogStderr, "failed to export the new manifest '%s'",
220 env->manifest_path.path().c_str());
221 return false;
222 }
223
224 // disable the unlink guard in order to keep the newly exported manifest file
225 env->manifest_path.Disable();
226 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
227 "exported manifest (%" PRIu64 ") with new history '%s'",
228 env->manifest->revision(), new_history_hash.ToString().c_str());
229
230 return true;
231 }
232
233
234 bool CommandTag::UploadCatalogAndUpdateManifest(
235 CommandTag::Environment *env, catalog::WritableCatalog *catalog) {
236 assert(env->spooler.IsValid());
237
238 // gather information about catalog to be uploaded and update manifest
239 UniquePtr<catalog::WritableCatalog> wr_catalog(catalog);
240 const std::string catalog_path = wr_catalog->database_path();
241 env->manifest->set_ttl(wr_catalog->GetTTL());
242 env->manifest->set_revision(wr_catalog->GetRevision());
243 env->manifest->set_publish_timestamp(wr_catalog->GetLastModified());
244
245 // close the catalog
246 catalog::WritableCatalog *weak_catalog = wr_catalog.Release();
247 delete weak_catalog;
248
249 // upload the catalog
250 Future<shash::Any> catalog_hash;
251 upload::Spooler::CallbackPtr callback = env->spooler->RegisterListener(
252 &CommandTag::UploadClosure, this, &catalog_hash);
253 env->spooler->ProcessCatalog(catalog_path);
254 env->spooler->WaitForUpload();
255 const shash::Any new_catalog_hash = catalog_hash.Get();
256 env->spooler->UnregisterListener(callback);
257
258 // check if the upload succeeded
259 if (new_catalog_hash.IsNull()) {
260 LogCvmfs(kLogCvmfs, kLogStderr, "failed to upload catalog '%s'",
261 catalog_path.c_str());
262 return false;
263 }
264
265 // update the catalog size and hash in the manifest
266 const size_t catalog_size = GetFileSize(catalog_path);
267 env->manifest->set_catalog_size(catalog_size);
268 env->manifest->set_catalog_hash(new_catalog_hash);
269
270 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "uploaded new catalog (%lu bytes) '%s'",
271 catalog_size, new_catalog_hash.ToString().c_str());
272
273 return true;
274 }
275
276 void CommandTag::UploadClosure(const upload::SpoolerResult &result,
277 Future<shash::Any> *hash) {
278 assert(!result.IsChunked());
279 if (result.return_code != 0) {
280 LogCvmfs(kLogCvmfs, kLogStderr, "failed to upload history database (%d)",
281 result.return_code);
282 hash->Set(shash::Any());
283 } else {
284 hash->Set(result.content_hash);
285 }
286 }
287
288 bool CommandTag::UpdateUndoTags(
289 Environment *env, const history::History::Tag &current_head_template,
290 const bool undo_rollback) {
291 assert(env->history.IsValid());
292
293 history::History::Tag current_head;
294 history::History::Tag current_old_head;
295
296 // remove previous HEAD tag
297 if (!env->history->Remove(CommandTag::kPreviousHeadTag)) {
298 LogCvmfs(kLogCvmfs, kLogVerboseMsg, "didn't find a previous HEAD tag");
299 }
300
301 // check if we have a current HEAD tag that needs to renamed to previous
302 // HEAD
303 if (env->history->GetByName(CommandTag::kHeadTag, &current_head)) {
304 // remove current HEAD tag
305 if (!env->history->Remove(CommandTag::kHeadTag)) {
306 LogCvmfs(kLogCvmfs, kLogStderr, "failed to remove current HEAD tag");
307 return false;
308 }
309
310 // set previous HEAD tag where current HEAD used to be
311 if (!undo_rollback) {
312 current_old_head = current_head;
313 current_old_head.name = CommandTag::kPreviousHeadTag;
314 current_old_head.description = CommandTag::kPreviousHeadTagDescription;
315 if (!env->history->Insert(current_old_head)) {
316 LogCvmfs(kLogCvmfs, kLogStderr, "failed to set previous HEAD tag");
317 return false;
318 }
319 }
320 }
321
322 // set the current HEAD to the catalog provided by the template HEAD
323 current_head = current_head_template;
324 current_head.name = CommandTag::kHeadTag;
325 current_head.description = CommandTag::kHeadTagDescription;
326 if (!env->history->Insert(current_head)) {
327 LogCvmfs(kLogCvmfs, kLogStderr, "failed to set new current HEAD");
328 return false;
329 }
330
331 return true;
332 }
333
334 bool CommandTag::FetchObject(const std::string &repository_url,
335 const shash::Any &object_hash,
336 const std::string &destination_path) const {
337 assert(!object_hash.IsNull());
338
339 download::Failures dl_retval;
340 const std::string url = repository_url + "/data/" + object_hash.MakePath();
341
342 cvmfs::PathSink pathsink(destination_path);
343 download::JobInfo download_object(&url, true, false, &object_hash, &pathsink);
344 dl_retval = download_manager()->Fetch(&download_object);
345
346 if (dl_retval != download::kFailOk) {
347 LogCvmfs(kLogCvmfs, kLogStderr, "failed to download object '%s' (%d - %s)",
348 object_hash.ToStringWithSuffix().c_str(), dl_retval,
349 download::Code2Ascii(dl_retval));
350 return false;
351 }
352
353 return true;
354 }
355
356 history::History *CommandTag::GetHistory(const manifest::Manifest *manifest,
357 const std::string &repository_url,
358 const std::string &history_path,
359 const bool read_write) const {
360 const shash::Any history_hash = manifest->history();
361 history::History *history;
362
363 if (history_hash.IsNull()) {
364 history = history::SqliteHistory::Create(history_path,
365 manifest->repository_name());
366 if (NULL == history) {
367 LogCvmfs(kLogCvmfs, kLogStderr, "failed to create history database");
368 return NULL;
369 }
370 } else {
371 if (!FetchObject(repository_url, history_hash, history_path)) {
372 return NULL;
373 }
374
375 history = (read_write) ? history::SqliteHistory::OpenWritable(history_path)
376 : history::SqliteHistory::Open(history_path);
377 if (NULL == history) {
378 LogCvmfs(kLogCvmfs, kLogStderr, "failed to open history database (%s)",
379 history_path.c_str());
380 unlink(history_path.c_str());
381 return NULL;
382 }
383
384 assert(history->fqrn() == manifest->repository_name());
385 }
386
387 return history;
388 }
389
390 catalog::Catalog *CommandTag::GetCatalog(const std::string &repository_url,
391 const shash::Any &catalog_hash,
392 const std::string catalog_path,
393 const bool read_write) const {
394 assert(shash::kSuffixCatalog == catalog_hash.suffix);
395 if (!FetchObject(repository_url, catalog_hash, catalog_path)) {
396 return NULL;
397 }
398
399 const std::string catalog_root_path = "";
400 return (read_write) ? catalog::WritableCatalog::AttachFreely(
401 catalog_root_path, catalog_path, catalog_hash)
402 : catalog::Catalog::AttachFreely(
403 catalog_root_path, catalog_path, catalog_hash);
404 }
405
406 void CommandTag::PrintTagMachineReadable(
407 const history::History::Tag &tag) const {
408 LogCvmfs(kLogCvmfs, kLogStdout, "%s %s %" PRIu64 " %" PRIu64 " %ld %s %s",
409 tag.name.c_str(),
410 tag.root_hash.ToString().c_str(), tag.size, tag.revision,
411 tag.timestamp,
412 (tag.branch == "") ? "(default)" : tag.branch.c_str(),
413 tag.description.c_str());
414 }
415
416 std::string CommandTag::AddPadding(const std::string &str, const size_t padding,
417 const bool align_right,
418 const std::string &fill_char) const {
419 assert(str.size() <= padding);
420 std::string result(str);
421 result.resize(padding);
422 const size_t pos = (align_right) ? 0 : str.size();
423 const size_t padding_width = padding - str.size();
424 for (size_t i = 0; i < padding_width; ++i) result.insert(pos, fill_char);
425 return result;
426 }
427
428 bool CommandTag::IsUndoTagName(const std::string &tag_name) const {
429 return tag_name == CommandTag::kHeadTag ||
430 tag_name == CommandTag::kPreviousHeadTag;
431 }
432
433 //------------------------------------------------------------------------------
434
435 ParameterList CommandEditTag::GetParams() const {
436 ParameterList r;
437 InsertCommonParameters(&r);
438
439 r.push_back(Parameter::Optional('d', "space separated tags to be deleted"));
440 r.push_back(Parameter::Optional('a', "name of the new tag"));
441 r.push_back(Parameter::Optional('D', "description of the tag"));
442 r.push_back(Parameter::Optional('B', "branch of the new tag"));
443 r.push_back(Parameter::Optional('P', "predecessor branch"));
444 r.push_back(Parameter::Optional('h', "root hash of the new tag"));
445 r.push_back(Parameter::Switch('x', "maintain undo tags"));
446 return r;
447 }
448
449 int CommandEditTag::Main(const ArgumentList &args) {
450 if ((args.find('d') == args.end()) && (args.find('a') == args.end()) &&
451 (args.find('x') == args.end())) {
452 LogCvmfs(kLogCvmfs, kLogStderr, "nothing to do");
453 return 1;
454 }
455
456 // initialize the Environment (taking ownership)
457 const bool history_read_write = true;
458 UniquePtr<Environment> env(InitializeEnvironment(args, history_read_write));
459 if (!env.IsValid()) {
460 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
461 return 1;
462 }
463
464 int retval;
465 if (args.find('d') != args.end()) {
466 retval = RemoveTags(args, env.weak_ref());
467 if (retval != 0) return retval;
468 }
469 if ((args.find('a') != args.end()) || (args.find('x') != args.end())) {
470 retval = AddNewTag(args, env.weak_ref());
471 if (retval != 0) return retval;
472 }
473
474 // finalize processing and upload new history database
475 if (!CloseAndPublishHistory(env.weak_ref())) {
476 return 1;
477 }
478 return 0;
479 }
480
481 int CommandEditTag::AddNewTag(const ArgumentList &args, Environment *env) {
482 const std::string tag_name =
483 (args.find('a') != args.end()) ? *args.find('a')->second : "";
484 const std::string tag_description =
485 (args.find('D') != args.end()) ? *args.find('D')->second : "";
486 const bool undo_tags = (args.find('x') != args.end());
487 const std::string root_hash_string =
488 (args.find('h') != args.end()) ? *args.find('h')->second : "";
489 const std::string branch_name =
490 (args.find('B') != args.end()) ? *args.find('B')->second : "";
491 const std::string previous_branch_name =
492 (args.find('P') != args.end()) ? *args.find('P')->second : "";
493
494 if (tag_name.find(" ") != std::string::npos) {
495 LogCvmfs(kLogCvmfs, kLogStderr, "tag names must not contain spaces");
496 return 1;
497 }
498
499 assert(!tag_name.empty() || undo_tags);
500
501 if (IsUndoTagName(tag_name)) {
502 LogCvmfs(kLogCvmfs, kLogStderr, "undo tags are managed internally");
503 return 1;
504 }
505
506 // set the root hash to be tagged to the current HEAD if no other hash was
507 // given by the user
508 shash::Any root_hash = GetTagRootHash(env, root_hash_string);
509 if (root_hash.IsNull()) {
510 return 1;
511 }
512
513 // open the catalog to be tagged (to check for existence and for meta info)
514 const UnlinkGuard catalog_path(
515 CreateTempPath(env->tmp_path + "/catalog", 0600));
516 const bool catalog_read_write = false;
517 const UniquePtr<catalog::Catalog> catalog(GetCatalog(
518 env->repository_url, root_hash, catalog_path.path(), catalog_read_write));
519 if (!catalog.IsValid()) {
520 LogCvmfs(kLogCvmfs, kLogStderr, "catalog with hash '%s' does not exist",
521 root_hash.ToString().c_str());
522 return 1;
523 }
524
525 // check if the catalog is a root catalog
526 if (!catalog->root_prefix().IsEmpty()) {
527 LogCvmfs(kLogCvmfs, kLogStderr,
528 "cannot tag catalog '%s' that is not a "
529 "root catalog.",
530 root_hash.ToString().c_str());
531 return 1;
532 }
533
534 // create a template for the new tag to be created, moved or used as undo tag
535 history::History::Tag tag_template;
536 tag_template.name = "<template>";
537 tag_template.root_hash = root_hash;
538 tag_template.size = GetFileSize(catalog_path.path());
539 tag_template.revision = catalog->GetRevision();
540 tag_template.timestamp = catalog->GetLastModified();
541 tag_template.branch = branch_name;
542 tag_template.description = tag_description;
543
544 // manipulate the tag database by creating a new tag or moving an existing one
545 if (!tag_name.empty()) {
546 tag_template.name = tag_name;
547 const bool user_provided_hash = (!root_hash_string.empty());
548
549 if (!env->history->ExistsBranch(tag_template.branch)) {
550 history::History::Branch branch(
551 tag_template.branch,
552 previous_branch_name,
553 tag_template.revision);
554 if (!env->history->InsertBranch(branch)) {
555 LogCvmfs(kLogCvmfs, kLogStderr, "cannot insert branch '%s'",
556 tag_template.branch.c_str());
557 return 1;
558 }
559 }
560
561 if (!ManipulateTag(env, tag_template, user_provided_hash)) {
562 return 1;
563 }
564 }
565
566 // handle undo tags ('trunk' and 'trunk-previous') if necessary
567 if (undo_tags && !UpdateUndoTags(env, tag_template)) {
568 return 1;
569 }
570
571 return 0;
572 }
573
574 shash::Any CommandEditTag::GetTagRootHash(
575 Environment *env, const std::string &root_hash_string) const {
576 shash::Any root_hash;
577
578 if (root_hash_string.empty()) {
579 LogCvmfs(kLogCvmfs, kLogVerboseMsg,
580 "no catalog hash provided, using hash"
581 "of current HEAD catalog (%s)",
582 env->manifest->catalog_hash().ToString().c_str());
583 root_hash = env->manifest->catalog_hash();
584 } else {
585 root_hash = shash::MkFromHexPtr(shash::HexPtr(root_hash_string),
586 shash::kSuffixCatalog);
587 if (root_hash.IsNull()) {
588 LogCvmfs(kLogCvmfs, kLogStderr,
589 "failed to read provided catalog hash '%s'",
590 root_hash_string.c_str());
591 }
592 }
593
594 return root_hash;
595 }
596
597 bool CommandEditTag::ManipulateTag(Environment *env,
598 const history::History::Tag &tag_template,
599 const bool user_provided_hash) {
600 const std::string &tag_name = tag_template.name;
601
602 // check if the tag already exists, otherwise create it and return
603 if (!env->history->Exists(tag_name)) {
604 return CreateTag(env, tag_template);
605 }
606
607 // tag does exist already, now we need to see if we can move it
608 if (!user_provided_hash) {
609 LogCvmfs(kLogCvmfs, kLogStderr,
610 "a tag with the name '%s' already exists. Do you want to move it? "
611 "(-h <root hash>)",
612 tag_name.c_str());
613 return false;
614 }
615
616 // move the already existing tag and return
617 return MoveTag(env, tag_template);
618 }
619
620 bool CommandEditTag::MoveTag(Environment *env,
621 const history::History::Tag &tag_template)
622 {
623 const std::string &tag_name = tag_template.name;
624 history::History::Tag new_tag = tag_template;
625
626 // get the already existent tag
627 history::History::Tag old_tag;
628 if (!env->history->GetByName(tag_name, &old_tag)) {
629 LogCvmfs(kLogCvmfs, kLogStderr, "failed to retrieve tag '%s' for moving",
630 tag_name.c_str());
631 return false;
632 }
633
634 // check if we would move the tag to the same hash
635 if (old_tag.root_hash == new_tag.root_hash) {
636 LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' already points to '%s'",
637 tag_name.c_str(), old_tag.root_hash.ToString().c_str());
638 return false;
639 }
640
641 // copy over old description if no new description was given
642 if (new_tag.description.empty()) {
643 new_tag.description = old_tag.description;
644 }
645 new_tag.branch = old_tag.branch;
646
647 // remove the old tag from the database
648 if (!env->history->Remove(tag_name)) {
649 LogCvmfs(kLogCvmfs, kLogStderr, "removing old tag '%s' before move failed",
650 tag_name.c_str());
651 return false;
652 }
653 if (!env->history->PruneBranches()) {
654 LogCvmfs(kLogCvmfs, kLogStderr, "could not prune unused branches");
655 return false;
656 }
657 bool retval = env->history->Vacuum();
658 assert(retval);
659
660 LogCvmfs(kLogCvmfs, kLogStdout, "moving tag '%s' from '%s' to '%s'",
661 tag_name.c_str(), old_tag.root_hash.ToString().c_str(),
662 tag_template.root_hash.ToString().c_str());
663
664 // re-create the moved tag
665 return CreateTag(env, new_tag);
666 }
667
668 bool CommandEditTag::CreateTag(Environment *env,
669 const history::History::Tag &new_tag) {
670 if (!env->history->Insert(new_tag)) {
671 LogCvmfs(kLogCvmfs, kLogStderr, "failed to insert new tag '%s'",
672 new_tag.name.c_str());
673 return false;
674 }
675
676 return true;
677 }
678
679 int CommandEditTag::RemoveTags(const ArgumentList &args, Environment *env) {
680 typedef std::vector<std::string> TagNames;
681 const std::string tags_to_delete = *args.find('d')->second;
682
683 const TagNames condemned_tags = SplitString(tags_to_delete, ' ');
684
685 // check if user tries to remove a magic undo tag
686 TagNames::const_iterator i = condemned_tags.begin();
687 const TagNames::const_iterator iend = condemned_tags.end();
688 for (; i != iend; ++i) {
689 if (IsUndoTagName(*i)) {
690 LogCvmfs(kLogCvmfs, kLogStderr,
691 "undo tags are handled internally and cannot be deleted");
692 return 1;
693 }
694 }
695
696 LogCvmfs(kLogCvmfs, kLogDebug, "proceeding to delete %lu tags",
697 condemned_tags.size());
698
699 // check if the tags to be deleted exist
700 bool all_exist = true;
701 for (i = condemned_tags.begin(); i != iend; ++i) {
702 if (!env->history->Exists(*i)) {
703 LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist", i->c_str());
704 all_exist = false;
705 }
706 }
707 if (!all_exist) {
708 return 1;
709 }
710
711 // delete the tags from the tag database and print their root hashes
712 i = condemned_tags.begin();
713 env->history->BeginTransaction();
714 for (; i != iend; ++i) {
715 // print some information about the tag to be deleted
716 history::History::Tag condemned_tag;
717 const bool found_tag = env->history->GetByName(*i, &condemned_tag);
718 assert(found_tag);
719 LogCvmfs(kLogCvmfs, kLogStdout, "deleting '%s' (%s)",
720 condemned_tag.name.c_str(),
721 condemned_tag.root_hash.ToString().c_str());
722
723 // remove the tag
724 if (!env->history->Remove(*i)) {
725 LogCvmfs(kLogCvmfs, kLogStderr, "failed to remove tag '%s' from history",
726 i->c_str());
727 return 1;
728 }
729 }
730 bool retval = env->history->PruneBranches();
731 if (!retval) {
732 LogCvmfs(kLogCvmfs, kLogStderr,
733 "failed to prune unused branches from history");
734 return 1;
735 }
736 env->history->CommitTransaction();
737 retval = env->history->Vacuum();
738 assert(retval);
739
740 return 0;
741 }
742
743 //------------------------------------------------------------------------------
744
745
746 ParameterList CommandListTags::GetParams() const {
747 ParameterList r;
748 InsertCommonParameters(&r);
749 r.push_back(Parameter::Switch('x', "machine readable output"));
750 r.push_back(Parameter::Switch('B', "print branch hierarchy"));
751 return r;
752 }
753
754 void CommandListTags::PrintHumanReadableTagList(
755 const CommandListTags::TagList &tags) const {
756 // go through the list of tags and figure out the column widths
757 const std::string name_label = "Name";
758 const std::string rev_label = "Revision";
759 const std::string time_label = "Timestamp";
760 const std::string branch_label = "Branch";
761 const std::string desc_label = "Description";
762
763 // figure out the maximal lengths of the fields in the lists
764 TagList::const_reverse_iterator i = tags.rbegin();
765 const TagList::const_reverse_iterator iend = tags.rend();
766 size_t max_name_len = name_label.size();
767 size_t max_rev_len = rev_label.size();
768 size_t max_time_len = desc_label.size();
769 size_t max_branch_len = branch_label.size();
770 for (; i != iend; ++i) {
771 max_name_len = std::max(max_name_len, i->name.size());
772 max_rev_len = std::max(max_rev_len, StringifyInt(i->revision).size());
773 max_time_len =
774 std::max(max_time_len, StringifyTime(i->timestamp, true).size());
775 max_branch_len = std::max(max_branch_len, i->branch.size());
776 }
777
778 // print the list header
779 LogCvmfs(kLogCvmfs, kLogStdout,
780 "%s \u2502 %s \u2502 %s \u2502 %s \u2502 %s",
781 AddPadding(name_label, max_name_len).c_str(),
782 AddPadding(rev_label, max_rev_len).c_str(),
783 AddPadding(time_label, max_time_len).c_str(),
784 AddPadding(branch_label, max_branch_len).c_str(),
785 desc_label.c_str());
786 LogCvmfs(kLogCvmfs, kLogStdout,
787 "%s\u2500\u253C\u2500%s\u2500\u253C\u2500%s"
788 "\u2500\u253C\u2500%s\u2500\u253C\u2500%s",
789 AddPadding("", max_name_len, false, "\u2500").c_str(),
790 AddPadding("", max_rev_len, false, "\u2500").c_str(),
791 AddPadding("", max_time_len, false, "\u2500").c_str(),
792 AddPadding("", max_branch_len, false, "\u2500").c_str(),
793 AddPadding("", desc_label.size() + 1, false, "\u2500").c_str());
794
795 // print the rows of the list
796 i = tags.rbegin();
797 for (; i != iend; ++i) {
798 LogCvmfs(
799 kLogCvmfs, kLogStdout,
800 "%s \u2502 %s \u2502 %s \u2502 %s \u2502 %s",
801 AddPadding(i->name, max_name_len).c_str(),
802 AddPadding(StringifyInt(i->revision), max_rev_len, true).c_str(),
803 AddPadding(StringifyTime(i->timestamp, true), max_time_len).c_str(),
804 AddPadding(i->branch, max_branch_len).c_str(),
805 i->description.c_str());
806 }
807
808 // print the list footer
809 LogCvmfs(kLogCvmfs, kLogStdout,
810 "%s\u2500\u2534\u2500%s\u2500\u2534\u2500%s"
811 "\u2500\u2534\u2500%s\u2500\u2534\u2500%s",
812 AddPadding("", max_name_len, false, "\u2500").c_str(),
813 AddPadding("", max_rev_len, false, "\u2500").c_str(),
814 AddPadding("", max_time_len, false, "\u2500").c_str(),
815 AddPadding("", max_branch_len, false, "\u2500").c_str(),
816 AddPadding("", desc_label.size() + 1, false, "\u2500").c_str());
817
818 // print the number of tags listed
819 LogCvmfs(kLogCvmfs, kLogStdout, "listing contains %lu tags", tags.size());
820 }
821
822 void CommandListTags::PrintMachineReadableTagList(const TagList &tags) const {
823 TagList::const_iterator i = tags.begin();
824 const TagList::const_iterator iend = tags.end();
825 for (; i != iend; ++i) {
826 PrintTagMachineReadable(*i);
827 }
828 }
829
830
831 void CommandListTags::PrintHumanReadableBranchList(
832 const BranchHierarchy &branches) const
833 {
834 unsigned N = branches.size();
835 for (unsigned i = 0; i < N; ++i) {
836 for (unsigned l = 0; l < branches[i].level; ++l) {
837 LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, "%s",
838 ((l + 1) == branches[i].level) ? "\u251c " : "\u2502 ");
839 }
840 LogCvmfs(kLogCvmfs, kLogStdout, "%s @%" PRIu64,
841 branches[i].branch.branch.c_str(),
842 branches[i].branch.initial_revision);
843 }
844 }
845
846
847 void CommandListTags::PrintMachineReadableBranchList(
848 const BranchHierarchy &branches) const
849 {
850 unsigned N = branches.size();
851 for (unsigned i = 0; i < N; ++i) {
852 LogCvmfs(kLogCvmfs, kLogStdout, "[%u] %s%s @%" PRIu64,
853 branches[i].level,
854 AddPadding("", branches[i].level, false, " ").c_str(),
855 branches[i].branch.branch.c_str(),
856 branches[i].branch.initial_revision);
857 }
858 }
859
860
861 void CommandListTags::SortBranchesRecursively(
862 unsigned level,
863 const string &parent_branch,
864 const BranchList &branches,
865 BranchHierarchy *hierarchy) const
866 {
867 // For large numbers of branches, this should be turned into the O(n) version
868 // using a linked list
869 unsigned N = branches.size();
870 for (unsigned i = 0; i < N; ++i) {
871 if (branches[i].branch == "")
872 continue;
873 if (branches[i].parent == parent_branch) {
874 hierarchy->push_back(BranchLevel(branches[i], level));
875 SortBranchesRecursively(
876 level + 1, branches[i].branch, branches, hierarchy);
877 }
878 }
879 }
880
881
882 CommandListTags::BranchHierarchy CommandListTags::SortBranches(
883 const BranchList &branches) const
884 {
885 BranchHierarchy hierarchy;
886 hierarchy.push_back(
887 BranchLevel(history::History::Branch("(default)", "", 0), 0));
888 SortBranchesRecursively(1, "", branches, &hierarchy);
889 return hierarchy;
890 }
891
892
893 int CommandListTags::Main(const ArgumentList &args) {
894 const bool machine_readable = (args.find('x') != args.end());
895 const bool branch_hierarchy = (args.find('B') != args.end());
896
897 // initialize the Environment (taking ownership)
898 const bool history_read_write = false;
899 UniquePtr<Environment> env(InitializeEnvironment(args, history_read_write));
900 if (!env.IsValid()) {
901 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
902 return 1;
903 }
904
905 if (branch_hierarchy) {
906 BranchList branch_list;
907 if (!env->history->ListBranches(&branch_list)) {
908 LogCvmfs(kLogCvmfs, kLogStderr,
909 "failed to list branches in history database");
910 return 1;
911 }
912 BranchHierarchy branch_hierarchy = SortBranches(branch_list);
913
914 if (machine_readable) {
915 PrintMachineReadableBranchList(branch_hierarchy);
916 } else {
917 PrintHumanReadableBranchList(branch_hierarchy);
918 }
919 } else {
920 // obtain a full list of all tags
921 TagList tags;
922 if (!env->history->List(&tags)) {
923 LogCvmfs(kLogCvmfs, kLogStderr,
924 "failed to list tags in history database");
925 return 1;
926 }
927
928 if (machine_readable) {
929 PrintMachineReadableTagList(tags);
930 } else {
931 PrintHumanReadableTagList(tags);
932 }
933 }
934
935 return 0;
936 }
937
938 //------------------------------------------------------------------------------
939
940 ParameterList CommandInfoTag::GetParams() const {
941 ParameterList r;
942 InsertCommonParameters(&r);
943
944 r.push_back(Parameter::Mandatory('n', "name of the tag to be inspected"));
945 r.push_back(Parameter::Switch('x', "machine readable output"));
946 return r;
947 }
948
949 std::string CommandInfoTag::HumanReadableFilesize(const size_t filesize) const {
950 const size_t kiB = 1024;
951 const size_t MiB = kiB * 1024;
952 const size_t GiB = MiB * 1024;
953
954 if (filesize > GiB) {
955 return StringifyDouble(static_cast<double>(filesize) / GiB) + " GiB";
956 } else if (filesize > MiB) {
957 return StringifyDouble(static_cast<double>(filesize) / MiB) + " MiB";
958 } else if (filesize > kiB) {
959 return StringifyDouble(static_cast<double>(filesize) / kiB) + " kiB";
960 } else {
961 return StringifyInt(filesize) + " Byte";
962 }
963 }
964
965 void CommandInfoTag::PrintHumanReadableInfo(
966 const history::History::Tag &tag) const {
967 LogCvmfs(kLogCvmfs, kLogStdout,
968 "Name: %s\n"
969 "Revision: %" PRIu64 "\n"
970 "Timestamp: %s\n"
971 "Branch: %s\n"
972 "Root Hash: %s\n"
973 "Catalog Size: %s\n"
974 "%s",
975 tag.name.c_str(), tag.revision,
976 StringifyTime(tag.timestamp, true /* utc */).c_str(),
977 tag.branch.c_str(),
978 tag.root_hash.ToString().c_str(),
979 HumanReadableFilesize(tag.size).c_str(),
980 tag.description.c_str());
981 }
982
983 int CommandInfoTag::Main(const ArgumentList &args) {
984 const std::string tag_name = *args.find('n')->second;
985 const bool machine_readable = (args.find('x') != args.end());
986
987 // initialize the Environment (taking ownership)
988 const bool history_read_write = false;
989 UniquePtr<Environment> env(InitializeEnvironment(args, history_read_write));
990 if (!env.IsValid()) {
991 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
992 return 1;
993 }
994
995 history::History::Tag tag;
996 const bool found = env->history->GetByName(tag_name, &tag);
997 if (!found) {
998 LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist",
999 tag_name.c_str());
1000 return 1;
1001 }
1002
1003 if (machine_readable) {
1004 PrintTagMachineReadable(tag);
1005 } else {
1006 PrintHumanReadableInfo(tag);
1007 }
1008
1009 return 0;
1010 }
1011
1012 //------------------------------------------------------------------------------
1013
1014 ParameterList CommandRollbackTag::GetParams() const {
1015 ParameterList r;
1016 InsertCommonParameters(&r);
1017
1018 r.push_back(Parameter::Optional('n', "name of the tag to be republished"));
1019 return r;
1020 }
1021
1022 int CommandRollbackTag::Main(const ArgumentList &args) {
1023 const bool undo_rollback = (args.find('n') == args.end());
1024 const std::string tag_name =
1025 (!undo_rollback) ? *args.find('n')->second : CommandTag::kPreviousHeadTag;
1026
1027 // initialize the Environment (taking ownership)
1028 const bool history_read_write = true;
1029 UniquePtr<Environment> env(InitializeEnvironment(args, history_read_write));
1030 if (!env.IsValid()) {
1031 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
1032 return 1;
1033 }
1034
1035 // find tag to be rolled back to
1036 history::History::Tag target_tag;
1037 const bool found = env->history->GetByName(tag_name, &target_tag);
1038 if (!found) {
1039 if (undo_rollback) {
1040 LogCvmfs(kLogCvmfs, kLogStderr,
1041 "only one anonymous rollback supported - "
1042 "perhaps you want to provide a tag name?");
1043 } else {
1044 LogCvmfs(kLogCvmfs, kLogStderr, "tag '%s' does not exist",
1045 tag_name.c_str());
1046 }
1047 return 1;
1048 }
1049 if (target_tag.branch != "") {
1050 LogCvmfs(kLogCvmfs, kLogStderr,
1051 "rollback is only supported on the default branch");
1052 return 1;
1053 }
1054
1055 // list the tags that will be deleted
1056 TagList affected_tags;
1057 if (!env->history->ListTagsAffectedByRollback(tag_name, &affected_tags)) {
1058 LogCvmfs(kLogCvmfs, kLogStderr,
1059 "failed to list condemned tags prior to rollback to '%s'",
1060 tag_name.c_str());
1061 return 1;
1062 }
1063
1064 // check if tag is valid to be rolled back to
1065 const uint64_t current_revision = env->manifest->revision();
1066 assert(target_tag.revision <= current_revision);
1067 if (target_tag.revision == current_revision) {
1068 LogCvmfs(kLogCvmfs, kLogStderr,
1069 "not rolling back to current head (%" PRIu64 ")",
1070 current_revision);
1071 return 1;
1072 }
1073
1074 // open the catalog to be rolled back to
1075 const UnlinkGuard catalog_path(
1076 CreateTempPath(env->tmp_path + "/catalog", 0600));
1077 const bool catalog_read_write = true;
1078 UniquePtr<catalog::WritableCatalog> catalog(
1079 dynamic_cast<catalog::WritableCatalog *>(
1080 GetCatalog(env->repository_url, target_tag.root_hash,
1081 catalog_path.path(), catalog_read_write)));
1082 if (!catalog.IsValid()) {
1083 LogCvmfs(kLogCvmfs, kLogStderr, "failed to open catalog with hash '%s'",
1084 target_tag.root_hash.ToString().c_str());
1085 return 1;
1086 }
1087
1088 // check if the catalog has a supported schema version
1089 if (catalog->schema() < catalog::CatalogDatabase::kLatestSupportedSchema -
1090 catalog::CatalogDatabase::kSchemaEpsilon) {
1091 LogCvmfs(kLogCvmfs, kLogStderr,
1092 "not rolling back to outdated and "
1093 "incompatible catalog schema (%.1f < %.1f)",
1094 catalog->schema(),
1095 catalog::CatalogDatabase::kLatestSupportedSchema);
1096 return 1;
1097 }
1098
1099 // update the catalog to be republished
1100 catalog->Transaction();
1101 catalog->UpdateLastModified();
1102 catalog->SetRevision(current_revision + 1);
1103 catalog->SetPreviousRevision(env->manifest->catalog_hash());
1104 catalog->Commit();
1105
1106 // Upload catalog (handing over ownership of catalog pointer)
1107 if (!UploadCatalogAndUpdateManifest(env.weak_ref(), catalog.Release())) {
1108 LogCvmfs(kLogCvmfs, kLogStderr, "catalog upload failed");
1109 return 1;
1110 }
1111
1112 // update target tag with newly published root catalog information
1113 history::History::Tag updated_target_tag(target_tag);
1114 updated_target_tag.root_hash = env->manifest->catalog_hash();
1115 updated_target_tag.size = env->manifest->catalog_size();
1116 updated_target_tag.revision = env->manifest->revision();
1117 updated_target_tag.timestamp = env->manifest->publish_timestamp();
1118 if (!env->history->Rollback(updated_target_tag)) {
1119 LogCvmfs(kLogCvmfs, kLogStderr, "failed to rollback history to '%s'",
1120 updated_target_tag.name.c_str());
1121 return 1;
1122 }
1123 bool retval = env->history->Vacuum();
1124 assert(retval);
1125
1126 // set the magic undo tags
1127 if (!UpdateUndoTags(env.weak_ref(), updated_target_tag, undo_rollback)) {
1128 LogCvmfs(kLogCvmfs, kLogStderr, "failed to update magic undo tags");
1129 return 1;
1130 }
1131
1132 // finalize the history and upload it
1133 if (!CloseAndPublishHistory(env.weak_ref())) {
1134 return 1;
1135 }
1136
1137 // print the tags that have been removed by the rollback
1138 PrintDeletedTagList(affected_tags);
1139
1140 return 0;
1141 }
1142
1143 void CommandRollbackTag::PrintDeletedTagList(const TagList &tags) const {
1144 size_t longest_name = 0;
1145 TagList::const_iterator i = tags.begin();
1146 const TagList::const_iterator iend = tags.end();
1147 for (; i != iend; ++i) {
1148 longest_name = std::max(i->name.size(), longest_name);
1149 }
1150
1151 i = tags.begin();
1152 for (; i != iend; ++i) {
1153 LogCvmfs(kLogCvmfs, kLogStdout, "removed tag %s (%s)",
1154 AddPadding(i->name, longest_name).c_str(),
1155 i->root_hash.ToString().c_str());
1156 }
1157 }
1158
1159 //------------------------------------------------------------------------------
1160
1161 ParameterList CommandEmptyRecycleBin::GetParams() const {
1162 ParameterList r;
1163 InsertCommonParameters(&r);
1164 return r;
1165 }
1166
1167 int CommandEmptyRecycleBin::Main(const ArgumentList &args) {
1168 // initialize the Environment (taking ownership)
1169 const bool history_read_write = true;
1170 UniquePtr<Environment> env(InitializeEnvironment(args, history_read_write));
1171 if (!env.IsValid()) {
1172 LogCvmfs(kLogCvmfs, kLogStderr, "failed to init environment");
1173 return 1;
1174 }
1175
1176 if (!env->history->EmptyRecycleBin()) {
1177 LogCvmfs(kLogCvmfs, kLogStderr, "failed to empty recycle bin");
1178 return 1;
1179 }
1180
1181 // finalize the history and upload it
1182 if (!CloseAndPublishHistory(env.weak_ref())) {
1183 return 1;
1184 }
1185
1186 return 0;
1187 }
1188