Skip to content

Commit 935453a

Browse files
authored
Merge pull request #1364 from adamoutler/improve-mount-built-in-test
Improving mount diagnostics
2 parents 1f355ad + 95e9315 commit 935453a

9 files changed

Lines changed: 1925 additions & 359 deletions

install/production-filesystem/entrypoint.d/10-mounts.py

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ class MountCheckResult:
3131
var_name: str
3232
path: str = ""
3333
is_writeable: bool = False
34+
is_readable: bool = False
3435
is_mounted: bool = False
3536
is_mount_point: bool = False
3637
is_ramdisk: bool = False
3738
underlying_fs_is_ramdisk: bool = False # Track this separately
3839
fstype: str = "N/A"
3940
error: bool = False
4041
write_error: bool = False
42+
read_error: bool = False
4143
performance_issue: bool = False
4244
dataloss_risk: bool = False
4345
category: str = ""
@@ -97,7 +99,7 @@ def _resolve_writeable_state(target_path: str) -> bool:
9799
if os.path.exists(current):
98100
if not os.access(current, os.W_OK):
99101
return False
100-
102+
101103
# OverlayFS/Copy-up check: Try to actually write a file to verify
102104
if os.path.isdir(current):
103105
test_file = os.path.join(current, f".netalertx_write_test_{os.getpid()}")
@@ -108,7 +110,7 @@ def _resolve_writeable_state(target_path: str) -> bool:
108110
return True
109111
except OSError:
110112
return False
111-
113+
112114
return True
113115

114116
parent_dir = os.path.dirname(current)
@@ -119,6 +121,27 @@ def _resolve_writeable_state(target_path: str) -> bool:
119121
return False
120122

121123

124+
def _resolve_readable_state(target_path: str) -> bool:
125+
"""Determine if a path is readable, ascending to the first existing parent."""
126+
127+
current = target_path
128+
seen: set[str] = set()
129+
while True:
130+
if current in seen:
131+
break
132+
seen.add(current)
133+
134+
if os.path.exists(current):
135+
return os.access(current, os.R_OK)
136+
137+
parent_dir = os.path.dirname(current)
138+
if not parent_dir or parent_dir == current:
139+
break
140+
current = parent_dir
141+
142+
return False
143+
144+
122145
def analyze_path(
123146
spec: PathSpec,
124147
mounted_filesystems,
@@ -142,14 +165,20 @@ def analyze_path(
142165

143166
result.path = target_path
144167

145-
# --- 1. Check Write Permissions ---
168+
# --- 1. Check Read/Write Permissions ---
146169
result.is_writeable = _resolve_writeable_state(target_path)
170+
result.is_readable = _resolve_readable_state(target_path)
147171

148172
if not result.is_writeable:
149173
result.error = True
150174
if spec.role != "secondary":
151175
result.write_error = True
152176

177+
if not result.is_readable:
178+
result.error = True
179+
if spec.role != "secondary":
180+
result.read_error = True
181+
153182
# --- 2. Check Filesystem Type (Parent and Self) ---
154183
parent_mount_fstype = ""
155184
longest_mount = ""
@@ -184,6 +213,8 @@ def analyze_path(
184213
result.is_ramdisk = parent_mount_fstype in non_persistent_fstypes
185214

186215
# --- 4. Apply Risk Logic ---
216+
# Keep risk flags about persistence/performance properties of the mount itself.
217+
# Read/write permission problems are surfaced via the R/W columns and error flags.
187218
if spec.category == "persist":
188219
if result.underlying_fs_is_ramdisk or result.is_ramdisk:
189220
result.dataloss_risk = True
@@ -198,25 +229,40 @@ def analyze_path(
198229
return result
199230

200231

201-
def print_warning_message():
232+
def print_warning_message(results: list[MountCheckResult]):
202233
"""Prints a formatted warning to stderr."""
203234
YELLOW = "\033[1;33m"
204235
RESET = "\033[0m"
205236

237+
print(f"{YELLOW}══════════════════════════════════════════════════════════════════════════════", file=sys.stderr)
238+
print("⚠️ ATTENTION: Configuration issues detected (marked with ❌).\n", file=sys.stderr)
239+
240+
for r in results:
241+
issues = []
242+
if not r.is_writeable:
243+
issues.append("error writing")
244+
if not r.is_readable:
245+
issues.append("error reading")
246+
if not r.is_mounted and (r.category == "persist" or r.category == "ramdisk"):
247+
issues.append("not mounted")
248+
if r.dataloss_risk:
249+
issues.append("risk of dataloss")
250+
if r.performance_issue:
251+
issues.append("performance issue")
252+
253+
if issues:
254+
print(f" * {r.path} {', '.join(issues)}", file=sys.stderr)
255+
206256
message = (
207-
"══════════════════════════════════════════════════════════════════════════════\n"
208-
"⚠️ ATTENTION: Configuration issues detected (marked with ❌).\n\n"
209-
" Your configuration has write permission, dataloss, or performance issues\n"
210-
" as shown in the table above.\n\n"
211-
" We recommend starting with the default docker-compose.yml as the\n"
257+
"\n We recommend starting with the default docker-compose.yml as the\n"
212258
" configuration can be quite complex.\n\n"
213259
" Review the documentation for a correct setup:\n"
214260
" https://github.com/jokob-sk/NetAlertX/blob/main/docs/DOCKER_COMPOSE.md\n"
215261
" https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/mount-configuration-issues.md\n"
216262
"══════════════════════════════════════════════════════════════════════════════\n"
217263
)
218264

219-
print(f"{YELLOW}{message}{RESET}", file=sys.stderr)
265+
print(f"{message}{RESET}", file=sys.stderr)
220266

221267

222268
def _get_active_specs() -> list[PathSpec]:
@@ -231,14 +277,14 @@ def _sub_result_is_healthy(result: MountCheckResult) -> bool:
231277
if result.category == "persist":
232278
if not result.is_mounted:
233279
return False
234-
if result.dataloss_risk or result.write_error or result.error:
280+
if result.dataloss_risk or result.write_error or result.read_error or result.error:
235281
return False
236282
return True
237283

238284
if result.category == "ramdisk":
239285
if not result.is_mounted or not result.is_ramdisk:
240286
return False
241-
if result.performance_issue or result.write_error or result.error:
287+
if result.performance_issue or result.write_error or result.read_error or result.error:
242288
return False
243289
return True
244290

@@ -278,19 +324,9 @@ def _apply_primary_rules(specs: list[PathSpec], results_map: dict[str, MountChec
278324
)
279325
all_core_subs_are_mounts = bool(core_sub_results) and len(core_mount_points) == len(core_sub_results)
280326

281-
if all_core_subs_healthy:
282-
if result.write_error:
283-
result.write_error = False
284-
if not result.is_writeable:
285-
result.is_writeable = True
286-
if spec.category == "persist" and result.dataloss_risk:
287-
result.dataloss_risk = False
288-
if result.error and not (result.performance_issue or result.dataloss_risk or result.write_error):
289-
result.error = False
290-
291327
suppress_primary = False
292328
if all_core_subs_healthy and all_core_subs_are_mounts:
293-
if not result.is_mount_point and not result.error and not result.write_error:
329+
if not result.is_mount_point and not result.error and not result.write_error and not result.read_error:
294330
suppress_primary = True
295331

296332
if suppress_primary:
@@ -329,14 +365,14 @@ def main():
329365
results = _apply_primary_rules(active_specs, results_map)
330366

331367
has_issues = any(
332-
r.dataloss_risk or r.error or r.write_error or r.performance_issue
368+
r.dataloss_risk or r.error or r.write_error or r.read_error or r.performance_issue
333369
for r in results
334370
)
335-
has_write_errors = any(r.write_error for r in results)
371+
has_rw_errors = any(r.write_error or r.read_error for r in results)
336372

337373
if has_issues or True: # Always print table for diagnostic purposes
338374
# --- Print Table ---
339-
headers = ["Path", "Writeable", "Mount", "RAMDisk", "Performance", "DataLoss"]
375+
headers = ["Path", "R", "W", "Mount", "RAMDisk", "Performance", "DataLoss"]
340376

341377
CHECK_SYMBOL = "✅"
342378
CROSS_SYMBOL = "❌"
@@ -355,7 +391,8 @@ def bool_to_check(is_good):
355391
f" {{:^{col_widths[2]}}} |"
356392
f" {{:^{col_widths[3]}}} |"
357393
f" {{:^{col_widths[4]}}} |"
358-
f" {{:^{col_widths[5]}}} "
394+
f" {{:^{col_widths[5]}}} |"
395+
f" {{:^{col_widths[6]}}} "
359396
)
360397

361398
row_fmt = (
@@ -364,7 +401,8 @@ def bool_to_check(is_good):
364401
f" {{:^{col_widths[2]}}}|" # No space
365402
f" {{:^{col_widths[3]}}}|" # No space
366403
f" {{:^{col_widths[4]}}}|" # No space
367-
f" {{:^{col_widths[5]}}} " # DataLoss is last, needs space
404+
f" {{:^{col_widths[5]}}}|" # No space
405+
f" {{:^{col_widths[6]}}} " # DataLoss is last, needs space
368406
)
369407

370408
separator = "".join([
@@ -378,13 +416,16 @@ def bool_to_check(is_good):
378416
"+",
379417
"-" * (col_widths[4] + 2),
380418
"+",
381-
"-" * (col_widths[5] + 2)
419+
"-" * (col_widths[5] + 2),
420+
"+",
421+
"-" * (col_widths[6] + 2)
382422
])
383423

384-
print(header_fmt.format(*headers))
385-
print(separator)
424+
print(header_fmt.format(*headers), file=sys.stderr)
425+
print(separator, file=sys.stderr)
386426
for r in results:
387427
# Symbol Logic
428+
read_symbol = bool_to_check(r.is_readable)
388429
write_symbol = bool_to_check(r.is_writeable)
389430

390431
mount_symbol = CHECK_SYMBOL if r.is_mounted else CROSS_SYMBOL
@@ -407,21 +448,23 @@ def bool_to_check(is_good):
407448
print(
408449
row_fmt.format(
409450
r.path,
451+
read_symbol,
410452
write_symbol,
411453
mount_symbol,
412454
ramdisk_symbol,
413455
perf_symbol,
414456
dataloss_symbol,
415-
)
457+
),
458+
file=sys.stderr
416459
)
417460

418461
# --- Print Warning ---
419462
if has_issues:
420463
print("\n", file=sys.stderr)
421-
print_warning_message()
464+
print_warning_message(results)
422465

423-
# Exit with error only if there are write permission issues
424-
if has_write_errors and os.environ.get("NETALERTX_DEBUG") != "1":
466+
# Exit with error only if there are read/write permission issues
467+
if has_rw_errors and os.environ.get("NETALERTX_DEBUG") != "1":
425468
sys.exit(1)
426469

427470

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Expected outcome: Mounts table shows /tmp/api is mounted and writable but NOT readable (R=❌, W=✅)
2+
# Note: This is a diagnostic-only container (entrypoint sleeps); the test chmods/chowns /tmp/api to mode 0300.
3+
services:
4+
netalertx:
5+
network_mode: host
6+
build:
7+
context: ../../../
8+
dockerfile: Dockerfile
9+
image: netalertx-test
10+
container_name: netalertx-test-mount-api_noread
11+
entrypoint: ["sh", "-lc", "sleep infinity"]
12+
cap_drop:
13+
- ALL
14+
cap_add:
15+
- NET_ADMIN
16+
- NET_RAW
17+
- NET_BIND_SERVICE
18+
environment:
19+
NETALERTX_DEBUG: 0
20+
NETALERTX_DATA: /data
21+
NETALERTX_DB: /data/db
22+
NETALERTX_CONFIG: /data/config
23+
SYSTEM_SERVICES_RUN_TMP: /tmp
24+
NETALERTX_API: /tmp/api
25+
NETALERTX_LOG: /tmp/log
26+
SYSTEM_SERVICES_RUN: /tmp/run
27+
SYSTEM_SERVICES_ACTIVE_CONFIG: /tmp/nginx/active-config
28+
29+
volumes:
30+
- type: volume
31+
source: test_netalertx_data
32+
target: /data
33+
read_only: false
34+
35+
tmpfs:
36+
- "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
37+
38+
volumes:
39+
test_netalertx_data:
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Expected outcome: Mounts table shows /data is mounted and writable but NOT readable (R=❌, W=✅)
2+
# Note: This is a diagnostic-only container (entrypoint sleeps); the test chmods/chowns /data to mode 0300.
3+
services:
4+
netalertx:
5+
network_mode: host
6+
build:
7+
context: ../../../
8+
dockerfile: Dockerfile
9+
image: netalertx-test
10+
container_name: netalertx-test-mount-data_noread
11+
entrypoint: ["sh", "-lc", "sleep infinity"]
12+
cap_drop:
13+
- ALL
14+
cap_add:
15+
- NET_ADMIN
16+
- NET_RAW
17+
- NET_BIND_SERVICE
18+
environment:
19+
NETALERTX_DEBUG: 0
20+
NETALERTX_DATA: /data
21+
NETALERTX_DB: /data/db
22+
NETALERTX_CONFIG: /data/config
23+
SYSTEM_SERVICES_RUN_TMP: /tmp
24+
NETALERTX_API: /tmp/api
25+
NETALERTX_LOG: /tmp/log
26+
SYSTEM_SERVICES_RUN: /tmp/run
27+
SYSTEM_SERVICES_ACTIVE_CONFIG: /tmp/nginx/active-config
28+
29+
volumes:
30+
- type: volume
31+
source: test_netalertx_data
32+
target: /data
33+
read_only: false
34+
35+
tmpfs:
36+
- "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
37+
38+
volumes:
39+
test_netalertx_data:
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Expected outcome: Mounts table shows /data/db is mounted and writable but NOT readable (R=❌, W=✅)
2+
# Note: This is a diagnostic-only container (entrypoint sleeps); the test chmods/chowns /data/db to mode 0300.
3+
services:
4+
netalertx:
5+
network_mode: host
6+
build:
7+
context: ../../../
8+
dockerfile: Dockerfile
9+
image: netalertx-test
10+
container_name: netalertx-test-mount-db_noread
11+
entrypoint: ["sh", "-lc", "sleep infinity"]
12+
cap_drop:
13+
- ALL
14+
cap_add:
15+
- NET_ADMIN
16+
- NET_RAW
17+
- NET_BIND_SERVICE
18+
environment:
19+
NETALERTX_DEBUG: 0
20+
NETALERTX_DATA: /data
21+
NETALERTX_DB: /data/db
22+
NETALERTX_CONFIG: /data/config
23+
SYSTEM_SERVICES_RUN_TMP: /tmp
24+
NETALERTX_API: /tmp/api
25+
NETALERTX_LOG: /tmp/log
26+
SYSTEM_SERVICES_RUN: /tmp/run
27+
SYSTEM_SERVICES_ACTIVE_CONFIG: /tmp/nginx/active-config
28+
29+
volumes:
30+
- type: volume
31+
source: test_netalertx_data
32+
target: /data
33+
read_only: false
34+
35+
tmpfs:
36+
- "/tmp:uid=20211,gid=20211,mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
37+
38+
volumes:
39+
test_netalertx_data:

0 commit comments

Comments
 (0)