inmanta-module-files 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- inmanta-module-files-0.1.0/MANIFEST.in +5 -0
- inmanta-module-files-0.1.0/PKG-INFO +13 -0
- inmanta-module-files-0.1.0/README.md +3 -0
- inmanta-module-files-0.1.0/inmanta_module_files.egg-info/PKG-INFO +13 -0
- inmanta-module-files-0.1.0/inmanta_module_files.egg-info/SOURCES.txt +19 -0
- inmanta-module-files-0.1.0/inmanta_module_files.egg-info/dependency_links.txt +1 -0
- inmanta-module-files-0.1.0/inmanta_module_files.egg-info/not-zip-safe +1 -0
- inmanta-module-files-0.1.0/inmanta_module_files.egg-info/requires.txt +1 -0
- inmanta-module-files-0.1.0/inmanta_module_files.egg-info/top_level.txt +1 -0
- inmanta-module-files-0.1.0/inmanta_plugins/files/__init__.py +17 -0
- inmanta-module-files-0.1.0/inmanta_plugins/files/base.py +83 -0
- inmanta-module-files-0.1.0/inmanta_plugins/files/host.py +204 -0
- inmanta-module-files-0.1.0/inmanta_plugins/files/json.py +176 -0
- inmanta-module-files-0.1.0/inmanta_plugins/files/systemd_unit.py +73 -0
- inmanta-module-files-0.1.0/pyproject.toml +3 -0
- inmanta-module-files-0.1.0/setup.cfg +47 -0
- inmanta-module-files-0.1.0/tests/test_basics.py +23 -0
- inmanta-module-files-0.1.0/tests/test_host_file.py +120 -0
- inmanta-module-files-0.1.0/tests/test_json_file.py +122 -0
- inmanta-module-files-0.1.0/tests/test_systemd_unit_file.py +134 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: inmanta-module-files
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple module containing various types of resource to manage files.
|
|
5
|
+
Author: Guillaume Everarts de Velp
|
|
6
|
+
Author-email: edvgui@gmail.com
|
|
7
|
+
License: ASL 2.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: inmanta-module-std
|
|
10
|
+
|
|
11
|
+
# inmanta-module-files
|
|
12
|
+
|
|
13
|
+
This package is an adapter that is meant to be used with the inmanta orchestrator: https://docs.inmanta.com
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: inmanta-module-files
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Simple module containing various types of resource to manage files.
|
|
5
|
+
Author: Guillaume Everarts de Velp
|
|
6
|
+
Author-email: edvgui@gmail.com
|
|
7
|
+
License: ASL 2.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: inmanta-module-std
|
|
10
|
+
|
|
11
|
+
# inmanta-module-files
|
|
12
|
+
|
|
13
|
+
This package is an adapter that is meant to be used with the inmanta orchestrator: https://docs.inmanta.com
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.cfg
|
|
5
|
+
inmanta_module_files.egg-info/PKG-INFO
|
|
6
|
+
inmanta_module_files.egg-info/SOURCES.txt
|
|
7
|
+
inmanta_module_files.egg-info/dependency_links.txt
|
|
8
|
+
inmanta_module_files.egg-info/not-zip-safe
|
|
9
|
+
inmanta_module_files.egg-info/requires.txt
|
|
10
|
+
inmanta_module_files.egg-info/top_level.txt
|
|
11
|
+
inmanta_plugins/files/__init__.py
|
|
12
|
+
inmanta_plugins/files/base.py
|
|
13
|
+
inmanta_plugins/files/host.py
|
|
14
|
+
inmanta_plugins/files/json.py
|
|
15
|
+
inmanta_plugins/files/systemd_unit.py
|
|
16
|
+
tests/test_basics.py
|
|
17
|
+
tests/test_host_file.py
|
|
18
|
+
tests/test_json_file.py
|
|
19
|
+
tests/test_systemd_unit_file.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
inmanta-module-std
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
inmanta_plugins
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import typing
|
|
20
|
+
|
|
21
|
+
import inmanta.agent.agent
|
|
22
|
+
import inmanta.agent.handler
|
|
23
|
+
import inmanta.agent.io.local
|
|
24
|
+
import inmanta.const
|
|
25
|
+
import inmanta.execute.proxy
|
|
26
|
+
import inmanta.export
|
|
27
|
+
import inmanta.resources
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BaseFileResource(inmanta.resources.PurgeableResource):
|
|
31
|
+
fields = ("path", "permissions", "owner", "group")
|
|
32
|
+
path: str
|
|
33
|
+
permissions: typing.Optional[int]
|
|
34
|
+
owner: typing.Optional[str]
|
|
35
|
+
group: typing.Optional[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
X = typing.TypeVar("X", bound=BaseFileResource)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BaseFileHandler(inmanta.agent.handler.CRUDHandlerGeneric[X]):
|
|
42
|
+
_io: inmanta.agent.io.local.LocalIO
|
|
43
|
+
|
|
44
|
+
def read_resource(
|
|
45
|
+
self, ctx: inmanta.agent.handler.HandlerContext, resource: X
|
|
46
|
+
) -> None:
|
|
47
|
+
if not self._io.file_exists(resource.path):
|
|
48
|
+
raise inmanta.agent.handler.ResourcePurged()
|
|
49
|
+
|
|
50
|
+
for key, value in self._io.file_stat(resource.path).items():
|
|
51
|
+
if getattr(resource, key) is not None:
|
|
52
|
+
setattr(resource, key, value)
|
|
53
|
+
|
|
54
|
+
def create_resource(
|
|
55
|
+
self, ctx: inmanta.agent.handler.HandlerContext, resource: X
|
|
56
|
+
) -> None:
|
|
57
|
+
if resource.permissions is not None:
|
|
58
|
+
self._io.chmod(resource.path, str(resource.permissions))
|
|
59
|
+
|
|
60
|
+
if resource.owner is not None or resource.group is not None:
|
|
61
|
+
self._io.chown(resource.path, resource.owner, resource.group)
|
|
62
|
+
|
|
63
|
+
ctx.set_created()
|
|
64
|
+
|
|
65
|
+
def update_resource(
|
|
66
|
+
self,
|
|
67
|
+
ctx: inmanta.agent.handler.HandlerContext,
|
|
68
|
+
changes: dict[str, dict[str, object]],
|
|
69
|
+
resource: X,
|
|
70
|
+
) -> None:
|
|
71
|
+
if "permissions" in changes:
|
|
72
|
+
self._io.chmod(resource.path, str(resource.permissions))
|
|
73
|
+
|
|
74
|
+
if "owner" in changes or "group" in changes:
|
|
75
|
+
self._io.chown(resource.path, resource.owner, resource.group)
|
|
76
|
+
|
|
77
|
+
ctx.set_updated()
|
|
78
|
+
|
|
79
|
+
def delete_resource(
|
|
80
|
+
self, ctx: inmanta.agent.handler.HandlerContext, resource: X
|
|
81
|
+
) -> None:
|
|
82
|
+
self._io.remove(resource.path)
|
|
83
|
+
ctx.set_purged()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import collections
|
|
20
|
+
import copy
|
|
21
|
+
import ipaddress
|
|
22
|
+
import typing
|
|
23
|
+
|
|
24
|
+
import inmanta.agent.agent
|
|
25
|
+
import inmanta.agent.handler
|
|
26
|
+
import inmanta.agent.io.local
|
|
27
|
+
import inmanta.const
|
|
28
|
+
import inmanta.execute.proxy
|
|
29
|
+
import inmanta.export
|
|
30
|
+
import inmanta.resources
|
|
31
|
+
import inmanta_plugins.files.base
|
|
32
|
+
import inmanta_plugins.files.json
|
|
33
|
+
from inmanta.util import dict_path
|
|
34
|
+
|
|
35
|
+
PARSED_HOST_FILE = dict[str, dict[str, typing.Optional[str]]]
|
|
36
|
+
"""
|
|
37
|
+
Define an alias for the parsed host file format. The parsed host file
|
|
38
|
+
is a dict which has as keys, hostnames, and as values, a dict containing
|
|
39
|
+
two keys: address4 and address6, respectively representing the ipv4 and
|
|
40
|
+
ipv6 address defined in the file for the given hostname.
|
|
41
|
+
|
|
42
|
+
And example of such dict is:
|
|
43
|
+
|
|
44
|
+
.. code-block::python
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
"localhost": {"address4": "127.0.0.1", "address6": "::1"},
|
|
48
|
+
"ip6-loopback": {"address4": None, "address6": "::1"},
|
|
49
|
+
"example.com": {"address4": "93.184.216.34", "address6": None},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_host_file(raw_content: str) -> PARSED_HOST_FILE:
|
|
56
|
+
"""
|
|
57
|
+
Parse the content of an host file, and return it as a dict having as
|
|
58
|
+
keys the hostnames, and as values, the corresponding ips.
|
|
59
|
+
"""
|
|
60
|
+
parsed_file: PARSED_HOST_FILE = collections.defaultdict(
|
|
61
|
+
lambda: {
|
|
62
|
+
"address4": None,
|
|
63
|
+
"address6": None,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
for line in raw_content.splitlines():
|
|
68
|
+
if not line:
|
|
69
|
+
# No info in that line
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if line[0] not in "0123456789:":
|
|
73
|
+
# This is not a valid entry, probably a comment
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
parsed_line = line.split()
|
|
77
|
+
|
|
78
|
+
# Parse the ip address
|
|
79
|
+
address = ipaddress.ip_address(parsed_line[0])
|
|
80
|
+
|
|
81
|
+
# For each hostname, add the ipaddress to the parsed file
|
|
82
|
+
for hostname in parsed_line[1:]:
|
|
83
|
+
parsed_file[hostname][f"address{address.version}"] = str(address)
|
|
84
|
+
|
|
85
|
+
return dict(parsed_file)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def write_host_file(parsed_file: PARSED_HOST_FILE) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Generate the content of the hostfile in a single string, that
|
|
91
|
+
can be written down into a file.
|
|
92
|
+
"""
|
|
93
|
+
raw_content = ""
|
|
94
|
+
|
|
95
|
+
for hostname, addresses in parsed_file.items():
|
|
96
|
+
if addresses.get("address4") is not None:
|
|
97
|
+
raw_content += f"{addresses['address4']} {hostname}\n"
|
|
98
|
+
|
|
99
|
+
if addresses.get("address6") is not None:
|
|
100
|
+
raw_content += f"{addresses['address6']} {hostname}\n"
|
|
101
|
+
|
|
102
|
+
return raw_content
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@inmanta.resources.resource(
|
|
106
|
+
name="files::HostFile",
|
|
107
|
+
id_attribute="path",
|
|
108
|
+
agent="host.name",
|
|
109
|
+
)
|
|
110
|
+
class HostFileResource(inmanta_plugins.files.base.BaseFileResource):
|
|
111
|
+
fields = ("values",)
|
|
112
|
+
values: list[dict]
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_values(
|
|
116
|
+
cls,
|
|
117
|
+
_: inmanta.export.Exporter,
|
|
118
|
+
entity: inmanta.execute.proxy.DynamicProxy,
|
|
119
|
+
) -> list[dict]:
|
|
120
|
+
return [
|
|
121
|
+
{
|
|
122
|
+
"path": str(dict_path.InDict(entry.hostname)),
|
|
123
|
+
"operation": entry.operation,
|
|
124
|
+
"value": {
|
|
125
|
+
"address4": entry.address4,
|
|
126
|
+
"address6": entry.address6,
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
for entry in entity.entries
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@inmanta.agent.handler.provider("files::HostFile", "")
|
|
134
|
+
class HostFileHandler(inmanta_plugins.files.base.BaseFileHandler[HostFileResource]):
|
|
135
|
+
_io: inmanta.agent.io.local.LocalIO
|
|
136
|
+
|
|
137
|
+
def read_resource(
|
|
138
|
+
self, ctx: inmanta.agent.handler.HandlerContext, resource: HostFileResource
|
|
139
|
+
) -> None:
|
|
140
|
+
super().read_resource(ctx, resource)
|
|
141
|
+
|
|
142
|
+
# Load the content of the existing file
|
|
143
|
+
raw_content = self._io.read_binary(resource.path).decode()
|
|
144
|
+
ctx.debug("Reading existing file", raw_content=raw_content)
|
|
145
|
+
ctx.set("current_content", parse_host_file(raw_content))
|
|
146
|
+
|
|
147
|
+
def calculate_diff(
|
|
148
|
+
self,
|
|
149
|
+
ctx: inmanta.agent.handler.HandlerContext,
|
|
150
|
+
current: HostFileResource,
|
|
151
|
+
desired: HostFileResource,
|
|
152
|
+
) -> dict[str, dict[str, object]]:
|
|
153
|
+
# For file permissions and ownership, we delegate to the parent class
|
|
154
|
+
changes = super().calculate_diff(ctx, current, desired)
|
|
155
|
+
|
|
156
|
+
# To check if some change content needs to be applied, we perform a "stable" addition
|
|
157
|
+
# operation: We apply our desired state to the current state, and check if we can then
|
|
158
|
+
# see any difference.
|
|
159
|
+
current_content = ctx.get("current_content")
|
|
160
|
+
desired_content = copy.deepcopy(current_content)
|
|
161
|
+
|
|
162
|
+
for value in desired.values:
|
|
163
|
+
inmanta_plugins.files.json.update(
|
|
164
|
+
desired_content,
|
|
165
|
+
dict_path.to_path(value["path"]),
|
|
166
|
+
inmanta_plugins.files.json.Operation(value["operation"]),
|
|
167
|
+
value["value"],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if current_content != desired_content:
|
|
171
|
+
changes["content"] = {
|
|
172
|
+
"current": current_content,
|
|
173
|
+
"desired": desired_content,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return changes
|
|
177
|
+
|
|
178
|
+
def create_resource(
|
|
179
|
+
self, ctx: inmanta.agent.handler.HandlerContext, resource: HostFileResource
|
|
180
|
+
) -> None:
|
|
181
|
+
# Build a config based on all the values we want to manage
|
|
182
|
+
content = {}
|
|
183
|
+
for value in resource.values:
|
|
184
|
+
inmanta_plugins.files.json.update(
|
|
185
|
+
content,
|
|
186
|
+
dict_path.to_path(value["path"]),
|
|
187
|
+
inmanta_plugins.files.json.Operation(value["operation"]),
|
|
188
|
+
value["value"],
|
|
189
|
+
)
|
|
190
|
+
raw_content = write_host_file(content)
|
|
191
|
+
self._io.put(resource.path, raw_content.encode())
|
|
192
|
+
super().create_resource(ctx, resource)
|
|
193
|
+
|
|
194
|
+
def update_resource(
|
|
195
|
+
self,
|
|
196
|
+
ctx: inmanta.agent.handler.HandlerContext,
|
|
197
|
+
changes: dict[str, dict[str, object]],
|
|
198
|
+
resource: HostFileResource,
|
|
199
|
+
) -> None:
|
|
200
|
+
if "content" in changes:
|
|
201
|
+
raw_content = write_host_file(changes["content"]["desired"])
|
|
202
|
+
self._io.put(resource.path, raw_content.encode())
|
|
203
|
+
|
|
204
|
+
super().update_resource(ctx, changes, resource)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import copy
|
|
20
|
+
import enum
|
|
21
|
+
import json
|
|
22
|
+
|
|
23
|
+
import inmanta.agent.agent
|
|
24
|
+
import inmanta.agent.handler
|
|
25
|
+
import inmanta.agent.io.local
|
|
26
|
+
import inmanta.const
|
|
27
|
+
import inmanta.execute.proxy
|
|
28
|
+
import inmanta.export
|
|
29
|
+
import inmanta.resources
|
|
30
|
+
import inmanta_plugins.files.base
|
|
31
|
+
from inmanta.util import dict_path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Operation(str, enum.Enum):
|
|
35
|
+
REPLACE = "replace"
|
|
36
|
+
REMOVE = "remove"
|
|
37
|
+
MERGE = "merge"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def update(
|
|
41
|
+
config: dict, path: dict_path.DictPath, operation: Operation, desired: object
|
|
42
|
+
) -> dict:
|
|
43
|
+
"""
|
|
44
|
+
Update the config config at the specified type, using given operation and desired value.
|
|
45
|
+
|
|
46
|
+
:param config: The configuration to update
|
|
47
|
+
:param path: The path pointing to an element of the config that should be modified
|
|
48
|
+
:param operation: The type of operation to apply to the config element
|
|
49
|
+
:param desired: The desired state to apply to the config element
|
|
50
|
+
"""
|
|
51
|
+
if operation == Operation.REMOVE:
|
|
52
|
+
path.remove(config)
|
|
53
|
+
return config
|
|
54
|
+
|
|
55
|
+
if operation == Operation.REPLACE:
|
|
56
|
+
path.set_element(config, value=desired)
|
|
57
|
+
return config
|
|
58
|
+
|
|
59
|
+
if operation == Operation.MERGE:
|
|
60
|
+
if not isinstance(desired, dict):
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Merge operation is only supported for dicts, but got {type(desired)} "
|
|
63
|
+
f"({desired})"
|
|
64
|
+
)
|
|
65
|
+
current = path.get_element(config, construct=True)
|
|
66
|
+
if not isinstance(current, dict):
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"A dict can only me merged to a dict, current value at path {path} "
|
|
69
|
+
f"is not a dict: {current} ({type(current)})"
|
|
70
|
+
)
|
|
71
|
+
current.update({k: v for k, v in desired.items() if v is not None})
|
|
72
|
+
return config
|
|
73
|
+
|
|
74
|
+
raise ValueError(f"Unsupported operation: {operation}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@inmanta.resources.resource(
|
|
78
|
+
name="files::JsonFile",
|
|
79
|
+
id_attribute="path",
|
|
80
|
+
agent="host.name",
|
|
81
|
+
)
|
|
82
|
+
class JsonFileResource(inmanta_plugins.files.base.BaseFileResource):
|
|
83
|
+
fields = (
|
|
84
|
+
"indent",
|
|
85
|
+
"values",
|
|
86
|
+
)
|
|
87
|
+
values: list[dict]
|
|
88
|
+
indent: int
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get_values(cls, _, entity: inmanta.execute.proxy.DynamicProxy) -> list[dict]:
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
"path": value.path,
|
|
95
|
+
"operation": value.operation,
|
|
96
|
+
"value": value.value,
|
|
97
|
+
}
|
|
98
|
+
for value in entity.values
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@inmanta.agent.handler.provider("files::JsonFile", "")
|
|
103
|
+
class JsonFileHandler(inmanta_plugins.files.base.BaseFileHandler[JsonFileResource]):
|
|
104
|
+
_io: inmanta.agent.io.local.LocalIO
|
|
105
|
+
|
|
106
|
+
def read_resource(
|
|
107
|
+
self, ctx: inmanta.agent.handler.HandlerContext, resource: JsonFileResource
|
|
108
|
+
) -> None:
|
|
109
|
+
super().read_resource(ctx, resource)
|
|
110
|
+
|
|
111
|
+
# Load the content of the existing file
|
|
112
|
+
raw_content = self._io.read_binary(resource.path).decode()
|
|
113
|
+
ctx.debug("Reading existing file", raw_content=raw_content)
|
|
114
|
+
ctx.set("current_content", json.loads(raw_content))
|
|
115
|
+
|
|
116
|
+
def calculate_diff(
|
|
117
|
+
self,
|
|
118
|
+
ctx: inmanta.agent.handler.HandlerContext,
|
|
119
|
+
current: JsonFileResource,
|
|
120
|
+
desired: JsonFileResource,
|
|
121
|
+
) -> dict[str, dict[str, object]]:
|
|
122
|
+
# For file permissions and ownership, we delegate to the parent class
|
|
123
|
+
changes = super().calculate_diff(ctx, current, desired)
|
|
124
|
+
|
|
125
|
+
# To check if some change content needs to be applied, we perform a "stable" addition
|
|
126
|
+
# operation: We apply our desired state to the current state, and check if we can then
|
|
127
|
+
# see any difference.
|
|
128
|
+
current_content = ctx.get("current_content")
|
|
129
|
+
desired_content = copy.deepcopy(current_content)
|
|
130
|
+
|
|
131
|
+
for value in desired.values:
|
|
132
|
+
update(
|
|
133
|
+
desired_content,
|
|
134
|
+
dict_path.to_path(value["path"]),
|
|
135
|
+
Operation(value["operation"]),
|
|
136
|
+
value["value"],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if current_content != desired_content:
|
|
140
|
+
changes["content"] = {
|
|
141
|
+
"current": current_content,
|
|
142
|
+
"desired": desired_content,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return changes
|
|
146
|
+
|
|
147
|
+
def create_resource(
|
|
148
|
+
self, ctx: inmanta.agent.handler.HandlerContext, resource: JsonFileResource
|
|
149
|
+
) -> None:
|
|
150
|
+
# Build a config based on all the elements we want to manage
|
|
151
|
+
content = {}
|
|
152
|
+
for value in resource.values:
|
|
153
|
+
update(
|
|
154
|
+
content,
|
|
155
|
+
dict_path.to_path(value["path"]),
|
|
156
|
+
Operation(value["operation"]),
|
|
157
|
+
value["value"],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
indent = resource.indent if resource.indent != 0 else None
|
|
161
|
+
raw_content = json.dumps(content, indent=indent)
|
|
162
|
+
self._io.put(resource.path, raw_content.encode())
|
|
163
|
+
super().create_resource(ctx, resource)
|
|
164
|
+
|
|
165
|
+
def update_resource(
|
|
166
|
+
self,
|
|
167
|
+
ctx: inmanta.agent.handler.HandlerContext,
|
|
168
|
+
changes: dict[str, dict[str, object]],
|
|
169
|
+
resource: JsonFileResource,
|
|
170
|
+
) -> None:
|
|
171
|
+
if "content" in changes:
|
|
172
|
+
indent = resource.indent if resource.indent != 0 else None
|
|
173
|
+
raw_content = json.dumps(changes["content"]["desired"], indent=indent)
|
|
174
|
+
self._io.put(resource.path, raw_content.encode())
|
|
175
|
+
|
|
176
|
+
super().update_resource(ctx, changes, resource)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import inmanta.agent.agent
|
|
20
|
+
import inmanta.agent.handler
|
|
21
|
+
import inmanta.agent.io.local
|
|
22
|
+
import inmanta.const
|
|
23
|
+
import inmanta.execute.proxy
|
|
24
|
+
import inmanta.export
|
|
25
|
+
import inmanta.resources
|
|
26
|
+
import inmanta_plugins.files.base
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@inmanta.resources.resource(
|
|
30
|
+
name="files::SystemdUnitFile",
|
|
31
|
+
id_attribute="path",
|
|
32
|
+
agent="host.name",
|
|
33
|
+
)
|
|
34
|
+
class SystemdUnitFileResource(inmanta_plugins.files.base.BaseFileResource):
|
|
35
|
+
fields = ("content",)
|
|
36
|
+
content: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@inmanta.agent.handler.provider("files::SystemdUnitFile", "")
|
|
40
|
+
class SystemdUnitFileHandler(
|
|
41
|
+
inmanta_plugins.files.base.BaseFileHandler[SystemdUnitFileResource]
|
|
42
|
+
):
|
|
43
|
+
_io: inmanta.agent.io.local.LocalIO
|
|
44
|
+
|
|
45
|
+
def read_resource(
|
|
46
|
+
self,
|
|
47
|
+
ctx: inmanta.agent.handler.HandlerContext,
|
|
48
|
+
resource: SystemdUnitFileResource,
|
|
49
|
+
) -> None:
|
|
50
|
+
super().read_resource(ctx, resource)
|
|
51
|
+
|
|
52
|
+
# Load the content of the existing file
|
|
53
|
+
resource.content = self._io.read_binary(resource.path).decode()
|
|
54
|
+
ctx.debug("Reading existing file", content=resource.content)
|
|
55
|
+
|
|
56
|
+
def create_resource(
|
|
57
|
+
self,
|
|
58
|
+
ctx: inmanta.agent.handler.HandlerContext,
|
|
59
|
+
resource: SystemdUnitFileResource,
|
|
60
|
+
) -> None:
|
|
61
|
+
self._io.put(resource.path, resource.content.encode())
|
|
62
|
+
super().create_resource(ctx, resource)
|
|
63
|
+
|
|
64
|
+
def update_resource(
|
|
65
|
+
self,
|
|
66
|
+
ctx: inmanta.agent.handler.HandlerContext,
|
|
67
|
+
changes: dict[str, dict[str, object]],
|
|
68
|
+
resource: SystemdUnitFileResource,
|
|
69
|
+
) -> None:
|
|
70
|
+
if "content" in changes:
|
|
71
|
+
self._io.put(resource.path, resource.content.encode())
|
|
72
|
+
|
|
73
|
+
super().update_resource(ctx, changes, resource)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = inmanta-module-files
|
|
3
|
+
version = 0.1.0
|
|
4
|
+
description = Simple module containing various types of resource to manage files.
|
|
5
|
+
long_description = file: README.md
|
|
6
|
+
long_description_content_type = text/markdown
|
|
7
|
+
author = Guillaume Everarts de Velp
|
|
8
|
+
author_email = edvgui@gmail.com
|
|
9
|
+
license = ASL 2.0
|
|
10
|
+
copyright = 2023 Guillaume Everarts de Velp
|
|
11
|
+
|
|
12
|
+
[options]
|
|
13
|
+
zip_safe = False
|
|
14
|
+
include_package_data = True
|
|
15
|
+
packages = find_namespace:
|
|
16
|
+
install_requires =
|
|
17
|
+
inmanta-module-std
|
|
18
|
+
|
|
19
|
+
[options.packages.find]
|
|
20
|
+
include = inmanta_plugins*
|
|
21
|
+
|
|
22
|
+
[flake8]
|
|
23
|
+
ignore = H405,H404,H302,H306,H301,H101,H801,E402,W503,E252,E203
|
|
24
|
+
builtins = string,number,bool
|
|
25
|
+
max-line-length = 128
|
|
26
|
+
exclude = **/.env,.venv,.git,.tox,dist,doc,**egg
|
|
27
|
+
copyright-check = True
|
|
28
|
+
copyright-author = Guillaume Everarts de Velp
|
|
29
|
+
select = E,F,W,C,BLK,I
|
|
30
|
+
|
|
31
|
+
[isort]
|
|
32
|
+
multi_line_output = 3
|
|
33
|
+
include_trailing_comma = True
|
|
34
|
+
force_grid_wrap = 0
|
|
35
|
+
use_parentheses = True
|
|
36
|
+
line_length = 88
|
|
37
|
+
known_first_party = inmanta
|
|
38
|
+
known_third_party = pytest,pydantic,Jinja2
|
|
39
|
+
|
|
40
|
+
[black]
|
|
41
|
+
line-length = 128
|
|
42
|
+
target-version = 'py36', 'py37', 'py38'
|
|
43
|
+
|
|
44
|
+
[egg_info]
|
|
45
|
+
tag_build =
|
|
46
|
+
tag_date = 0
|
|
47
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from pytest_inmanta.plugin import Project
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_basics(project: Project) -> None:
|
|
23
|
+
project.compile("import files")
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import grp
|
|
20
|
+
import os
|
|
21
|
+
import pathlib
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
import pytest_inmanta.plugin
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.parametrize(
|
|
28
|
+
(
|
|
29
|
+
"file_path",
|
|
30
|
+
"purged",
|
|
31
|
+
),
|
|
32
|
+
[
|
|
33
|
+
(pathlib.Path("/tmp/example"), False),
|
|
34
|
+
],
|
|
35
|
+
)
|
|
36
|
+
def test_model(
|
|
37
|
+
project: pytest_inmanta.plugin.Project, file_path: pathlib.Path, purged: bool
|
|
38
|
+
) -> None:
|
|
39
|
+
user = os.getlogin()
|
|
40
|
+
group = grp.getgrgid(os.getgid()).gr_name
|
|
41
|
+
model = f"""
|
|
42
|
+
import files
|
|
43
|
+
import files::host
|
|
44
|
+
|
|
45
|
+
import std
|
|
46
|
+
|
|
47
|
+
host = std::Host(
|
|
48
|
+
name="localhost",
|
|
49
|
+
os=std::linux,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
files::HostFile(
|
|
53
|
+
host=host,
|
|
54
|
+
path={repr(str(file_path))},
|
|
55
|
+
owner={repr(user)},
|
|
56
|
+
group={repr(group)},
|
|
57
|
+
purged={str(purged).lower()},
|
|
58
|
+
entries=[
|
|
59
|
+
files::host::Entry(
|
|
60
|
+
hostname="example.com",
|
|
61
|
+
address4="192.168.10.10",
|
|
62
|
+
operation="replace",
|
|
63
|
+
),
|
|
64
|
+
files::host::Entry(
|
|
65
|
+
hostname="example.be",
|
|
66
|
+
operation="remove",
|
|
67
|
+
),
|
|
68
|
+
files::host::Entry(
|
|
69
|
+
hostname="example.eu",
|
|
70
|
+
address4="192.168.10.10",
|
|
71
|
+
operation="merge",
|
|
72
|
+
),
|
|
73
|
+
],
|
|
74
|
+
)
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
project.compile(model.strip("\n"), no_dedent=False)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_deploy(project: pytest_inmanta.plugin.Project, tmp_path: pathlib.Path) -> None:
|
|
81
|
+
file = tmp_path / "host"
|
|
82
|
+
|
|
83
|
+
# Create the file
|
|
84
|
+
test_model(project, file, purged=False)
|
|
85
|
+
assert project.dryrun_resource("files::HostFile")
|
|
86
|
+
project.deploy_resource("files::HostFile")
|
|
87
|
+
assert not project.dryrun_resource("files::HostFile")
|
|
88
|
+
|
|
89
|
+
# Manually remove a managed line from the file and make sure we detect a change
|
|
90
|
+
lines = file.read_text().splitlines()
|
|
91
|
+
lines = lines[1:]
|
|
92
|
+
file.write_text("\n".join(lines))
|
|
93
|
+
assert project.dryrun_resource("files::HostFile")
|
|
94
|
+
project.deploy_resource("files::HostFile")
|
|
95
|
+
assert not project.dryrun_resource("files::HostFile")
|
|
96
|
+
|
|
97
|
+
# Insert an extra entry in the file and me sure we don't detect any change as we don't
|
|
98
|
+
# manage that entry
|
|
99
|
+
lines = file.read_text().splitlines()
|
|
100
|
+
lines.append("127.0.0.1 localhost")
|
|
101
|
+
file.write_text("\n".join(lines))
|
|
102
|
+
assert not project.dryrun_resource("files::HostFile")
|
|
103
|
+
|
|
104
|
+
# Add the entry that should not be there and make sure it is removed, the unmanaged
|
|
105
|
+
# entry should remain untouched
|
|
106
|
+
lines.append("::1 example.be")
|
|
107
|
+
file.write_text("\n".join(lines))
|
|
108
|
+
assert project.dryrun_resource("files::HostFile")
|
|
109
|
+
project.deploy_resource("files::HostFile")
|
|
110
|
+
assert not project.dryrun_resource("files::HostFile")
|
|
111
|
+
lines = file.read_text().splitlines()
|
|
112
|
+
assert "::1 example.be" not in lines
|
|
113
|
+
assert "127.0.0.1 localhost" in lines
|
|
114
|
+
|
|
115
|
+
# Delete the file
|
|
116
|
+
test_model(project, file, purged=True)
|
|
117
|
+
assert project.dryrun_resource("files::HostFile")
|
|
118
|
+
project.deploy_resource("files::HostFile")
|
|
119
|
+
assert not project.dryrun_resource("files::HostFile")
|
|
120
|
+
assert not file.exists()
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import grp
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import pathlib
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
import pytest_inmanta.plugin
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.mark.parametrize(
|
|
29
|
+
(
|
|
30
|
+
"file_path",
|
|
31
|
+
"purged",
|
|
32
|
+
),
|
|
33
|
+
[
|
|
34
|
+
(pathlib.Path("/tmp/example"), False),
|
|
35
|
+
],
|
|
36
|
+
)
|
|
37
|
+
def test_model(
|
|
38
|
+
project: pytest_inmanta.plugin.Project, file_path: pathlib.Path, purged: bool
|
|
39
|
+
) -> None:
|
|
40
|
+
user = os.getlogin()
|
|
41
|
+
group = grp.getgrgid(os.getgid()).gr_name
|
|
42
|
+
model = f"""
|
|
43
|
+
import files
|
|
44
|
+
import files::json
|
|
45
|
+
|
|
46
|
+
import std
|
|
47
|
+
|
|
48
|
+
host = std::Host(
|
|
49
|
+
name="localhost",
|
|
50
|
+
os=std::linux,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
files::JsonFile(
|
|
54
|
+
host=host,
|
|
55
|
+
path={repr(str(file_path))},
|
|
56
|
+
owner={repr(user)},
|
|
57
|
+
group={repr(group)},
|
|
58
|
+
purged={str(purged).lower()},
|
|
59
|
+
values=[
|
|
60
|
+
files::json::Object(
|
|
61
|
+
path="people[name=bob]",
|
|
62
|
+
operation="replace",
|
|
63
|
+
value={{"name": "bob", "age": 20}},
|
|
64
|
+
),
|
|
65
|
+
files::json::Object(
|
|
66
|
+
path="people[name=alice]",
|
|
67
|
+
operation="merge",
|
|
68
|
+
value={{"name": "alice", "age": 20}},
|
|
69
|
+
),
|
|
70
|
+
files::json::Object(
|
|
71
|
+
path="people[name=eve]",
|
|
72
|
+
operation="remove",
|
|
73
|
+
value={{}},
|
|
74
|
+
),
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
project.compile(model.strip("\n"), no_dedent=False)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_deploy(project: pytest_inmanta.plugin.Project, tmp_path: pathlib.Path) -> None:
|
|
83
|
+
file = tmp_path / "friends.json"
|
|
84
|
+
|
|
85
|
+
# Create the file
|
|
86
|
+
test_model(project, file, purged=False)
|
|
87
|
+
assert project.dryrun_resource("files::JsonFile")
|
|
88
|
+
project.deploy_resource("files::JsonFile")
|
|
89
|
+
assert not project.dryrun_resource("files::JsonFile")
|
|
90
|
+
|
|
91
|
+
# Manually remove a managed line from the file and make sure we detect a change
|
|
92
|
+
friends = json.loads(file.read_text())
|
|
93
|
+
del friends["people"]
|
|
94
|
+
file.write_text(json.dumps(friends))
|
|
95
|
+
assert project.dryrun_resource("files::JsonFile")
|
|
96
|
+
project.deploy_resource("files::JsonFile")
|
|
97
|
+
assert not project.dryrun_resource("files::JsonFile")
|
|
98
|
+
|
|
99
|
+
# Insert an extra entry in the file and me sure we don't detect any change as we don't
|
|
100
|
+
# manage that entry
|
|
101
|
+
friends = json.loads(file.read_text())
|
|
102
|
+
friends["people"].append({"name": "chris"})
|
|
103
|
+
file.write_text(json.dumps(friends))
|
|
104
|
+
assert not project.dryrun_resource("files::JsonFile")
|
|
105
|
+
|
|
106
|
+
# Add the entry that should not be there and make sure it is removed, the unmanaged
|
|
107
|
+
# entry should remain untouched
|
|
108
|
+
friends["people"].append({"name": "eve"})
|
|
109
|
+
file.write_text(json.dumps(friends))
|
|
110
|
+
assert project.dryrun_resource("files::JsonFile")
|
|
111
|
+
project.deploy_resource("files::JsonFile")
|
|
112
|
+
assert not project.dryrun_resource("files::JsonFile")
|
|
113
|
+
friends = json.loads(file.read_text())
|
|
114
|
+
assert friends["people"][2] == {"name": "chris"}
|
|
115
|
+
assert len(friends["people"]) == 3
|
|
116
|
+
|
|
117
|
+
# Delete the file
|
|
118
|
+
test_model(project, file, purged=True)
|
|
119
|
+
assert project.dryrun_resource("files::JsonFile")
|
|
120
|
+
project.deploy_resource("files::JsonFile")
|
|
121
|
+
assert not project.dryrun_resource("files::JsonFile")
|
|
122
|
+
assert not file.exists()
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2023 Guillaume Everarts de Velp
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
|
|
16
|
+
Contact: edvgui@gmail.com
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import grp
|
|
20
|
+
import os
|
|
21
|
+
import pathlib
|
|
22
|
+
|
|
23
|
+
import pytest
|
|
24
|
+
import pytest_inmanta.plugin
|
|
25
|
+
|
|
26
|
+
EXAMPLE_UNIT = """
|
|
27
|
+
[Unit]
|
|
28
|
+
Description=Podman inmanta-orchestrator-net.service
|
|
29
|
+
Documentation=https://github.com/edvgui/inmanta-module-podman
|
|
30
|
+
RequiresMountsFor=%t/containers
|
|
31
|
+
Wants=network-online.target
|
|
32
|
+
Wants=inmanta-orchestrator-db.service
|
|
33
|
+
Wants=inmanta-orchestrator-server.service
|
|
34
|
+
Before=inmanta-orchestrator-db.service
|
|
35
|
+
Before=inmanta-orchestrator-server.service
|
|
36
|
+
After=network-online.target
|
|
37
|
+
|
|
38
|
+
[Service]
|
|
39
|
+
Restart=on-failure
|
|
40
|
+
TimeoutStopSec=70
|
|
41
|
+
ExecStartPre=/usr/bin/podman network create --ignore --subnet=172.42.0.0/24 inmanta-orchestrator-net
|
|
42
|
+
ExecStart=/usr/bin/bash -c "/usr/bin/sleep infinity & /usr/bin/podman network inspect inmanta-orchestrator-net"
|
|
43
|
+
ExecStopPost=/usr/bin/podman network rm -f inmanta-orchestrator-net
|
|
44
|
+
Type=forking
|
|
45
|
+
|
|
46
|
+
[Install]
|
|
47
|
+
WantedBy=default.target
|
|
48
|
+
|
|
49
|
+
""".lstrip(
|
|
50
|
+
"\n"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize(
|
|
55
|
+
(
|
|
56
|
+
"file_path",
|
|
57
|
+
"purged",
|
|
58
|
+
),
|
|
59
|
+
[
|
|
60
|
+
(pathlib.Path("/tmp/example"), False),
|
|
61
|
+
],
|
|
62
|
+
)
|
|
63
|
+
def test_model(
|
|
64
|
+
project: pytest_inmanta.plugin.Project, file_path: pathlib.Path, purged: bool
|
|
65
|
+
) -> None:
|
|
66
|
+
user = os.getlogin()
|
|
67
|
+
group = grp.getgrgid(os.getgid()).gr_name
|
|
68
|
+
model = f"""
|
|
69
|
+
import files
|
|
70
|
+
import files::systemd_unit
|
|
71
|
+
|
|
72
|
+
import std
|
|
73
|
+
|
|
74
|
+
host = std::Host(
|
|
75
|
+
name="localhost",
|
|
76
|
+
os=std::linux,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
files::SystemdUnitFile(
|
|
80
|
+
host=host,
|
|
81
|
+
path={repr(str(file_path))},
|
|
82
|
+
owner={repr(user)},
|
|
83
|
+
group={repr(group)},
|
|
84
|
+
purged={str(purged).lower()},
|
|
85
|
+
unit=Unit(
|
|
86
|
+
description="Podman inmanta-orchestrator-net.service",
|
|
87
|
+
documentation=["https://github.com/edvgui/inmanta-module-podman"],
|
|
88
|
+
requires_mounts_for=["%t/containers"],
|
|
89
|
+
wants=[
|
|
90
|
+
"network-online.target",
|
|
91
|
+
"inmanta-orchestrator-db.service",
|
|
92
|
+
"inmanta-orchestrator-server.service",
|
|
93
|
+
],
|
|
94
|
+
before=[
|
|
95
|
+
"inmanta-orchestrator-db.service",
|
|
96
|
+
"inmanta-orchestrator-server.service",
|
|
97
|
+
],
|
|
98
|
+
after=["network-online.target"],
|
|
99
|
+
),
|
|
100
|
+
service=Service(
|
|
101
|
+
restart="on-failure",
|
|
102
|
+
timeout_stop_sec=70,
|
|
103
|
+
exec_start_pre="/usr/bin/podman network create --ignore --subnet=172.42.0.0/24 inmanta-orchestrator-net",
|
|
104
|
+
exec_start="/usr/bin/bash -c \\"/usr/bin/sleep infinity & /usr/bin/podman network inspect inmanta-orchestrator-net\\"",
|
|
105
|
+
exec_stop_post="/usr/bin/podman network rm -f inmanta-orchestrator-net",
|
|
106
|
+
type="forking",
|
|
107
|
+
),
|
|
108
|
+
install=Install(
|
|
109
|
+
wanted_by=["default.target"],
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
""" # noqa: E501
|
|
113
|
+
|
|
114
|
+
project.compile(model.strip("\n"), no_dedent=False)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_deploy(project: pytest_inmanta.plugin.Project, tmp_path: pathlib.Path) -> None:
|
|
118
|
+
file = tmp_path / "inmanta-orchestrator-net.service"
|
|
119
|
+
|
|
120
|
+
# Create the file
|
|
121
|
+
test_model(project, file, purged=False)
|
|
122
|
+
assert project.dryrun_resource("files::SystemdUnitFile")
|
|
123
|
+
project.deploy_resource("files::SystemdUnitFile")
|
|
124
|
+
assert not project.dryrun_resource("files::SystemdUnitFile")
|
|
125
|
+
|
|
126
|
+
# Check that the file content is the expected one
|
|
127
|
+
assert file.read_text() == EXAMPLE_UNIT
|
|
128
|
+
|
|
129
|
+
# Delete the file
|
|
130
|
+
test_model(project, file, purged=True)
|
|
131
|
+
assert project.dryrun_resource("files::SystemdUnitFile")
|
|
132
|
+
project.deploy_resource("files::SystemdUnitFile")
|
|
133
|
+
assert not project.dryrun_resource("files::SystemdUnitFile")
|
|
134
|
+
assert not file.exists()
|