Skip to content

Commit 077910a

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

4 files changed

Lines changed: 263 additions & 0 deletions

File tree

promise-types/symlinks/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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. 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.
35+
36+
37+
## Authors
38+
39+
This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community.
40+
Thanks everyone!
41+
42+
## Contribute
43+
44+
Feel free to open pull requests to expand this documentation, add features or fix problems.
45+
You can also pick up an existing task or file an issue in [our bug tracker](https://northerntech.atlassian.net/).
46+
47+
## License
48+
49+
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: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import os
2+
from cfengine import PromiseModule, ValidationError, Result
3+
4+
5+
class SymlinksPromiseTypeModule(PromiseModule):
6+
7+
def __init__(self):
8+
super(SymlinksPromiseTypeModule, self).__init__(
9+
name="symlinks_promise_module",
10+
version="0.0.1",
11+
)
12+
13+
def is_absolute_dir(v):
14+
if not os.path.isabs(v):
15+
raise ValidationError("must be an absolute path, not '{v}'".format(v=v))
16+
if not os.path.exists(v):
17+
raise ValidationError("dir must exists")
18+
if not os.path.isdir(v):
19+
raise ValidationError("must be a dir")
20+
21+
def is_absolute_file(v):
22+
if not os.path.isabs(v):
23+
raise ValidationError("must be an absolute path, not '{v}'".format(v=v))
24+
if not os.path.exists(v):
25+
raise ValidationError("file must exists")
26+
if not os.path.isfile(v):
27+
raise ValidationError("must be a file")
28+
29+
self.add_attribute("directory", str, validator=is_absolute_dir)
30+
self.add_attribute("file", str, validator=is_absolute_file)
31+
32+
def validate_promise(self, promiser, attributes, metadata):
33+
model = self.create_attribute_object(promiser, attributes)
34+
35+
if not model.file and not model.directory:
36+
raise ValidationError("missing file or directory attribute")
37+
38+
if model.file and model.directory:
39+
raise ValidationError(
40+
"file and directory attributes are mutually exclusive"
41+
)
42+
43+
def evaluate_promise(self, promiser, attributes, metadata):
44+
model = self.create_attribute_object(promiser, attributes)
45+
link_target = model.file if model.file else model.directory
46+
47+
if not os.path.exists(promiser):
48+
try:
49+
os.symlink(
50+
link_target, promiser, target_is_directory=bool(model.directory)
51+
)
52+
except FileExistsError:
53+
self.log_error(
54+
"Couldn't symlink '{}' to '{}'. A link already exists".format(
55+
link_target, promiser
56+
)
57+
)
58+
return (Result.NOT_KEPT, ["old_link"])
59+
except:
60+
self.log_error(
61+
"Couldn't symlink '{}' to '{}'".format(link_target, promiser)
62+
)
63+
return (Result.NOT_KEPT, ["unknown"])
64+
return (Result.KEPT, ["link_created"])
65+
66+
if not os.path.islink(promiser):
67+
self.log_warning("Symlink '{}' is already a path".format(promiser))
68+
return (Result.NOT_KEPT, ["link_is_path"])
69+
70+
if os.path.realpath(promiser) != link_target:
71+
self.log_info(
72+
"Symlink '{}' had wrong target. Updated from '{}' to '{}'".format(
73+
promiser, os.path.realpath(promiser), link_target
74+
)
75+
)
76+
try:
77+
os.unlink(promiser)
78+
os.symlink(
79+
link_target, promiser, target_is_directory=bool(model.directory)
80+
)
81+
except FileExistsError:
82+
self.log_error(
83+
"Couldn't symlink '{}' to '{}'. A link already exists".format(
84+
link_target, promiser
85+
)
86+
)
87+
return (Result.NOT_KEPT, ["old_link"])
88+
except:
89+
self.log_error(
90+
"Couldn't symlink '{}' to '{}'".format(link_target, promiser)
91+
)
92+
return (Result.NOT_KEPT, ["unknown"])
93+
return (Result.KEPT, ["changed_target"])
94+
95+
return (Result.KEPT, ["already_existing_link"])
96+
97+
98+
if __name__ == "__main__":
99+
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)