Skip to content

Commit 3dc5aae

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

File tree

5 files changed

+307
-0
lines changed

5 files changed

+307
-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/enable.cf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
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+
}

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: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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("directory 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+
try:
47+
os.symlink(link_target, promiser, target_is_directory=bool(model.directory))
48+
self.log_info("Created symlink '{}' -> '{}'".format(promiser, link_target))
49+
return Result.REPAIRED
50+
except FileExistsError:
51+
52+
if not os.path.islink(promiser):
53+
self.log_error("Symlink '{}' is already a path".format(promiser))
54+
return Result.NOT_KEPT
55+
56+
if os.path.realpath(promiser) != link_target:
57+
self.log_warning(
58+
"Symlink '{}' already exists but has wrong target '{}'".format(
59+
promiser, os.path.realpath(promiser)
60+
)
61+
)
62+
try:
63+
os.unlink(promiser)
64+
except FileNotFoundError:
65+
self.log_error(
66+
"'{}' is already unlinked from its old target".format(promiser)
67+
)
68+
return Result.NOT_KEPT
69+
except Exception:
70+
self.log_error(
71+
"'{}' has wrong target but couldn't be unlinked: {}".format(
72+
promiser, e
73+
)
74+
)
75+
return Result.NOT_KEPT
76+
try:
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 symlink already exists".format(
83+
link_target, promiser
84+
)
85+
)
86+
return Result.NOT_KEPT
87+
except FileNotFoundError:
88+
self.log_error("'{}' doesn't exist".format(link_target))
89+
return Result.NOT_KEPT
90+
except Exception as e:
91+
self.log_error(
92+
"Couldn't symlink '{}' to '{}': {}".format(
93+
link_target, promiser, e
94+
)
95+
)
96+
return Result.NOT_KEPT
97+
98+
self.log_info(
99+
"Corrected symlink '{}' -> '{}'".format(promiser, link_target)
100+
)
101+
return Result.REPAIRED
102+
103+
return Result.KEPT
104+
105+
except FileNotFoundError:
106+
self.log_error("'{}' doesn't exist".format(promiser))
107+
return Result.NOT_KEPT
108+
109+
except Exception as e:
110+
self.log_error(
111+
"Couldn't symlink '{}' to '{}': {}".format(link_target, promiser, e)
112+
)
113+
return Result.NOT_KEPT
114+
115+
116+
if __name__ == "__main__":
117+
SymlinksPromiseTypeModule().start()

promise-types/symlinks/test.cf

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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+
body classes outcome(arg)
34+
{
35+
promise_kept => { "$(arg)_kept" };
36+
promise_repaired => { "$(arg)_repaired" };
37+
}
38+
39+
bundle agent test
40+
{
41+
meta:
42+
"description" -> { "CFE-4541" }
43+
string => "Test the symlinks promise module";
44+
45+
symlinks:
46+
"/tmp/file-link"
47+
file => "/tmp/my-file",
48+
classes => outcome("created_file");
49+
"/tmp/dir-link"
50+
directory => "/tmp/my-dir",
51+
classes => outcome("created_dir");
52+
"/tmp/replaced-link"
53+
directory => "/tmp/my-dir",
54+
classes => outcome("corrected");
55+
"/tmp/already-existing-link"
56+
directory => "/tmp/other-dir",
57+
classes => outcome("didnothing");
58+
59+
}
60+
61+
#######################################################
62+
63+
bundle agent check
64+
{
65+
66+
vars:
67+
"my_file_stat"
68+
string => filestat("/tmp/file-link", "linktarget");
69+
"my_dir_stat"
70+
string => filestat("/tmp/dir-link", "linktarget");
71+
"replaced_link_stat"
72+
string => filestat("/tmp/replaced-link", "linktarget");
73+
"already_existing_link_stat"
74+
string => filestat("/tmp/already-existing-link", "linktarget");
75+
76+
classes:
77+
"ok"
78+
expression => and (
79+
strcmp("$(my_file_stat)", "/tmp/my-file"),
80+
strcmp("$(my_dir_stat)", "/tmp/my-dir"),
81+
strcmp("$(replaced_link_stat)", "/tmp/my-dir"),
82+
strcmp("$(already_existing_link_stat)", "/tmp/other-dir"),
83+
"created_file_repaired",
84+
"created_dir_repaired",
85+
"corrected_repaired",
86+
"didnothing_kept"
87+
);
88+
89+
reports:
90+
ok::
91+
"$(this.promise_filename) Pass";
92+
!ok::
93+
"$(this.promise_filename) FAIL";
94+
95+
}
96+
97+
# #######################################################
98+
99+
bundle agent cleanup
100+
{
101+
files:
102+
"/tmp/file-link"
103+
delete => tidy;
104+
"/tmp/dir-link"
105+
delete => tidy;
106+
"/tmp/my-file"
107+
delete => tidy;
108+
"/tmp/my-dir/."
109+
delete => tidy;
110+
"/tmp/other-dir/."
111+
delete => tidy;
112+
"/tmp/replaced-link"
113+
delete => tidy;
114+
"/tmp/already-existing-link"
115+
delete => tidy;
116+
}

0 commit comments

Comments
 (0)