Skip to content

Commit fe9733c

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

4 files changed

Lines changed: 248 additions & 0 deletions

File tree

promise-types/symlinks/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
The `symlink` promise type enables concise policy for symbolic links
2+
3+
## Attributes
4+
5+
| Name | Type |Description | Mandatory | Default |
6+
|---------------|---------------|-----------------------------------------------------------|-------------------|---------------|
7+
| `file` | `string` | Path to file. Cannot be used together with `directory` | Yes | - |
8+
| `directory` | `string` | Path to directory. Cannot be used together with `file` | Yes | - |
9+
10+
## Examples
11+
12+
```cfengine3
13+
bundle agent main
14+
{
15+
symlinks:
16+
"/tmp/my-link"
17+
directory => "/tmp/my-dir";
18+
}
19+
```
20+
21+
```cfengine3
22+
bundle agent main
23+
{
24+
symlinks:
25+
"/tmp/my-link"
26+
file => "/tmp/my-file";
27+
}
28+
```
29+
30+
## Authors
31+
32+
This software was created by the team at [Northern.tech](https://northern.tech), with many contributions from the community.
33+
Thanks everyone!
34+
35+
## Contribute
36+
37+
Feel free to open pull requests to expand this documentation, add features or fix problems.
38+
You can also pick up an existing task or file an issue in [our bug tracker](https://tracker.mender.io/issues/).
39+
40+
## License
41+
42+
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: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
record_file_path="/home/vagrant/OUTPUT.txt",
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 evaluate_promise(self, promiser, attributes, metadata):
34+
model = self.create_attribute_object(promiser, attributes)
35+
promisee = model.file if model.file else model.directory
36+
37+
if model.file and model.directory:
38+
self.log_error("file and directory are mutually exclusive")
39+
return (Result.ERROR, ["incompatible_attributes"])
40+
41+
if not promisee:
42+
return (Result.ERROR, ["missing_link_path"])
43+
44+
if not os.path.exists(promiser):
45+
try:
46+
os.symlink(
47+
promisee, promiser, target_is_directory=bool(model.directory)
48+
)
49+
except FileExistsError:
50+
self.log_error(
51+
"Couldn't symlink '{}' to '{}'. A sym already exists".format(
52+
promisee, promiser
53+
)
54+
)
55+
return (Result.NOT_KEPT, ["old_link"])
56+
except:
57+
self.log_error(
58+
"Couldn't symlink '{}' to '{}'".format(promisee, promiser)
59+
)
60+
return (Result.NOT_KEPT, ["unknown"])
61+
return (Result.KEPT, ["link_created"])
62+
63+
if not os.path.islink(promiser):
64+
self.log_warning("Symlink '{}' is already a path".format(promiser))
65+
return (Result.NOT_KEPT, ["link_is_path"])
66+
67+
if os.path.realpath(promiser) != promisee:
68+
self.log_info(
69+
"Symlink '{}' had wrong target. Updated from '{}' to '{}'".format(
70+
promiser, os.path.realpath(promiser), promisee
71+
)
72+
)
73+
try:
74+
os.unlink(promiser)
75+
os.symlink(
76+
promisee, promiser, target_is_directory=bool(model.directory)
77+
)
78+
except FileExistsError:
79+
self.log_error(
80+
"Couldn't symlink '{}' to '{}'. A link already exists".format(
81+
promisee, promiser
82+
)
83+
)
84+
return (Result.NOT_KEPT, ["old_link"])
85+
except:
86+
self.log_error(
87+
"Couldn't symlink '{}' to '{}'".format(promisee, promiser)
88+
)
89+
return (Result.NOT_KEPT, ["unknown"])
90+
return (Result.KEPT, ["changed_target"])
91+
92+
return (Result.KEPT, ["already_existing_link"])
93+
94+
95+
if __name__ == "__main__":
96+
SymlinksPromiseTypeModule().start()

promise-types/symlinks/test.cf

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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" string => filestat("/tmp/file-link", "linktarget");
57+
"my_dir_stat" string => filestat("/tmp/dir-link", "linktarget");
58+
"replaced_link_stat" string => filestat("/tmp/replaced-link", "linktarget");
59+
"already_existing_link_stat" string => filestat("/tmp/already-existing-link", "linktarget");
60+
61+
classes:
62+
"ok" expression => and (
63+
strcmp("$(my_file_stat)", "/tmp/my-file"),
64+
strcmp("$(my_dir_stat)", "/tmp/my-dir"),
65+
strcmp("$(replaced_link_stat)", "/tmp/my-dir"),
66+
strcmp("$(already_existing_link_stat)", "/tmp/other-dir")
67+
);
68+
69+
reports:
70+
ok::
71+
"$(this.promise_filename) Pass";
72+
!ok::
73+
"$(this.promise_filename) FAIL";
74+
}
75+
76+
# #######################################################
77+
78+
bundle agent cleanup
79+
{
80+
files:
81+
"/tmp/file-link"
82+
delete => tidy;
83+
"/tmp/dir-link"
84+
delete => tidy;
85+
"/tmp/my-file"
86+
delete => tidy;
87+
"/tmp/my-dir/."
88+
delete => tidy;
89+
"/tmp/other-dir/."
90+
delete => tidy;
91+
"/tmp/replaced-link"
92+
delete => tidy;
93+
"/tmp/already-existing-link"
94+
delete => tidy;
95+
}

0 commit comments

Comments
 (0)