diff --git a/cf-agent/verify_files.c b/cf-agent/verify_files.c index f9d3dd7116..e568fb4f86 100644 --- a/cf-agent/verify_files.c +++ b/cf-agent/verify_files.c @@ -587,6 +587,20 @@ static PromiseResult VerifyFilePromise(EvalContext *ctx, char *path, const Promi result = PromiseResultUpdate(result, ScheduleLinkOperation(ctx, path, a.link.source, &a, pp)); } + if (a.haveedit || a.content || a.edit_template_string) + { + if (exists || link) + { + if (!HandleFileObstruction(ctx, changes_path, &oslb, &a, pp, &result)) + { + goto exit; + } + // After moving, it no longer exists at the original path + exists = (lstat(changes_path, &oslb) != -1); + link = false; + } + } + /* Phase 3a - direct content */ if (a.content) diff --git a/cf-agent/verify_files_utils.c b/cf-agent/verify_files_utils.c index 353d7c4cc1..a743df1fb4 100644 --- a/cf-agent/verify_files_utils.c +++ b/cf-agent/verify_files_utils.c @@ -22,6 +22,7 @@ included file COSL.txt. */ +#include #include #include @@ -2775,6 +2776,57 @@ static PromiseResult VerifyFileAttributes(EvalContext *ctx, const char *file, co return result; } +bool HandleFileObstruction(EvalContext *ctx, const char *path, const struct stat *sb, const Attributes *attr, const Promise *pp, PromiseResult *result) +{ + // Tell static analysis tools that these pointers do not need to be checked for NULL before dereferencing + assert(sb != NULL); + assert(attr != NULL); + + const mode_t st_mode = sb->st_mode; + const bool move_obstructions = attr->move_obstructions; + + // If path exists, but is not a regular file, it's an obstruction + if (!S_ISREG(st_mode)) + { + if (move_obstructions) + { + if (MakingChanges(ctx, pp, attr, result, "Moving obstructing file '%s'", path)) + { + char backup[CF_BUFSIZE]; + int ret = snprintf(backup, sizeof(backup), "%s.cf-moved", path); + if (ret < 0 || (size_t) ret >= sizeof(backup)) + { + RecordFailure(ctx, pp, attr, "Could not move obstruction '%s': Path too long", + path); + *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL); + return false; + } + + if (rename(path, backup) == -1) + { + RecordFailure(ctx, pp, attr, "Could not move obstruction '%s' to '%s'. (rename: %s)", + path, backup, GetErrorStr()); + *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL); + return false; + } + else + { + RecordChange(ctx, pp, attr, "Moved obstructing path '%s' to '%s'", path, backup); + *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE); + return true; + } + } + } + else if (!S_ISLNK(st_mode)) + { + RecordFailure(ctx, pp, attr, "Path '%s' is not a regular file and move_obstructions is not set", path); + *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL); + return false; + } + } + return true; +} + bool DepthSearch(EvalContext *ctx, char *name, const struct stat *sb, int rlevel, const Attributes *attr, const Promise *pp, dev_t rootdevice, PromiseResult *result) { diff --git a/cf-agent/verify_files_utils.h b/cf-agent/verify_files_utils.h index 30a573531d..9f1c863d6b 100644 --- a/cf-agent/verify_files_utils.h +++ b/cf-agent/verify_files_utils.h @@ -35,6 +35,7 @@ extern StringSet *SINGLE_COPY_CACHE; void SetFileAutoDefineList(const Rlist *auto_define_list); void VerifyFileLeaf(EvalContext *ctx, char *path, const struct stat *sb, const Attributes *attr, const Promise *pp, PromiseResult *result); +bool HandleFileObstruction(EvalContext *ctx, const char *path, const struct stat *sb, const Attributes *attr, const Promise *pp, PromiseResult *result); bool DepthSearch(EvalContext *ctx, char *name, const struct stat *sb, int rlevel, const Attributes *attr, const Promise *pp, dev_t rootdevice, PromiseResult *result); bool CfCreateFile(EvalContext *ctx, char *file, const Promise *pp, const Attributes *attr, PromiseResult *result_out); void SetSearchDevice(struct stat *sb, const Promise *pp); diff --git a/tests/acceptance/10_files/move_obstructions-promiser-is-symlink.cf b/tests/acceptance/10_files/move_obstructions-promiser-is-symlink.cf new file mode 100644 index 0000000000..8e07372895 --- /dev/null +++ b/tests/acceptance/10_files/move_obstructions-promiser-is-symlink.cf @@ -0,0 +1,206 @@ + +############################################################################## +# +# Test that move_obstructions works consistently for various types of files +# promises targeting a specific file where the promiser is a symlink +# - copy_from +# +############################################################################## + +body file control +{ + inputs => { "../default.cf.sub" }; +} + +############################################################################## +bundle agent __main__ +{ + methods: + "init"; + "test"; + "check"; + "cleanup"; +} + +bundle agent cleanup +{ + vars: + "potential_files_to_delete" + slist => { + # The files we tested + "@(init.test_files)", + "$(init.orig_ln_target_filename)", + # .cfsaved files get created as copy_from replaces files + maplist( "$(this).cfsaved", @(init.test_files) ), + # The special case source file to copy from + maplist( "$(this).source", @(init.test_files) ), + }; + + files: + "$(potential_files_to_delete)" + delete => tidy, + if => fileexists( "$(this.promiser)" ); +} + +bundle agent init +{ + vars: + # Create test files that will serve as obstructions + "test_files" slist => { + "$(G.testdir)/copy_from", + "$(G.testdir)/content", + "$(G.testdir)/edit_template_string_inline_mustache", + "$(G.testdir)/edit_template_cfengine", + "$(G.testdir)/edit_template_mustache", + #"$(G.testdir)/edit_line", + }; + + "orig_ln_target_filename" + string => "$(G.testdir)/orig_ln_target"; + + "orig_ln_target_content" + string => "This content originally lived in a symlink target"; + + files: + # Here we initialize a file which will be a symlinks target + "$(orig_ln_target_filename)" + create => "true", + content => "$(orig_ln_target_content)"; + + # Here we have a symlink that we will be targeting with a content files promise + "$(test_files)" + create => "true", + move_obstructions => "true", + link_from => ln_s( "$(G.testdir)/orig_ln_target" ); + + reports: + DEBUG:: + "$(test_files) initalized" + if => and( fileexists( "$(test_files)" ), + islink( $(test_files) ) ); +} + +body link_from ln_s(x) +{ + link_type => "symlink"; + source => "$(x)"; + when_no_source => "force"; +} +############################################################################## + +bundle agent test +{ + meta: + "description" + string => concat( + "Test move_obstructions with content, edit_template,", + " and edit_template_string" + ); + + files: + # Test move_obstructions with copy_from + # This isn't in init because it's specific to copy_from needing to have a source file. + "$(G.testdir)/copy_from.source" + content => "$(check.expected_content)", + if => fileexists( "$(G.testdir)/copy_from" ); + + "$(G.testdir)/copy_from" + move_obstructions => "true", + copy_from => local_dcp("$(G.testdir)/copy_from.source"), + if => fileexists( "$(this.promiser)" ); + + "$(G.testdir)/edit_template_cfengine" + move_obstructions => "true", + template_method => "cfengine", + edit_template => "$(G.testdir)/copy_from.source", + if => fileexists( "$(this.promiser)" ); + + "$(G.testdir)/edit_template_mustache" + move_obstructions => "true", + template_method => "mustache", + edit_template => "$(G.testdir)/copy_from.source", + if => fileexists( "$(this.promiser)" ); + + # Test move_obstructions with content attribute + "$(G.testdir)/content" + move_obstructions => "true", + content => "$(check.expected_content)", + if => fileexists( "$(this.promiser)" ); + + # Test move_obstructions with template_method inline_mustache + "$(G.testdir)/edit_template_string_inline_mustache" + move_obstructions => "true", + template_method => "inline_mustache", + edit_template_string => "$(check.expected_content)", + if => fileexists( "$(this.promiser)" ); + + # # Test move_obstructions with edit_line + # "$(G.testdir)/edit_line" + # move_obstructions => "true", + # edit_line => insert_lines("$(check.expected_content)"), + # if => fileexists( "$(this.promiser)" ); + +} + +############################################################################## + +bundle agent check +{ + vars: + "expected_content" string => "This is content promised targeting a symlink."; + "num_test_files" int => length( @(init.test_files) ); + + # We check the promised: + # - for each test file + # - content + # - type + + "num_expected_test_ok_classes" + int => int( eval( "$(num_test_files)*2", math, infix) ); + + classes: + "DEBUG" expression => "any"; + "all_test_files_exist" + expression => filesexist( @(init.test_files) ); + + all_test_files_exist:: + # Once we verified that all the test files exist we can check all the expectations + + # These class strings will be canonified as classes, they are tagged for easy identification + + "$(init.test_files) type as expected" + meta => { "test_ok_class" }, + if => isplain( "$(init.test_files)" ); + + "$(init.test_files) content as expected" + meta => { "test_ok_class" }, + if => strcmp( readfile("$(init.test_files)"), + "$(expected_content)"); + + "overall_success" + expression => strcmp( length( classesmatching( ".*", "test_ok_class" ) ), + "$(num_expected_test_ok_classes)" ); + + reports: + !all_test_files_exist:: + "The test does not appear to have been initialized, the expected test files are missing $(with)" + with => concat( "(", join( ",", @(init.test_files) ), ")" ); + + DEBUG:: + + "Number of test files: $(num_test_files)"; + + "Number of expected test ok classes to pass: $(num_expected_test_ok_classes)"; + + "$(with) test_ok_classes:" + with => length( classesmatching( ".*", "test_ok_class" ) ); + + "$(with)" + with => join( "$(const.n)", classesmatching( ".*", "test_ok_class" ) ); + + overall_success:: + "$(this.promise_filename) Pass"; + + !overall_success:: + "$(this.promise_filename) FAIL"; +}