Skip to content

Commit 9dba93f

Browse files
disk_check: Check & mount RO as RW using tmpfs (#1569)
What I did There is a bug that occasionally turn root-overlay as RO. This makes /etc & /home as RO. This blocks any new remote user login, as that needs to write into /etc & /home. This tool scans /etc & /home (or given dirs) as in RW or RO state. If RO, it could create a writable overlay using tmpfs. This is transient and stays until next reboot. Any write after the overlay will be lost upon reboot. But this allows new remote users login. How I did it Create upper & work dirs in /run/mount (tmpfs). Mount /etc & /home as lowerdirs and use the same name for final merge. This allows anyone opening a file in /etc or /home to operate on the merged overlay, transparently. How to verify it Mount any dir on tmpfs ( mount -t tmpfs tmpfs test_dir) remount as RO (mount -o remount,ro test_dir) Pass that dir to this script. (disk_check.py -d ./test_dir) Now it should be RW
1 parent c3963c5 commit 9dba93f

File tree

3 files changed

+313
-0
lines changed

3 files changed

+313
-0
lines changed

scripts/disk_check.py

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
What:
6+
There have been cases, where disk turns Read-only due to kernel bug.
7+
In Read-only state, system blocks new remote user login via TACACS.
8+
This utility is to check & make transient recovery as needed.
9+
10+
How:
11+
check for Read-Write permission. If Read-only, create writable overlay using tmpfs.
12+
13+
By default "/etc" & "/home" are checked and if in Read-only state, make them Read-Write
14+
using overlay on top of tmpfs.
15+
16+
Making /etc & /home as writable lets successful new remote user login.
17+
18+
If in Read-only state or in Read-Write state with the help of tmpfs overlay,
19+
syslog ERR messages are written, to help raise alerts.
20+
21+
Monit may be used to invoke it periodically, to help scan & fix and
22+
report via syslog.
23+
24+
"""
25+
26+
import argparse
27+
import os
28+
import sys
29+
import syslog
30+
import subprocess
31+
32+
UPPER_DIR = "/run/mount/upper"
33+
WORK_DIR = "/run/mount/work"
34+
MOUNTS_FILE = "/proc/mounts"
35+
36+
def log_err(m):
37+
print("Err: {}".format(m), file=sys.stderr)
38+
syslog.syslog(syslog.LOG_ERR, m)
39+
40+
41+
def log_info(m):
42+
print("Info: {}".format(m))
43+
syslog.syslog(syslog.LOG_INFO, m)
44+
45+
46+
def log_debug(m):
47+
print("debug: {}".format(m))
48+
syslog.syslog(syslog.LOG_DEBUG, m)
49+
50+
51+
def test_writable(dirs):
52+
for d in dirs:
53+
rw = os.access(d, os.W_OK)
54+
if not rw:
55+
log_err("{} is not read-write".format(d))
56+
return False
57+
else:
58+
log_debug("{} is Read-Write".format(d))
59+
return True
60+
61+
62+
def run_cmd(cmd):
63+
proc = subprocess.run(cmd, shell=True, text=True, capture_output=True)
64+
ret = proc.returncode
65+
if ret:
66+
log_err("failed: ret={} cmd={}".format(ret, cmd))
67+
else:
68+
log_info("ret={} cmd: {}".format(ret, cmd))
69+
70+
if proc.stdout:
71+
log_info("stdout: {}".format(str(proc.stdout)))
72+
if proc.stderr:
73+
log_info("stderr: {}".format(str(proc.stderr)))
74+
return ret
75+
76+
77+
def get_dname(path_name):
78+
return os.path.basename(os.path.normpath(path_name))
79+
80+
81+
def do_mnt(dirs):
82+
if os.path.exists(UPPER_DIR):
83+
log_err("Already mounted")
84+
return 1
85+
86+
for i in (UPPER_DIR, WORK_DIR):
87+
try:
88+
os.mkdir(i)
89+
except OSError as error:
90+
log_err("Failed to create {}".format(i))
91+
return 1
92+
93+
for d in dirs:
94+
ret = run_cmd("mount -t overlay overlay_{} -o lowerdir={},"
95+
"upperdir={},workdir={} {}".format(
96+
get_dname(d), d, UPPER_DIR, WORK_DIR, d))
97+
if ret:
98+
break
99+
100+
if ret:
101+
log_err("Failed to mount {} as Read-Write".format(dirs))
102+
else:
103+
log_info("{} are mounted as Read-Write".format(dirs))
104+
return ret
105+
106+
107+
def is_mounted(dirs):
108+
if not os.path.exists(UPPER_DIR):
109+
return False
110+
111+
onames = set()
112+
for d in dirs:
113+
onames.add("overlay_{}".format(get_dname(d)))
114+
115+
with open(MOUNTS_FILE, "r") as s:
116+
for ln in s.readlines():
117+
n = ln.strip().split()[0]
118+
if n in onames:
119+
log_debug("Mount exists for {}".format(n))
120+
return True
121+
return False
122+
123+
124+
def do_check(skip_mount, dirs):
125+
ret = 0
126+
if not test_writable(dirs):
127+
if not skip_mount:
128+
ret = do_mnt(dirs)
129+
130+
# Check if mounted
131+
if (not ret) and is_mounted(dirs):
132+
log_err("READ-ONLY: Mounted {} to make Read-Write".format(dirs))
133+
134+
return ret
135+
136+
137+
def main():
138+
parser=argparse.ArgumentParser(
139+
description="check disk for Read-Write and mount etc & home as Read-Write")
140+
parser.add_argument('-s', "--skip-mount", action='store_true', default=False,
141+
help="Skip mounting /etc & /home as Read-Write")
142+
parser.add_argument('-d', "--dirs", default="/etc,/home",
143+
help="dirs to mount")
144+
args = parser.parse_args()
145+
146+
ret = do_check(args.skip_mount, args.dirs.split(","))
147+
return ret
148+
149+
150+
if __name__ == "__main__":
151+
sys.exit(main())

setup.py

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
'scripts/db_migrator.py',
8282
'scripts/decode-syseeprom',
8383
'scripts/dropcheck',
84+
'scripts/disk_check.py',
8485
'scripts/dropconfig',
8586
'scripts/dropstat',
8687
'scripts/dump_nat_entries.py',

tests/disk_check_test.py

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import sys
2+
import syslog
3+
from unittest.mock import patch
4+
import pytest
5+
6+
sys.path.append("scripts")
7+
import disk_check
8+
9+
disk_check.MOUNTS_FILE = "/tmp/proc_mounts"
10+
11+
test_data = {
12+
"0": {
13+
"desc": "All good as /tmp is read-write",
14+
"args": ["", "-d", "/tmp"],
15+
"err": ""
16+
},
17+
"1": {
18+
"desc": "Not good as /tmpx is not read-write; But fix skipped",
19+
"args": ["", "-d", "/tmpx", "-s"],
20+
"err": "/tmpx is not read-write"
21+
},
22+
"2": {
23+
"desc": "Not good as /tmpx is not read-write; expect mount",
24+
"args": ["", "-d", "/tmpx"],
25+
"upperdir": "/tmp/tmpx",
26+
"workdir": "/tmp/tmpy",
27+
"mounts": "overlay_tmpx blahblah",
28+
"err": "/tmpx is not read-write|READ-ONLY: Mounted ['/tmpx'] to make Read-Write",
29+
"cmds": ['mount -t overlay overlay_tmpx -o lowerdir=/tmpx,upperdir=/tmp/tmpx,workdir=/tmp/tmpy /tmpx']
30+
},
31+
"3": {
32+
"desc": "Not good as /tmpx is not read-write; mount fail as create of upper fails",
33+
"args": ["", "-d", "/tmpx"],
34+
"upperdir": "/tmpx",
35+
"expect_ret": 1
36+
},
37+
"4": {
38+
"desc": "Not good as /tmpx is not read-write; mount fail as upper exist",
39+
"args": ["", "-d", "/tmpx"],
40+
"upperdir": "/tmp",
41+
"err": "/tmpx is not read-write|Already mounted",
42+
"expect_ret": 1
43+
},
44+
"5": {
45+
"desc": "/tmp is read-write, but as well mount exists; hence report",
46+
"args": ["", "-d", "/tmp"],
47+
"upperdir": "/tmp",
48+
"mounts": "overlay_tmp blahblah",
49+
"err": "READ-ONLY: Mounted ['/tmp'] to make Read-Write"
50+
},
51+
"6": {
52+
"desc": "Test another code path for good case",
53+
"args": ["", "-d", "/tmp"],
54+
"upperdir": "/tmp"
55+
}
56+
}
57+
58+
err_data = ""
59+
cmds = []
60+
current_tc = None
61+
62+
def mount_file(d):
63+
with open(disk_check.MOUNTS_FILE, "w") as s:
64+
s.write(d)
65+
66+
67+
def report_err_msg(lvl, m):
68+
global err_data
69+
if lvl == syslog.LOG_ERR:
70+
if err_data:
71+
err_data += "|"
72+
err_data += m
73+
74+
75+
class proc:
76+
returncode = 0
77+
stdout = None
78+
stderr = None
79+
80+
def __init__(self, proc_upd = None):
81+
if proc_upd:
82+
self.returncode = proc_upd.get("ret", 0)
83+
self.stdout = proc_upd.get("stdout", None)
84+
self.stderr = proc_upd.get("stderr", None)
85+
86+
87+
def mock_subproc_run(cmd, shell, text, capture_output):
88+
global cmds
89+
90+
upd = (current_tc["proc"][len(cmds)]
91+
if len(current_tc.get("proc", [])) > len(cmds) else None)
92+
cmds.append(cmd)
93+
94+
return proc(upd)
95+
96+
97+
def init_tc(tc):
98+
global err_data, cmds, current_tc
99+
100+
err_data = ""
101+
cmds = []
102+
mount_file(tc.get("mounts", ""))
103+
current_tc = tc
104+
105+
106+
def swap_upper(tc):
107+
tmp_u = tc["upperdir"]
108+
tc["upperdir"] = disk_check.UPPER_DIR
109+
disk_check.UPPER_DIR = tmp_u
110+
111+
112+
def swap_work(tc):
113+
tmp_w = tc["workdir"]
114+
tc["upperdir"] = disk_check.WORK_DIR
115+
disk_check.WORK_DIR = tmp_w
116+
117+
118+
class TestDiskCheck(object):
119+
def setup(self):
120+
pass
121+
122+
123+
@patch("disk_check.syslog.syslog")
124+
@patch("disk_check.subprocess.run")
125+
def test_readonly(self, mock_proc, mock_log):
126+
global err_data, cmds
127+
128+
mock_proc.side_effect = mock_subproc_run
129+
mock_log.side_effect = report_err_msg
130+
131+
for i, tc in test_data.items():
132+
print("-----------Start tc {}---------".format(i))
133+
init_tc(tc)
134+
135+
with patch('sys.argv', tc["args"]):
136+
if "upperdir" in tc:
137+
swap_upper(tc)
138+
139+
if "workdir" in tc:
140+
# restore
141+
swap_work(tc)
142+
143+
ret = disk_check.main()
144+
145+
if "upperdir" in tc:
146+
# restore
147+
swap_upper(tc)
148+
149+
if "workdir" in tc:
150+
# restore
151+
swap_work(tc)
152+
153+
print("ret = {}".format(ret))
154+
print("err_data={}".format(err_data))
155+
print("cmds: {}".format(cmds))
156+
157+
assert ret == tc.get("expect_ret", 0)
158+
if "err" in tc:
159+
assert err_data == tc["err"]
160+
assert cmds == tc.get("cmds", [])
161+
print("-----------End tc {}-----------".format(i))

0 commit comments

Comments
 (0)