Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions promise-types/symlinks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
The `symlink` promise type enables concise policy for symbolic links.

## Attributes

| Name | Type | Description | Default |
|---------------|---------------|-----------------------------------------------------------|---------------|
| `file` | `string` | Path to file. Cannot be used together with `directory`. | - |
| `directory` | `string` | Path to directory. Cannot be used together with `file`. | - |

## Examples

To create a symlink to the directory `/tmp/my-dir` with the name `/tmp/my-link`, we can do:

```cfengine3
bundle agent main
{
symlinks:
"/tmp/my-link"
directory => "/tmp/my-dir";
}
```

In similar fashion, to create a symlink to the file `/tmp/my-dir` with the name `/tmp/my-link`, we can do:

```cfengine3
bundle agent main
{
symlinks:
"/tmp/my-link"
file => "/tmp/my-file";
}
```

If the path to the file/directory given in the promise is not an absolute, doesn't exist or its type doesn't correspond with the promise's attribute ("file" or "directory"), then the promise will fail.

Trying to symlink to a file/directory where the link name is the same as an existing file/directory will also make the promise fail.

Already exisiting symlinks with incorrect target will be corrected according to the policy.


## Authors

This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community.
Thanks everyone!

## Contribute

Feel free to open pull requests to expand this documentation, add features, or fix problems.
You can also pick up an existing task or file an issue in [our bug tracker](https://northerntech.atlassian.net/).

## License

This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text.
6 changes: 6 additions & 0 deletions promise-types/symlinks/enable.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
promise agent symlinks
# @brief Define symlinks promise type
{
path => "$(sys.workdir)/modules/promises/symlinks.py";
interpreter => "/usr/bin/python3";
}
15 changes: 15 additions & 0 deletions promise-types/symlinks/example.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
promise agent symlinks
# @brief Define symlinks promise type
{
path => "$(sys.workdir)/modules/promises/symlinks.py";
interpreter => "/usr/bin/python3";
}

bundle agent main
{
symlinks:
"/tmp/myfilelink"
file => "tmp/myfile";
"/tmp/mydirlink"
directory => "tmp/mydirectory";
}
117 changes: 117 additions & 0 deletions promise-types/symlinks/symlinks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import os
from cfengine import PromiseModule, ValidationError, Result


class SymlinksPromiseTypeModule(PromiseModule):

def __init__(self, **kwargs):
super(SymlinksPromiseTypeModule, self).__init__(
name="symlinks_promise_module",
version="0.0.1",
**kwargs,
)

def is_absolute_dir(v):
if not os.path.isabs(v):
raise ValidationError("must be an absolute path, not '{v}'".format(v=v))
if not os.path.exists(v):
raise ValidationError("directory must exists")
if not os.path.isdir(v):
raise ValidationError("must be a dir")

def is_absolute_file(v):
if not os.path.isabs(v):
raise ValidationError("must be an absolute path, not '{v}'".format(v=v))
if not os.path.exists(v):
raise ValidationError("file must exists")
if not os.path.isfile(v):
raise ValidationError("must be a file")

self.add_attribute("directory", str, validator=is_absolute_dir)
self.add_attribute("file", str, validator=is_absolute_file)

def validate_promise(self, promiser, attributes, metadata):
model = self.create_attribute_object(promiser, attributes)

if not model.file and not model.directory:
raise ValidationError("missing 'file' or 'directory' attribute")

if model.file and model.directory:
raise ValidationError("must specify either 'file' or 'directory', not both")

def evaluate_promise(self, promiser, attributes, metadata):
model = self.create_attribute_object(promiser, attributes)
link_target = model.file if model.file else model.directory

try:
os.symlink(link_target, promiser, target_is_directory=bool(model.directory))
self.log_info("Created symlink '{}' -> '{}'".format(promiser, link_target))
return Result.REPAIRED
except FileExistsError:

if not os.path.islink(promiser):
self.log_error("Symlink '{}' is already a path".format(promiser))
return Result.NOT_KEPT

if os.path.realpath(promiser) != link_target:
self.log_warning(
"Symlink '{}' already exists but has wrong target '{}'".format(
promiser, os.path.realpath(promiser)
)
)
try:
os.unlink(promiser)
except FileNotFoundError:
self.log_error(
"'{}' is already unlinked from its old target".format(promiser)
)
return Result.NOT_KEPT
except Exception:
self.log_error(
"'{}' has wrong target but couldn't be unlinked: {}".format(
promiser, e
)
)
return Result.NOT_KEPT
try:
os.symlink(
link_target, promiser, target_is_directory=bool(model.directory)
)
except FileExistsError:
self.log_error(
"Couldn't symlink '{}' to '{}'. A symlink already exists".format(
link_target, promiser
)
)
return Result.NOT_KEPT
except FileNotFoundError:
self.log_error("'{}' doesn't exist".format(link_target))
return Result.NOT_KEPT
except Exception as e:
self.log_error(
"Couldn't symlink '{}' to '{}': {}".format(
link_target, promiser, e
)
)
return Result.NOT_KEPT

self.log_info(
"Corrected symlink '{}' -> '{}'".format(promiser, link_target)
)
return Result.REPAIRED

return Result.KEPT

except FileNotFoundError:
self.log_error("'{}' doesn't exist".format(promiser))
return Result.NOT_KEPT

except Exception as e:
self.log_error(
"Couldn't symlink '{}' to '{}': {}".format(link_target, promiser, e)
)
return Result.NOT_KEPT


if __name__ == "__main__":
SymlinksPromiseTypeModule().start()
116 changes: 116 additions & 0 deletions promise-types/symlinks/test.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
body common control
{
inputs => { "$(sys.libdir)/stdlib.cf" };
version => "1.0";
bundlesequence => { "init", "test", "check", "cleanup"};
}

#######################################################

bundle agent init
{
files:
"/tmp/my-file"
create => "true";
"/tmp/my-dir/."
create => "true";
"/tmp/other-dir/."
create => "true";
"/tmp/replaced-link"
link_from => ln_s("/tmp/other-dir");
"/tmp/already-existing-link"
link_from => ln_s("/tmp/other-dir");
}

#######################################################

promise agent symlinks
{
path => "$(this.promise_dirname)/symlinks.py";
interpreter => "/usr/bin/python3";
}

body classes outcome(arg)
{
promise_kept => { "$(arg)_kept" };
promise_repaired => { "$(arg)_repaired" };
}

bundle agent test
{
meta:
"description" -> { "CFE-4541" }
string => "Test the symlinks promise module";

symlinks:
"/tmp/file-link"
file => "/tmp/my-file",
classes => outcome("created_file");
"/tmp/dir-link"
directory => "/tmp/my-dir",
classes => outcome("created_dir");
"/tmp/replaced-link"
directory => "/tmp/my-dir",
classes => outcome("corrected");
"/tmp/already-existing-link"
directory => "/tmp/other-dir",
classes => outcome("didnothing");

}

#######################################################

bundle agent check
{

vars:
"my_file_stat"
string => filestat("/tmp/file-link", "linktarget");
"my_dir_stat"
string => filestat("/tmp/dir-link", "linktarget");
"replaced_link_stat"
string => filestat("/tmp/replaced-link", "linktarget");
"already_existing_link_stat"
string => filestat("/tmp/already-existing-link", "linktarget");

classes:
"ok"
expression => and (
strcmp("$(my_file_stat)", "/tmp/my-file"),
strcmp("$(my_dir_stat)", "/tmp/my-dir"),
strcmp("$(replaced_link_stat)", "/tmp/my-dir"),
strcmp("$(already_existing_link_stat)", "/tmp/other-dir"),
"created_file_repaired",
"created_dir_repaired",
"corrected_repaired",
"didnothing_kept"
);

reports:
ok::
"$(this.promise_filename) Pass";
!ok::
"$(this.promise_filename) FAIL";

}

# #######################################################

bundle agent cleanup
{
files:
"/tmp/file-link"
delete => tidy;
"/tmp/dir-link"
delete => tidy;
"/tmp/my-file"
delete => tidy;
"/tmp/my-dir/."
delete => tidy;
"/tmp/other-dir/."
delete => tidy;
"/tmp/replaced-link"
delete => tidy;
"/tmp/already-existing-link"
delete => tidy;
}