Skip to content

Commit 352bd62

Browse files
feat: configurable volume mountpoints
- Read default additional directories from optional config file /etc/nethserver/volumes.conf - Pass volumes configuration to cluster/add-module action and validate it. - Pre-create additional directories for application volumes in node's add-module. - Pre-create application volumes during create-module, with volume bind mount to additional directories. - Implement node's list-mountpoints action.
1 parent c65c8e9 commit 352bd62

File tree

10 files changed

+244
-2
lines changed

10 files changed

+244
-2
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# Copyright (C) 2025 Nethesis S.r.l.
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
#
7+
8+
import agent
9+
import agent.volumes
10+
import json
11+
import sys
12+
import os
13+
from pathlib import Path
14+
15+
def main():
16+
request = json.load(sys.stdin)
17+
module_id = os.environ["MODULE_ID"]
18+
# Init defaults from /etc/nethserver/volumes.conf
19+
volpaths = agent.volumes.get_configuration(module_id)
20+
# Set values from request:
21+
volpaths.update(request.get('volumes', {}))
22+
for volname, basepath in volpaths.items():
23+
# volpath example: /srv/pool0/samba1/shares/_data
24+
volpath = Path(basepath) / module_id / volname.strip("/") / "_data"
25+
os.makedirs(volpath)
26+
agent.run_helper("podman", "volume", "create", "--ignore", "--opt=device=" + str(volpath), "--opt=type=bind", volname)
27+
28+
if __name__ == "__main__":
29+
agent.set_weight('05pullimages', '5')
30+
main()

core/imageroot/usr/local/agent/actions/create-module/05pullimages

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ request = json.load(sys.stdin)
3030
images = request['images']
3131
agent_id = os.environ['AGENT_ID']
3232

33-
agent.set_weight('05pullimages', '5')
34-
3533
for image_url in images:
3634
image_name = agent.get_image_name_from_url(image_url)
3735

core/imageroot/usr/local/agent/actions/create-module/validate-input.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@
1212
],
1313
"type": "object",
1414
"properties": {
15+
"volumes": {
16+
"type": "object",
17+
"description": "Optional mapping of application volume names to filesystem mount points.",
18+
"patternProperties": {
19+
"[^/]+": {
20+
"description": "Absolute filesystem directory path under /",
21+
"example": "/srv/pool0",
22+
"type": "string",
23+
"pattern": "^/.+$"
24+
}
25+
}
26+
},
1527
"images": {
1628
"type": "array",
1729
"items": {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#
2+
# Copyright (C) 2025 Nethesis S.r.l.
3+
# SPDX-License-Identifier: GPL-3.0-or-later
4+
#
5+
6+
import sys
7+
import subprocess
8+
import json
9+
from configparser import ConfigParser
10+
11+
def _parse_config() -> ConfigParser:
12+
config = ConfigParser(
13+
delimiters=("="),
14+
comment_prefixes=("#"),
15+
inline_comment_prefixes=("#"),
16+
empty_lines_in_values=False,
17+
)
18+
try:
19+
config.read("/etc/nethserver/volumes.conf")
20+
except FileNotFoundError:
21+
pass
22+
return config
23+
24+
def get_configuration(module_id:str) -> dict:
25+
"""Return the volume configuration for the given module_id by reading
26+
the config file /etc/nethserver/volumes.conf."""
27+
app_type = module_id.rstrip('0123456789')
28+
oconfig = _parse_config()
29+
if module_id in oconfig:
30+
section_key = module_id
31+
elif app_type in oconfig:
32+
section_key = app_type
33+
else:
34+
return {}
35+
return dict(oconfig[section_key].items())
36+
37+
def get_base_paths() -> list:
38+
with subprocess.Popen(["findmnt", "--real", "--json", "--bytes", "--output", "SOURCE,TARGET,SIZE,USED,LABEL", "--nofsroot", "--type", "novfat"], stdout=subprocess.PIPE) as hproc:
39+
ofindmnt = json.load(hproc.stdout)
40+
orootfs = ofindmnt["filesystems"][0]
41+
paths = []
42+
for ofs in orootfs.get("children", []):
43+
if ofs["source"] == orootfs["source"]:
44+
continue # ignore bind mounts
45+
elif ofs["target"].startswith("/boot"):
46+
continue # ignore /boot* mountpoints
47+
opath = {
48+
"path": ofs["target"],
49+
"label": ofs["label"] or "",
50+
}
51+
if "size" in ofs:
52+
opath["size"] = ofs["size"]
53+
opath["used"] = ofs["used"]
54+
paths.append(opath)
55+
return paths
56+
57+
if __name__ == "__main__":
58+
json.dump(get_base_paths(), fp=sys.stdout)
59+

core/imageroot/var/lib/nethserver/cluster/actions/add-module/50update

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ request = json.load(sys.stdin)
3535
node_id = int(request['node'])
3636
agent.assert_exp(node_id > 0)
3737
check_idle_time = request.get('check_idle_time')
38+
volumes = request.get('volumes', {})
39+
40+
if volumes:
41+
# Retrieve the list of valid mountpoint paths from the node:
42+
list_mountpoints_result = agent.tasks.run(
43+
agent_id=f'node/{node_id}',
44+
action='list-mountpoints',
45+
endpoint="redis://cluster-leader",
46+
)
47+
valid_paths = [x["path"] for x in list_mountpoints_result['output'] or []]
48+
for qpath in volumes.values():
49+
if qpath not in valid_paths:
50+
print(agent.SD_ERR + f'Unknown mountpoint path "{qpath}" in node {node_id}.', file=sys.stderr)
51+
agent.set_status('validation-failed')
52+
json.dump([{'field':'volumes', 'parameter':'volumes','value': qpath, 'error':'unknown_mountpoint_path'}], fp=sys.stdout)
53+
sys.exit(3)
3854

3955
rdb = agent.redis_connect(privileged=True)
4056

@@ -147,6 +163,7 @@ add_module_result = agent.tasks.run(
147163
"environment": module_environment,
148164
"tcp_ports_demand": tcp_ports_demand,
149165
"udp_ports_demand": udp_ports_demand,
166+
'volumes': volumes,
150167
},
151168
endpoint="redis://cluster-leader",
152169
progress_callback=agent.get_progress_callback(34,66),
@@ -188,6 +205,7 @@ create_module_result = agent.tasks.run(
188205
action="create-module",
189206
data={
190207
'images': extra_images,
208+
'volumes': volumes,
191209
},
192210
endpoint="redis://cluster-leader",
193211
check_idle_time=0, # disable idle check and wait until the agent is up

core/imageroot/var/lib/nethserver/cluster/actions/add-module/validate-input.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
"$id": "http://schema.nethserver.org/cluster/add-module-input.json",
55
"description": "Input schema of the add-module action",
66
"examples": [
7+
{
8+
"image": "samba",
9+
"node": 2,
10+
"volumes": {
11+
"shares": "/srv/pool0"
12+
}
13+
},
714
{
815
"image": "traefik",
916
"node": 1
@@ -19,6 +26,18 @@
1926
"node"
2027
],
2128
"properties": {
29+
"volumes": {
30+
"type": "object",
31+
"description": "Optional mapping of application volume names to filesystem mount points.",
32+
"patternProperties": {
33+
"[^/]+": {
34+
"description": "Absolute filesystem directory path under /",
35+
"example": "/srv/pool0",
36+
"type": "string",
37+
"pattern": "^/.+$"
38+
}
39+
}
40+
},
2241
"check_idle_time": {
2342
"title": "Agent liveness check limit",
2443
"description": "Change the default check limit value (default 8 seconds)",

core/imageroot/var/lib/nethserver/node/actions/add-module/50update

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ import os
2424
import sys
2525
import json
2626
import agent
27+
import agent.volumes
2728
import uuid
2829
import hashlib
2930
import node.ports_manager
31+
import pwd
3032

3133
def save_environment(env):
3234
with open('state/environment', 'w') as envf:
@@ -113,6 +115,22 @@ else: # rootless
113115
# Start the module agent
114116
agent.run_helper('loginctl', 'enable-linger', module_id).check_returncode()
115117

118+
# Create base paths for the module volumes
119+
volumes = agent.volumes.get_configuration(module_id)
120+
volumes.update(request.get('volumes', {}))
121+
if volumes:
122+
pwent = pwd.getpwnam(module_id)
123+
valid_mountpoint_paths = [x["path"] for x in agent.volumes.get_base_paths()]
124+
for volbase in set(volumes.values()):
125+
if volbase not in valid_mountpoint_paths:
126+
print(agent.SD_WARNING + 'Skip creation of unknown mountpoint path:', volbase, file=sys.stderr)
127+
continue
128+
try:
129+
os.mkdir(f"{volbase}/{module_id}", mode=0o700)
130+
os.chown(f"{volbase}/{module_id}", pwent[2], pwent[3]) # chown uid:gid
131+
except FileExistsError:
132+
pass
133+
116134
json.dump({
117135
"redis_sha256":redis_sha256,
118136
}, sys.stdout)

core/imageroot/var/lib/nethserver/node/actions/add-module/validate-input.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44
"$id": "http://schema.nethserver.org/node/add-module-input.json",
55
"description": "Install a module on the worker node",
66
"examples": [
7+
{
8+
"volumes": {
9+
"shares": "/srv/pool0"
10+
},
11+
"module_id": "samba1",
12+
"is_rootfull": false,
13+
"environment": {
14+
"IMAGE_URL": "ghcr.io/nethserver/samba:3.2.0"
15+
}
16+
},
717
{
818
"module_id": "mymodule2",
919
"is_rootfull": false,
@@ -21,6 +31,18 @@
2131
"udp_ports_demand"
2232
],
2333
"properties": {
34+
"volumes": {
35+
"type": "object",
36+
"description": "Optional mapping of application volume names to filesystem mount points.",
37+
"patternProperties": {
38+
"[^/]+": {
39+
"description": "Absolute filesystem directory path under /",
40+
"example": "/srv/pool0",
41+
"type": "string",
42+
"pattern": "^/.+$"
43+
}
44+
}
45+
},
2446
"environment": {
2547
"type": "object",
2648
"title": "Initial module environment",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# Copyright (C) 2025 Nethesis S.r.l.
5+
# SPDX-License-Identifier: GPL-3.0-or-later
6+
#
7+
8+
import agent
9+
import agent.volumes
10+
import json
11+
import sys
12+
import os
13+
14+
def main():
15+
json.dump({"mountpoints":agent.volumes.get_base_paths()}, fp=sys.stdout)
16+
17+
if __name__ == "__main__":
18+
main()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "list-mountpoints output",
4+
"$id": "http://schema.nethserver.org/node/list-mountpoints.json",
5+
"description": "Output schema of the list-mountpoints action",
6+
"examples": [
7+
{
8+
"mountpoints": [
9+
{
10+
"path": "/mnt/xvolume_davidep_cluster0_rl1",
11+
"label": "VOL01",
12+
"size": 2136997888,
13+
"used": 49049600
14+
}
15+
]
16+
}
17+
],
18+
"type": "object",
19+
"properties": {
20+
"mountpoints": {
21+
"type": "array",
22+
"items": {
23+
"$ref": "#/$defs/mountpoint"
24+
}
25+
}
26+
},
27+
"$defs": {
28+
"mountpoint": {
29+
"type": "object",
30+
"properties": {
31+
"path": {
32+
"type": "string"
33+
},
34+
"label": {
35+
"type": "string"
36+
},
37+
"size": {
38+
"type": "number",
39+
"description": "bytes"
40+
},
41+
"used": {
42+
"type": "number",
43+
"description": "bytes"
44+
}
45+
}
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)