Skip to content

Commit 43caf20

Browse files
committed
Added symlinks promise type
Ticket: CFE-4541 Signed-off-by: Victor Moene <victor.moene@northern.tech>
1 parent 7c6456c commit 43caf20

File tree

4 files changed

+266
-0
lines changed

4 files changed

+266
-0
lines changed

promise-types/symlinks/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
The `symlink` promise type enables concise policy for symbolic links
2+
3+
## Attributes
4+
5+
| Name | Type | Description | Default |
6+
|---------------|---------------|-----------------------------------------------------------|---------------|
7+
| `file` | `string` | Path to file. Cannot be used together with `directory`. | - |
8+
| `directory` | `string` | Path to directory. Cannot be used together with `file`. | - |
9+
10+
## Examples
11+
12+
To create a symlink to the directory `/tmp/my-dir` with the name `/tmp/my-link`, we can do:
13+
14+
```cfengine3
15+
bundle agent main
16+
{
17+
symlinks:
18+
"/tmp/my-link"
19+
directory => "/tmp/my-dir";
20+
}
21+
```
22+
23+
In similar fashion, to create a symlink to the file `/tmp/my-dir` with the name `/tmp/my-link`, we can do:
24+
25+
```cfengine3
26+
bundle agent main
27+
{
28+
symlinks:
29+
"/tmp/my-link"
30+
file => "/tmp/my-file";
31+
}
32+
```
33+
34+
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.
35+
36+
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.
37+
38+
Already exisiting symlinks with incorrect target will be corrected according to the policy.
39+
40+
41+
## Authors
42+
43+
This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community.
44+
Thanks everyone!
45+
46+
## Contribute
47+
48+
Feel free to open pull requests to expand this documentation, add features, or fix problems.
49+
You can also pick up an existing task or file an issue in [our bug tracker](https://northerntech.atlassian.net/).
50+
51+
## License
52+
53+
This software is licensed under the MIT License. See LICENSE in the root of the repository for the full license text.

promise-types/symlinks/example.cf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
promise agent symlinks
2+
# @brief Define symlinks promise type
3+
{
4+
path => "$(sys.workdir)/modules/promises/symlinks.py";
5+
interpreter => "/usr/bin/python3";
6+
}
7+
8+
bundle agent main
9+
{
10+
symlinks:
11+
"/tmp/myfilelink"
12+
file => "tmp/myfile";
13+
"/tmp/mydirlink"
14+
directory => "tmp/mydirectory";
15+
}

promise-types/symlinks/symlinks.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import os
2+
from cfengine import PromiseModule, ValidationError, Result
3+
4+
5+
class SymlinksPromiseTypeModule(PromiseModule):
6+
7+
def __init__(self, **kwargs):
8+
super(SymlinksPromiseTypeModule, self).__init__(
9+
name="symlinks_promise_module",
10+
version="0.0.1",
11+
**kwargs,
12+
)
13+
14+
def is_absolute_dir(v):
15+
if not os.path.isabs(v):
16+
raise ValidationError("must be an absolute path, not '{v}'".format(v=v))
17+
if not os.path.exists(v):
18+
raise ValidationError("dir must exists")
19+
if not os.path.isdir(v):
20+
raise ValidationError("must be a dir")
21+
22+
def is_absolute_file(v):
23+
if not os.path.isabs(v):
24+
raise ValidationError("must be an absolute path, not '{v}'".format(v=v))
25+
if not os.path.exists(v):
26+
raise ValidationError("file must exists")
27+
if not os.path.isfile(v):
28+
raise ValidationError("must be a file")
29+
30+
self.add_attribute("directory", str, validator=is_absolute_dir)
31+
self.add_attribute("file", str, validator=is_absolute_file)
32+
33+
def validate_promise(self, promiser, attributes, metadata):
34+
model = self.create_attribute_object(promiser, attributes)
35+
36+
if not model.file and not model.directory:
37+
raise ValidationError("missing 'file' or 'directory' attribute")
38+
39+
if model.file and model.directory:
40+
raise ValidationError("must specify either 'file' or 'directory', not both")
41+
42+
def evaluate_promise(self, promiser, attributes, metadata):
43+
model = self.create_attribute_object(promiser, attributes)
44+
link_target = model.file if model.file else model.directory
45+
46+
if not os.path.exists(promiser):
47+
try:
48+
os.symlink(
49+
link_target, promiser, target_is_directory=bool(model.directory)
50+
)
51+
except FileExistsError:
52+
self.log_error(
53+
"Couldn't symlink '{}' to '{}'. A link already exists".format(
54+
link_target, promiser
55+
)
56+
)
57+
return (Result.NOT_KEPT, ["old_link"])
58+
except:
59+
self.log_error(
60+
"Couldn't symlink '{}' to '{}'".format(link_target, promiser)
61+
)
62+
return (Result.NOT_KEPT, ["unknown"])
63+
return (Result.KEPT, ["link_created"])
64+
65+
if not os.path.islink(promiser):
66+
self.log_warning("Symlink '{}' is already a path".format(promiser))
67+
return (Result.NOT_KEPT, ["link_is_path"])
68+
69+
if os.path.realpath(promiser) != link_target:
70+
self.log_info(
71+
"Symlink '{}' had wrong target. Updated from '{}' to '{}'".format(
72+
promiser, os.path.realpath(promiser), link_target
73+
)
74+
)
75+
try:
76+
os.unlink(promiser)
77+
os.symlink(
78+
link_target, promiser, target_is_directory=bool(model.directory)
79+
)
80+
except FileExistsError:
81+
self.log_error(
82+
"Couldn't symlink '{}' to '{}'. A link already exists".format(
83+
link_target, promiser
84+
)
85+
)
86+
return (Result.NOT_KEPT, ["old_link"])
87+
except:
88+
self.log_error(
89+
"Couldn't symlink '{}' to '{}'".format(link_target, promiser)
90+
)
91+
return (Result.NOT_KEPT, ["unknown"])
92+
return (Result.KEPT, ["changed_target"])
93+
94+
return (Result.KEPT, ["already_existing_link"])
95+
96+
97+
if __name__ == "__main__":
98+
SymlinksPromiseTypeModule().start()

promise-types/symlinks/test.cf

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
body common control
2+
{
3+
inputs => { "$(sys.libdir)/stdlib.cf" };
4+
version => "1.0";
5+
bundlesequence => { "init", "test", "check", "cleanup"};
6+
}
7+
8+
#######################################################
9+
10+
bundle agent init
11+
{
12+
files:
13+
"/tmp/my-file"
14+
create => "true";
15+
"/tmp/my-dir/."
16+
create => "true";
17+
"/tmp/other-dir/."
18+
create => "true";
19+
"/tmp/replaced-link"
20+
link_from => ln_s("/tmp/other-dir");
21+
"/tmp/already-existing-link"
22+
link_from => ln_s("/tmp/other-dir");
23+
}
24+
25+
#######################################################
26+
27+
promise agent symlinks
28+
{
29+
path => "$(this.promise_dirname)/symlinks.py";
30+
interpreter => "/usr/bin/python3";
31+
}
32+
33+
bundle agent test
34+
{
35+
meta:
36+
"description" -> { "CFE-4541" }
37+
string => "Test the symlinks promise module";
38+
39+
symlinks:
40+
"/tmp/file-link"
41+
file => "/tmp/my-file";
42+
"/tmp/dir-link"
43+
directory => "/tmp/my-dir";
44+
"/tmp/replaced-link"
45+
directory => "/tmp/my-dir";
46+
"/tmp/already-existing-link"
47+
directory => "/tmp/other-dir";
48+
}
49+
50+
#######################################################
51+
52+
bundle agent check
53+
{
54+
55+
vars:
56+
"my_file_stat"
57+
string => filestat("/tmp/file-link", "linktarget");
58+
"my_dir_stat"
59+
string => filestat("/tmp/dir-link", "linktarget");
60+
"replaced_link_stat"
61+
string => filestat("/tmp/replaced-link", "linktarget");
62+
"already_existing_link_stat"
63+
string => filestat("/tmp/already-existing-link", "linktarget");
64+
65+
classes:
66+
"ok"
67+
expression => and (
68+
strcmp("$(my_file_stat)", "/tmp/my-file"),
69+
strcmp("$(my_dir_stat)", "/tmp/my-dir"),
70+
strcmp("$(replaced_link_stat)", "/tmp/my-dir"),
71+
strcmp("$(already_existing_link_stat)", "/tmp/other-dir")
72+
);
73+
74+
reports:
75+
ok::
76+
"$(this.promise_filename) Pass";
77+
!ok::
78+
"$(this.promise_filename) FAIL";
79+
}
80+
81+
# #######################################################
82+
83+
bundle agent cleanup
84+
{
85+
files:
86+
"/tmp/file-link"
87+
delete => tidy;
88+
"/tmp/dir-link"
89+
delete => tidy;
90+
"/tmp/my-file"
91+
delete => tidy;
92+
"/tmp/my-dir/."
93+
delete => tidy;
94+
"/tmp/other-dir/."
95+
delete => tidy;
96+
"/tmp/replaced-link"
97+
delete => tidy;
98+
"/tmp/already-existing-link"
99+
delete => tidy;
100+
}

0 commit comments

Comments
 (0)