Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CAF::Path: add methods to manage links #225

Merged
merged 7 commits into from
Mar 17, 2017
Merged

Conversation

jouvin
Copy link
Contributor

@jouvin jouvin commented Mar 4, 2017

3 methods added to manage symlinks:

  • is_symlink(): returns true if the argument is a symlink
  • symlink(): creates a new symlink or updates an existing one. If the symlink path already exists and is not a symlink, returns an error.
  • hardlink(): same as symlink() for hardlinks.
  • has_hardlinks(): test if a file has hardlinks (several entries refering to the same inode)
  • is_hardlink(): compare to paths and check if they refer to the same inode
    • Note that there it is impossible to say if a file is a hardlink in itself... A hardlink is just a normal file.

@@ -346,6 +346,54 @@ sub any_exists
return $path && (-e $path || -l $path);
}

=item is_symlink

Test if C<path> is a symlink.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pod should mention this reutrn true for broken symlink


=cut

sub make_symlink
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

like all other methods in CAF::Path, this has to support NoAction (incl unittests). i'd suggest to check the LC code and use that for now. we can replace LC later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would also rename it to just symlink. the result of this method should be a symlink, it's possible none should be created.

Copy link
Contributor Author

@jouvin jouvin Mar 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at directory() and I don't see NoAction being handled (NoAction is handled only for temporary directories)... This may explain quattor/maven-tools#139... Or is NoAction handled by LC?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NoAction is handled in the LC call, the Noaction flag is passed in https://github.com/quattor/CAF/blob/master/src/main/perl/Path.pm#L252; only functionality not in LC requires explicit NoAction code.
best point to start is to look how symlinking is handled in LC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I was doing and I have the feeling that the method in CAF::Path can be almost as simple as calling LC::Check::symlink... Good news!

{
my ($self, $target, $path) = @_;
my $status = SUCCESS; # Assume success
unless ($path) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i propose to keep the same style as the rest of CAF::Path, so don't check this. lets assume that people who use this know what the are doing.

unless ($target) {
$self->error("create_symlink(): 'target' argument missing");
}

Copy link
Member

@stdweird stdweird Mar 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have too untaint path and target, look at eg the directory code (you can ignore the tempdir part)


if ( $self->is_symlink($path) ) {
$self->debug(1, "Removing symlink $path");
unlink ($path);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use CAF::Path methods to remove things.

}

if ( $self->is_symlink($path) ) {
$self->debug(1, "Removing symlink $path");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should only remove of something target is different

return;
}

$status = symlink $target, $path;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use LC method

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and make (or try to make) the parent dir first

}

$status = symlink $target, $path;
$status = SUCCESS if $status;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

success should be eturned when nothing has changed (eg link already existed and pointed to same target).
return CHANGED if a link was actually created

$self->debug(1, "Removing symlink $path");
unlink ($path);
} elsif ( $self->any_exists($path) ) {
$self->error("$path already exists and is not a symlink");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

report failures with fail, eg. return $self->fail("$path already exists and is not a symlink");
in particular, CAF should try to minimise the reported errors. there might be a good reason that something fails, it's up to the component to promote a failure as an error (since any error means component failed).

@stdweird
Copy link
Member

stdweird commented Mar 5, 2017

@jouvin please have a look at eg the directory code. try to use the wrapped LC calls or existing CAF::Path methods.
there has to be support for NoAction and there have to be unittests for NoAction

@jouvin
Copy link
Contributor Author

jouvin commented Mar 5, 2017

@stdweird thanks for all the remarks, I'll try to improve the code later today...

@jouvin
Copy link
Contributor Author

jouvin commented Mar 5, 2017

@stdweird @jrha @ned21 any opinion on the argument order (link path versus target)? Perl symlink is following ln command with target first, link second. LC::Check::symlink is following the order I had in mind initially: link first, target second. As CAF::Path::symlink is mainly a wrapper over LC::Check::symlink, I'm inclined to follow its order but I don't have a strong opinion. And I am happy to change if you think the Perl symlink order is less confusion/more appropriate...

@jouvin jouvin changed the title CAF::Path: add methods to manage symlinks CAF::Path: add methods to manage links Mar 5, 2017
@jouvin
Copy link
Contributor Author

jouvin commented Mar 5, 2017

Reimplemented over LC::Check:link() as suggested by @stdweird. Some problems in tests, to be fixed later...

@ned21
Copy link
Contributor

ned21 commented Mar 5, 2017

@jouvin - tough question! Following LC::Check::symlink makes it easier to refactor code in the short term but long term following symlink() is better for everyone and less confusing. Since I think we want to take a long term view, I vote for following the Perl and UNIX symlink ordering.

@jouvin jouvin force-pushed the symlink branch 2 times, most recently from a21e6cc to aaac8fa Compare March 5, 2017 22:49
@jouvin
Copy link
Contributor Author

jouvin commented Mar 5, 2017

I think the code is now in pretty good shape. We may just need to adjust the argument order and in fact part of the refactoring we want to do after having these methods is replacing symlink by CAF::Path. For this @ned21 proposal is probably less confusing, I agree...

I am still struggling with the tests: @stdweird verify_exception() is failing and I don't really understand what it is supposed to do (and I have not seen any comment in the code to help with it!). Any explanation much appreciated! Once tests are working for symlink(), I still need to add tests for hardlink() and tests for NoAction but it should be easy.

@jouvin jouvin force-pushed the symlink branch 2 times, most recently from 9d86d33 to ea7aafd Compare March 6, 2017 07:14
Copy link
Member

@stdweird stdweird left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jouvin i'm reading the unittests, my head is starting to spin . so i vote for perl/linux ln usage and variable nameing, so ln TARGET LINK_NAME (and i'd use $link_name instead of $path)

sub _make_link
{
my ($self, $path, $target, %opts) = @_;
$path = $self->_untaint_path($path, "symlink") || return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be symlink path as message, and why isn't the target untainted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mistake of mine! I thought I did it!


$self->_reset_exception_fail('symlink');

my $status = LC::Check::link($path, $target, %opts);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return ($status ? CHANGED : SUCCESS);
} else {
my $link_type = $opts{hard} ? "hardlink" : "symlink";
$self->fail("Failed to create $link_type $path (target=$target)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return $self->fail()


Create a hardlink C<path> whose target is C<target>.

Returns an error if C<path> already exists and is not a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returns an error -> Returns undef on failure and sets the fail attribute


Create a symlink C<path> whose target is C<target>.

Returns an error if C<path> already exists and is not a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same Returns error as above

$opts{nocheck} = 1;
}
$self->verbose("nocheck=$opts{nocheck}");
my $status = $self->_make_link($path, $target, %opts);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simply return $self->_make_links

@@ -108,9 +108,9 @@ sub verify_exception
} else {
ok(! $ec_check->error(), "Error reset after $msg");
};
if ($noreset) {
if ($noreset && $mc->{fail}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't modify existing tests. using the LC wrapper is probably enough to make this pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't really modify existing tests but made a commit explictly dedicated to this as I think the test is wrong... I don't like that you may want to test/print something undefined. It doesn't make sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it changes the logic of the test, from one condition to two. if $mc->{fail} is not defined, something is wrong and test will fail, as it is supposed to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that my problem was coming from the fact I was not using _safe_eval(). Nevertheless, I think we should protect in some way against an undef argument rather than adding a warning to the error...

BTW, I think there is a typo at the end of _safe_eval(). Should be chomp $res_text and verbose() should print $res_text, shouldn't it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch on the res_txt! (i'm waiting for quattor/maven-tools#130 to catch these sort of errors)

if you want to, you can make the tests more robust, but not by changing the logic like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is changing the logic, at least it was not the goal! I need to reread what I did may be too quickly...

Copy link
Contributor Author

@jouvin jouvin Mar 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right that $noreset was not honored when $mc->{fail} was undefined. Should be fixed now and I really think that nothing is changed in the logic, just meaningless like() are not executed. @stdweird do you agree?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really. now there's no catch-all else block anymore. in particular i don't see what you gain with this. what did the error message look like when $mc->{fail} is undefined?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jouvin this still needs to be addressed. i would revert the changes and keep the old logic. it's in any case more important that it fails rather that the message is readable/human friendly

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jouvin ping

makefile("$basetest/$target_file1");
makefile($target_file2);
my %opts;
is($mc->symlink($dirlink, $target_directory), CHANGED, "directory symlink created");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a symlink test $mc->is_symlink( for every symlink created

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the main reason for this is so you can use the same/similar code block under noaction with minimal changes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure to understand... Why is_symlink() is improving the ability to use the same code with NoAction?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the code block for NoAction will be the the smae, except where you now have/would have ok($mc->is_symlink,..), you wil have ok(!$mc->is_symlink; since noaction is not supposed to create actual symlink (unless keepsstate is set).

is($mc->symlink($filelink, $target_file2), SUCCESS, "file symlink already exists: nothing done");
is($mc->symlink($filelink, $target_file1), CHANGED, "file symlink already exists: nothing done");
my $link_status = $mc->symlink($target_file2, $target_file1);
ok(! $link_status, "existing file not replaced by a symlink");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it has to return undef, so use ok(!defined($link_status))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and the message should also have symlink failed failed, reason

is($mc->symlink($filelink, $target_file1), CHANGED, "file symlink already exists: nothing done");
my $link_status = $mc->symlink($target_file2, $target_file1);
ok(! $link_status, "existing file not replaced by a symlink");
is($mc->{fail}, "Failed to create symlink $target_file2 (target=$target_file1)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a message to the test symlink failed because existing file not replaced by a symlink sets correct fail attrribute

is($mc->{fail}, "Failed to create symlink $target_file2 (target=$target_file1)");
$opts{force} = 1;
is($mc->symlink($target_file2, $target_file1, %opts), CHANGED, "existing file replaced by a symlink (force option set)");
ok(! $mc->symlink("$basetest/$target_directory", $target_file1, %opts), "directory not replaced by a symlink (force option set)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, i reread the pod, and this is implied, but it would be better to make it explicit in the pod (or use the not replacing of a directory as an exmaple in the pod).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same remark as above, here (and everywhere else) add an explicit test that nothing was created when it shouldn't have.

$opts{force} = 1;
is($mc->symlink($target_file2, $target_file1, %opts), CHANGED, "existing file replaced by a symlink (force option set)");
ok(! $mc->symlink("$basetest/$target_directory", $target_file1, %opts), "directory not replaced by a symlink (force option set)");
$opts{nocheck} = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh, this is confusing. so the pod is wrong?

$opts{nocheck} = 0;
ok(! $mc->symlink($brokenlink, "really_really_missing", %opts), "broken symlink not created (target existence enforced)");
delete $opts{nocheck};
is($mc->symlink($brokenlink, "really_missing"), CHANGED, "broken symlink created");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have to test this, either use !-e $brokenlink && -l $brokenlink or a combination of CAF::Path methods

@jouvin jouvin force-pushed the symlink branch 4 times, most recently from 9e778fa to e5907a4 Compare March 7, 2017 22:23
@jouvin
Copy link
Contributor Author

jouvin commented Mar 7, 2017

@ned21 @jrha @stdweird Adding more unit tests for hardlinks (in particular hardlinks to symlinks, something allowed by ln command whatever the symlink target is), I found 2 bugs in in LC::Check::symlink. See quattor/LC#17.
Basically we have the choice between fixing LC::Check (easy fix, see quattor/LC#16) or preventing some (probably marginal) use cases in CAF::Path. What do you think?

@jouvin
Copy link
Contributor Author

jouvin commented Mar 10, 2017

PR should be almost ok but I still have to figure out the problem with verify_exceptions(). Role of the noreset argument is unclear for me. @stdweird could you give me some info?

@jouvin
Copy link
Contributor Author

jouvin commented Mar 11, 2017

I realized that we had a test-specific version of LC::Check (and Process and Files). It caused some tests to fail because the version of the module was not advertized but CAF::Path had a requirement on it. It is unclear for me why this was happening for certains tests only (seems to be those for modules using CAF::Path, except CAF::Path itself). Do we really need these test-specific versions of LC modules. Just removing them is unfortunately not working, so there are probably some subtilties...

@jouvin
Copy link
Contributor Author

jouvin commented Mar 11, 2017

Ready for final review 😄

is honored (overrides C<NoAction> if true). One important
difference is the order of the arguments: C<CAF::Path:_make_link>
and the methods based on it are following the Perl C<symlink>
(and Shell C<ln>) argument order.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ln has nothing to do with Shell, it's part of base utils on a lot of Unix systems.

and the methods based on it are following the Perl C<symlink>
(and Shell C<ln>) argument order.

This in internal method, not supposed to be called directly.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in -> is an

If C<link_path> exists and is a file, it is updated.
C<target> must exist (C<check> flag available in symlink()
is ignored for hardlinks) and it must reside in the same
file system as C<link_path>. If C<target_path> is a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file system -> filesystem


ok(! $mc->directory_exists($basetestfile), "directory_exists false on created file");
ok($mc->any_exists($basetestfile), "any_exists true on created file");
ok($mc->file_exists($basetestfile), "file_exists true on created file");
ok(! $mc->is_symlink($basetestfile), "is_symlink false on created file");
ok(! $mc->has_hardlinks($basetestfile), "has_hardlinks false on created file");
is($mc->is_hardlink($basetestfile, $basetestfile), 0, "is_hardlink false with a non-hardlinked file");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is false because the files are the same. should change the message

ok($mc->file_exists($target_file2) && ! $mc->is_symlink($target_file2), "File $target_file2 has not be replaced by a symlink");
$opts{force} = 1;
$CAF::Object::NoAction = 1;
is($mc->symlink($target_file1, $target_file2, %opts), CHANGED, "existing file replaced by a symlink (force option set)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adapt message. this is force=1, but also NoAction=1; so the file shouldn't be replaced.

@stdweird
Copy link
Member

@jouvin more minor fixes. can you address them in a separate commit(s)? you can squash them before merging (it would make the review easier)

jouvin added 5 commits March 16, 2017 23:14
- Protect against uninitialized variables
- Clarify messages
- Creates or updates a symlink or hardlink respectively
- Wrapper over LC::Check::link()
- has_hardlinks(): returns the number of hardlinks of a file
- is_hardlink(): test if two paths refer to the same file (inode)
- CAF::Path requires LC::Check >= 1.22
- CAF::Path has a requirement on LC::Check version that causes the
tests for other CAF modules using CAF::Path to fail.
@stdweird stdweird merged commit 9d804bc into quattor:master Mar 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

3 participants